Source code for webviz_plotly

import jinja2
from os import path
from webviz import JSONPageElement
from abc import ABCMeta, abstractmethod
import pandas as pd
from six import iteritems
import warnings

env = jinja2.Environment(
    loader=jinja2.PackageLoader('webviz_plotly', 'templates'),
    trim_blocks=True,
    lstrip_blocks=True,
    undefined=jinja2.StrictUndefined
)


[docs]class Plotly(JSONPageElement): """ Plotly page element. Arguments are the same as ``plotly.plot()`` from `plotly.js`. See https://plot.ly/javascript/ for usage. :param xaxis: Will create a label for the x-axis. :param yaxis: Will create a label for the y-axis. :param logx: boolean value to toggle x-axis logarithmic scale. :param logy: boolean value to toggle y-axis logarithmic scale. :param xrange: list of minimum and maximum value. Ex: [3, 15]. :param yrange: list of minimum and maximum value. Ex: [3, 15]. .. note:: :class:`Plotly` will not allow the modebarbuttons in :const:`DISALLOWED_BUTTONS`, as these are not useful for the visualizations implemented in webviz. """ DISALLOWED_BUTTONS = ['sendDataToCloud', 'resetScale2d'] def __init__(self, data, layout={}, config={}, **kwargs): super(Plotly, self).__init__() config['responsive'] = True if 'displaylogo' not in config: config['displaylogo'] = False if 'modeBarButtonsToRemove' not in config: config['modeBarButtonsToRemove'] = Plotly.DISALLOWED_BUTTONS else: need_to_add_buttons = ( button for button in Plotly.DISALLOWED_BUTTONS if button not in config['modeBarButtonsToRemove'] ) for button in need_to_add_buttons: config['modeBarButtonsToRemove'].append(button) warnings.warn( '{} is required in modeBarButtonsToRemove.'.format(button), Warning ) self['data'] = data self['config'] = config.copy() self['layout'] = layout.copy() self.handle_args(**kwargs) self.add_js_file(path.join( path.dirname(__file__), 'resources', 'js', 'plotly.js'))
[docs] def handle_args( self, title=None, xrange=None, yrange=None, xaxis=None, yaxis=None, logx=False, logy=False): if title: self['layout']['title'] = title if (xrange or xaxis or logx) and ('xaxis' not in self['layout']): self['layout']['xaxis'] = {} if (yrange or yaxis or logy) and ('yaxis' not in self['layout']): self['layout']['yaxis'] = {} if xrange: self['layout']['xaxis']['range'] = xrange if yrange: self['layout']['yaxis']['range'] = yrange if xaxis: self['layout']['xaxis']['title'] = xaxis if yaxis: self['layout']['yaxis']['title'] = yaxis if logx: self['layout']['xaxis']['type'] = 'log' if logy: self['layout']['yaxis']['type'] = 'log'
[docs] def add_annotation(self, **kwargs): if 'annotations' not in self['layout']: self['layout']['annotations'] = [] self['layout']['annotations'].append(dict(**kwargs))
[docs] def get_template(self): """ Overrides :meth:`webviz.PageElement.get_template`. """ return env.get_template('plotly.html')
[docs]class FilteredPlotly(Plotly): """ Page Element for adding filtering controls to Plotly plots that take a dataframe. Values are grouped by labels, for instance: :: index,value,labels 01-01-2020,3,A 02-01-2020,4,B If 'labels' is chosen as a dropdown_column, then the value 4 will be chosen if the dropdown menu is set to the label B, and the value 3 will be chosen if the dropdown is set to A. The :py:meth:`FilteredPlotly.process_data` handles the generation of the plot data. For the example above, it is given the following dataframes: :: index,value 01-01-2020,3 and :: index,value, 02-01-2020,4 Layout and config is then generated that insert the required controls. :param data: A dataframe, or list of dataframes, that can be processed by `process_data`. Each dataframe will be grouped based on check_box_columns and given as a parameter list to process data. A special label, FilteredPlotly.wildcard ('*' by default), signifies that the data should be present in all groups. If a dataframe does not contain a column it is treated as if all rows have the wildcard label. :param check_box_columns: Columns in the dataframes that contain labels to be filtered on by check boxes. :param slider_columns: Columns in the dataframe that contain labels to be filtered on by a slider. :param dropdown_columns: Columns in the dataframe that contain labels to be filtered on by a dropdown menu. """ __metaclass__ = ABCMeta wildcard = '*'
[docs] def names_match(self, filters, names1, names2): if len(filters) == 1: return (names1 == self.wildcard or names2 == self.wildcard or names1 == names2) else: return all( name1 == self.wildcard or name2 == self.wildcard or name1 == name2 for name1, name2 in zip(names1, names2))
def __init__( self, data, check_box_columns=[], slider_columns=[], dropdown_columns=[], *args, **kwargs): self.data = [] _data = [] if isinstance(data, list): _data = data else: _data = [data] for frame in _data: if isinstance(frame, pd.DataFrame): self.data.append(frame.copy()) else: _frame = pd.read_csv(frame) if 'index' in _frame.columns: _frame.set_index( _frame['index'], inplace=True) del _frame['index'] self.data.append(_frame) filtered_data = [] self.labels = {} filters = (check_box_columns + slider_columns + dropdown_columns) for frame in self.data: for filt in filters: if filt not in frame.columns: frame[filt] = self.wildcard if filters: ordered = {} for frame in self.data: for names, group in frame.groupby(filters): if names not in ordered: ordered[names] = [] ordered[names].append(group.drop(filters, axis=1)) filled = {} for names, group in iteritems(ordered): if ((len(filters) == 1 and self.wildcard != names) or (len(filters) != 1 and self.wildcard not in names)): if names not in filled: filled[names] = [] filled[names].extend(group) for names, group in iteritems(ordered): if ((len(filters) == 1 and self.wildcard == names) or (len(filters) != 1 and self.wildcard in names)): for filled_names, filled_group in iteritems(filled): if self.names_match(filters, filled_names, names): filled_group.extend(group) self.labels = {} for names, group in iteritems(filled): processed = self.process_data( *group ) for point in processed: point['labels'] = {} iter_names = names if len(filters) > 1 else [names] for key, label in zip(filters, iter_names): if key not in self.labels: self.labels[key] = [] point['labels'][key] = label if label not in self.labels[key]: self.labels[key].append(label) filtered_data.extend(processed) else: processed = self.process_data(*self.data) for data in processed: data['labels'] = {} filtered_data.extend(processed) super(FilteredPlotly, self).__init__( filtered_data, *args, **kwargs) self.add_js_file(path.join( path.dirname(__file__), 'resources', 'js', 'filtered_plotly.js')) self['check_box_filters'] = [str(label) for label in check_box_columns] self.labels = {key: [str(label) for label in keylabels] for key, keylabels in iteritems(self.labels)} self['labels'] = self.labels self['slider_filters'] = {key: self.labels[key] for key in slider_columns[:]} self['dropdown_filters'] = {key: self.labels[key] for key in dropdown_columns[:]}
[docs] def get_template(self): """ overrides :py:meth:`webviz.PageElement.get_template`. """ return env.get_template('filtered_plotly.html')
[docs] @abstractmethod def process_data(self, *datas): """ :returns: List of traces to be used a data for the Plotly Page Element. """ pass