Source code for node_bokeh_figure

# This file is part of Sympathy for Data.
# Copyright (c) 2022, Combine Control Systems AB
#
# SYMPATHY FOR DATA COMMERCIAL LICENSE
# You should have received a link to the License with Sympathy for Data.
import numpy as np
from bokeh.embed import components
from bokeh import layouts

from sympathy import api
from sympathy.api import node as synode
from sympathy.api import qt2 as qt_compat
from sympathy.api import text
from sympathy.api.nodeconfig import Port, Ports, Tag, Tags

from sympathy.utils import preview

from sylib.bokeh import drawing, backend, bokeh as sybokeh
from sylib.figure import gui

QtCore = qt_compat.QtCore
QtGui = qt_compat.QtGui
QtWidgets = qt_compat.QtWidgets


_RELATED_NODEIDS = [
    'org.sysess.sympathy.visualize.bokehfigure',
    'org.sysess.sympathy.visualize.bokehfiguresubplot',
    'org.sysess.sympathy.export.exportbokehfigures',
    'org.sysess.sympathy.visualize.figure',
]


BOKEH_PLOT_TYPES = """
Heatmaps
--------
Heatmaps are two-dimensional histograms. Z data should be supplied as a
two-dimensional array and X/Y data should be the minimum and maximum values for
each dimension. Uniform bins are currently always assumed.

"""

BOKEH_RELATED_NODES_DOCS = """
This node can not create subplots, but by creating multiple figure objects
you can use the node
:ref:`org.sysess.sympathy.visualize.bokehfiguresubplot` to arrange them as
subplots.

Use the node :ref:`org.sysess.sympathy.export.exportbokehfigures` to write
the figures you produce to files.

For creating non-interactive figures using the matplotlib backend see
:ref:`org.sysess.sympathy.visualize.figure`.
"""


[docs] class BokehFigure(synode.Node): __doc__ = gui.DOCS.format( templates='', extra_plot_types=BOKEH_PLOT_TYPES, legend_outside='', related_nodes=BOKEH_RELATED_NODES_DOCS, ) author = 'Magnus Sandén' icon = 'figure.svg' name = 'Bokeh Figure' description = 'Create a Bokeh Figure from some data.' nodeid = 'org.sysess.sympathy.visualize.bokehfigure' tags = Tags(Tag.Visual.Figure) related = _RELATED_NODEIDS parameters = synode.parameters() parameters.set_json( 'parameters', value={}, description='The full configuration for this figure.') inputs = Ports([Port.Custom('<a>', 'Input', name='input')]) outputs = Ports([ Port.Custom('bokeh', 'Output figure', name='figure', preview=True)]) def _parameter_view(self, node_context, input_data): figure_widget = gui.FigureFromTableWidget( input_data, node_context.parameters['parameters'], backend.bokeh_backend) try: preview_widget = preview.PreviewWidget( self, node_context, node_context.parameters) except ImportError as e: # QtWebEngineWidgets will fail import when running tests # on windows/servercore image. widget = QtWidgets.QLabel(f'Failed to load due to: {e}') else: widget = preview.ParameterPreviewWidget( figure_widget, preview_widget) return widget def exec_parameter_view(self, node_context): input_data = node_context.input['input'] if not input_data.is_valid(): input_data = api.table.File() return self._parameter_view(node_context, input_data) def execute(self, node_context): data_table = node_context.input['input'] output_figure = node_context.output['figure'] parameters = node_context.parameters['parameters'].value figure = drawing.create_figure(data_table, parameters) output_figure.set_figure(figure)
[docs] class SubplotBokehFigures(synode.Node): """ Layout the Figures in a list of Figures into subplots. Unless specified the number of rows and columns is automatically adjusted to an approximate square. Empty axes in a non-empty row will be not shown. """ author = 'Magnus Sandén' icon = 'figuresubplots.svg' name = 'Layout Bokeh Figures in Subplots' description = 'Layout a list of Bokeh Figures in a Subplot' nodeid = 'org.sysess.sympathy.visualize.bokehfiguresubplot' tags = Tags(Tag.Visual.Figure) related = ['org.sysess.sympathy.visualize.bokehfigure'] inputs = Ports([Port.Custom('[bokeh]', 'List of Figures', name='input')]) outputs = Ports([Port.Custom( 'bokeh', 'A Figure with several subplot axes', name='figure')]) parameters = synode.parameters() parameters.set_integer( 'rows', value=0, label='Number of rows', description='Specify the number of rows, or 0 for auto.' 'If rows and columns are both 0, the node with attempt ' 'to create an approximately square layout.', editor=synode.editors.bounded_spinbox_editor(0, 100, 1)) parameters.set_integer( 'columns', value=0, label='Number of columns', description='Specify the number of columns, or 0 for auto.' 'If rows and columns are both 0, the node with attempt ' 'to create an approximately square layout.', editor=synode.editors.bounded_spinbox_editor(0, 100, 1)) def execute(self, node_context): input_figures = node_context.input['input'] output_figure = node_context.output['figure'] parameters = node_context.parameters rows = parameters['rows'].value cols = parameters['columns'].value # calculate the number of rows and columns if any is =0 nb_input_figures = len(input_figures) if rows == 0 and cols == 0: rows = int(np.ceil(np.sqrt(nb_input_figures))) cols = int(np.ceil(np.sqrt(nb_input_figures))) if rows * cols - cols >= nb_input_figures > 0: rows -= 1 elif rows == 0 and cols > 0: rows = int(np.ceil(nb_input_figures / float(cols))) elif rows > 0 and cols == 0: cols = int(np.ceil(nb_input_figures / float(rows))) bokeh_figures = [f.get_figure() for f in input_figures] grid = layouts.grid(bokeh_figures, nrows=rows, ncols=cols) output_figure.set_figure(grid)
[docs] class BokehComponents(synode.Node): """ To embed the figure in an html template you can use the node :ref:`org.sysess.sympathy.texts.generic_jinja2template`. See the example flow "Custom bokeh template.syx" for an example of this. """ author = 'Magnus Sandén' icon = 'html_components.svg' name = 'Bokeh Components' description = ( 'Export HTML components for embedding a Bokeh Figure in a custom ' 'HTML template.') nodeid = 'org.sysess.sympathy.visualize.bokehfigureparts' tags = Tags(Tag.Visual.Figure) related = ['org.sysess.sympathy.visualize.bokehfigure'] parameters = synode.parameters() parameters.set_boolean( 'use_cdn', value=False, label='Use CDN', description='If unchecked, the "bokeh_js" component of the output ' 'will be the entire BokehJS front-end script. If checked, ' 'the "bokeh_js" component will instead contain a ' 'reference to the same script online.') inputs = Ports([ Port.Custom('bokeh', 'Bokeh figure', name='figure')]) outputs = Ports([ Port.Custom('{text}', 'Bokeh figure components', name='components')]) def execute(self, node_context): use_cdn = node_context.parameters['use_cdn'].value figure = node_context.input['figure'] output = node_context.output['components'] script, div = components(figure.get_figure()) output['plot_div'] = text.Text.from_str(div) output['plot_script'] = text.Text.from_str(script) output['bokeh_js'] = text.Text.from_str(sybokeh.bokeh_js_tag(use_cdn))