1#----------------------------------------------------------------------------- 2# Copyright (c) 2012 - 2021, Anaconda, Inc., and Bokeh Contributors. 3# All rights reserved. 4# 5# The full license is in the file LICENSE.txt, distributed with this software. 6#----------------------------------------------------------------------------- 7''' 8 9''' 10 11#----------------------------------------------------------------------------- 12# Boilerplate 13#----------------------------------------------------------------------------- 14import logging # isort:skip 15log = logging.getLogger(__name__) 16 17#----------------------------------------------------------------------------- 18# Imports 19#----------------------------------------------------------------------------- 20 21# Standard library imports 22from typing import Any, Dict, Optional, Sequence, Tuple, Type, Union, cast 23 24# External imports 25from jinja2 import Template 26 27# Bokeh imports 28from ..core.templates import AUTOLOAD_JS, AUTOLOAD_TAG, FILE, MACROS, ROOT_DIV 29from ..document.document import DEFAULT_TITLE, Document 30from ..model import Model 31from ..resources import CSSResources, JSResources, Resources 32from ..themes import Theme 33from .bundle import Script, bundle_for_objs_and_resources 34from .elements import html_page_for_render_items, script_for_render_items 35from .util import ( 36 FromCurdoc, 37 OutputDocumentFor, 38 RenderRoot, 39 standalone_docs_json, 40 standalone_docs_json_and_render_items, 41) 42from .wrappers import wrap_in_onload 43 44#----------------------------------------------------------------------------- 45# Globals and constants 46#----------------------------------------------------------------------------- 47 48__all__ = ( 49 'autoload_static', 50 'components', 51 'file_html', 52 'json_item', 53) 54 55ModelLike = Union[Model, Document] 56ModelLikeCollection = Union[Sequence[ModelLike], Dict[str, ModelLike]] 57 58#----------------------------------------------------------------------------- 59# General API 60#----------------------------------------------------------------------------- 61 62ThemeLike = Union[None, Theme, Type[FromCurdoc]] 63 64def autoload_static(model: Union[Model, Document], resources: Resources, script_path: str) -> Tuple[str, str]: 65 ''' Return JavaScript code and a script tag that can be used to embed 66 Bokeh Plots. 67 68 The data for the plot is stored directly in the returned JavaScript code. 69 70 Args: 71 model (Model or Document) : 72 73 resources (Resources) : 74 75 script_path (str) : 76 77 Returns: 78 (js, tag) : 79 JavaScript code to be saved at ``script_path`` and a ``<script>`` 80 tag to load it 81 82 Raises: 83 ValueError 84 85 ''' 86 # TODO: maybe warn that it's not exactly useful, but technically possible 87 # if resources.mode == 'inline': 88 # raise ValueError("autoload_static() requires non-inline resources") 89 90 if isinstance(model, Model): 91 models = [model] 92 elif isinstance (model, Document): 93 models = model.roots 94 else: 95 raise ValueError("autoload_static expects a single Model or Document") 96 97 with OutputDocumentFor(models): 98 (docs_json, [render_item]) = standalone_docs_json_and_render_items([model]) 99 100 bundle = bundle_for_objs_and_resources(None, resources) 101 bundle.add(Script(script_for_render_items(docs_json, [render_item]))) 102 103 (_, elementid) = list(render_item.roots.to_json().items())[0] 104 105 js = wrap_in_onload(AUTOLOAD_JS.render(bundle=bundle, elementid=elementid)) 106 107 tag = AUTOLOAD_TAG.render( 108 src_path = script_path, 109 elementid = elementid, 110 ) 111 112 return js, tag 113 114def components(models: Union[ModelLike, ModelLikeCollection], wrap_script: bool = True, 115 wrap_plot_info: bool = True, theme: ThemeLike = None) -> Tuple[str, Any]: 116 ''' Return HTML components to embed a Bokeh plot. The data for the plot is 117 stored directly in the returned HTML. 118 119 An example can be found in examples/embed/embed_multiple.py 120 121 The returned components assume that BokehJS resources are **already loaded**. 122 The html template in which they will be embedded needs to include the following 123 scripts tags. The widgets and tables resources are only necessary if the components 124 make use of widgets and tables. 125 126 .. code-block:: html 127 128 <script src="https://cdn.bokeh.org/bokeh/release/bokeh-x.y.z.min.js"></script> 129 <script src="https://cdn.bokeh.org/bokeh/release/bokeh-widgets-x.y.z.min.js"></script> 130 <script src="https://cdn.bokeh.org/bokeh/release/bokeh-tables-x.y.z.min.js"></script> 131 132 Note that in Jupyter Notebooks, it is not possible to use components and show in 133 the same notebook cell. 134 135 Args: 136 models (Model|list|dict|tuple) : 137 A single Model, a list/tuple of Models, or a dictionary of keys and Models. 138 139 wrap_script (boolean, optional) : 140 If True, the returned javascript is wrapped in a script tag. 141 (default: True) 142 143 wrap_plot_info (boolean, optional) : If True, returns ``<div>`` strings. 144 Otherwise, return dicts that can be used to build your own divs. 145 (default: True) 146 147 If False, the returned dictionary contains the following information: 148 149 .. code-block:: python 150 151 { 152 'modelid': 'The model ID, used with Document.get_model_by_id', 153 'elementid': 'The css identifier the BokehJS will look for to target the plot', 154 'docid': 'Used by Bokeh to find the doc embedded in the returned script', 155 } 156 157 theme (Theme, optional) : 158 Applies the specified theme when creating the components. If None, 159 or not specified, and the supplied models constitute the full set of 160 roots of a document, applies the theme of that document to the components. 161 Otherwise applies the default theme. 162 163 Returns: 164 UTF-8 encoded *(script, div[s])* or *(raw_script, plot_info[s])* 165 166 Examples: 167 168 With default wrapping parameter values: 169 170 .. code-block:: python 171 172 components(plot) 173 # => (script, plot_div) 174 175 components((plot1, plot2)) 176 # => (script, (plot1_div, plot2_div)) 177 178 components({"Plot 1": plot1, "Plot 2": plot2}) 179 # => (script, {"Plot 1": plot1_div, "Plot 2": plot2_div}) 180 181 Examples: 182 183 With wrapping parameters set to ``False``: 184 185 .. code-block:: python 186 187 components(plot, wrap_script=False, wrap_plot_info=False) 188 # => (javascript, plot_dict) 189 190 components((plot1, plot2), wrap_script=False, wrap_plot_info=False) 191 # => (javascript, (plot1_dict, plot2_dict)) 192 193 components({"Plot 1": plot1, "Plot 2": plot2}, wrap_script=False, wrap_plot_info=False) 194 # => (javascript, {"Plot 1": plot1_dict, "Plot 2": plot2_dict}) 195 196 ''' 197 # 1) Convert single items and dicts into list 198 199 was_single_object = isinstance(models, Model) or isinstance(models, Document) 200 201 models = _check_models_or_docs(models) 202 203 # now convert dict to list, saving keys in the same order 204 model_keys = None 205 dict_type: Type[Dict[Any, Any]] = dict 206 if isinstance(models, dict): 207 model_keys = models.keys() 208 dict_type = models.__class__ 209 values = [] 210 # don't just use .values() to ensure we are in the same order as key list 211 for k in model_keys: 212 values.append(models[k]) 213 models = values 214 215 # 2) Append models to one document. Either pre-existing or new and render 216 with OutputDocumentFor(models, apply_theme=theme): 217 (docs_json, [render_item]) = standalone_docs_json_and_render_items(models) 218 219 bundle = bundle_for_objs_and_resources(None, None) 220 bundle.add(Script(script_for_render_items(docs_json, [render_item]))) 221 222 script = bundle.scripts(tag=wrap_script) 223 224 def div_for_root(root: RenderRoot) -> str: 225 return ROOT_DIV.render(root=root, macros=MACROS) 226 227 if wrap_plot_info: 228 results = list(div_for_root(root) for root in render_item.roots) 229 else: 230 results = render_item.roots 231 232 # 3) convert back to the input shape 233 result: Any 234 if was_single_object: 235 result = results[0] 236 elif model_keys is not None: 237 result = dict_type(zip(model_keys, results)) 238 else: 239 result = tuple(results) 240 241 return script, result 242 243def file_html(models: Union[Model, Document, Sequence[Model]], 244 resources: Union[Resources, Tuple[JSResources, CSSResources]], 245 title: Optional[str] = None, 246 template: Union[Template, str] = FILE, 247 template_variables: Dict[str, Any] = {}, 248 theme: ThemeLike = None, 249 suppress_callback_warning: bool = False, 250 _always_new: bool = False) -> str: 251 ''' Return an HTML document that embeds Bokeh Model or Document objects. 252 253 The data for the plot is stored directly in the returned HTML, with 254 support for customizing the JS/CSS resources independently and 255 customizing the jinja2 template. 256 257 Args: 258 models (Model or Document or seq[Model]) : Bokeh object or objects to render 259 typically a Model or Document 260 261 resources (Resources or tuple(JSResources or None, CSSResources or None)) : 262 A resource configuration for Bokeh JS & CSS assets. 263 264 title (str, optional) : 265 A title for the HTML document ``<title>`` tags or None. (default: None) 266 267 If None, attempt to automatically find the Document title from the given 268 plot objects. 269 270 template (Template, optional) : HTML document template (default: FILE) 271 A Jinja2 Template, see bokeh.core.templates.FILE for the required 272 template parameters 273 274 template_variables (dict, optional) : variables to be used in the Jinja2 275 template. If used, the following variable names will be overwritten: 276 title, bokeh_js, bokeh_css, plot_script, plot_div 277 278 theme (Theme, optional) : 279 Applies the specified theme to the created html. If ``None``, or 280 not specified, and the function is passed a document or the full set 281 of roots of a document, applies the theme of that document. Otherwise 282 applies the default theme. 283 284 suppress_callback_warning (bool, optional) : 285 Normally generating standalone HTML from a Bokeh Document that has 286 Python callbacks will result in a warning stating that the callbacks 287 cannot function. However, this warning can be suppressed by setting 288 this value to True (default: False) 289 290 Returns: 291 UTF-8 encoded HTML 292 293 ''' 294 295 models_seq: Sequence[Model] = [] 296 if isinstance(models, Model): 297 models_seq = [models] 298 elif isinstance(models, Document): 299 models_seq = models.roots 300 else: 301 models_seq = models 302 303 with OutputDocumentFor(models_seq, apply_theme=theme, always_new=_always_new) as doc: 304 (docs_json, render_items) = standalone_docs_json_and_render_items(models_seq, suppress_callback_warning=suppress_callback_warning) 305 title = _title_from_models(models_seq, title) 306 bundle = bundle_for_objs_and_resources([doc], resources) 307 return html_page_for_render_items(bundle, docs_json, render_items, title=title, 308 template=template, template_variables=template_variables) 309 310def json_item(model: Model, target: Optional[str] = None, theme: ThemeLike = None) -> Any: # TODO: TypedDict? 311 ''' Return a JSON block that can be used to embed standalone Bokeh content. 312 313 Args: 314 model (Model) : 315 The Bokeh object to embed 316 317 target (string, optional) 318 A div id to embed the model into. If None, the target id must 319 be supplied in the JavaScript call. 320 321 theme (Theme, optional) : 322 Applies the specified theme to the created html. If ``None``, or 323 not specified, and the function is passed a document or the full set 324 of roots of a document, applies the theme of that document. Otherwise 325 applies the default theme. 326 327 Returns: 328 JSON-like 329 330 This function returns a JSON block that can be consumed by the BokehJS 331 function ``Bokeh.embed.embed_item``. As an example, a Flask endpoint for 332 ``/plot`` might return the following content to embed a Bokeh plot into 333 a div with id *"myplot"*: 334 335 .. code-block:: python 336 337 @app.route('/plot') 338 def plot(): 339 p = make_plot('petal_width', 'petal_length') 340 return json.dumps(json_item(p, "myplot")) 341 342 Then a web page can retrieve this JSON and embed the plot by calling 343 ``Bokeh.embed.embed_item``: 344 345 .. code-block:: html 346 347 <script> 348 fetch('/plot') 349 .then(function(response) { return response.json(); }) 350 .then(function(item) { Bokeh.embed.embed_item(item); }) 351 </script> 352 353 Alternatively, if is more convenient to supply the target div id directly 354 in the page source, that is also possible. If `target_id` is omitted in the 355 call to this function: 356 357 .. code-block:: python 358 359 return json.dumps(json_item(p)) 360 361 Then the value passed to ``embed_item`` is used: 362 363 .. code-block:: javascript 364 365 Bokeh.embed.embed_item(item, "myplot"); 366 367 ''' 368 with OutputDocumentFor([model], apply_theme=theme) as doc: 369 doc.title = "" 370 docs_json = standalone_docs_json([model]) 371 372 doc = list(docs_json.values())[0] 373 root_id = doc['roots']['root_ids'][0] 374 375 return { 376 'target_id' : target, 377 'root_id' : root_id, 378 'doc' : doc, 379 } 380 381 382#----------------------------------------------------------------------------- 383# Dev API 384#----------------------------------------------------------------------------- 385 386#----------------------------------------------------------------------------- 387# Private API 388#----------------------------------------------------------------------------- 389 390def _check_models_or_docs(models: Union[ModelLike, ModelLikeCollection]) -> ModelLikeCollection: 391 ''' 392 393 ''' 394 input_type_valid = False 395 396 # Check for single item 397 if isinstance(models, (Model, Document)): 398 models = [models] 399 400 # Check for sequence 401 if isinstance(models, Sequence) and all(isinstance(x, (Model, Document)) for x in models): 402 input_type_valid = True 403 404 if isinstance(models, dict) and \ 405 all(isinstance(x, str) for x in models.keys()) and \ 406 all(isinstance(x, (Model, Document)) for x in models.values()): 407 input_type_valid = True 408 409 if not input_type_valid: 410 raise ValueError( 411 'Input must be a Model, a Document, a Sequence of Models and Document, or a dictionary from string to Model and Document' 412 ) 413 414 return models 415 416def _title_from_models(models: Sequence[Union[Model, Document]], title: Optional[str]) -> str: 417 # use override title 418 if title is not None: 419 return title 420 421 # use title from any listed document 422 for p in models: 423 if isinstance(p, Document): 424 return p.title 425 426 # use title from any model's document 427 for p in cast(Sequence[Model], models): 428 if p.document is not None: 429 return p.document.title 430 431 # use default title 432 return DEFAULT_TITLE 433 434#----------------------------------------------------------------------------- 435# Code 436#----------------------------------------------------------------------------- 437