Source code for neutcurve.fig_utils

"""
=========
fig_utils
=========

Utilities for rendering `matplotlib <https://matplotlib.org/>`_ figures as HTML.

"""

import base64
import io

import matplotlib
import matplotlib.figure


[docs] def fig_html(fig, display_method): """Render a large matplotlib figure as a self-contained HTML string. This function is designed for when you have a very large matplotlib figure (eg, as produced by the plotting functions of :class:`neutcurve.curvefits.CurveFits`) and you want to put it in an HTML page. For ``display_method="svg"`` and ``"pdf"`` the figure is placed in a fixed-height scrollable box, so that a figure with many panels does not force the rest of the page arbitrarily far down. The same figure always gives the same HTML, so a report built from this can be tracked in version control without every run showing a difference. Requires `pillow <https://python-pillow.github.io/>`_ if you are using ``display_method="png8"``; it is installed with `matplotlib` in any case. Args: `fig` (matplotlib.figure.Figure) The figure we want to render. `display_method` {"svg", "pdf", "png8"} Render the figure as a SVG, as a PDF, or as a PNG8. In general, rendering as a PNG8 will be the smallest size although also the lowest resolution. Returns: `str` HTML that can be embedded in a page, or passed to ``marimo.Html``. Example: >>> import matplotlib.figure >>> fig = matplotlib.figure.Figure(figsize=(2, 2)) >>> _ = fig.subplots().plot([0, 1], [0, 1]) The SVG is wrapped in the scrollable box, and is a real SVG rather than an image: >>> html = fig_html(fig, "svg") >>> '<div id="svgwrap" style="width:100%;height:80vh;overflow:auto">' in html True >>> "<svg" in html True The other methods embed the figure as a data URI: >>> fig_html(fig, "pdf").startswith("<iframe src='data:application/pdf;base64,") True >>> fig_html(fig, "png8").startswith('<img src="data:image/png;base64,') True Rendering the same figure twice gives the same HTML: >>> all(fig_html(fig, m) == fig_html(fig, m) for m in ["svg", "pdf", "png8"]) True Anything else is an error. Note in particular that there is no ``"inline"`` method here, as a bare figure has no HTML form; see :func:`neutcurve.marimo_utils.display_fig_marimo` for that: >>> fig_html(fig, "inline") Traceback (most recent call last): ... ValueError: Invalid display_method='inline', valid methods are 'svg', 'pdf', 'png8' """ if not isinstance(fig, matplotlib.figure.Figure): raise ValueError( f"Expected `fig` to be matplotlib.figure.Figure, instead {type(fig)=}" ) if display_method == "svg": buf = io.BytesIO() with matplotlib.rc_context( { "svg.fonttype": "none", # keep text as text, not paths "svg.image_inline": True, # embed small images if present "svg.hashsalt": "fixed-1", # deterministic ids in the SVG "path.simplify": True, "path.simplify_threshold": 0.2, } ): # `Date` is omitted so that the same figure always gives the same SVG; # matplotlib otherwise stamps the current time into it fig.savefig(buf, format="svg", metadata={"Date": None}) svg_text = buf.getvalue().decode("utf-8") return f""" <style> #svgwrap svg {{ width: 100% !important; height: auto !important; max-width: 100%; display: block; }} </style> <div id="svgwrap" style="width:100%;height:80vh;overflow:auto"> {svg_text} </div> """ elif display_method == "pdf": buf = io.BytesIO() with matplotlib.rc_context( { "pdf.fonttype": 42, "pdf.compression": 7, "path.simplify": True, "path.simplify_threshold": 0.2, } ): # `CreationDate` is omitted for the same reason as `Date` above fig.savefig(buf, format="pdf", metadata={"CreationDate": None}) data = base64.b64encode(buf.getvalue()).decode("ascii") return ( f"<iframe src='data:application/pdf;base64,{data}' " "style='border-radius: 4px;width: 100%;height: 80vh' " "frameborder='0'></iframe>" ) elif display_method == "png8": import PIL.Image import PIL.PngImagePlugin buf = io.BytesIO() fig.savefig(buf, format="png", dpi=80, metadata={}) im = PIL.Image.open(io.BytesIO(buf.getvalue())).quantize( colors=48, dither=PIL.Image.Dither.NONE ) out = io.BytesIO() im.save(out, format="PNG", optimize=True, pnginfo=PIL.PngImagePlugin.PngInfo()) data = base64.b64encode(out.getvalue()).decode("ascii") return f'<img src="data:image/png;base64,{data}" alt="figure">' else: raise ValueError( f"Invalid {display_method=}, valid methods are 'svg', 'pdf', 'png8'" )
if __name__ == "__main__": import doctest doctest.testmod()