import base64 import pathlib import textwrap from functools import reduce import pytest REFERENCE_IMAGES_PATH = pathlib.Path(__file__).parent / "reference-images" DECORATORS = [ pytest.mark.xfail_browsers(node="No supported matplotlib backends on node"), pytest.mark.skip_refcount_check, pytest.mark.skip_pyproxy_check, ] def matplotlib_test_decorator(f): return reduce(lambda x, g: g(x), DECORATORS, f) def run_with_resolve(selenium, code): selenium.run_js( f""" try {{ let promise = new Promise((resolve) => self.resolve = resolve); pyodide.runPython({code!r}); await promise; }} finally {{ delete self.resolve; }} """ ) def patch_font_loading_and_dpi(target_font=""): """Monkey-patches font loading and dpi to allow testing""" return textwrap.dedent( f"""from matplotlib.backends.html5_canvas_backend import RendererHTMLCanvas from matplotlib.backends.html5_canvas_backend import FigureCanvasHTMLCanvas 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) target_font = {target_font!r} if not target_font or target_font == fontface.family: try: from js import resolve resolve() except Exception as e: raise ValueError("unable to resolve") from e RendererHTMLCanvas.load_font_into_web = load_font_into_web_wrapper """ ) 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) def compare_with_reference_image(selenium, reference_image): reference_image_encoded = base64.b64encode(reference_image.read_bytes()) deviation = selenium.run( f""" import io import base64 import numpy as np from PIL import Image canvas_data = plt.gcf().canvas.get_pixel_data() ref_data = np.asarray(Image.open(io.BytesIO(base64.b64decode({reference_image_encoded!r})))) deviation = np.mean(np.abs(canvas_data - ref_data)) float(deviation) """ ) # Note: uncomment this line if you want to save the output canvas image (for comparison). # save_canvas_data(selenium, reference_image.with_name(f"output-{reference_image.name}")) return deviation == 0.0 @matplotlib_test_decorator def test_matplotlib(selenium): selenium.load_package("matplotlib") selenium.run( """ from matplotlib import pyplot as plt plt.figure() plt.plot([1,2,3]) plt.show() """ ) @matplotlib_test_decorator def test_svg(selenium): selenium.load_package("matplotlib") content = selenium.run( """ from matplotlib import pyplot as plt import io plt.figure() x = plt.plot([1,2,3]) fd = io.BytesIO() plt.savefig(fd, format='svg') 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. / 255., 209. / 256., 212. / 255.) mpl_orange_rvb = (202. / 255., 121. / 256., 0. / 255.) mpl_grey_rvb = (51. / 255., 51. / 255., 51. / 255.) # 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., 1.) plt.gca().set_ylim(0., 1.) 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. / (n_lines)) # Plotting header demonstration formula full_demo = mathext_demos[0] plt.annotate(full_demo, xy=(0.5, 1. - 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., 1.], [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() """, ) assert compare_with_reference_image( selenium, REFERENCE_IMAGES_PATH / f"canvas-math-text-{selenium.browser}.png" ) @matplotlib_test_decorator def test_custom_font_text(selenium_standalone): selenium = selenium_standalone selenium.load_package("matplotlib") selenium.set_script_timeout(60) run_with_resolve( selenium, f""" {patch_font_loading_and_dpi(target_font='cmsy10')} import matplotlib matplotlib.use("module://matplotlib.backends.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() """, ) assert compare_with_reference_image( selenium, REFERENCE_IMAGES_PATH / f"canvas-custom-font-text-{selenium.browser}.png", ) @matplotlib_test_decorator def test_zoom_on_polar_plot(selenium_standalone): selenium = selenium_standalone selenium.load_package("matplotlib") selenium.set_script_timeout(60) run_with_resolve( selenium, f""" {patch_font_loading_and_dpi()} import matplotlib matplotlib.use("module://matplotlib.backends.html5_canvas_backend") import numpy as np import matplotlib.pyplot as plt 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): bar.set_facecolor(plt.cm.viridis(r / 10.)) bar.set_alpha(0.5) ax.set_rlim([0,5]) plt.show() """, ) assert compare_with_reference_image( selenium, REFERENCE_IMAGES_PATH / f"canvas-polar-zoom-{selenium.browser}.png" ) @matplotlib_test_decorator def test_transparency(selenium_standalone): selenium = selenium_standalone selenium.load_package("matplotlib") selenium.set_script_timeout(60) run_with_resolve( selenium, f""" {patch_font_loading_and_dpi()} import matplotlib matplotlib.use("module://matplotlib.backends.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() """, ) assert compare_with_reference_image( selenium, REFERENCE_IMAGES_PATH / f"canvas-transparency-{selenium.browser}.png" )