Source code for bokeh.embed.standalone

#-----------------------------------------------------------------------------
# Copyright (c) 2012 - 2024, Anaconda, Inc., and Bokeh Contributors.
# All rights reserved.
#
# The full license is in the file LICENSE.txt, distributed with this software.
#-----------------------------------------------------------------------------
'''

'''

#-----------------------------------------------------------------------------
# Boilerplate
#-----------------------------------------------------------------------------
from __future__ import annotations

import logging # isort:skip
log = logging.getLogger(__name__)

#-----------------------------------------------------------------------------
# Imports
#-----------------------------------------------------------------------------

# Standard library imports
from typing import (
    TYPE_CHECKING,
    Any,
    Literal,
    Sequence,
    TypedDict,
    Union,
    cast,
    overload,
)

# Bokeh imports
from .. import __version__
from ..core.templates import (
    AUTOLOAD_JS,
    AUTOLOAD_TAG,
    FILE,
    MACROS,
    ROOT_DIV,
)
from ..document.document import DEFAULT_TITLE, Document
from ..model import Model
from ..resources import Resources, ResourcesLike
from ..themes import Theme
from .bundle import Script, bundle_for_objs_and_resources
from .elements import html_page_for_render_items, script_for_render_items
from .util import (
    FromCurdoc,
    OutputDocumentFor,
    RenderRoot,
    standalone_docs_json,
    standalone_docs_json_and_render_items,
)
from .wrappers import wrap_in_onload

if TYPE_CHECKING:
    from jinja2 import Template
    from typing_extensions import TypeAlias

    from ..core.types import ID
    from ..document.document import DocJson

#-----------------------------------------------------------------------------
# Globals and constants
#-----------------------------------------------------------------------------

__all__ = (
    'autoload_static',
    'components',
    'file_html',
    'json_item',
)

ModelLike: TypeAlias = Union[Model, Document]
ModelLikeCollection: TypeAlias = Union[Sequence[ModelLike], dict[str, ModelLike]]

#-----------------------------------------------------------------------------
# General API
#-----------------------------------------------------------------------------

ThemeLike: TypeAlias = Union[None, Theme, type[FromCurdoc]]

[docs] def autoload_static(model: Model | Document, resources: Resources, script_path: str) -> tuple[str, str]: ''' Return JavaScript code and a script tag that can be used to embed Bokeh Plots. The data for the plot is stored directly in the returned JavaScript code. Args: model (Model or Document) : resources (Resources) : script_path (str) : Returns: (js, tag) : JavaScript code to be saved at ``script_path`` and a ``<script>`` tag to load it Raises: ValueError ''' # TODO: maybe warn that it's not exactly useful, but technically possible # if resources.mode == 'inline': # raise ValueError("autoload_static() requires non-inline resources") if isinstance(model, Model): models = [model] elif isinstance (model, Document): models = model.roots else: raise ValueError("autoload_static expects a single Model or Document") with OutputDocumentFor(models): (docs_json, [render_item]) = standalone_docs_json_and_render_items([model]) bundle = bundle_for_objs_and_resources(None, resources) bundle.add(Script(script_for_render_items(docs_json, [render_item]))) (_, elementid) = next(iter(render_item.roots.to_json().items())) js = wrap_in_onload(AUTOLOAD_JS.render(bundle=bundle, elementid=elementid)) tag = AUTOLOAD_TAG.render( src_path = script_path, elementid = elementid, ) return js, tag
@overload def components(models: Model, wrap_script: bool = ..., # type: ignore[overload-overlap] # XXX: mypy bug wrap_plot_info: Literal[True] = ..., theme: ThemeLike = ...) -> tuple[str, str]: ... @overload def components(models: Model, wrap_script: bool = ..., wrap_plot_info: Literal[False] = ..., theme: ThemeLike = ...) -> tuple[str, RenderRoot]: ... @overload def components(models: Sequence[Model], wrap_script: bool = ..., # type: ignore[overload-overlap] # XXX: mypy bug wrap_plot_info: Literal[True] = ..., theme: ThemeLike = ...) -> tuple[str, Sequence[str]]: ... @overload def components(models: Sequence[Model], wrap_script: bool = ..., wrap_plot_info: Literal[False] = ..., theme: ThemeLike = ...) -> tuple[str, Sequence[RenderRoot]]: ... @overload def components(models: dict[str, Model], wrap_script: bool = ..., # type: ignore[overload-overlap] # XXX: mypy bug wrap_plot_info: Literal[True] = ..., theme: ThemeLike = ...) -> tuple[str, dict[str, str]]: ... @overload def components(models: dict[str, Model], wrap_script: bool = ..., wrap_plot_info: Literal[False] = ..., theme: ThemeLike = ...) -> tuple[str, dict[str, RenderRoot]]: ...
[docs] def components(models: Model | Sequence[Model] | dict[str, Model], wrap_script: bool = True, wrap_plot_info: bool = True, theme: ThemeLike = None) -> tuple[str, Any]: ''' Return HTML components to embed a Bokeh plot. The data for the plot is stored directly in the returned HTML. An example can be found in examples/embed/embed_multiple.py The returned components assume that BokehJS resources are **already loaded**. The HTML document or template in which they will be embedded needs to include scripts tags, either from a local URL or Bokeh's CDN (replacing ``x.y.z`` with the version of Bokeh you are using): .. code-block:: html <script src="https://cdn.bokeh.org/bokeh/release/bokeh-x.y.z.min.js"></script> <script src="https://cdn.bokeh.org/bokeh/release/bokeh-widgets-x.y.z.min.js"></script> <script src="https://cdn.bokeh.org/bokeh/release/bokeh-tables-x.y.z.min.js"></script> <script src="https://cdn.bokeh.org/bokeh/release/bokeh-gl-x.y.z.min.js"></script> <script src="https://cdn.bokeh.org/bokeh/release/bokeh-mathjax-x.y.z.min.js"></script> Only the Bokeh core library ``bokeh-x.y.z.min.js`` is always required. The other scripts are optional and only need to be included if you want to use corresponding features: * The ``"bokeh-widgets"`` files are only necessary if you are using any of the :ref:`Bokeh widgets <ug_interaction_widgets>`. * The ``"bokeh-tables"`` files are only necessary if you are using Bokeh's :ref:`data tables <ug_interaction_widgets_examples_datatable>`. * The ``"bokeh-api"`` files are required to use the :ref:`BokehJS API <ug_advanced_bokehjs>` and must be loaded *after* the core BokehJS library. * The ``"bokeh-gl"`` files are required to enable :ref:`WebGL support <ug_output_webgl>`. * the ``"bokeh-mathjax"`` files are required to enable :ref:`MathJax support <ug_styling_mathtext>`. Args: models (Model|list|dict|tuple) : A single Model, a list/tuple of Models, or a dictionary of keys and Models. wrap_script (boolean, optional) : If True, the returned javascript is wrapped in a script tag. (default: True) wrap_plot_info (boolean, optional) : If True, returns ``<div>`` strings. Otherwise, return :class:`~bokeh.embed.RenderRoot` objects that can be used to build your own divs. (default: True) theme (Theme, optional) : Applies the specified theme when creating the components. If None, or not specified, and the supplied models constitute the full set of roots of a document, applies the theme of that document to the components. Otherwise applies the default theme. Returns: UTF-8 encoded *(script, div[s])* or *(raw_script, plot_info[s])* Examples: With default wrapping parameter values: .. code-block:: python components(plot) # => (script, plot_div) components((plot1, plot2)) # => (script, (plot1_div, plot2_div)) components({"Plot 1": plot1, "Plot 2": plot2}) # => (script, {"Plot 1": plot1_div, "Plot 2": plot2_div}) Examples: With wrapping parameters set to ``False``: .. code-block:: python components(plot, wrap_script=False, wrap_plot_info=False) # => (javascript, plot_root) components((plot1, plot2), wrap_script=False, wrap_plot_info=False) # => (javascript, (plot1_root, plot2_root)) components({"Plot 1": plot1, "Plot 2": plot2}, wrap_script=False, wrap_plot_info=False) # => (javascript, {"Plot 1": plot1_root, "Plot 2": plot2_root}) ''' # 1) Convert single items and dicts into list # XXX: was_single_object = isinstance(models, Model) #or isinstance(models, Document) was_single_object = False if isinstance(models, Model): was_single_object = True models = [models] models = _check_models_or_docs(models) # type: ignore # XXX: this API needs to be refined # now convert dict to list, saving keys in the same order model_keys = None dict_type: type[dict[Any, Any]] = dict if isinstance(models, dict): dict_type = models.__class__ model_keys = models.keys() models = list(models.values()) # 2) Append models to one document. Either pre-existing or new and render with OutputDocumentFor(models, apply_theme=theme): (docs_json, [render_item]) = standalone_docs_json_and_render_items(models) bundle = bundle_for_objs_and_resources(None, None) bundle.add(Script(script_for_render_items(docs_json, [render_item]))) script = bundle.scripts(tag=wrap_script) def div_for_root(root: RenderRoot) -> str: return ROOT_DIV.render(root=root, macros=MACROS) results: list[str] | list[RenderRoot] if wrap_plot_info: results = [div_for_root(root) for root in render_item.roots] else: results = list(render_item.roots) # 3) convert back to the input shape result: Any if was_single_object: result = results[0] elif model_keys is not None: result = dict_type(zip(model_keys, results)) else: result = tuple(results) return script, result
[docs] def file_html(models: Model | Document | Sequence[Model], resources: ResourcesLike | None = None, title: str | None = None, *, template: Template | str = FILE, template_variables: dict[str, Any] = {}, theme: ThemeLike = None, suppress_callback_warning: bool = False, _always_new: bool = False) -> str: ''' Return an HTML document that embeds Bokeh Model or Document objects. The data for the plot is stored directly in the returned HTML, with support for customizing the JS/CSS resources independently and customizing the jinja2 template. Args: models (Model or Document or seq[Model]) : Bokeh object or objects to render typically a Model or Document resources (ResourcesLike) : A resources configuration for Bokeh JS & CSS assets. title (str, optional) : A title for the HTML document ``<title>`` tags or None. (default: None) If None, attempt to automatically find the Document title from the given plot objects. template (Template, optional) : HTML document template (default: FILE) A Jinja2 Template, see bokeh.core.templates.FILE for the required template parameters template_variables (dict, optional) : variables to be used in the Jinja2 template. If used, the following variable names will be overwritten: title, bokeh_js, bokeh_css, plot_script, plot_div theme (Theme, optional) : Applies the specified theme to the created html. If ``None``, or not specified, and the function is passed a document or the full set of roots of a document, applies the theme of that document. Otherwise applies the default theme. suppress_callback_warning (bool, optional) : Normally generating standalone HTML from a Bokeh Document that has Python callbacks will result in a warning stating that the callbacks cannot function. However, this warning can be suppressed by setting this value to True (default: False) Returns: UTF-8 encoded HTML ''' models_seq: Sequence[Model] = [] if isinstance(models, Model): models_seq = [models] elif isinstance(models, Document): if len(models.roots) == 0: raise ValueError("Document has no root Models") models_seq = models.roots else: models_seq = models resources = Resources.build(resources) with OutputDocumentFor(models_seq, apply_theme=theme, always_new=_always_new) as doc: (docs_json, render_items) = standalone_docs_json_and_render_items(models_seq, suppress_callback_warning=suppress_callback_warning) title = _title_from_models(models_seq, title) bundle = bundle_for_objs_and_resources([doc], resources) return html_page_for_render_items(bundle, docs_json, render_items, title=title, template=template, template_variables=template_variables)
class StandaloneEmbedJson(TypedDict): target_id: ID | None root_id: ID doc: DocJson version: str
[docs] def json_item(model: Model, target: ID | None = None, theme: ThemeLike = None) -> StandaloneEmbedJson: ''' Return a JSON block that can be used to embed standalone Bokeh content. Args: model (Model) : The Bokeh object to embed target (string, optional) A div id to embed the model into. If None, the target id must be supplied in the JavaScript call. theme (Theme, optional) : Applies the specified theme to the created html. If ``None``, or not specified, and the function is passed a document or the full set of roots of a document, applies the theme of that document. Otherwise applies the default theme. Returns: JSON-like This function returns a JSON block that can be consumed by the BokehJS function ``Bokeh.embed.embed_item``. As an example, a Flask endpoint for ``/plot`` might return the following content to embed a Bokeh plot into a div with id *"myplot"*: .. code-block:: python @app.route('/plot') def plot(): p = make_plot('petal_width', 'petal_length') return json.dumps(json_item(p, "myplot")) Then a web page can retrieve this JSON and embed the plot by calling ``Bokeh.embed.embed_item``: .. code-block:: html <script> fetch('/plot') .then(function(response) { return response.json(); }) .then(function(item) { Bokeh.embed.embed_item(item); }) </script> Alternatively, if is more convenient to supply the target div id directly in the page source, that is also possible. If `target_id` is omitted in the call to this function: .. code-block:: python return json.dumps(json_item(p)) Then the value passed to ``embed_item`` is used: .. code-block:: javascript Bokeh.embed.embed_item(item, "myplot"); ''' with OutputDocumentFor([model], apply_theme=theme) as doc: doc.title = "" [doc_json] = standalone_docs_json([model]).values() root_id = doc_json["roots"][0]["id"] return StandaloneEmbedJson( target_id = target, root_id = root_id, doc = doc_json, version = __version__, )
#----------------------------------------------------------------------------- # Dev API #----------------------------------------------------------------------------- #----------------------------------------------------------------------------- # Private API #----------------------------------------------------------------------------- def _check_models_or_docs(models: ModelLike | ModelLikeCollection) -> ModelLikeCollection: ''' ''' input_type_valid = False # Check for single item if isinstance(models, (Model, Document)): models = [models] # Check for sequence if isinstance(models, Sequence) and all(isinstance(x, (Model, Document)) for x in models): input_type_valid = True if isinstance(models, dict) and \ all(isinstance(x, str) for x in models.keys()) and \ all(isinstance(x, (Model, Document)) for x in models.values()): input_type_valid = True if not input_type_valid: raise ValueError( 'Input must be a Model, a Document, a Sequence of Models and Document, or a dictionary from string to Model and Document', ) return models def _title_from_models(models: Sequence[Model | Document], title: str | None) -> str: # use override title if title is not None: return title # use title from any listed document for p in models: if isinstance(p, Document): return p.title # use title from any model's document for p in cast(Sequence[Model], models): if p.document is not None: return p.document.title # use default title return DEFAULT_TITLE #----------------------------------------------------------------------------- # Code #-----------------------------------------------------------------------------