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