import base64 import pathlib from functools import reduce import pytest from pytest_pyodide import run_in_pyodide REFERENCE_IMAGES_PATH = pathlib.Path(__file__).parent / "test_data" DECORATORS = [ pytest.mark.xfail_browsers(node="No supported matplotlib backends on node"), pytest.mark.skip_refcount_check, pytest.mark.skip_pyproxy_check, pytest.mark.driver_timeout(60), ] def matplotlib_test_decorator(f): return reduce(lambda x, g: g(x), DECORATORS, f) def save_canvas_data(selenium, output_path): canvas_data = selenium.run( """ import base64 canvas = plt.gcf().canvas.get_element("canvas") canvas_data = canvas.toDataURL("image/png")[21:] canvas_data """ ) canvas_png = base64.b64decode(canvas_data) output_path.write_bytes(canvas_png) @run_in_pyodide(packages=["matplotlib"]) def patch_font_loading_and_dpi(selenium, handle, target_font=""): """Monkey-patches font loading and dpi to allow testing""" from matplotlib_pyodide.html5_canvas_backend import ( FigureCanvasHTMLCanvas, RendererHTMLCanvas, ) FigureCanvasHTMLCanvas.get_dpi_ratio = lambda self, context: 2.0 load_font_into_web = RendererHTMLCanvas.load_font_into_web def load_font_into_web_wrapper( self, loaded_font, font_url, orig_function=load_font_into_web ): fontface = orig_function(self, loaded_font, font_url) if not target_font or target_font == fontface.family: try: handle.font_loaded = True except Exception as e: raise ValueError("unable to resolve") from e RendererHTMLCanvas.load_font_into_web = load_font_into_web_wrapper def compare_func_handle(selenium): @run_in_pyodide(packages=["matplotlib"]) def prepare(selenium): from pytest_pyodide.decorator import PyodideHandle class Handle: def __init__(self): self.font_loaded = False async def compare(self, ref): import asyncio import io import matplotlib.pyplot as plt import numpy as np from PIL import Image while not self.font_loaded: # wait until font is loading await asyncio.sleep(0.2) canvas_data = plt.gcf().canvas.get_pixel_data() ref_data = np.asarray(Image.open(io.BytesIO(ref))) deviation = np.mean(np.abs(canvas_data - ref_data)) assert float(deviation) == 0.0 return PyodideHandle(Handle()) handle = prepare(selenium) return handle @matplotlib_test_decorator @run_in_pyodide(packages=["matplotlib"]) def test_plot(selenium): from matplotlib import pyplot as plt plt.figure() plt.plot([1, 2, 3]) plt.show() @matplotlib_test_decorator @run_in_pyodide(packages=["matplotlib"]) def test_svg(selenium): import io from matplotlib import pyplot as plt plt.figure() plt.plot([1, 2, 3]) fd = io.BytesIO() plt.savefig(fd, format="svg") content = fd.getvalue().decode("utf8") assert len(content) == 14998 assert content.startswith(" \beta_i,\ " r"\alpha_{i+1}^j = {\rm sin}(2\pi f_j t_i) e^{-5 t_i/\tau},\ " r"\ldots$", 2: r"$\frac{3}{4},\ \binom{3}{4},\ \genfrac{}{}{0}{}{3}{4},\ " r"\left(\frac{5 - \frac{1}{x}}{4}\right),\ \ldots$", 3: r"$\sqrt{2},\ \sqrt[3]{x},\ \ldots$", 4: r"$\mathrm{Roman}\ , \ \mathit{Italic}\ , \ \mathtt{Typewriter} \ " r"\mathrm{or}\ \mathcal{CALLIGRAPHY}$", 5: r"$\acute a,\ \bar a,\ \breve a,\ \dot a,\ \ddot a, \ \grave a, \ " r"\hat a,\ \tilde a,\ \vec a,\ \widehat{xyz},\ \widetilde{xyz},\ " r"\ldots$", 6: r"$\alpha,\ \beta,\ \chi,\ \delta,\ \lambda,\ \mu,\ " r"\Delta,\ \Gamma,\ \Omega,\ \Phi,\ \Pi,\ \Upsilon,\ \nabla,\ " r"\aleph,\ \beth,\ \daleth,\ \gimel,\ \ldots$", 7: r"$\coprod,\ \int,\ \oint,\ \prod,\ \sum,\ " r"\log,\ \sin,\ \approx,\ \oplus,\ \star,\ \varpropto,\ " r"\infty,\ \partial,\ \Re,\ \leftrightsquigarrow, \ \ldots$", } def doall(): # Colors used in mpl online documentation. mpl_blue_rvb = (191.0 / 255.0, 209.0 / 256.0, 212.0 / 255.0) mpl_orange_rvb = (202.0 / 255.0, 121.0 / 256.0, 0.0 / 255.0) mpl_grey_rvb = (51.0 / 255.0, 51.0 / 255.0, 51.0 / 255.0) # Creating figure and axis. plt.figure(figsize=(6, 7)) plt.axes([0.01, 0.01, 0.98, 0.90], facecolor="white", frameon=True) plt.gca().set_xlim(0.0, 1.0) plt.gca().set_ylim(0.0, 1.0) plt.gca().set_title( "Matplotlib's math rendering engine", color=mpl_grey_rvb, fontsize=14, weight="bold", ) plt.gca().set_xticklabels("", visible=False) plt.gca().set_yticklabels("", visible=False) # Gap between lines in axes coords line_axesfrac = 1.0 / (n_lines) # Plotting header demonstration formula full_demo = mathext_demos[0] plt.annotate( full_demo, xy=(0.5, 1.0 - 0.59 * line_axesfrac), color=mpl_orange_rvb, ha="center", fontsize=20, ) # Plotting features demonstration formulae for i_line in range(1, n_lines): baseline = 1 - (i_line) * line_axesfrac baseline_next = baseline - line_axesfrac title = mathtext_titles[i_line] + ":" fill_color = ["white", mpl_blue_rvb][i_line % 2] plt.fill_between( [0.0, 1.0], [baseline, baseline], [baseline_next, baseline_next], color=fill_color, alpha=0.5, ) plt.annotate( title, xy=(0.07, baseline - 0.3 * line_axesfrac), color=mpl_grey_rvb, weight="bold", ) demo = mathext_demos[i_line] plt.annotate( demo, xy=(0.05, baseline - 0.75 * line_axesfrac), color=mpl_grey_rvb, fontsize=16, ) for i in range(n_lines): s = mathext_demos[i] print(i, s) plt.show() doall() handle.compare(ref) ref = ( REFERENCE_IMAGES_PATH / f"canvas-math-text-{selenium.browser}.png" ).read_bytes() handle = compare_func_handle(selenium) patch_font_loading_and_dpi(selenium, handle) run(selenium, handle, ref) @matplotlib_test_decorator def test_custom_font_text(selenium_standalone): selenium = selenium_standalone @run_in_pyodide(packages=["matplotlib"]) def run(selenium, handle, ref): import matplotlib matplotlib.use("module://matplotlib_pyodide.html5_canvas_backend") import matplotlib.pyplot as plt import numpy as np f = {"fontname": "cmsy10"} t = np.arange(0.0, 2.0, 0.01) s = 1 + np.sin(2 * np.pi * t) plt.figure() plt.title("A simple Sine Curve", **f) plt.plot(t, s, linewidth=1.0, marker=11) plt.plot(t, t) plt.grid(True) plt.show() handle.compare(ref) ref = ( REFERENCE_IMAGES_PATH / f"canvas-custom-font-text-{selenium.browser}.png" ).read_bytes() handle = compare_func_handle(selenium) patch_font_loading_and_dpi(selenium, handle) run(selenium, handle, ref) @matplotlib_test_decorator def test_zoom_on_polar_plot(selenium_standalone): selenium = selenium_standalone @run_in_pyodide(packages=["matplotlib"]) def run(selenium, handle, ref): import matplotlib matplotlib.use("module://matplotlib_pyodide.html5_canvas_backend") import matplotlib.pyplot as plt import numpy as np np.random.seed(42) # Compute pie slices N = 20 theta = np.linspace(0.0, 2 * np.pi, N, endpoint=False) radii = 10 * np.random.rand(N) width = np.pi / 4 * np.random.rand(N) ax = plt.subplot(111, projection="polar") bars = ax.bar(theta, radii, width=width, bottom=0.0) # Use custom colors and opacity for r, bar in zip(radii, bars, strict=True): bar.set_facecolor(plt.cm.viridis(r / 10.0)) bar.set_alpha(0.5) ax.set_rlim([0, 5]) plt.show() handle.compare(ref) ref = ( REFERENCE_IMAGES_PATH / f"canvas-polar-zoom-{selenium.browser}.png" ).read_bytes() handle = compare_func_handle(selenium) patch_font_loading_and_dpi(selenium, handle) run(selenium, handle, ref) @matplotlib_test_decorator def test_transparency(selenium_standalone): selenium = selenium_standalone @run_in_pyodide(packages=["matplotlib"]) def run(selenium, handle, ref): import matplotlib matplotlib.use("module://matplotlib_pyodide.html5_canvas_backend") import numpy as np np.random.seed(19680801) import matplotlib.pyplot as plt fig, ax = plt.subplots() for color in ["tab:blue", "tab:orange", "tab:green"]: n = 100 x, y = np.random.rand(2, n) scale = 200.0 * np.random.rand(n) ax.scatter( x, y, c=color, s=scale, label=color, alpha=0.3, edgecolors="none" ) ax.legend() ax.grid(True) plt.show() handle.compare(ref) ref = ( REFERENCE_IMAGES_PATH / f"canvas-transparency-{selenium.browser}.png" ).read_bytes() handle = compare_func_handle(selenium) patch_font_loading_and_dpi(selenium, handle) run(selenium, handle, ref) @matplotlib_test_decorator @run_in_pyodide(packages=["matplotlib"]) def test_triangulation(selenium): """This test uses setjmp/longjmp so hopefully prevents any more screw ups with that... """ import matplotlib.tri as tri import numpy as np # First create the x and y coordinates of the points. n_angles = 36 n_radii = 8 min_radius = 0.25 radii = np.linspace(min_radius, 0.95, n_radii) angles = np.linspace(0, 2 * np.pi, n_angles, endpoint=False) angles = np.repeat(angles[..., np.newaxis], n_radii, axis=1) angles[:, 1::2] += np.pi / n_angles x = (radii * np.cos(angles)).flatten() y = (radii * np.sin(angles)).flatten() # Create the Triangulation; no triangles so Delaunay triangulation created. tri.Triangulation(x, y)