Source code for masci_tools.vis.bokeh_plotter

###############################################################################
# Copyright (c), Forschungszentrum Jülich GmbH, IAS-1/PGI-1, Germany.         #
#                All rights reserved.                                         #
# This file is part of the Masci-tools package.                               #
# (Material science tools)                                                    #
#                                                                             #
# The code is hosted on GitHub at https://github.com/judftteam/masci-tools    #
# For further information on the license, see the LICENSE.txt file            #
# For further information please visit http://www.flapw.de or                 #
#                                                                             #
###############################################################################
"""
Here the :py:class:`masci_tools.vis.parameters.Plotter` subclass for the bokeh plotting backend
is defined with default values and many helper methods
"""
from .parameters import Plotter, _generate_plot_parameters_table
import copy


[docs]class BokehPlotter(Plotter): """ Class for plotting parameters and standard code snippets for plotting with the bokeh backend. Kwargs in the __init__ method are forwarded to setting default values for the instance For specific documentation about the parameter/defaults handling refer to :py:class:`~masci_tools.vis.parameters.Plotter`. Below the current defined default values are shown: """ _BOKEH_DEFAULTS = { 'figure_kwargs': { 'tools': 'pan,poly_select,tap,wheel_zoom,' 'box_zoom,redo,undo,reset,save,crosshair,zoom_out,zoom_in', 'y_axis_type': 'linear', 'x_axis_type': 'linear', 'active_inspect': None, 'toolbar_location': 'right', }, 'additional_tools': None, 'show_tooltips': True, 'global_tooltips': False, 'format_tooltips': True, 'tooltips': [('X', '@{x}'), ('Y', '@{y}')], 'additional_tooltips': None, 'axis_linewidth': 2, 'label_fontsize': '18pt', 'tick_label_fontsize': '16pt', 'background_fill_color': '#ffffff', 'x_axis_formatter': None, 'y_axis_formatter': None, 'x_ticks': None, 'x_ticklabels_overwrite': None, 'y_ticks': None, 'y_ticklabels_overwrite': None, 'x_range_padding': None, 'y_range_padding': None, 'limits': None, #legend options 'legend_location': 'top_right', 'legend_click_policy': 'hide', # "mute"#"hide" 'legend_orientation': 'vertical', 'legend_font_size': '14pt', 'legend_outside_plot_area': False, #plot parameters 'color_palette': None, 'color': None, 'legend_label': None, 'alpha': 1.0, 'name': None, 'line_color': None, 'line_alpha': 1.0, 'line_dash': None, 'line_width': 2.0, 'marker': 'circle', 'marker_size': 6, 'area_plot': False, 'area_vertical': False, 'fill_alpha': 1.0, 'fill_color': None, 'level': None, 'straight_lines': None, 'straight_line_options': { 'line_color': 'black', 'line_width': 1.0, 'line_dash': 'dashed' }, 'text_font_size': '10pt', 'text_font_style': 'normal', 'text_color': 'black', 'text_align': 'left', 'text_baseline': 'middle', #output control 'save_plots': False, 'save_format': 'html', 'show': True, } _BOKEH_DESCRIPTIONS = { 'figure_kwargs': 'Parameters for creating the bokeh figure. ' 'Includes things like axis type (x and y), tools ' 'plot width/height', 'additional_tools': 'tools to add to the tools already ' 'specified in ``figure_kwargs`` (Has to be in the same format)', 'show_tooltips': 'Switch whether to add hover tooltips', 'global_tooltips': 'Switch whether to add individual (for each renderer) or global hover tooltips. ', 'format_tooltips': 'Switch whether to enable the processing of formatted strings in tooltips. ', 'tooltips': 'List of tuples specifying the tooltips. ' 'For more information refer to the bokeh documentation. ' "Strings can contain format specifiers with the data keys of the function e.g. ``'@{x}'``. " 'Here the ``{x}`` will be replaced by the entry for x. If there are formatting specifications ' 'for bokeh they need to be escaped with double curly braces or ``format_tooltips=False``.', 'additional_tooltips': 'Tooltips to add to the already defined ``tooltips`` (See above)', 'axis_linewidth': 'Linewidth for the lines of the axis', 'label_fontsize': 'Fontsize for the labels of the axis', 'tick_label_fontsize': 'fontsize for the ticks on the axis', 'background_fill_color': 'Color of the background of the plot', 'x_axis_formatter': 'If set this formatter will be used for the ticks on the x-axis', 'y_axis_formatter': 'If set this formatter will be used for the ticks on the y-axis', 'x_ticks': 'Tick specification for the x-axis', 'x_ticklabels_overwrite': 'Overrides the labels for the ticks on the x-axis', 'y_ticks': 'Tick specification for the y-axis', 'y_ticklabels_overwrite': 'Overrides the labels for the ticks on the y-axis', 'x_range_padding': 'Specifies the amount of padding on the edges of the x-axis', 'y_range_padding': 'Specifies the amount of padding on the edges of the y-axis', 'limits': "Dict specifying the limits of the axis, e.g {'x': (-5,5)}", #legend options 'legend_location': 'Location of the legend inside the plot area', 'legend_click_policy': 'Policy for what happens when labels are clicked in the legend', 'legend_orientation': 'Orientation of the legend', 'legend_font_size': 'Fontsize for the labels inside the legend', 'legend_outside_plot_area': 'If True the legend will be placed outside of the plot area', #plot parameters 'color_palette': 'Color palette to use for the plot(s)', 'color': 'Specific colors to use for the plot(s)', 'legend_label': 'Labels to use for the legend of the plot(s)', 'alpha': 'Transparency to use for the plot(s)', 'name': 'Name used for identifying elements in the plot (not shown only internally)', 'line_color': 'Color to use for line plot(s)', 'line_alpha': 'Transparency to use for line plot(s)', 'line_dash': 'Dash styles to use for line plot(s)', 'line_width': 'Line width to use for line plot(s)', 'marker': 'Type of marker to use for scatter plot(s)', 'marker_size': 'Marker size to use for scatter plot(s)', 'area_plot': 'If True h(v)area will be used to produce the plot(s)', 'area_vertical': 'Determines, whether to use harea (False) or varea (True) for area plots', 'fill_alpha': 'Transparency to use for the area in area plot(s)', 'fill_color': 'Color to use for the area in area plot(s)', 'level': 'Can be used to specified, which elements are fore- or background', 'straight_lines': 'Dict specifying straight help-lines to draw. ' "For example {'vertical': 0, 'horizontal': [-1,1]} will draw a vertical line at 0 " 'and two horizontal at -1 and 1', 'straight_line_options': 'Color, width, and more options for the help-lines', 'text_font_size': 'fontsize for the text glyphs in the plot', 'text_font_style': 'fontstyle for the text glyphs in the plot', 'text_color': 'text color for the text glyphs in the plot', 'text_align': 'text alignment for the text glyphs in the plot', 'text_baseline': 'text baseline for the text glyphs in the plot', #output control 'save_plots': 'If True plots will be saved to file (Configuration beforehand is needed)', 'save_format': 'Formats to save the plots to, can be single or list of formats (html, png or svg)', 'show': 'If True bokeh.io.show will be called after the plotting routine', } _BOKEH_GENERAL_ARGS = { 'show', 'save_plots', 'save_format', 'color_palette', 'legend_location', 'legend_click_policy', 'legend_font_size', 'legend_orientation', 'legend_outside_plot_area', 'background_fill_color', 'tick_label_fontsize', 'label_fontsize', 'axis_linewidth', 'figure_kwargs', 'additional_tools', 'show_tooltips', 'global_tooltips', 'format_tooltips', 'tooltips', 'additional_tooltips', 'straight_lines', 'x_axis_formatter', 'y_axis_formatter', 'x_ticks', 'y_ticks', 'y_ticklabels_overwrite', 'x_ticklabels_overwrite', 'x_range_padding', 'y_range_padding', } _DEFAULT_KWARGS = {'color', 'alpha', 'legend_label', 'name'} _TYPE_TO_KWARGS = { 'default': _DEFAULT_KWARGS, 'line': _DEFAULT_KWARGS | {'line_color', 'line_alpha', 'line_dash', 'line_width'}, 'scatter': _DEFAULT_KWARGS | {'marker', 'marker_size', 'fill_alpha', 'fill_color'}, 'area': _DEFAULT_KWARGS | {'fill_alpha', 'fill_color'}, 'image': _DEFAULT_KWARGS | {'color', 'alpha', 'color_palette'}, 'text': _DEFAULT_KWARGS | {'text_font_style', 'text_font_size', 'text_color', 'text_align', 'text_baseline'} } _POSTPROCESS_RENAMES = {'marker_size': 'size', 'color_palette': 'palette'} __doc__ = __doc__ + _generate_plot_parameters_table(_BOKEH_DEFAULTS, _BOKEH_DESCRIPTIONS) def __init__(self, **kwargs): super().__init__(self._BOKEH_DEFAULTS, general_keys=self._BOKEH_GENERAL_ARGS, key_descriptions=self._BOKEH_DESCRIPTIONS, type_kwargs_mapping=self._TYPE_TO_KWARGS, kwargs_postprocess_rename=self._POSTPROCESS_RENAMES, **kwargs)
[docs] def prepare_figure(self, title, xlabel, ylabel, figure=None): """ Create a bokeh figure according to the set parameters or modify an existing one :param title: title of the figure :param xlabel: label on the x-axis :param ylabel: label on the y-axis :param figure: bokeh figure, optional, if given the operations are performed on the object otherwise a new figure is created :returns: the created or modified bokeh figure """ from bokeh.plotting import figure as bokeh_fig from bokeh.models import Title if self['additional_tools'] is not None: if isinstance(self['additional_tools'], str): default = '' else: default = [] tools = self['figure_kwargs'].get('tools', default) if isinstance(tools, list): if not isinstance(self['additional_tools'], list): raise ValueError('Got tools and additional_tools in different formats.' 'Expected list for additional_tools') tools = tools + self['additional_tools'] else: if not isinstance(self['additional_tools'], str): raise ValueError('Got tools and additional_tools in different formats.' 'Expected string for additional_tools') tools = f"{tools},{self['additional_tools']}" figure_kwargs = {**self['figure_kwargs'], **{'tools': tools}} else: figure_kwargs = self['figure_kwargs'] if figure is None: p = bokeh_fig(**figure_kwargs) else: p = figure if self['background_fill_color'] is not None: p.background_fill_color = self['background_fill_color'] if self['global_tooltips']: self.add_tooltips(p, 'auto', toggleable=True) if title is not None: p.title = Title(text=title) if xlabel is not None: p.xaxis.axis_label = xlabel if ylabel is not None: p.yaxis.axis_label = ylabel p.yaxis.axis_line_width = self['axis_linewidth'] p.xaxis.axis_line_width = self['axis_linewidth'] p.xaxis.axis_label_text_font_size = self['label_fontsize'] p.yaxis.axis_label_text_font_size = self['label_fontsize'] p.yaxis.major_label_text_font_size = self['tick_label_fontsize'] p.xaxis.major_label_text_font_size = self['tick_label_fontsize'] if self['y_axis_formatter'] is not None: p.yaxis.formatter = self['y_axis_formatter'] if self['x_axis_formatter'] is not None: p.xaxis.formatter = self['x_axis_formatter'] if self['x_ticks'] is not None: p.xaxis.ticker = self['x_ticks'] if self['y_ticks'] is not None: p.xaxis.ticker = self['y_ticks'] if self['x_ticklabels_overwrite'] is not None: p.xaxis.major_label_overrides = self['x_ticklabels_overwrite'] if self['y_ticklabels_overwrite'] is not None: p.yaxis.major_label_overrides = self['y_ticklabels_overwrite'] if self['x_range_padding'] is not None: p.x_range.range_padding = self['x_range_padding'] if self['y_range_padding'] is not None: p.y_range.range_padding = self['y_range_padding'] return p
[docs] def set_color_palette_by_num_plots(self): """ Set the colormap for the configured number of plots according to the set colormap or color copied from https://github.com/PatrikHlobil/Pandas-Bokeh/blob/master/pandas_bokeh/plot.py credits to PatrikHlobil modified for use in this Plotter class """ from bokeh.palettes import all_palettes #pylint: disable=no-name-in-module if self['color'] is not None: color = self['color'] if not isinstance(self['color'], (list, tuple)): color = [color] color = color * int(self.num_plots / len(color) + 1) color = color[:self.num_plots] elif self['color_palette'] is not None: if self['color_palette'] in all_palettes: color = all_palettes[self['color_palette']] max_key = max(color.keys()) if self.num_plots <= max_key: color = color[self.num_plots] else: color = color[max_key] color = color * int(self.num_plots / len(color) + 1) color = color[:self.num_plots] else: raise ValueError( f"Could not find <colormap> with name {self['color_palette']}. The following predefined colormaps are " f'supported (see also https://bokeh.pydata.org/en/latest/docs/reference/palettes.html ): {list(all_palettes.keys())}' ) else: if self.num_plots <= 10: color = all_palettes['Category10'][10][:self.num_plots] elif self.num_plots <= 20: color = all_palettes['Category20'][self.num_plots] else: color = all_palettes['Category20'][20] * int(self.num_plots / 20 + 1) color = color[:self.num_plots] self['color'] = color
@staticmethod def _format_tooltips(tooltips, **kwargs): """ Format the strings in tooltips with the given kwargs. :param tooltips: The tooltips list to be formatted :param kwargs: The keywords arguments used to format the strings :returns: list of tuples with the tooltips ready to be used for tooltips """ import string for indx, (label, value) in enumerate(tooltips): if len([val[0] for val in string.Formatter().parse(label)]) != 0: label = label.format(**kwargs) if len([val[0] for val in string.Formatter().parse(value)]) != 0: value = value.format(**kwargs) tooltips[indx] = (label, value) return tooltips
[docs] def add_tooltips(self, fig, renderers, columns=None, toggleable=False): """ Add Hover tooltips to the given renderers and figure :param fig: bokeh figure to apply changes to :param renderers: bokeh renderers to activate the tooltips for :param columns: namedtuple containing the data keys used for evtl. formatting :param toggleable: bool, if True these tooltips will be toggleable in the toolbar """ from bokeh.models import HoverTool if self['show_tooltips']: if self['global_tooltips'] and renderers != 'auto': return if columns is not None: kwargs = columns._asdict() else: kwargs = {} if not isinstance(renderers, list) and renderers != 'auto': renderers = [renderers] tooltips = self['tooltips'] if tooltips is None: tooltips = [] additional_tooltips = self['additional_tooltips'] if additional_tooltips is None: additional_tooltips = [] tooltips = tooltips.copy() + additional_tooltips.copy() if self['format_tooltips']: final_tooltips = self._format_tooltips(tooltips, **kwargs) else: final_tooltips = tooltips #We make them not toggleable for now since it would lead to multiple #hover icons in the toolbar, which is kind of annoying hover = HoverTool(tooltips=final_tooltips, toggleable=toggleable, renderers=renderers) fig.add_tools(hover) active_inspect = fig.toolbar.active_inspect if active_inspect is None: active_inspect = [] active_inspect.append(hover) fig.toolbar.active_inspect = active_inspect
[docs] def draw_straight_lines(self, fig): """ Draw horizontal and vertical lines specified in the lines argument :param fig: bokeh figure on which to perform the operation """ from bokeh.models import Span if self['straight_lines'] is not None: added_lines = [] if 'horizontal' in self['straight_lines']: lines = copy.deepcopy(self['straight_lines']['horizontal']) if not isinstance(lines, list): lines = [lines] for line_def in lines: options = copy.deepcopy(self['straight_line_options']) if isinstance(line_def, dict): positions = line_def.pop('pos') if not isinstance(positions, list): positions = [positions] options.update(line_def) elif isinstance(line_def, list): positions = line_def else: positions = [line_def] for pos in positions: added_lines.append(Span(location=pos, dimension='width', **options)) if 'vertical' in self['straight_lines']: lines = copy.deepcopy(self['straight_lines']['vertical']) if not isinstance(lines, list): lines = [lines] for line_def in lines: options = copy.deepcopy(self['straight_line_options']) if isinstance(line_def, dict): positions = line_def.pop('pos') if not isinstance(positions, list): positions = [positions] options.update(line_def) elif isinstance(line_def, list): positions = line_def else: positions = [line_def] for pos in positions: added_lines.append(Span(location=pos, dimension='height', **options)) fig.renderers.extend(added_lines)
[docs] def set_limits(self, fig): """ Set limits of the figure :param fig: bokeh figure on which to perform the operation """ from bokeh.models import Range1d if self['limits'] is not None: if 'x' in self['limits']: xmin = self['limits']['x'][0] xmax = self['limits']['x'][1] fig.x_range = Range1d(xmin, xmax) if 'y' in self['limits']: ymin = self['limits']['y'][0] ymax = self['limits']['y'][1] fig.y_range = Range1d(ymin, ymax)
[docs] def set_legend(self, fig): """ Set legend options for the figure :param fig: bokeh figure on which to perform the operation """ fig.legend.location = self['legend_location'] fig.legend.background_fill_color = self['background_fill_color'] fig.legend.click_policy = self['legend_click_policy'] fig.legend.orientation = self['legend_orientation'] fig.legend.label_text_font_size = self['legend_font_size'] if self['legend_outside_plot_area']: fig.add_layout(fig.legend[0], 'right')
[docs] def save_plot(self, figure, saveas): """ Show/save the bokeh figure :param figure: bokeh figure on which to perform the operation """ from bokeh.io import show, save, export_png, export_svgs if self['show']: show(figure) if self['save_plots']: if isinstance(self['save_format'], list): formats = self['save_format'] else: formats = [self['save_format']] for fmt in formats: savefilename = f'{saveas}.{fmt}' print(f'Save plot to: {savefilename}') if fmt == 'html': save(figure, filename=savefilename) elif fmt == 'png': export_png(figure, filename=savefilename) elif fmt == 'svg': export_svgs(figure, filename=savefilename)