Source code for ax.plot.render

#!/usr/bin/env python3
# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This source code is licensed under the MIT license found in the
# LICENSE file in the root directory of this source tree.

# pyre-strict

import enum
import json
import os
import pkgutil
import uuid

import plotly.offline as plotly_offline
from ax.plot.base import AxPlotConfig, AxPlotTypes
from jinja2 import Template

# Static plot module
PLOT_MODULE_NAME = "ax.plot"

# Rendering constants
DEFAULT_WIDTH = "100%"
DEFAULT_HEIGHT = 550
CSS_FILE = "ax/ax/plot/css/base.css"


# Common resources used in plotting (load with _load_js_resource)
class _AxPlotJSResources(enum.Enum):
    """Enum of common JS resources for plotting."""

    CSS_INJECTION = "css"
    HELPER_FXNS = "helpers"
    PLOTLY_OFFLINE = "plotly_offline"
    PLOTLY_ONLINE = "plotly_online"
    PLOTLY_REQUIRES = "plotly_requires"


# JS-based plots that are supported in Ax should be registered here
Ax_PLOT_REGISTRY: dict[enum.Enum, str] = {AxPlotTypes.GENERIC: "generic_plotly.js"}


def _load_js_resource(resource_type: _AxPlotJSResources) -> str:
    """Convert plot config to corresponding JS code."""
    resource = pkgutil.get_data(
        PLOT_MODULE_NAME, os.path.join("js", "common", resource_type.value + ".js")
    )
    if resource is None:
        raise ValueError(f"Cannot find JS resource {resource_type.value}.")
    return resource.decode("utf8")


def _load_css_resource() -> str:
    resource = pkgutil.get_data(PLOT_MODULE_NAME, os.path.join("css", "base.css"))
    assert resource is not None
    return resource.decode("utf8")


def _js_requires(offline: bool = False) -> str:
    """Format JS requires for Plotly dependency.

    Args:
        offline: if True, inject entire Plotly library for offline use.

    Returns:
        str: <script> block with Plotly dependency.

    """
    helper_fxns = _load_js_resource(_AxPlotJSResources.HELPER_FXNS)
    if offline:
        script = Template(_load_js_resource(_AxPlotJSResources.PLOTLY_OFFLINE)).render(
            library=plotly_offline.offline.get_plotlyjs()
        )
    else:
        script = _load_js_resource(_AxPlotJSResources.PLOTLY_ONLINE)
    return script + helper_fxns


def _get_plot_js(
    config: AxPlotConfig,
    plot_module_name: str,
    plot_resources: dict[enum.Enum, str],
    plotdivid: str,
) -> str:
    """Convert plot config to corresponding JS code."""
    if not isinstance(config, AxPlotConfig):
        raise ValueError("Config must be instance of AxPlotConfig.")
    js_template = pkgutil.get_data(
        plot_module_name, os.path.join("js", plot_resources[config.plot_type])
    )
    if js_template is None:
        raise ValueError(f"Cannot find JS template {plot_resources[config.plot_type]}.")
    return Template(js_template.decode("utf8")).render(
        id=json.dumps(plotdivid), **{k: json.dumps(v) for k, v in config.data.items()}
    )


def _wrap_js(script: str) -> str:
    """Wrap JS in <script></script> tag for injection into HTML."""
    return f"<script type='text/javascript'>{script}</script>"


def _plot_js_to_html(js_script: str, plotdivid: str) -> str:
    """Embed JS script for Plotly plot in HTML.

    Result is a <div> block with a unique id set in `plotdivid` and a
    <script> block that contains the JS needed to render the plot inside the
    div.

    Args:
        js_script: JS for rendering plot.
        plotdivid: unique string ID for div.

    """
    plot_div = (
        '<div id="{id}" style="width: {width};" class="plotly-graph-div">' "</div>"
    ).format(id=plotdivid, width=DEFAULT_WIDTH)
    plot_js = Template(_load_js_resource(_AxPlotJSResources.PLOTLY_REQUIRES)).render(
        script=js_script
    )
    return plot_div + _wrap_js(plot_js)


[docs] def plot_config_to_html( plot_config: AxPlotConfig, plot_module_name: str = PLOT_MODULE_NAME, plot_resources: dict[enum.Enum, str] = Ax_PLOT_REGISTRY, inject_helpers: bool = False, ) -> str: """Generate HTML + JS corresponding from a plot config.""" plotdivid = uuid.uuid4().hex plot_js = _get_plot_js(plot_config, plot_module_name, plot_resources, plotdivid) if inject_helpers: helper_fxns = _load_js_resource(_AxPlotJSResources.HELPER_FXNS) plot_js = helper_fxns + plot_js return _plot_js_to_html(plot_js, plotdivid)