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))