class histogram(Operation): """ Returns a Histogram of the input element data, binned into num_bins over the bin_range (if specified) along the specified dimension. """ bin_range = param.NumericTuple(default=None, length=2, doc=""" Specifies the range within which to compute the bins.""") bins = param.ClassSelector(default=None, class_=(np.ndarray, list, tuple, str), doc=""" An explicit set of bin edges or a method to find the optimal set of bin edges, e.g. 'auto', 'fd', 'scott' etc. For more documentation on these approaches see the np.histogram_bin_edges documentation.""") cumulative = param.Boolean(default=False, doc=""" Whether to compute the cumulative histogram""") dimension = param.String(default=None, doc=""" Along which dimension of the Element to compute the histogram.""") frequency_label = param.String(default=None, doc=""" Format string defining the label of the frequency dimension of the Histogram.""" ) groupby = param.ClassSelector(default=None, class_=(basestring, Dimension), doc=""" Defines a dimension to group the Histogram returning an NdOverlay of Histograms.""" ) log = param.Boolean(default=False, doc=""" Whether to use base 10 logarithmic samples for the bin edges.""") mean_weighted = param.Boolean(default=False, doc=""" Whether the weighted frequencies are averaged.""") normed = param.ObjectSelector(default=False, objects=[True, False, 'integral', 'height'], doc=""" Controls normalization behavior. If `True` or `'integral'`, then `density=True` is passed to np.histogram, and the distribution is normalized such that the integral is unity. If `False`, then the frequencies will be raw counts. If `'height'`, then the frequencies are normalized such that the max bin height is unity.""") nonzero = param.Boolean(default=False, doc=""" Whether to use only nonzero values when computing the histogram""") num_bins = param.Integer(default=20, doc=""" Number of bins in the histogram .""") weight_dimension = param.String(default=None, doc=""" Name of the dimension the weighting should be drawn from""") style_prefix = param.String(default=None, allow_None=None, doc=""" Used for setting a common style for histograms in a HoloMap or AdjointLayout.""" ) def _process(self, element, key=None): if self.p.groupby: if not isinstance(element, Dataset): raise ValueError( 'Cannot use histogram groupby on non-Dataset Element') grouped = element.groupby(self.p.groupby, group_type=Dataset, container_type=NdOverlay) self.p.groupby = None return grouped.map(self._process, Dataset) normed = False if self.p.mean_weighted and self.p.weight_dimension else self.p.normed if self.p.dimension: selected_dim = self.p.dimension else: selected_dim = [d.name for d in element.vdims + element.kdims][0] dim = element.get_dimension(selected_dim) if hasattr(element, 'interface'): data = element.interface.values(element, selected_dim, compute=False) else: data = element.dimension_values(selected_dim) is_datetime = isdatetime(data) if is_datetime: data = data.astype('datetime64[ns]').astype('int64') # Handle different datatypes is_finite = isfinite is_cupy = is_cupy_array(data) if is_cupy: import cupy full_cupy_support = LooseVersion(cupy.__version__) > '8.0' if not full_cupy_support and (normed or self.p.weight_dimension): data = cupy.asnumpy(data) is_cupy = False else: is_finite = cupy.isfinite # Mask data if is_ibis_expr(data): mask = data.notnull() if self.p.nonzero: mask = mask & (data != 0) data = data.to_projection() data = data[mask] no_data = not len(data.head(1).execute()) data = data[dim.name] else: mask = is_finite(data) if self.p.nonzero: mask = mask & (data != 0) data = data[mask] da = dask_array_module() no_data = False if da and isinstance(data, da.Array) else not len(data) # Compute weights if self.p.weight_dimension: if hasattr(element, 'interface'): weights = element.interface.values(element, self.p.weight_dimension, compute=False) else: weights = element.dimension_values(self.p.weight_dimension) weights = weights[mask] else: weights = None # Compute bins if isinstance(self.p.bins, str): bin_data = cupy.asnumpy(data) if is_cupy else data edges = np.histogram_bin_edges(bin_data, bins=self.p.bins) elif isinstance(self.p.bins, (list, np.ndarray)): edges = self.p.bins if isdatetime(edges): edges = edges.astype('datetime64[ns]').astype('int64') else: hist_range = self.p.bin_range or element.range(selected_dim) # Avoids range issues including zero bin range and empty bins if hist_range == (0, 0) or any(not isfinite(r) for r in hist_range): hist_range = (0, 1) steps = self.p.num_bins + 1 start, end = hist_range if is_datetime: start, end = dt_to_int(start, 'ns'), dt_to_int(end, 'ns') if self.p.log: bin_min = max([abs(start), data[data > 0].min()]) edges = np.logspace(np.log10(bin_min), np.log10(end), steps) else: edges = np.linspace(start, end, steps) if is_cupy: edges = cupy.asarray(edges) if not is_dask_array(data) and no_data: nbins = self.p.num_bins if self.p.bins is None else len( self.p.bins) - 1 hist = np.zeros(nbins) elif hasattr(element, 'interface'): density = True if normed else False hist, edges = element.interface.histogram(data, edges, density=density, weights=weights) if normed == 'height': hist /= hist.max() if self.p.weight_dimension and self.p.mean_weighted: hist_mean, _ = element.interface.histogram(data, density=False, bins=edges) hist /= hist_mean elif normed: # This covers True, 'height', 'integral' hist, edges = np.histogram(data, density=True, weights=weights, bins=edges) if normed == 'height': hist /= hist.max() else: hist, edges = np.histogram(data, normed=normed, weights=weights, bins=edges) if self.p.weight_dimension and self.p.mean_weighted: hist_mean, _ = np.histogram(data, density=False, bins=self.p.num_bins) hist /= hist_mean hist[np.isnan(hist)] = 0 if is_datetime: edges = (edges / 1e3).astype('datetime64[us]') params = {} if self.p.weight_dimension: params['vdims'] = [element.get_dimension(self.p.weight_dimension)] elif self.p.frequency_label: label = self.p.frequency_label.format(dim=dim.pprint_label) params['vdims'] = [Dimension('Frequency', label=label)] else: label = 'Frequency' if normed else 'Count' params['vdims'] = [ Dimension('{0}_{1}'.format(dim.name, label.lower()), label=label) ] if element.group != element.__class__.__name__: params['group'] = element.group if self.p.cumulative: hist = np.cumsum(hist) if self.p.normed in (True, 'integral'): hist *= edges[1] - edges[0] # Save off the computed bin edges so that if this operation instance # is used to compute another histogram, it will default to the same # bin edges. self.bins = list(edges) return Histogram((edges, hist), kdims=[element.get_dimension(selected_dim)], label=element.label, **params)
class RunBatchCommand(CommandTemplate): """ RunBatchCommand is designed to to format task specifications into Topographica run_batch tasks in a way that should be flexible enough to be used generally. Note that Topographica is invoked with the -a flag so all of topo.command is imported. Though some of the parameters required appear to duplicate those in run_batch, they are necessary to ensure some basic consistency in the use of run_batch between tasks. This CommandTemplate class constrains -all- the options for run_batch with the exception of 'times' which is free to vary as part of the task specification. This is allowed as you may wish to change the times at which the analysis function is invoked across batches. This command is queuable with both the queue and specify methods implemented. As necessary, specifications are generated in the 'specifications' subdirectory of the root directory. """ topo_switches = param.List(default=['-a'], doc=""" Specifies the Topographica qsub switches (flags without arguments) as a list of strings. By default the -a switch is always used to auto import commands.""") topo_flag_options = param.Dict(default={}, doc=""" Specifies Topographica flags and their corresponding options as a dictionary. This parameter is suitable for setting -c and -p flags for Topographica (eg. to customize OpenMP settings). This parameter is also important to introduce an analysis callable into the namespace if the analysis function is set to 'custom'. Tuples can be used to indicate groups of options using the same flag: {'-p':'retina_density=5'} => -p retina_density=5 {'-p':('retina_density=5', 'scale=2') => -p retina_density=5 -p scale=2 {'-p':' If a plain Python dictionary is used, the keys are alphanumerically sorted, otherwise the dictionary is assumed to be an OrderedDict (Python 2.7+, Python3 or param.external.OrderedDict) and the key ordering will be preserved. Finally note that the '-' is added to the key if missing (to make into a valid flag) to allow you to specify keywords in a dict constructor ie. dict(key1=value1, key2=value2,....)""") analysis = param.ObjectSelector( default='default', objects=['default', 'RunBatchAnalysis', 'custom'], constant=True, doc=""" The type of analysis to use with run_batch. The options are 'default' which runs the default run_batch analysis function, 'RunBatchAnalysis' which use the more sophisticated picklable analysis function and custom which requires the analysis callable to be created in the run_batch namespace via a -c option with topo_flag_options""") analysis_arguments = param.List(default=[], doc=''' The specifier keys to be consumed as analysis function arguments''') name_time_format = param.String(default='%Y%m%d%H%M', doc=""" The timestamp format for directories created by run_batch in python datetime format.""") max_name_length = param.Number( default=200, doc="Equivalent to the run_batch parameter of same name.") snapshot = param.Boolean( default=True, doc="Equivalent to the run_batch parameter of same name.") vc_info = param.Boolean( default=True, doc="Equivalent to the run_batch parameter of same name.") save_global_params = param.Boolean( default=True, doc="Equivalent to the run_batch parameter of same name.") param_formatter = param.Callable(param_formatter.instance(), doc=""" If None, defaults to normal run_batch formatting.""") def __init__(self, tyfile, analysis='default', **kwargs): if (analysis == 'custom') and ('-c' not in self.topo_flag_options): raise Exception, 'Please use -c option to introduce custom analysis to namespace!' executable = os.path.abspath(sys.argv[0]) super(RunBatchCommand, self).__init__(analysis=analysis, executable=executable, **kwargs) self.tyfile = os.path.abspath(tyfile) assert os.path.exists( self.tyfile), "Tyfile doesn't exist! Cannot proceed." self._prelude = [] if self.analysis == 'RunBatchAnalysis': self._prelude = ['from topo.misc.lancext import RunBatchAnalysis'] if self.analysis == 'default': self._prelude = ["analysis = default_analysis_function"] def topo_args(self, switch_override=[]): """ Method to generate Popen style argument list for Topographica using the topo_switches and topo_flag_options parameters. Switches are returned first, sorted alphanumerically. The qsub_flag_options follow in keys() ordered if not a vanilla Python dictionary (ie. a Python 2.7+ or param.external OrderedDict). Otherwise the keys are sorted alphanumerically.""" opt_dict = type(self.topo_flag_options)() opt_dict.update(self.topo_flag_options) if type(self.topo_flag_options ) == dict: # Alphanumeric sort if vanilla Python dictionary ordered_options = [(k, opt_dict[k]) for k in sorted(opt_dict)] else: ordered_options = list(opt_dict.items()) # Unpack tuple values so flag:(v1, v2,...)) => ..., flag:v1, flag:v2, ... unpacked_groups = [[(k, v) for v in val] if type(val) == tuple else [(k, val)] for (k, val) in ordered_options] unpacked_kvs = [el for group in unpacked_groups for el in group] # Adds '-' if missing (eg, keywords in dict constructor) and flattens lists. ordered_pairs = [(k, v) if (k[0] == '-') else ('-%s' % (k), v) for (k, v) in unpacked_kvs] ordered_options = [[k] + ([v] if type(v) == str else v) for (k, v) in ordered_pairs] flattened_options = [el for kvs in ordered_options for el in kvs] switches = [ s for s in switch_override if (s not in self.topo_switches) ] + self.topo_switches return sorted(switches) + flattened_options def queue(self, tid, info): ''' Uses load_kwargs helper function in topo.command.misc to get the run_batch settings from the specifications directory. ''' spec_path = os.path.join(info['root_directory'], 'specifications') spec_file_path = os.path.join(spec_path, 't%s.spec' % tid) prelude = self._prelude[:] prelude += [ "kwargs = load_kwargs('%s',globals(),locals())" % spec_file_path ] if self.analysis == 'RunBatchAnalysis': prelude += [ "analysis=RunBatchAnalysis.load(%s)" % repr(info['root_directory']) ] prelude += ["analysis.current_tid = %s" % tid] prelude += ["analysis.analysis_kwargs = dict(kwargs)"] run_batch_cmd = "run_batch('%s',**kwargs)" % self.tyfile topo_args = self.topo_args(['-a']) return [self.executable ] + topo_args + ['-c', ';'.join(prelude + [run_batch_cmd])] def specify(self, spec, tid, info): ''' Writes the specification as a Python dictionary to file (to the specifications subdirectory of root directory) as required by the load_kwargs helper method in topo.misc.command.''' spec_path = os.path.join(info['root_directory'], 'specifications') spec_file_path = os.path.join(spec_path, 't%d.spec' % tid) if not os.path.exists(spec_path): os.mkdir(spec_path) spec_file = open(spec_file_path, 'w') kwarg_opts = self._run_batch_kwargs(spec, tid, info) allopts = dict( spec, **kwarg_opts) # Override run_batch keys if (mistakenly) provided. keywords = ',\n'.join([ '%s=%s' % (k, allopts[k]) for k in sorted(kwarg_opts.keys()) + sorted(spec.keys()) ]) spec_file.write('dict(\n%s\n)' % keywords) spec_file.close() def _run_batch_kwargs(self, spec, tid, info): ''' Defines the keywords accepted by run_batch and so specifies run_batch behaviour. ''' options = { 'name_time_format': repr(self.name_time_format), 'max_name_length': self.max_name_length, 'snapshot': self.snapshot, 'vc_info': self.vc_info, 'save_global_params': self.save_global_params } if info['batch_tag'] == '': dirname_prefix = info['batch_name'] else: dirname_prefix = info['batch_name'] + ('[%s]' % info['batch_tag']) # Settings using information from launcher derived_options = { 'output_directory': repr(info['root_directory']), 'dirname_prefix': repr(dirname_prefix), 'tag': repr('t%s_' % tid) } # Use fixed timestamp argument to run_batch if available. if info['timestamp'] is not None: derived_options['timestamp'] = info['timestamp'] derived_options['analysis_fn'] = 'analysis' # Use the specified param_formatter (if desired) to create lambda in run_batch if self.param_formatter is not None: dir_format = self.param_formatter(info['constant_keys'], info['varying_keys'], spec) derived_options['dirname_params_filter'] = 'lambda p: %s' % repr( dir_format) return dict(options.items() + derived_options.items()) def __call__(self, spec, tid=None, info={}): """ Returns a Popen argument list to invoke Topographica and execute run_batch with all options appropriately set (in alphabetical order). Keywords that are not run_batch options are also in alphabetical order at the end of the keyword list""" kwarg_opts = self._run_batch_kwargs(spec, tid, info) allopts = dict( spec, **kwarg_opts) # Override spec values if mistakenly included. prelude = self._prelude[:] if self.analysis == 'RunBatchAnalysis': prelude += [ "analysis=RunBatchAnalysis.load(%s)" % repr(info['root_directory']) ] prelude += ["analysis.current_tid = %s" % tid] analysis_kwargs = [ "'%s':%s" % (k, v) for (k, v) in allopts.items() if k in self.analysis_arguments ] prelude += [ "analysis.analysis_kwargs = {%s}" % ', '.join(analysis_kwargs) ] keywords = ', '.join([ '%s=%s' % (k, allopts[k]) for k in # Was %s sorted(kwarg_opts.keys()) + sorted(spec.keys()) ]) run_batch_list = prelude + [ "run_batch(%s,%s)" % (repr(self.tyfile), keywords) ] topo_args = self.topo_args(['-a']) return [self.executable ] + topo_args + ['-c', '; '.join(run_batch_list)]
class Test(param.Parameterized): a = param.ObjectSelector(default='b', objects=[1, 'b', 'c'])
class PlotlyRenderer(Renderer): backend = param.String(default='plotly', doc="The backend name.") fig = param.ObjectSelector(default='auto', objects=['html', 'json', 'auto'], doc=""" Output render format for static figures. If None, no figure rendering will occur. """) mode_formats = { 'fig': { 'default': ['html', 'json'] }, 'holomap': { 'default': ['widgets', 'scrubber', 'auto'] } } widgets = { 'scrubber': PlotlyScrubberWidget, 'widgets': PlotlySelectionWidget } backend_dependencies = {'js': (get_plotlyjs(), )} comm_msg_handler = plotly_msg_handler _loaded = False def __call__(self, obj, fmt='html', divuuid=None): plot, fmt = self._validate(obj, fmt) mime_types = {'file-ext': fmt, 'mime_type': MIME_TYPES[fmt]} if isinstance(plot, tuple(self.widgets.values())): return plot(), mime_types elif fmt == 'html': return self._figure_data(plot, divuuid=divuuid), mime_types elif fmt == 'json': return self.diff(plot), mime_types def diff(self, plot, serialize=True): """ Returns a json diff required to update an existing plot with the latest plot data. """ diff = plot.state.to_plotly_json() if serialize: return json.dumps(diff, cls=utils.PlotlyJSONEncoder) else: return diff def _figure_data(self, plot, fmt=None, divuuid=None, comm=True, as_script=False, width=800, height=600): figure = plot.state if divuuid is None: divuuid = plot.id jdata = json.dumps(figure.data, cls=utils.PlotlyJSONEncoder) jlayout = json.dumps(figure.layout, cls=utils.PlotlyJSONEncoder) config = {} config['showLink'] = False jconfig = json.dumps(config) if as_script: header = 'window.PLOTLYENV=window.PLOTLYENV || {};' else: header = ('<script type="text/javascript">' 'window.PLOTLYENV=window.PLOTLYENV || {};' '</script>') script = '\n'.join([ 'var plotly = window._Plotly || window.Plotly;' 'plotly.plot("{id}", {data}, {layout}, {config}).then(function() {{', ' var elem = document.getElementById("{id}.loading"); elem.parentNode.removeChild(elem);', '}})' ]).format(id=divuuid, data=jdata, layout=jlayout, config=jconfig) html = ('<div id="{id}.loading" style="color: rgb(50,50,50);">' 'Drawing...</div>' '<div id="{id}" style="height: {height}; width: {width};" ' 'class="plotly-graph-div">' '</div>'.format(id=divuuid, height=height, width=width)) if as_script: return html, header + script content = ('{html}' '<script type="text/javascript">' ' {script}' '</script>').format(html=html, script=script) return '\n'.join([header, content]) @classmethod def plot_options(cls, obj, percent_size): factor = percent_size / 100.0 obj = obj.last if isinstance(obj, HoloMap) else obj plot = Store.registry[cls.backend].get(type(obj), None) options = plot.lookup_options(obj, 'plot').options width = options.get('width', plot.width) * factor height = options.get('height', plot.height) * factor return dict(options, **{'width': int(width), 'height': int(height)}) @classmethod def load_nb(cls, inline=True): """ Loads the plotly notebook resources. """ from IPython.display import display, HTML, publish_display_data if not cls._loaded: display(HTML(PLOTLY_WARNING)) cls._loaded = True init_notebook_mode(connected=not inline) publish_display_data(data={MIME_TYPES['jlab-hv-load']: get_plotlyjs()})
'tabs': [ { 'tabTitle':'Tab 1', 'tabDesc': 'A description of the tabbed section.', 'tables': [ { 'tableTitle':'Table 1', 'tableDesc':'A description of table 1.', 'tableLayout':[['A','B','C'],['D','E','F']], 'tableParams':{'A':param.String('A')} }, { 'tableTitle':'Table 2', 'tableDesc':'A description of table 1.', 'tableLayout':[['A'],['J','K','L']], 'tableParams':{'A':param.ObjectSelector(default='Asia', objects=['Africa', 'Asia', 'Europe'])} } ] }, { 'tabTitle':'Tab 2', 'tabDesc': 'A description of the second tabbed section.', 'tables': [ { 'tableTitle':'Table 3', 'tableDesc':'A description of table 1.', 'tableLayout':[['A','B','C'],['D','E','F']], 'tableParams':{'A':param.String('A')} }, { 'tableTitle':'Table 4',
class PointPlot(LegendPlot, ColorbarPlot): color_index = param.ClassSelector(default=None, class_=(basestring, int), allow_None=True, doc=""" Index of the dimension from which the color will the drawn""") size_index = param.ClassSelector(default=None, class_=(basestring, int), allow_None=True, doc=""" Index of the dimension from which the sizes will the drawn.""") scaling_method = param.ObjectSelector(default="area", objects=["width", "area"], doc=""" Determines whether the `scaling_factor` should be applied to the width or area of each point (default: "area").""") scaling_factor = param.Number(default=1, bounds=(0, None), doc=""" Scaling factor which is applied to either the width or area of each point, depending on the value of `scaling_method`.""") size_fn = param.Callable(default=np.abs, doc=""" Function applied to size values before applying scaling, to remove values lower than zero.""") style_opts = (['cmap', 'palette', 'marker', 'size'] + line_properties + fill_properties) _plot_methods = dict(single='scatter', batched='scatter') _batched_style_opts = line_properties + fill_properties + ['size'] def _get_size_data(self, element, ranges, style): data, mapping = {}, {} sdim = element.get_dimension(self.size_index) if sdim: map_key = 'size_' + sdim.name ms = style.get('size', np.sqrt(6))**2 sizes = element.dimension_values(self.size_index) sizes = compute_sizes(sizes, self.size_fn, self.scaling_factor, self.scaling_method, ms) if sizes is None: eltype = type(element).__name__ self.warning('%s dimension is not numeric, cannot ' 'use to scale %s size.' % (sdim.pprint_label, eltype)) else: data[map_key] = np.sqrt(sizes) mapping['size'] = map_key return data, mapping def get_data(self, element, ranges=None, empty=False): style = self.style[self.cyclic_index] dims = element.dimensions(label=True) xidx, yidx = (1, 0) if self.invert_axes else (0, 1) mapping = dict(x=dims[xidx], y=dims[yidx]) data = {} xdim, ydim = dims[xidx], dims[yidx] data[xdim] = [] if empty else element.dimension_values(xidx) data[ydim] = [] if empty else element.dimension_values(yidx) self._categorize_data(data, (xdim, ydim), element.dimensions()) cdata, cmapping = self._get_color_data(element, ranges, style) data.update(cdata) mapping.update(cmapping) sdata, smapping = self._get_size_data(element, ranges, style) data.update(sdata) mapping.update(smapping) self._get_hover_data(data, element, empty) return data, mapping def get_batched_data(self, element, ranges=None, empty=False): data = defaultdict(list) zorders = self._updated_zorders(element) styles = self.lookup_options(element.last, 'style') styles = styles.max_cycles(len(self.ordering)) for (key, el), zorder in zip(element.data.items(), zorders): self.set_param(**self.lookup_options(el, 'plot').options) eldata, elmapping = self.get_data(el, ranges, empty) for k, eld in eldata.items(): data[k].append(eld) # Apply static styles nvals = len(list(eldata.values())[0]) style = styles[zorder] sdata, smapping = expand_batched_style(style, self._batched_style_opts, elmapping, nvals) elmapping.update(smapping) for k, v in sdata.items(): data[k].append(v) if any(isinstance(t, HoverTool) for t in self.state.tools): for dim, k in zip(element.dimensions(), key): sanitized = dimension_sanitizer(dim.name) data[sanitized].append([k] * nvals) data = {k: np.concatenate(v) for k, v in data.items()} return data, elmapping
class ElementPlot(PlotlyPlot, GenericElementPlot): aspect = param.Parameter(default='cube', doc=""" The aspect ratio mode of the plot. By default, a plot may select its own appropriate aspect ratio but sometimes it may be necessary to force a square aspect ratio (e.g. to display the plot as an element of a grid). The modes 'auto' and 'equal' correspond to the axis modes of the same name in matplotlib, a numeric value may also be passed.""") bgcolor = param.ClassSelector(class_=(str, tuple), default=None, doc=""" If set bgcolor overrides the background color of the axis.""") invert_axes = param.ObjectSelector(default=False, doc=""" Inverts the axes of the plot. Note that this parameter may not always be respected by all plots but should be respected by adjoined plots when appropriate.""") invert_xaxis = param.Boolean(default=False, doc=""" Whether to invert the plot x-axis.""") invert_yaxis = param.Boolean(default=False, doc=""" Whether to invert the plot y-axis.""") invert_zaxis = param.Boolean(default=False, doc=""" Whether to invert the plot z-axis.""") labelled = param.List(default=['x', 'y', 'z'], doc=""" Whether to label the 'x' and 'y' axes.""") logx = param.Boolean(default=False, doc=""" Whether to apply log scaling to the x-axis of the Chart.""") logy = param.Boolean(default=False, doc=""" Whether to apply log scaling to the y-axis of the Chart.""") logz = param.Boolean(default=False, doc=""" Whether to apply log scaling to the y-axis of the Chart.""") margins = param.NumericTuple(default=(50, 50, 50, 50), doc=""" Margins in pixel values specified as a tuple of the form (left, bottom, right, top).""") show_legend = param.Boolean(default=False, doc=""" Whether to show legend for the plot.""") xaxis = param.ObjectSelector( default='bottom', objects=['top', 'bottom', 'bare', 'top-bare', 'bottom-bare', None], doc=""" Whether and where to display the xaxis, bare options allow suppressing all axis labels including ticks and xlabel. Valid options are 'top', 'bottom', 'bare', 'top-bare' and 'bottom-bare'.""") xticks = param.Parameter(default=None, doc=""" Ticks along x-axis specified as an integer, explicit list of tick locations, list of tuples containing the locations.""") yaxis = param.ObjectSelector( default='left', objects=['left', 'right', 'bare', 'left-bare', 'right-bare', None], doc=""" Whether and where to display the yaxis, bare options allow suppressing all axis labels including ticks and ylabel. Valid options are 'left', 'right', 'bare' 'left-bare' and 'right-bare'.""") yticks = param.Parameter(default=None, doc=""" Ticks along y-axis specified as an integer, explicit list of tick locations, list of tuples containing the locations.""") zlabel = param.String(default=None, doc=""" An explicit override of the z-axis label, if set takes precedence over the dimension label.""") zticks = param.Parameter(default=None, doc=""" Ticks along z-axis specified as an integer, explicit list of tick locations, list of tuples containing the locations.""") trace_kwargs = {} _style_key = None # Whether vectorized styles are applied per trace _per_trace = False # Declare which styles cannot be mapped to a non-scalar dimension _nonvectorized_styles = [] def initialize_plot(self, ranges=None): """ Initializes a new plot object with the last available frame. """ # Get element key and ranges for frame fig = self.generate_plot(self.keys[-1], ranges) self.drawn = True return fig def generate_plot(self, key, ranges, element=None): if element is None: element = self._get_frame(key) if element is None: return self.handles['fig'] # Set plot options plot_opts = self.lookup_options(element, 'plot').options self.param.set_param( **{k: v for k, v in plot_opts.items() if k in self.params()}) # Get ranges ranges = self.compute_ranges(self.hmap, key, ranges) ranges = util.match_spec(element, ranges) # Get style self.style = self.lookup_options(element, 'style') style = self.style[self.cyclic_index] # Get data and options and merge them data = self.get_data(element, ranges, style) opts = self.graph_options(element, ranges, style) graphs = [] for i, d in enumerate(data): # Initialize traces traces = self.init_graph(d, opts, index=i) graphs.extend(traces) self.handles['graphs'] = graphs # Initialize layout layout = self.init_layout(key, element, ranges) self.handles['layout'] = layout # Create figure and return it self.drawn = True fig = dict(data=graphs, layout=layout) self.handles['fig'] = fig return fig def graph_options(self, element, ranges, style): if self.overlay_dims: legend = ', '.join([ d.pprint_value_string(v) for d, v in self.overlay_dims.items() ]) else: legend = element.label opts = dict(name=legend, **self.trace_kwargs) if self.trace_kwargs.get('type', None) in legend_trace_types: opts.update(showlegend=self.show_legend, legendgroup=element.group) if self._style_key is not None: styles = self._apply_transforms(element, ranges, style) opts[self._style_key] = { STYLE_ALIASES.get(k, k): v for k, v in styles.items() } else: opts.update({ STYLE_ALIASES.get(k, k): v for k, v in style.items() if k != 'cmap' }) return opts def init_graph(self, data, options, index=0): trace = dict(options) for k, v in data.items(): if k in trace and isinstance(trace[k], dict): trace[k].update(v) else: trace[k] = v if self._style_key and self._per_trace: vectorized = { k: v for k, v in options[self._style_key].items() if isinstance(v, np.ndarray) } trace[self._style_key] = dict(trace[self._style_key]) for s, val in vectorized.items(): trace[self._style_key][s] = val[index] return [trace] def get_data(self, element, ranges, style): return [] def get_aspect(self, xspan, yspan): """ Computes the aspect ratio of the plot """ return self.width / self.height def _get_axis_dims(self, element): """Returns the dimensions corresponding to each axis. Should return a list of dimensions or list of lists of dimensions, which will be formatted to label the axis and to link axes. """ dims = element.dimensions()[:3] pad = [None] * max(3 - len(dims), 0) return dims + pad def _apply_transforms(self, element, ranges, style): new_style = dict(style) for k, v in dict(style).items(): if isinstance(v, util.basestring): if k == 'marker' and v in 'xsdo': continue elif v in element: v = dim(v) elif any(d == v for d in self.overlay_dims): v = dim([d for d in self.overlay_dims if d == v][0]) if not isinstance(v, dim): continue elif (not v.applies(element) and v.dimension not in self.overlay_dims): new_style.pop(k) self.warning( 'Specified %s dim transform %r could not be applied, as not all ' 'dimensions could be resolved.' % (k, v)) continue if len(v.ops) == 0 and v.dimension in self.overlay_dims: val = self.overlay_dims[v.dimension] else: val = v.apply(element, ranges=ranges, flat=True) if (not util.isscalar(val) and len(util.unique_array(val)) == 1 and not 'color' in k): val = val[0] if not util.isscalar(val): if k in self._nonvectorized_styles: element = type(element).__name__ raise ValueError( 'Mapping a dimension to the "{style}" ' 'style option is not supported by the ' '{element} element using the {backend} ' 'backend. To map the "{dim}" dimension ' 'to the {style} use a groupby operation ' 'to overlay your data along the dimension.'.format( style=k, dim=v.dimension, element=element, backend=self.renderer.backend)) # If color is not valid colorspec add colormapper numeric = isinstance(val, np.ndarray) and val.dtype.kind in 'uifMm' if ('color' in k and isinstance(val, np.ndarray) and numeric): copts = self.get_color_opts(v, element, ranges, style) new_style.pop('cmap', None) new_style.update(copts) new_style[k] = val return new_style def init_layout(self, key, element, ranges): el = element.traverse(lambda x: x, [Element]) el = el[0] if el else element extent = self.get_extents(element, ranges) if len(extent) == 4: l, b, r, t = extent else: l, b, z0, r, t, z1 = extent options = {} dims = self._get_axis_dims(el) if len(dims) > 2: xdim, ydim, zdim = dims else: xdim, ydim = dims zdim = None xlabel, ylabel, zlabel = self._get_axis_labels(dims) if self.invert_axes: xlabel, ylabel = ylabel, xlabel ydim, xdim = xdim, ydim l, b, r, t = b, l, t, r if 'x' not in self.labelled: xlabel = '' if 'y' not in self.labelled: ylabel = '' if 'z' not in self.labelled: zlabel = '' if xdim: xrange = [r, l] if self.invert_xaxis else [l, r] xaxis = dict(range=xrange, title=xlabel) if self.logx: xaxis['type'] = 'log' self._get_ticks(xaxis, self.xticks) else: xaxis = {} if ydim: yrange = [t, b] if self.invert_yaxis else [b, t] yaxis = dict(range=yrange, title=ylabel) if self.logy: yaxis['type'] = 'log' self._get_ticks(yaxis, self.yticks) else: yaxis = {} if self.projection == '3d': scene = dict(xaxis=xaxis, yaxis=yaxis) if zdim: zrange = [z1, z0] if self.invert_zaxis else [z0, z1] zaxis = dict(range=zrange, title=zlabel) if self.logz: zaxis['type'] = 'log' self._get_ticks(zaxis, self.zticks) scene['zaxis'] = zaxis if self.aspect == 'cube': scene['aspectmode'] = 'cube' else: scene['aspectmode'] = 'manual' scene['aspectratio'] = self.aspect options['scene'] = scene else: l, b, r, t = self.margins options['xaxis'] = xaxis options['yaxis'] = yaxis options['margin'] = dict(l=l, r=r, b=b, t=t, pad=4) return dict(width=self.width, height=self.height, title=self._format_title(key, separator=' '), plot_bgcolor=self.bgcolor, **options) def _get_ticks(self, axis, ticker): axis_props = {} if isinstance(ticker, (tuple, list)): if all(isinstance(t, tuple) for t in ticker): ticks, labels = zip(*ticker) labels = [ l if isinstance(l, util.basestring) else str(l) for l in labels ] axis_props['tickvals'] = ticks axis_props['ticktext'] = labels else: axis_props['tickvals'] = ticker axis.update(axis_props) def update_frame(self, key, ranges=None, element=None): """ Updates an existing plot with data corresponding to the key. """ self.generate_plot(key, ranges, element)
class stack(Operation): """ The stack operation allows compositing multiple RGB Elements using the defined compositing operator. """ compositor = param.ObjectSelector( objects=['add', 'over', 'saturate', 'source'], default='over', doc=""" Defines how the compositing operation combines the images""") def uint8_to_uint32(self, element): img = np.dstack( [element.dimension_values(d, flat=False) for d in element.vdims]) if img.shape[2] == 3: # alpha channel not included alpha = np.ones(img.shape[:2]) if img.dtype.name == 'uint8': alpha = (alpha * 255).astype('uint8') img = np.dstack([img, alpha]) if img.dtype.name != 'uint8': img = (img * 255).astype(np.uint8) N, M, _ = img.shape return img.view(dtype=np.uint32).reshape((N, M)) def _process(self, overlay, key=None): if not isinstance(overlay, CompositeOverlay): return overlay elif len(overlay) == 1: return overlay.last if isinstance(overlay, NdOverlay) else overlay.get(0) imgs = [] for rgb in overlay: if not isinstance(rgb, RGB): raise TypeError('stack operation expect RGB type elements, ' 'not %s name.' % type(rgb).__name__) rgb = rgb.rgb dims = [kd.name for kd in rgb.kdims][::-1] coords = { kd.name: rgb.dimension_values(kd, False) for kd in rgb.kdims } imgs.append( tf.Image(self.uint8_to_uint32(rgb), coords=coords, dims=dims)) try: imgs = xr.align(*imgs, join='exact') except ValueError: raise ValueError( 'RGB inputs to stack operation could not be aligned, ' 'ensure they share the same grid sampling.') stacked = tf.stack(*imgs, how=self.p.compositor) arr = shade.uint32_to_uint8(stacked.data)[::-1] data = (coords[dims[1]], coords[dims[0]], arr[:, :, 0], arr[:, :, 1], arr[:, :, 2]) if arr.shape[-1] == 4: data = data + (arr[:, :, 3], ) return rgb.clone(data, datatype=[rgb.interface.datatype] + rgb.datatype)
class dynspread(Operation): """ Spreading expands each pixel in an Image based Element a certain number of pixels on all sides according to a given shape, merging pixels using a specified compositing operator. This can be useful to make sparse plots more visible. Dynamic spreading determines how many pixels to spread based on a density heuristic. See the datashader documentation for more detail: http://datashader.readthedocs.io/en/latest/api.html#datashader.transfer_functions.dynspread """ how = param.ObjectSelector(default='source', objects=['source', 'over', 'saturate', 'add'], doc=""" The name of the compositing operator to use when combining pixels.""") max_px = param.Integer(default=3, doc=""" Maximum number of pixels to spread on all sides.""") shape = param.ObjectSelector(default='circle', objects=['circle', 'square'], doc=""" The shape to spread by. Options are 'circle' [default] or 'square'.""") threshold = param.Number(default=0.5, bounds=(0, 1), doc=""" When spreading, determines how far to spread. Spreading starts at 1 pixel, and stops when the fraction of adjacent non-empty pixels reaches this threshold. Higher values give more spreading, up to the max_px allowed.""") link_inputs = param.Boolean(default=True, doc=""" By default, the link_inputs parameter is set to True so that when applying dynspread, backends that support linked streams update RangeXY streams on the inputs of the dynspread operation. Disable when you do not want the resulting plot to be interactive, e.g. when trying to display an interactive plot a second time.""") @classmethod def uint8_to_uint32(cls, img): shape = img.shape flat_shape = np.multiply.reduce(shape[:2]) rgb = img.reshape((flat_shape, 4)).view('uint32').reshape(shape[:2]) return rgb def _apply_dynspread(self, array): img = tf.Image(array) return tf.dynspread(img, max_px=self.p.max_px, threshold=self.p.threshold, how=self.p.how, shape=self.p.shape).data def _process(self, element, key=None): if not isinstance(element, RGB): raise ValueError('dynspread can only be applied to RGB Elements.') rgb = element.rgb new_data = { kd.name: rgb.dimension_values(kd, expanded=False) for kd in rgb.kdims } rgbarray = np.dstack( [element.dimension_values(vd, flat=False) for vd in element.vdims]) data = self.uint8_to_uint32(rgbarray) array = self._apply_dynspread(data) img = datashade.uint32_to_uint8(array) for i, vd in enumerate(element.vdims): if i < img.shape[-1]: new_data[vd.name] = np.flipud(img[..., i]) return element.clone(new_data)
class trimesh_rasterize(aggregate): """ Rasterize the TriMesh element using the supplied aggregator. If the TriMesh nodes or edges define a value dimension will plot filled and shaded polygons otherwise returns a wiremesh of the data. """ aggregator = param.ClassSelector(class_=ds.reductions.Reduction, default=None) interpolation = param.ObjectSelector(default='bilinear', objects=['bilinear', None], doc=""" The interpolation method to apply during rasterization.""") def _precompute(self, element): from datashader.utils import mesh if element.vdims: simplices = element.dframe([0, 1, 2, 3]) verts = element.nodes.dframe([0, 1]) elif element.nodes.vdims: simplices = element.dframe([0, 1, 2]) verts = element.nodes.dframe([0, 1, 3]) return { 'mesh': mesh(verts, simplices), 'simplices': simplices, 'vertices': verts } def _process(self, element, key=None): if isinstance(element, TriMesh): x, y = element.nodes.kdims[:2] else: x, y = element.kdims info = self._get_sampling(element, x, y) (x_range, y_range), _, (width, height), (xtype, ytype) = info cvs = ds.Canvas(plot_width=width, plot_height=height, x_range=x_range, y_range=y_range) if not (element.vdims or element.nodes.vdims): return aggregate._process(self, element, key) elif element._plot_id in self._precomputed: precomputed = self._precomputed[element._plot_id] else: precomputed = self._precompute(element) simplices = precomputed['simplices'] pts = precomputed['vertices'] mesh = precomputed['mesh'] if self.p.precompute: self._precomputed = {element._plot_id: precomputed} vdim = element.vdims[0] if element.vdims else element.nodes.vdims[0] interpolate = bool(self.p.interpolation) agg = cvs.trimesh(pts, simplices, agg=self.p.aggregator, interp=interpolate, mesh=mesh) params = dict(get_param_values(element), kdims=[x, y], datatype=['xarray'], vdims=[vdim]) return Image(agg, **params)
class rasterize(ResamplingOperation): """ Rasterize is a high-level operation which will rasterize any Element or combination of Elements aggregating it with the supplied aggregator and interpolation method. The default aggregation method depends on the type of Element but usually defaults to the count of samples in each bin, other aggregators can be supplied implementing mean, max, min and other reduction operations. The bins of the aggregate are defined by the width and height and the x_range and y_range. If x_sampling or y_sampling are supplied the operation will ensure that a bin is no smaller than the minimum sampling distance by reducing the width and height when zoomed in beyond the minimum sampling distance. By default, the PlotSize and RangeXY streams are applied when this operation is used dynamically, which means that the width, height, x_range and y_range will automatically be set to match the inner dimensions of the linked plot and the ranges of the axes. """ aggregator = param.ClassSelector(class_=ds.reductions.Reduction, default=None) interpolation = param.ObjectSelector(default='bilinear', objects=['bilinear', None], doc=""" The interpolation method to apply during rasterization.""") def _process(self, element, key=None): # Get input Images to avoid multiple rasterization imgs = element.traverse(lambda x: x, [Image]) # Rasterize TriMeshes tri_params = dict( {k: v for k, v in self.p.items() if k in aggregate.params()}, dynamic=False) trirasterize = trimesh_rasterize.instance(**tri_params) trirasterize._precomputed = self._precomputed element = element.map(trirasterize, TriMesh) self._precomputed = trirasterize._precomputed # Rasterize QuadMesh quad_params = dict( {k: v for k, v in self.p.items() if k in aggregate.params()}, dynamic=False) quadrasterize = quadmesh_rasterize.instance(**quad_params) quadrasterize._precomputed = self._precomputed element = element.map(quadrasterize, QuadMesh) self._precomputed = quadrasterize._precomputed # Rasterize NdOverlay of objects agg_params = dict( {k: v for k, v in self.p.items() if k in aggregate.params()}, dynamic=False) dsrasterize = aggregate.instance(**agg_params) dsrasterize._precomputed = self._precomputed predicate = lambda x: (isinstance(x, NdOverlay) and issubclass( x.type, Dataset) and not issubclass(x.type, Image)) element = element.map(dsrasterize, predicate) # Rasterize other Dataset types predicate = lambda x: (isinstance(x, Dataset) and (not isinstance(x, Image) or x in imgs)) element = element.map(dsrasterize, predicate) self._precomputed = dsrasterize._precomputed return element
class regrid(ResamplingOperation): """ regrid allows resampling a HoloViews Image type using specified up- and downsampling functions defined using the aggregator and interpolation parameters respectively. By default upsampling is disabled to avoid unnecessarily upscaling an image that has to be sent to the browser. Also disables expanding the image beyond its original bounds avoiding unneccessarily padding the output array with nan values. """ aggregator = param.ObjectSelector( default='mean', objects=['first', 'last', 'mean', 'mode', 'std', 'var', 'min', 'max'], doc=""" Aggregation method. """) expand = param.Boolean(default=False, doc=""" Whether the x_range and y_range should be allowed to expand beyond the extent of the data. Setting this value to True is useful for the case where you want to ensure a certain size of output grid, e.g. if you are doing masking or other arithmetic on the grids. A value of False ensures that the grid is only just as large as it needs to be to contain the data, which will be faster and use less memory if the resulting aggregate is being overlaid on a much larger background.""") interpolation = param.ObjectSelector(default='nearest', objects=['linear', 'nearest'], doc=""" Interpolation method""") upsample = param.Boolean(default=False, doc=""" Whether to allow upsampling if the source array is smaller than the requested array. Setting this value to True will enable upsampling using the interpolation method, when the requested width and height are larger than what is available on the source grid. If upsampling is disabled (the default) the width and height are clipped to what is available on the source array.""") def _process(self, element, key=None): if ds_version <= '0.5.0': raise RuntimeError('regrid operation requires datashader>=0.6.0') x, y = element.kdims info = self._get_sampling(element, x, y) (x_range, y_range), _, (width, height), (xtype, ytype) = info coords = tuple( element.dimension_values(d, expanded=False) for d in [x, y]) coord_dict = {x.name: coords[0], y.name: coords[1]} dims = [y.name, x.name] arrays = [] for vd in element.vdims: if element.interface is XArrayInterface: xarr = element.data[vd.name] if 'datetime' in (xtype, ytype): xarr = xarr.copy() if dims != xarr.dims: xarr = xarr.transpose(*dims) else: arr = element.dimension_values(vd, flat=False) xarr = xr.DataArray(arr, coords=coord_dict, dims=dims) if xtype == "datetime": xarr[x.name] = [dt_to_int(v) for v in xarr[x.name].values] if ytype == "datetime": xarr[y.name] = [dt_to_int(v) for v in xarr[y.name].values] arrays.append(xarr) # Disable upsampling if requested (xstart, xend), (ystart, yend) = (x_range, y_range) xspan, yspan = (xend - xstart), (yend - ystart) if not self.p.upsample and self.p.target is None: (x0, x1), (y0, y1) = element.range(0), element.range(1) if isinstance(x0, datetime_types): x0, x1 = dt_to_int(x0), dt_to_int(x1) if isinstance(y0, datetime_types): y0, y1 = dt_to_int(y0), dt_to_int(y1) exspan, eyspan = (x1 - x0), (y1 - y0) width = min([int((xspan / exspan) * len(coords[0])), width]) height = min([int((yspan / eyspan) * len(coords[1])), height]) # Get expanded or bounded ranges cvs = ds.Canvas(plot_width=width, plot_height=height, x_range=x_range, y_range=y_range) regridded = [] for xarr in arrays: rarray = cvs.raster(xarr, upsample_method=self.p.interpolation, downsample_method=self.p.aggregator) if xtype == "datetime": rarray[x.name] = rarray[x.name].astype('datetime64[us]') if ytype == "datetime": rarray[y.name] = rarray[y.name].astype('datetime64[us]') regridded.append(rarray) regridded = xr.Dataset( {vd.name: xarr for vd, xarr in zip(element.vdims, regridded)}) if xtype == 'datetime': xstart, xend = np.array([xstart, xend]).astype('datetime64[us]') if ytype == 'datetime': ystart, yend = np.array([ystart, yend]).astype('datetime64[us]') bbox = BoundingBox(points=[(xstart, ystart), (xend, yend)]) return element.clone(regridded, bounds=bbox, datatype=['xarray'])
class TestPattern(SheetPanel): sheet_type = GeneratorSheet dock = param.Boolean(False) edit_sheet = param.ObjectSelector(doc=""" Sheet for which to edit pattern properties.""") plastic = param.Boolean(default=False, doc=""" Whether to enable plasticity during presentation.""") duration = param.Number(default=1.0, softbounds=(0.0, 10.0), doc=""" How long to run the simulation for each presentation.""") Present = tk.Button(doc="""Present this pattern to the simulation.""") pattern_generator = param.ClassSelector(default=Constant(), class_=PatternGenerator, doc=""" Type of pattern to present. Each type has various parameters that can be changed.""" ) def __init__(self, master, plotgroup=None, **params): plotgroup = plotgroup or TestPatternPlotGroup() super(TestPattern, self).__init__(master, plotgroup, **params) self.auto_refresh = True self.plotcommand_frame.pack_forget() for name in ['pre_plot_hooks', 'plot_hooks', 'Fwd', 'Back']: self.hide_param(name) edit_sheet_param = self.get_parameter_object('edit_sheet') edit_sheet_param.objects = self.plotgroup.sheets() self.pg_control_pane = Frame(self) #,bd=1,relief="sunken") self.pg_control_pane.pack(side="top", expand='yes', fill='x') self.params_frame = tk.ParametersFrame( self.pg_control_pane, parameterized_object=self.pattern_generator, on_modify=self.conditional_refresh, msg_handler=master.status) self.params_frame.hide_param('Close') self.params_frame.hide_param('Refresh') # CEB: 'new_default=True' is temporary so that the current # behavior is the same as before; shoudl make None the # default and mean 'apply to all sheets'. self.pack_param('edit_sheet', parent=self.pg_control_pane, on_modify=self.switch_sheet, widget_options={ 'new_default': True, 'sort_fn_args': { 'cmp': lambda x, y: cmp(-x.precedence, -y.precedence) } }) self.pack_param('pattern_generator', parent=self.pg_control_pane, on_modify=self.change_pattern_generator, side="top") present_frame = Frame(self) present_frame.pack(side='bottom') self.pack_param('plastic', side='bottom', parent=present_frame) self.params_frame.pack(side='bottom', expand='yes', fill='x') self.pack_param('duration', parent=present_frame, side='left') self.pack_param('Present', parent=present_frame, on_set=self.present_pattern, side="right") def setup_plotgroup(self): super(TestPattern, self).setup_plotgroup() # CB: could copy the sheets instead (deleting connections etc) self.plotgroup._sheets = [ GeneratorSheet(name=gs.name, nominal_bounds=gs.nominal_bounds, nominal_density=gs.nominal_density) for gs in topo.sim.objects(GeneratorSheet).values() ] self.plotgroup._set_name("Test Pattern") def switch_sheet(self): if self.edit_sheet is not None: self.pattern_generator = self.edit_sheet.input_generator self.change_pattern_generator() def change_pattern_generator(self): """ Set the current PatternGenerator to the one selected and get the ParametersFrameWithApply to draw the relevant widgets """ # CEBALERT: if pattern generator is set to None, there will be # an error. Need to handle None in the appropriate place # (presumably tk.py). self.params_frame.set_PO(self.pattern_generator) for sheet in self.plotgroup.sheets(): if sheet == self.edit_sheet: sheet.set_input_generator(self.pattern_generator) self.conditional_refresh() def refresh(self, update=True): """ Simply update the plots: skip all handling of history. """ self.refresh_plots(update) def present_pattern(self): """ Move the user created patterns into the GeneratorSheets, run for the specified length of time, then restore the original patterns. """ input_dict = dict([(sheet.name,sheet.input_generator) \ for sheet in self.plotgroup.sheets()]) pattern_present(inputs=input_dict, durations=[self.duration], plastic=self.plastic, overwrite_previous=False, install_sheetview=True, restore_state=True) topo.guimain.auto_refresh(update=False)
class interpolate_curve(Operation): """ Resamples a Curve using the defined interpolation method, e.g. to represent changes in y-values as steps. """ interpolation = param.ObjectSelector( objects=['steps-pre', 'steps-mid', 'steps-post', 'linear'], default='steps-mid', doc=""" Controls the transition point of the step along the x-axis.""") _per_element = True @classmethod def pts_to_prestep(cls, x, values): steps = np.zeros(2 * len(x) - 1) value_steps = tuple( np.empty(2 * len(x) - 1, dtype=v.dtype) for v in values) steps[0::2] = x steps[1::2] = steps[0:-2:2] val_arrays = [] for v, s in zip(values, value_steps): s[0::2] = v s[1::2] = s[2::2] val_arrays.append(s) return steps, tuple(val_arrays) @classmethod def pts_to_midstep(cls, x, values): steps = np.zeros(2 * len(x)) value_steps = tuple( np.empty(2 * len(x), dtype=v.dtype) for v in values) steps[1:-1:2] = steps[2::2] = x[:-1] + (x[1:] - x[:-1]) / 2 steps[0], steps[-1] = x[0], x[-1] val_arrays = [] for v, s in zip(values, value_steps): s[0::2] = v s[1::2] = s[0::2] val_arrays.append(s) return steps, tuple(val_arrays) @classmethod def pts_to_poststep(cls, x, values): steps = np.zeros(2 * len(x) - 1) value_steps = tuple( np.empty(2 * len(x) - 1, dtype=v.dtype) for v in values) steps[0::2] = x steps[1::2] = steps[2::2] val_arrays = [] for v, s in zip(values, value_steps): s[0::2] = v s[1::2] = s[0:-2:2] val_arrays.append(s) return steps, tuple(val_arrays) def _process_layer(self, element, key=None): INTERPOLATE_FUNCS = { 'steps-pre': self.pts_to_prestep, 'steps-mid': self.pts_to_midstep, 'steps-post': self.pts_to_poststep } if self.p.interpolation not in INTERPOLATE_FUNCS: return element x = element.dimension_values(0) is_datetime = isdatetime(x) if is_datetime: dt_type = 'datetime64[ns]' x = x.astype(dt_type) dvals = tuple( element.dimension_values(d) for d in element.dimensions()[1:]) xs, dvals = INTERPOLATE_FUNCS[self.p.interpolation](x, dvals) if is_datetime: xs = xs.astype(dt_type) return element.clone((xs, ) + dvals) def _process(self, element, key=None): return element.map(self._process_layer, Element)
class EchartsApp(param.Parameterized): """An Echarts app that showcases the Echart component""" title = param.String("Awesome Panel") plot_type = param.ObjectSelector("bar", objects=["bar", "scatter"], label="Plot Type") shirt = param.Integer(default=5, bounds=BOUNDS) cardign = param.Integer(default=20, bounds=BOUNDS) chiffon_shirt = param.Integer(default=36, bounds=BOUNDS) pants = param.Integer(default=10, bounds=BOUNDS) heels = param.Integer(default=10, bounds=BOUNDS) socks = param.Integer(default=20, bounds=BOUNDS) def __init__(self, **params): super().__init__(**params) self.plot = ECharts(height=500, min_width=200, sizing_mode="stretch_width") self._update_plot() @param.depends( "title", "plot_type", "shirt", "cardign", "chiffon_shirt", "pants", "heels", "socks", watch=True, ) def _update_plot(self): echart = { "title": { "text": self.title }, "tooltip": {}, "legend": { "data": ["Sales"] }, "xAxis": { "data": [ "shirt", "cardign", "chiffon shirt", "pants", "heels", "socks" ] }, "yAxis": {}, "series": [{ "name": "Sales", "type": self.plot_type, "data": [ self.shirt, self.cardign, self.chiffon_shirt, self.pants, self.heels, self.socks, ], "itemStyle": { "color": "#A01346" }, }], "responsive": True, } self.plot.object = echart def view(self) -> pn.Column: """Returns the view of the application Returns: pn.Column: The view of the app """ pn.config.sizing_mode = "stretch_width" pn.extension("echarts") top_app_bar = pn.Row( pn.pane.PNG( "https://echarts.apache.org/en/images/logo.png", sizing_mode="fixed", height=40, margin=(15, 0, 5, 25), embed=False, ), pn.layout.VSpacer(), "", background="lightgray", height=70, ) settings_pane = pn.Param( self, show_name=False, width=200, sizing_mode="fixed", background="rgb(245, 247, 250)", ) main = [ APPLICATION.intro_section(), pn.Column( top_app_bar, pn.layout.HSpacer(height=50), pn.Row(self.plot, settings_pane, sizing_mode="stretch_width"), ), ] return pn.template.FastListTemplate(title="Test ECharts", theme="default", main=main, theme_toggle=False)
class GlobalPowerPlantDatabaseApp(param.Parameterized): data = param.DataFrame(precedence=-1) opacity = param.Number(default=0.8, step=0.05, bounds=(0, 1)) pitch = param.Number(default=0, bounds=(0, 90)) zoom = param.Integer(default=1, bounds=(1, 22)) view_state = param.ObjectSelector(default=VIEW_STATES["World"], objects=VIEW_STATES) def __init__(self, nrows: Optional[int] = None, **params): if "data" not in params: if nrows: params["data"] = self._get_pp_data(nrows=nrows) else: params["data"] = self._get_pp_data() super(GlobalPowerPlantDatabaseApp, self).__init__(**params) self._view_state = pdk.ViewState( latitude=52.2323, longitude=-1.415, zoom=self.zoom, min_zoom=self.param.zoom.bounds[0], max_zoom=self.param.zoom.bounds[1], ) self._scatter = pdk.Layer( "ScatterplotLayer", data=self.data, get_position=["longitude", "latitude"], get_fill_color="[color_r, color_g, color_b, color_a]", get_radius="capacity_mw*10", pickable=True, opacity=self.opacity, filled=True, wireframe=True, ) self._deck = pdk.Deck( map_style="mapbox://styles/mapbox/light-v9", initial_view_state=self._view_state, layers=[self._scatter], tooltip=True, mapbox_key=MAPBOX_KEY, ) self.pane = pn.pane.DeckGL(self._deck, sizing_mode="stretch_width", height=700) self.param.watch(self._update, ["data", "opacity", "pitch", "zoom"]) @staticmethod def _get_pp_data(nrows: Optional[int] = None): pp_data = pd.read_csv(POWER_PLANT_PATH, nrows=nrows) pp_data["primary_fuel_color"] = pp_data.primary_fuel.map(FUEL_COLORS) pp_data["primary_fuel_color"] = pp_data["primary_fuel_color"].fillna( "gray") pp_data["color_r"] = pp_data["primary_fuel_color"].map(COLORS_R) pp_data["color_g"] = pp_data["primary_fuel_color"].map(COLORS_G) pp_data["color_b"] = pp_data["primary_fuel_color"].map(COLORS_B) pp_data["color_a"] = 140 # "name", "primary_fuel", "capacity_mw", pp_data = pp_data[[ "latitude", "longitude", "name", "capacity_mw", "color_r", "color_g", "color_b", "color_a", ]] return pp_data @pn.depends("pane.hover_state", "data") def _info_pane(self): index = self.pane.hover_state.get("index", -1) if index == -1: index = slice(0, 0) return self.data.iloc[index][["name", "capacity_mw"]] @pn.depends("view_state", watch=True) def _update_view_state_from_selection(self): self._view_state.latitude = self.view_state.latitude self._view_state.longitude = self.view_state.longitude self._view_state.zoom = self.view_state.zoom self.pane.param.trigger("object") print(self._view_state) @pn.depends("pane.view_State", watch=True) def _update(self): print("update") state = self.pane.view_state self._view_state.longitude = state["longitude"] self._view_state.latitude = state["latitude"] def _update2(self, event): print(event.name) if event.name == "data": self._scatter.data = self.data if event.name == "opacity": self._scatter.opacity = self.opacity if event.name == "zoom": self._view_state.zoom = self.zoom if event.name == "pitch": self._view_state.pitch = self.pitch self.pane.param.trigger("object") def _view_state_pane(self): return pn.Param( self, parameters=["view_state"], show_name=False, widgets={"view_state": pn.widgets.RadioButtonGroup}, default_layout=pn.Column, ) def _settings_pane(self): return pn.Param( self, parameters=["opacity", "pitch", "zoom"], show_name=False, widgets={"view_state": pn.widgets.RadioButtonGroup}, ) def view(self): # self._info_pane, does not work return pn.Row( pn.Column(self._view_state_pane, self.pane), pn.Column(self._settings_pane, width=300, sizing_mode="fixed"), )
class VectorFieldPlot(ColorbarPlot): arrow_heads = param.Boolean(default=True, doc=""" Whether or not to draw arrow heads.""") color_index = param.ClassSelector(default=None, class_=(basestring, int), allow_None=True, doc=""" Index of the dimension from which the color will the drawn""") size_index = param.ClassSelector(default=None, class_=(basestring, int), allow_None=True, doc=""" Index of the dimension from which the sizes will the drawn.""") normalize_lengths = param.Boolean(default=True, doc=""" Whether to normalize vector magnitudes automatically. If False, it will be assumed that the lengths have already been correctly normalized.""") pivot = param.ObjectSelector(default='mid', objects=['mid', 'tip', 'tail'], doc=""" The point around which the arrows should pivot valid options include 'mid', 'tip' and 'tail'.""") rescale_lengths = param.Boolean(default=True, doc=""" Whether the lengths will be rescaled to take into account the smallest non-zero distance between two vectors.""") style_opts = line_properties + ['scale', 'cmap'] _plot_methods = dict(single='segment') def _get_lengths(self, element, ranges): mag_dim = element.get_dimension(self.size_index) (x0, x1), (y0, y1) = (element.range(i) for i in range(2)) base_dist = get_min_distance(element) if mag_dim: magnitudes = element.dimension_values(mag_dim) _, max_magnitude = ranges[mag_dim.name] if self.normalize_lengths and max_magnitude != 0: magnitudes = magnitudes / max_magnitude if self.rescale_lengths: magnitudes *= base_dist else: magnitudes = np.ones(len(element)) if self.rescale_lengths: magnitudes *= base_dist return magnitudes def _glyph_properties(self, *args): properties = super(VectorFieldPlot, self)._glyph_properties(*args) properties.pop('scale', None) return properties def get_data(self, element, ranges=None, empty=False): style = self.style[self.cyclic_index] input_scale = style.pop('scale', 1.0) # Get x, y, angle, magnitude and color data xidx, yidx = (1, 0) if self.invert_axes else (0, 1) rads = element.dimension_values(2) lens = self._get_lengths(element, ranges) / input_scale cdim = element.get_dimension(self.color_index) cdata, cmapping = self._get_color_data(element, ranges, style, name='line_color') # Compute segments and arrowheads xs = element.dimension_values(xidx) ys = element.dimension_values(yidx) # Compute offset depending on pivot option xoffsets = np.cos(rads) * lens / 2. yoffsets = np.sin(rads) * lens / 2. if self.pivot == 'mid': nxoff, pxoff = xoffsets, xoffsets nyoff, pyoff = yoffsets, yoffsets elif self.pivot == 'tip': nxoff, pxoff = 0, xoffsets * 2 nyoff, pyoff = 0, yoffsets * 2 elif self.pivot == 'tail': nxoff, pxoff = xoffsets * 2, 0 nyoff, pyoff = yoffsets * 2, 0 x0s, x1s = (xs + nxoff, xs - pxoff) y0s, y1s = (ys + nyoff, ys - pyoff) if self.arrow_heads: arrow_len = (lens / 4.) xa1s = x0s - np.cos(rads + np.pi / 4) * arrow_len ya1s = y0s - np.sin(rads + np.pi / 4) * arrow_len xa2s = x0s - np.cos(rads - np.pi / 4) * arrow_len ya2s = y0s - np.sin(rads - np.pi / 4) * arrow_len x0s = np.concatenate([x0s, x0s, x0s]) x1s = np.concatenate([x1s, xa1s, xa2s]) y0s = np.concatenate([y0s, y0s, y0s]) y1s = np.concatenate([y1s, ya1s, ya2s]) if cdim: color = cdata.get(cdim.name) color = np.concatenate([color, color, color]) elif cdim: color = cdata.get(cdim.name) data = {'x0': x0s, 'x1': x1s, 'y0': y0s, 'y1': y1s} mapping = dict(x0='x0', x1='x1', y0='y0', y1='y1') if cdim: data[cdim.name] = color mapping.update(cmapping) return (data, mapping)
class bivariate_kde(Operation): """ Computes a 2D kernel density estimate (KDE) of the first two dimensions in the input data. Kernel density estimation is a non-parametric way to estimate the probability density function of a random variable. The KDE works by placing 2D Gaussian kernel at each sample with the supplied bandwidth. These kernels are then summed to produce the density estimate. By default a good bandwidth is determined using the bw_method but it may be overridden by an explicit value. """ contours = param.Boolean(default=True, doc=""" Whether to compute contours from the KDE, determines whether to return an Image or Contours/Polygons.""") bw_method = param.ObjectSelector(default='scott', objects=['scott', 'silverman'], doc=""" Method of automatically determining KDE bandwidth""") bandwidth = param.Number(default=None, doc=""" Allows supplying explicit bandwidth value rather than relying on scott or silverman method.""") cut = param.Number(default=3, doc=""" Draw the estimate to cut * bw from the extreme data points.""") filled = param.Boolean(default=False, doc=""" Controls whether to return filled or unfilled contours.""") levels = param.ClassSelector(default=10, class_=(list, int), doc=""" A list of scalar values used to specify the contour levels.""") n_samples = param.Integer(default=100, doc=""" Number of samples to compute the KDE over.""") x_range = param.NumericTuple(default=None, length=2, doc=""" The x_range as a tuple of min and max x-value. Auto-ranges if set to None.""") y_range = param.NumericTuple(default=None, length=2, doc=""" The x_range as a tuple of min and max y-value. Auto-ranges if set to None.""") _per_element = True def _process(self, element, key=None): try: from scipy import stats except ImportError: raise ImportError('%s operation requires SciPy to be installed.' % type(self).__name__) if len(element.dimensions()) < 2: raise ValueError("bivariate_kde can only be computed on elements " "declaring at least two dimensions.") xdim, ydim = element.dimensions()[:2] params = {} if isinstance(element, Bivariate): if element.group != type(element).__name__: params['group'] = element.group params['label'] = element.label vdim = element.vdims[0] else: vdim = 'Density' data = element.array([0, 1]).T xmin, xmax = self.p.x_range or element.range(0) ymin, ymax = self.p.y_range or element.range(1) if any(not isfinite(v) for v in (xmin, xmax)): xmin, xmax = -0.5, 0.5 elif xmin == xmax: xmin, xmax = xmin - 0.5, xmax + 0.5 if any(not isfinite(v) for v in (ymin, ymax)): ymin, ymax = -0.5, 0.5 elif ymin == ymax: ymin, ymax = ymin - 0.5, ymax + 0.5 data = data[:, isfinite(data).min( axis=0)] if data.shape[1] > 1 else np.empty((2, 0)) if data.shape[1] > 1: kde = stats.gaussian_kde(data) if self.p.bandwidth: kde.set_bandwidth(self.p.bandwidth) bw = kde.scotts_factor() * data.std(ddof=1) if self.p.x_range: xs = np.linspace(xmin, xmax, self.p.n_samples) else: xs = _kde_support((xmin, xmax), bw, self.p.n_samples, self.p.cut, xdim.range) if self.p.y_range: ys = np.linspace(ymin, ymax, self.p.n_samples) else: ys = _kde_support((ymin, ymax), bw, self.p.n_samples, self.p.cut, ydim.range) xx, yy = cartesian_product([xs, ys], False) positions = np.vstack([xx.ravel(), yy.ravel()]) f = np.reshape(kde(positions).T, xx.shape) elif self.p.contours: eltype = Polygons if self.p.filled else Contours return eltype([], kdims=[xdim, ydim], vdims=[vdim]) else: xs = np.linspace(xmin, xmax, self.p.n_samples) ys = np.linspace(ymin, ymax, self.p.n_samples) f = np.zeros((self.p.n_samples, self.p.n_samples)) img = Image((xs, ys, f.T), kdims=element.dimensions()[:2], vdims=[vdim], **params) if self.p.contours: cntr = contours(img, filled=self.p.filled, levels=self.p.levels) return cntr.clone(cntr.data[1:], **params) return img
class CurvePlot(ElementPlot): interpolation = param.ObjectSelector( objects=['linear', 'steps-mid', 'steps-pre', 'steps-post'], default='linear', doc=""" Defines how the samples of the Curve are interpolated, default is 'linear', other options include 'steps-mid', 'steps-pre' and 'steps-post'.""") style_opts = line_properties _plot_methods = dict(single='line', batched='multi_line') _batched_style_opts = line_properties def get_data(self, element, ranges=None, empty=False): if 'steps' in self.interpolation: element = interpolate_curve(element, interpolation=self.interpolation) xidx, yidx = (1, 0) if self.invert_axes else (0, 1) x = element.get_dimension(xidx).name y = element.get_dimension(yidx).name data = { x: [] if empty else element.dimension_values(xidx), y: [] if empty else element.dimension_values(yidx) } self._get_hover_data(data, element, empty) self._categorize_data(data, (x, y), element.dimensions()) return (data, dict(x=x, y=y)) def _hover_opts(self, element): if self.batched: dims = list(self.hmap.last.kdims) line_policy = 'prev' else: dims = list(self.overlay_dims.keys()) + element.dimensions() line_policy = 'nearest' return dims, dict(line_policy=line_policy) def get_batched_data(self, overlay, ranges=None, empty=False): data = defaultdict(list) zorders = self._updated_zorders(overlay) styles = self.lookup_options(overlay.last, 'style') styles = styles.max_cycles(len(self.ordering)) for (key, el), zorder in zip(overlay.data.items(), zorders): self.set_param(**self.lookup_options(el, 'plot').options) eldata, elmapping = self.get_data(el, ranges, empty) for k, eld in eldata.items(): data[k].append(eld) # Apply static styles style = styles[zorder] sdata, smapping = expand_batched_style(style, self._batched_style_opts, elmapping, nvals=1) elmapping.update(smapping) for k, v in sdata.items(): data[k].append(v[0]) for d, k in zip(overlay.kdims, key): sanitized = dimension_sanitizer(d.name) data[sanitized].append(k) data = { opt: vals for opt, vals in data.items() if not any(v is None for v in vals) } mapping = {{ 'x': 'xs', 'y': 'ys' }.get(k, k): v for k, v in elmapping.items()} return data, mapping
class univariate_kde(Operation): """ Computes a 1D kernel density estimate (KDE) along the supplied dimension. Kernel density estimation is a non-parametric way to estimate the probability density function of a random variable. The KDE works by placing a Gaussian kernel at each sample with the supplied bandwidth. These kernels are then summed to produce the density estimate. By default a good bandwidth is determined using the bw_method but it may be overridden by an explicit value. """ bw_method = param.ObjectSelector(default='scott', objects=['scott', 'silverman'], doc=""" Method of automatically determining KDE bandwidth""") bandwidth = param.Number(default=None, doc=""" Allows supplying explicit bandwidth value rather than relying on scott or silverman method.""" ) cut = param.Number(default=3, doc=""" Draw the estimate to cut * bw from the extreme data points.""") bin_range = param.NumericTuple(default=None, length=2, doc=""" Specifies the range within which to compute the KDE.""") dimension = param.String(default=None, doc=""" Along which dimension of the Element to compute the KDE.""") filled = param.Boolean(default=True, doc=""" Controls whether to return filled or unfilled KDE.""") n_samples = param.Integer(default=100, doc=""" Number of samples to compute the KDE over.""") groupby = param.ClassSelector(default=None, class_=(basestring, Dimension), doc=""" Defines a dimension to group the Histogram returning an NdOverlay of Histograms.""" ) _per_element = True def _process(self, element, key=None): if self.p.groupby: if not isinstance(element, Dataset): raise ValueError( 'Cannot use histogram groupby on non-Dataset Element') grouped = element.groupby(self.p.groupby, group_type=Dataset, container_type=NdOverlay) self.p.groupby = None return grouped.map(self._process, Dataset) try: from scipy import stats from scipy.linalg import LinAlgError except ImportError: raise ImportError('%s operation requires SciPy to be installed.' % type(self).__name__) params = {} if isinstance(element, Distribution): selected_dim = element.kdims[0] if element.group != type(element).__name__: params['group'] = element.group params['label'] = element.label vdim = element.vdims[0] vdim_name = '{}_density'.format(selected_dim.name) vdims = [ vdim.clone(vdim_name, label='Density') if vdim.name == 'Density' else vdim ] else: if self.p.dimension: selected_dim = element.get_dimension(self.p.dimension) else: dimensions = element.vdims + element.kdims if not dimensions: raise ValueError( "%s element does not declare any dimensions " "to compute the kernel density estimate on." % type(element).__name__) selected_dim = dimensions[0] vdim_name = '{}_density'.format(selected_dim.name) vdims = [Dimension(vdim_name, label='Density')] data = element.dimension_values(selected_dim) bin_range = self.p.bin_range or element.range(selected_dim) if bin_range == (0, 0) or any(not isfinite(r) for r in bin_range): bin_range = (0, 1) elif bin_range[0] == bin_range[1]: bin_range = (bin_range[0] - 0.5, bin_range[1] + 0.5) element_type = Area if self.p.filled else Curve data = data[isfinite(data)] if len(data) else [] if len(data) > 1: try: kde = stats.gaussian_kde(data) except LinAlgError: return element_type([], selected_dim, vdims, **params) if self.p.bandwidth: kde.set_bandwidth(self.p.bandwidth) bw = kde.scotts_factor() * data.std(ddof=1) if self.p.bin_range: xs = np.linspace(bin_range[0], bin_range[1], self.p.n_samples) else: xs = _kde_support(bin_range, bw, self.p.n_samples, self.p.cut, selected_dim.range) ys = kde.evaluate(xs) else: xs = np.linspace(bin_range[0], bin_range[1], self.p.n_samples) ys = np.full_like(xs, 0) return element_type((xs, ys), kdims=[selected_dim], vdims=vdims, **params)
class PlotlyRenderer(Renderer): backend = param.String(default='plotly', doc="The backend name.") fig = param.ObjectSelector(default='auto', objects=['html', 'json', 'auto'], doc=""" Output render format for static figures. If None, no figure rendering will occur. """) mode_formats = { 'fig': { 'default': ['html', 'json'] }, 'holomap': { 'default': ['widgets', 'scrubber', 'auto'] } } widgets = { 'scrubber': PlotlyScrubberWidget, 'widgets': PlotlySelectionWidget } comms = {'default': (JupyterComm, plotly_msg_handler)} _loaded = False def __call__(self, obj, fmt='html', divuuid=None): plot, fmt = self._validate(obj, fmt) mime_types = {'file-ext': fmt, 'mime_type': MIME_TYPES[fmt]} if isinstance(plot, tuple(self.widgets.values())): return plot(), mime_types elif fmt == 'html': return self.figure_data(plot, divuuid=divuuid), mime_types elif fmt == 'json': return self.diff(plot), mime_types def diff(self, plot, serialize=True): """ Returns a json diff required to update an existing plot with the latest plot data. """ diff = { 'data': plot.state.get('data', []), 'layout': plot.state.get('layout', {}) } if serialize: return json.dumps(diff, cls=utils.PlotlyJSONEncoder) else: return diff def figure_data(self, plot, divuuid=None, comm=True, width=800, height=600): figure = plot.state if divuuid is None: if plot.comm: divuuid = plot.comm.id else: divuuid = uuid.uuid4().hex jdata = json.dumps(figure.get('data', []), cls=utils.PlotlyJSONEncoder) jlayout = json.dumps(figure.get('layout', {}), cls=utils.PlotlyJSONEncoder) config = {} config['showLink'] = False jconfig = json.dumps(config) header = ('<script type="text/javascript">' 'window.PLOTLYENV=window.PLOTLYENV || {};' '</script>') script = '\n'.join([ 'Plotly.plot("{id}", {data}, {layout}, {config}).then(function() {{', ' $(".{id}.loading").remove();', '}})' ]).format(id=divuuid, data=jdata, layout=jlayout, config=jconfig) content = ('<div class="{id} loading" style="color: rgb(50,50,50);">' 'Drawing...</div>' '<div id="{id}" style="height: {height}; width: {width};" ' 'class="plotly-graph-div">' '</div>' '<script type="text/javascript">' '{script}' '</script>').format(id=divuuid, script=script, height=height, width=width) joined = '\n'.join([header, content]) if comm and plot.comm is not None: comm, msg_handler = self.comms[self.mode] msg_handler = msg_handler.format(comm_id=plot.comm.id) return comm.template.format(init_frame=joined, msg_handler=msg_handler, comm_id=plot.comm.id) return joined @classmethod def plot_options(cls, obj, percent_size): factor = percent_size / 100.0 obj = obj.last if isinstance(obj, HoloMap) else obj plot = Store.registry[cls.backend].get(type(obj), None) options = Store.lookup_options(cls.backend, obj, 'plot').options width = options.get('width', plot.width) * factor height = options.get('height', plot.height) * factor return dict(options, **{'width': int(width), 'height': int(height)}) @classmethod def load_nb(cls, inline=True): """ Loads the plotly notebook resources. """ from IPython.display import display, HTML if not cls._loaded: display(HTML(PLOTLY_WARNING)) cls._loaded = True display(HTML(plotly_include()))
class BokehRenderer(Renderer): backend = param.String(default='bokeh', doc="The backend name.") fig = param.ObjectSelector(default='auto', objects=['html', 'json', 'auto', 'png'], doc=""" Output render format for static figures. If None, no figure rendering will occur. """) holomap = param.ObjectSelector(default='auto', objects=['widgets', 'scrubber', 'server', None, 'auto'], doc=""" Output render multi-frame (typically animated) format. If None, no multi-frame rendering will occur.""") mode = param.ObjectSelector(default='default', objects=['default', 'server'], doc=""" Whether to render the object in regular or server mode. In server mode a bokeh Document will be returned which can be served as a bokeh server app. By default renders all output is rendered to HTML.""") # Defines the valid output formats for each mode. mode_formats = {'fig': {'default': ['html', 'json', 'auto', 'png'], 'server': ['html', 'json', 'auto']}, 'holomap': {'default': ['widgets', 'scrubber', 'auto', None], 'server': ['server', 'auto', None]}} webgl = param.Boolean(default=False, doc="""Whether to render plots with WebGL if bokeh version >=0.10""") widgets = {'scrubber': BokehScrubberWidget, 'widgets': BokehSelectionWidget, 'server': BokehServerWidgets} backend_dependencies = {'js': CDN.js_files if CDN.js_files else tuple(INLINE.js_raw), 'css': CDN.css_files if CDN.css_files else tuple(INLINE.css_raw)} comms = {'default': (JupyterComm, None), 'server': (Comm, None)} _loaded = False def __call__(self, obj, fmt=None, doc=None): """ Render the supplied HoloViews component using the appropriate backend. The output is not a file format but a suitable, in-memory byte stream together with any suitable metadata. """ plot, fmt = self._validate(obj, fmt) info = {'file-ext': fmt, 'mime_type': MIME_TYPES[fmt]} if self.mode == 'server': return self.server_doc(plot, doc), info elif isinstance(plot, tuple(self.widgets.values())): return plot(), info elif fmt == 'png': from bokeh.io.export import get_screenshot_as_png img = get_screenshot_as_png(plot.state, None) imgByteArr = BytesIO() img.save(imgByteArr, format='PNG') return imgByteArr.getvalue(), info elif fmt == 'html': html = self._figure_data(plot, doc=doc) html = "<div style='display: table; margin: 0 auto;'>%s</div>" % html return self._apply_post_render_hooks(html, obj, fmt), info elif fmt == 'json': return self.diff(plot), info @bothmethod def _save_prefix(self_or_cls, ext): "Hook to prefix content for instance JS when saving HTML" if ext == 'html': return '\n'.join(self_or_cls.html_assets()).encode('utf8') return @bothmethod def get_plot(self_or_cls, obj, doc=None, renderer=None): """ Given a HoloViews Viewable return a corresponding plot instance. Allows supplying a document attach the plot to, useful when combining the bokeh model with another plot. """ plot = super(BokehRenderer, self_or_cls).get_plot(obj, renderer) if self_or_cls.mode == 'server' and doc is None: doc = curdoc() if doc is not None: plot.document = doc return plot @bothmethod def get_widget(self_or_cls, plot, widget_type, doc=None, **kwargs): if not isinstance(plot, Plot): plot = self_or_cls.get_plot(plot, doc) if self_or_cls.mode == 'server': return BokehServerWidgets(plot, renderer=self_or_cls.instance(), **kwargs) else: return super(BokehRenderer, self_or_cls).get_widget(plot, widget_type, **kwargs) @bothmethod def app(self_or_cls, plot, show=False, new_window=False, websocket_origin=None): """ Creates a bokeh app from a HoloViews object or plot. By default simply attaches the plot to bokeh's curdoc and returns the Document, if show option is supplied creates an Application instance and displays it either in a browser window or inline if notebook extension has been loaded. Using the new_window option the app may be displayed in a new browser tab once the notebook extension has been loaded. A websocket origin is required when launching from an existing tornado server (such as the notebook) and it is not on the default port ('localhost:8888'). """ renderer = self_or_cls.instance(mode='server') # If show=False and not in notebook context return document if not show and not self_or_cls.notebook_context: doc, _ = renderer(plot) return doc def modify_doc(doc): renderer(plot, doc=doc) handler = FunctionHandler(modify_doc) app = Application(handler) if not show: # If not showing and in notebook context return app return app elif self_or_cls.notebook_context and not new_window: # If in notebook, show=True and no new window requested # display app inline opts = dict(notebook_url=websocket_origin) if websocket_origin else {} return bkshow(app, **opts) # If app shown outside notebook or new_window requested # start server and open in new browser tab from tornado.ioloop import IOLoop loop = IOLoop.current() opts = dict(allow_websocket_origin=[websocket_origin]) if websocket_origin else {} opts['io_loop'] = loop server = Server({'/': app}, port=0, **opts) def show_callback(): server.show('/') server.io_loop.add_callback(show_callback) server.start() try: loop.start() except RuntimeError: pass return server @bothmethod def server_doc(self_or_cls, obj, doc=None): """ Get a bokeh Document with the plot attached. May supply an existing doc, otherwise bokeh.io.curdoc() is used to attach the plot to the global document instance. """ if doc is None: doc = curdoc() if not isinstance(obj, (Plot, BokehServerWidgets)): renderer = self_or_cls.instance(mode='server') plot, _ = renderer._validate(obj, 'auto') else: plot = obj root = plot.state if isinstance(plot, BokehServerWidgets): plot = plot.plot plot.document = doc plot.traverse(lambda x: attach_periodic(x), [GenericElementPlot]) doc.add_root(root) return doc def _figure_data(self, plot, fmt='html', doc=None, **kwargs): model = plot.state doc = Document() if doc is None else doc for m in model.references(): m._document = None doc.add_root(model) comm_id = plot.comm.id if plot.comm else None # Bokeh raises warnings about duplicate tools and empty subplots # but at the holoviews level these are not issues logger = logging.getLogger(bokeh.core.validation.check.__file__) logger.disabled = True try: js, div, _ = notebook_content(model, comm_id) html = NOTEBOOK_DIV.format(plot_script=js, plot_div=div) div = encode_utf8(html) doc.hold() except: logger.disabled = False raise logger.disabled = False plot.document = doc return div def diff(self, plot, binary=True): """ Returns a json diff required to update an existing plot with the latest plot data. """ events = list(plot.document._held_events) if not events: return None msg = Protocol("1.0").create("PATCH-DOC", events, use_buffers=binary) plot.document._held_events = [] return msg @classmethod def plot_options(cls, obj, percent_size): """ Given a holoviews object and a percentage size, apply heuristics to compute a suitable figure size. For instance, scaling layouts and grids linearly can result in unwieldy figure sizes when there are a large number of elements. As ad hoc heuristics are used, this functionality is kept separate from the plotting classes themselves. Used by the IPython Notebook display hooks and the save utility. Note that this can be overridden explicitly per object using the fig_size and size plot options. """ obj = obj.last if isinstance(obj, HoloMap) else obj plot = Store.registry[cls.backend].get(type(obj), None) if not hasattr(plot, 'width') or not hasattr(plot, 'height'): from .plot import BokehPlot plot = BokehPlot options = plot.lookup_options(obj, 'plot').options width = options.get('width', plot.width) height = options.get('height', plot.height) return dict(options, **{'width':int(width), 'height': int(height)}) @bothmethod def get_size(self_or_cls, plot): """ Return the display size associated with a plot before rendering to any particular format. Used to generate appropriate HTML display. Returns a tuple of (width, height) in pixels. """ if isinstance(plot, Plot): plot = plot.state elif not isinstance(plot, Model): raise ValueError('Can only compute sizes for HoloViews ' 'and bokeh plot objects.') return compute_plot_size(plot) @classmethod def load_nb(cls, inline=True): """ Loads the bokeh notebook resources. """ from bokeh.io.notebook import curstate load_notebook(hide_banner=True, resources=INLINE if inline else CDN) curstate().output_notebook()
class ElementPlot(GenericElementPlot, MPLPlot): apply_ticks = param.Boolean(default=True, doc=""" Whether to apply custom ticks.""") aspect = param.Parameter(default='square', doc=""" The aspect ratio mode of the plot. By default, a plot may select its own appropriate aspect ratio but sometimes it may be necessary to force a square aspect ratio (e.g. to display the plot as an element of a grid). The modes 'auto' and 'equal' correspond to the axis modes of the same name in matplotlib, a numeric value may also be passed.""") bgcolor = param.ClassSelector(class_=(str, tuple), default=None, doc=""" If set bgcolor overrides the background color of the axis.""") invert_axes = param.ObjectSelector(default=False, doc=""" Inverts the axes of the plot. Note that this parameter may not always be respected by all plots but should be respected by adjoined plots when appropriate.""") invert_xaxis = param.Boolean(default=False, doc=""" Whether to invert the plot x-axis.""") invert_yaxis = param.Boolean(default=False, doc=""" Whether to invert the plot y-axis.""") invert_zaxis = param.Boolean(default=False, doc=""" Whether to invert the plot z-axis.""") labelled = param.List(default=['x', 'y'], doc=""" Whether to plot the 'x' and 'y' labels.""") logx = param.Boolean(default=False, doc=""" Whether to apply log scaling to the x-axis of the Chart.""") logy = param.Boolean(default=False, doc=""" Whether to apply log scaling to the y-axis of the Chart.""") logz = param.Boolean(default=False, doc=""" Whether to apply log scaling to the y-axis of the Chart.""") show_legend = param.Boolean(default=False, doc=""" Whether to show legend for the plot.""") show_grid = param.Boolean(default=False, doc=""" Whether to show a Cartesian grid on the plot.""") xaxis = param.ObjectSelector( default='bottom', objects=['top', 'bottom', 'bare', 'top-bare', 'bottom-bare', None], doc=""" Whether and where to display the xaxis, bare options allow suppressing all axis labels including ticks and xlabel. Valid options are 'top', 'bottom', 'bare', 'top-bare' and 'bottom-bare'.""") yaxis = param.ObjectSelector( default='left', objects=['left', 'right', 'bare', 'left-bare', 'right-bare', None], doc=""" Whether and where to display the yaxis, bare options allow suppressing all axis labels including ticks and ylabel. Valid options are 'left', 'right', 'bare' 'left-bare' and 'right-bare'.""") zaxis = param.Boolean(default=True, doc=""" Whether to display the z-axis.""") xticks = param.Parameter(default=None, doc=""" Ticks along x-axis specified as an integer, explicit list of tick locations, list of tuples containing the locations and labels or a matplotlib tick locator object. If set to None default matplotlib ticking behavior is applied.""") xrotation = param.Integer(default=0, bounds=(0, 360), doc=""" Rotation angle of the xticks.""") yticks = param.Parameter(default=None, doc=""" Ticks along y-axis specified as an integer, explicit list of tick locations, list of tuples containing the locations and labels or a matplotlib tick locator object. If set to None default matplotlib ticking behavior is applied.""") yrotation = param.Integer(default=0, bounds=(0, 360), doc=""" Rotation angle of the yticks.""") zrotation = param.Integer(default=0, bounds=(0, 360), doc=""" Rotation angle of the zticks.""") zticks = param.Parameter(default=None, doc=""" Ticks along z-axis specified as an integer, explicit list of tick locations, list of tuples containing the locations and labels or a matplotlib tick locator object. If set to None default matplotlib ticking behavior is applied.""") # Element Plots should declare the valid style options for matplotlib call style_opts = [] # Whether plot has axes, disables setting axis limits, labels and ticks _has_axes = True def __init__(self, element, **params): super(ElementPlot, self).__init__(element, **params) check = self.hmap.last if isinstance(check, CompositeOverlay): check = check.values()[0] # Should check if any are 3D plots if isinstance(check, Element3D): self.projection = '3d' for hook in self.initial_hooks: try: hook(self, element) except Exception as e: self.warning("Plotting hook %r could not be applied:\n\n %s" % (hook, e)) def _finalize_axis(self, key, element=None, title=None, dimensions=None, ranges=None, xticks=None, yticks=None, zticks=None, xlabel=None, ylabel=None, zlabel=None): """ Applies all the axis settings before the axis or figure is returned. Only plots with zorder 0 get to apply their settings. When the number of the frame is supplied as n, this method looks up and computes the appropriate title, axis labels and axis bounds. """ if element is None: element = self._get_frame(key) self.current_frame = element if not dimensions and element and not self.subplots: el = element.traverse(lambda x: x, [Element]) if el: dimensions = el[0].dimensions() axis = self.handles['axis'] subplots = list(self.subplots.values()) if self.subplots else [] if self.zorder == 0 and key is not None: if self.bgcolor: if LooseVersion(mpl.__version__) <= '1.5.9': axis.set_axis_bgcolor(self.bgcolor) else: axis.set_facecolor(self.bgcolor) # Apply title title = self._format_title(key) if self.show_title and title is not None: fontsize = self._fontsize('title') if 'title' in self.handles: self.handles['title'].set_text(title) else: self.handles['title'] = axis.set_title(title, **fontsize) # Apply subplot label self._subplot_label(axis) # Apply axis options if axes are enabled if element and not any(not sp._has_axes for sp in [self] + subplots): # Set axis labels if dimensions: self._set_labels(axis, dimensions, xlabel, ylabel, zlabel) if not subplots: legend = axis.get_legend() if legend: legend.set_visible(self.show_legend) self.handles["bbox_extra_artists"] += [legend] axis.xaxis.grid(self.show_grid) axis.yaxis.grid(self.show_grid) # Apply log axes if self.logx: axis.set_xscale('log') if self.logy: axis.set_yscale('log') if not self.projection == '3d': self._set_axis_position(axis, 'x', self.xaxis) self._set_axis_position(axis, 'y', self.yaxis) # Apply ticks if self.apply_ticks: self._finalize_ticks(axis, dimensions, xticks, yticks, zticks) # Set axes limits self._set_axis_limits(axis, element, subplots, ranges) # Apply aspects if self.aspect is not None and self.projection != 'polar' and not self.adjoined: self._set_aspect(axis, self.aspect) if not subplots and not self.drawn: self._finalize_artist(key) for hook in self.finalize_hooks: try: hook(self, element) except Exception as e: self.warning("Plotting hook %r could not be applied:\n\n %s" % (hook, e)) return super(ElementPlot, self)._finalize_axis(key) def _finalize_ticks(self, axis, dimensions, xticks, yticks, zticks): """ Finalizes the ticks on the axes based on the supplied ticks and Elements. Sets the axes position as well as tick positions, labels and fontsize. """ ndims = len(dimensions) if dimensions else 0 xdim = dimensions[0] if ndims else None ydim = dimensions[1] if ndims > 1 else None # Tick formatting if xdim: self._set_axis_formatter(axis.xaxis, xdim) if ydim: self._set_axis_formatter(axis.yaxis, ydim) if self.projection == '3d': zdim = dimensions[2] if ndims > 2 else None if zdim: self._set_axis_formatter(axis.zaxis, zdim) xticks = xticks if xticks else self.xticks self._set_axis_ticks(axis.xaxis, xticks, log=self.logx, rotation=self.xrotation) yticks = yticks if yticks else self.yticks self._set_axis_ticks(axis.yaxis, yticks, log=self.logy, rotation=self.yrotation) if self.projection == '3d': zticks = zticks if zticks else self.zticks self._set_axis_ticks(axis.zaxis, zticks, log=self.logz, rotation=self.zrotation) for ax, ax_obj in zip('xy', [axis.xaxis, axis.yaxis]): tick_fontsize = self._fontsize('%sticks' % ax, 'labelsize', common=False) if tick_fontsize: ax_obj.set_tick_params(**tick_fontsize) def _finalize_artist(self, element): """ Allows extending the _finalize_axis method with Element specific options. """ pass def _set_labels(self, axes, dimensions, xlabel=None, ylabel=None, zlabel=None): """ Sets the labels of the axes using the supplied list of dimensions. Optionally explicit labels may be supplied to override the dimension label. """ xlabel, ylabel, zlabel = self._get_axis_labels(dimensions, xlabel, ylabel, zlabel) if self.invert_axes: xlabel, ylabel = ylabel, xlabel if xlabel and self.xaxis and 'x' in self.labelled: axes.set_xlabel(xlabel, **self._fontsize('xlabel')) if ylabel and self.yaxis and 'y' in self.labelled: axes.set_ylabel(ylabel, **self._fontsize('ylabel')) if zlabel and self.zaxis and 'z' in self.labelled: axes.set_zlabel(zlabel, **self._fontsize('zlabel')) def _set_axis_formatter(self, axis, dim): """ Set axis formatter based on dimension formatter. """ if isinstance(dim, list): dim = dim[0] formatter = None if dim.value_format: formatter = dim.value_format elif dim.type in dim.type_formatters: formatter = dim.type_formatters[dim.type] if formatter: axis.set_major_formatter(wrap_formatter(formatter)) def _set_aspect(self, axes, aspect): """ Set the aspect on the axes based on the aspect setting. """ if isinstance(aspect, util.basestring) and aspect != 'square': axes.set_aspect(aspect) return (x0, x1), (y0, y1) = axes.get_xlim(), axes.get_ylim() xsize = np.log(x1) - np.log(x0) if self.logx else x1 - x0 ysize = np.log(y1) - np.log(y0) if self.logy else y1 - y0 xsize = max(abs(xsize), 1e-30) ysize = max(abs(ysize), 1e-30) data_ratio = 1. / (ysize / xsize) if aspect != 'square': data_ratio = data_ratio / aspect axes.set_aspect(float(data_ratio)) def _set_axis_limits(self, axis, view, subplots, ranges): """ Compute extents for current view and apply as axis limits """ # Extents scalex, scaley = True, True extents = self.get_extents(view, ranges) if extents and not self.overlaid: coords = [ coord if np.isreal(coord) or isinstance(coord, np.datetime64) else np.NaN for coord in extents ] coords = [ date2num(util.dt64_to_dt(c)) if isinstance(c, np.datetime64) else c for c in coords ] valid_lim = lambda c: util.isnumeric(c) and not np.isnan(c) if self.projection == '3d' or len(extents) == 6: l, b, zmin, r, t, zmax = coords if self.invert_zaxis or any(p.invert_zaxis for p in subplots): zmin, zmax = zmax, zmin if zmin != zmax: if valid_lim(zmin): axis.set_zlim(bottom=zmin) if valid_lim(zmax): axis.set_zlim(top=zmax) else: l, b, r, t = coords if self.invert_axes: l, b, r, t = b, l, t, r if self.invert_xaxis or any(p.invert_xaxis for p in subplots): r, l = l, r if l != r: lims = {} if valid_lim(l): lims['left'] = l scalex = False if valid_lim(r): lims['right'] = r scalex = False if lims: axis.set_xlim(**lims) if self.invert_yaxis or any(p.invert_yaxis for p in subplots): t, b = b, t if b != t: lims = {} if valid_lim(b): lims['bottom'] = b scaley = False if valid_lim(t): lims['top'] = t scaley = False if lims: axis.set_ylim(**lims) axis.autoscale_view(scalex=scalex, scaley=scaley) def _set_axis_position(self, axes, axis, option): """ Set the position and visibility of the xaxis or yaxis by supplying the axes object, the axis to set, i.e. 'x' or 'y' and an option to specify the position and visibility of the axis. The option may be None, 'bare' or positional, i.e. 'left' and 'right' for the yaxis and 'top' and 'bottom' for the xaxis. May also combine positional and 'bare' into for example 'left-bare'. """ positions = {'x': ['bottom', 'top'], 'y': ['left', 'right']}[axis] axis = axes.xaxis if axis == 'x' else axes.yaxis if option is None: axis.set_visible(False) for pos in positions: axes.spines[pos].set_visible(False) else: if 'bare' in option: axis.set_ticklabels([]) axis.set_label_text('') if option != 'bare': option = option.split('-')[0] axis.set_ticks_position(option) axis.set_label_position(option) if not self.overlaid and not self.show_frame and self.projection != 'polar': pos = (positions[1] if (option and (option == 'bare' or positions[0] in option)) else positions[0]) axes.spines[pos].set_visible(False) def _set_axis_ticks(self, axis, ticks, log=False, rotation=0): """ Allows setting the ticks for a particular axis either with a tuple of ticks, a tick locator object, an integer number of ticks, a list of tuples containing positions and labels or a list of positions. Also supports enabling log ticking if an integer number of ticks is supplied and setting a rotation for the ticks. """ if isinstance(ticks, (list, tuple)) and all( isinstance(l, list) for l in ticks): axis.set_ticks(ticks[0]) axis.set_ticklabels(ticks[1]) elif isinstance(ticks, ticker.Locator): axis.set_major_locator(ticks) elif not ticks and ticks is not None: axis.set_ticks([]) elif isinstance(ticks, int): if log: locator = ticker.LogLocator(numticks=ticks, subs=range(1, 10)) else: locator = ticker.MaxNLocator(ticks) axis.set_major_locator(locator) elif isinstance(ticks, (list, tuple)): labels = None if all(isinstance(t, tuple) for t in ticks): ticks, labels = zip(*ticks) axis.set_ticks(ticks) if labels: axis.set_ticklabels(labels) for tick in axis.get_ticklabels(): tick.set_rotation(rotation) @mpl_rc_context def update_frame(self, key, ranges=None, element=None): """ Set the plot(s) to the given frame number. Operates by manipulating the matplotlib objects held in the self._handles dictionary. If n is greater than the number of available frames, update using the last available frame. """ reused = isinstance(self.hmap, DynamicMap) and self.overlaid if not reused and element is None: element = self._get_frame(key) elif element is not None: self.current_key = key self.current_frame = element if element is not None: self.set_param(**self.lookup_options(element, 'plot').options) axis = self.handles['axis'] axes_visible = element is not None or self.overlaid axis.xaxis.set_visible(axes_visible and self.xaxis) axis.yaxis.set_visible(axes_visible and self.yaxis) axis.patch.set_alpha(np.min([int(axes_visible), 1])) for hname, handle in self.handles.items(): hideable = hasattr(handle, 'set_visible') if hname not in ['axis', 'fig'] and hideable: handle.set_visible(element is not None) if element is None: return ranges = self.compute_ranges(self.hmap, key, ranges) ranges = util.match_spec(element, ranges) label = element.label if self.show_legend else '' style = dict(label=label, zorder=self.zorder, **self.style[self.cyclic_index]) axis_kwargs = self.update_handles(key, axis, element, ranges, style) self._finalize_axis(key, element=element, ranges=ranges, **(axis_kwargs if axis_kwargs else {})) @mpl_rc_context def initialize_plot(self, ranges=None): element = self.hmap.last ax = self.handles['axis'] key = list(self.hmap.data.keys())[-1] dim_map = dict(zip((d.name for d in self.hmap.kdims), key)) key = tuple(dim_map.get(d.name, None) for d in self.dimensions) ranges = self.compute_ranges(self.hmap, key, ranges) ranges = util.match_spec(element, ranges) style = dict(zorder=self.zorder, **self.style[self.cyclic_index]) if self.show_legend: style['label'] = element.label plot_data, plot_kwargs, axis_kwargs = self.get_data( element, ranges, style) with abbreviated_exception(): handles = self.init_artists(ax, plot_data, plot_kwargs) self.handles.update(handles) return self._finalize_axis(self.keys[-1], element=element, ranges=ranges, **axis_kwargs) def init_artists(self, ax, plot_args, plot_kwargs): """ Initializes the artist based on the plot method declared on the plot. """ plot_method = self._plot_methods.get( 'batched' if self.batched else 'single') plot_fn = getattr(ax, plot_method) artist = plot_fn(*plot_args, **plot_kwargs) return { 'artist': artist[0] if isinstance(artist, list) and len(artist) == 1 else artist } def update_handles(self, key, axis, element, ranges, style): """ Update the elements of the plot. """ self.teardown_handles() plot_data, plot_kwargs, axis_kwargs = self.get_data( element, ranges, style) with abbreviated_exception(): handles = self.init_artists(axis, plot_data, plot_kwargs) self.handles.update(handles) return axis_kwargs def teardown_handles(self): """ If no custom update_handles method is supplied this method is called to tear down any previous handles before replacing them. """ if 'artist' in self.handles: self.handles['artist'].remove()
class BokehPlot(DimensionedPlot): """ Plotting baseclass for the Bokeh backends, implementing the basic plotting interface for Bokeh based plots. """ width = param.Integer(default=300, doc=""" Width of the plot in pixels""") height = param.Integer(default=300, doc=""" Height of the plot in pixels""") sizing_mode = param.ObjectSelector(default='fixed', objects=[ 'fixed', 'stretch_both', 'scale_width', 'scale_height', 'scale_both' ], doc=""" How the item being displayed should size itself. "stretch_both" plots will resize to occupy all available space, even if this changes the aspect ratio of the element. "fixed" plots are not responsive and will retain their original width and height regardless of any subsequent browser window resize events. "scale_width" elements will responsively resize to fit to the width available, while maintaining the original aspect ratio. "scale_height" elements will responsively resize to fit to the height available, while maintaining the original aspect ratio. "scale_both" elements will responsively resize to for both the width and height available, while maintaining the original aspect ratio.""") shared_datasource = param.Boolean(default=True, doc=""" Whether Elements drawing the data from the same object should share their Bokeh data source allowing for linked brushing and other linked behaviors.""") title_format = param.String(default="{label} {group} {dimensions}", doc=""" The formatting string for the title of this plot, allows defining a label group separator and dimension labels.""") backend = 'bokeh' @property def document(self): return self._document @property def id(self): return self.state.ref['id'] @document.setter def document(self, doc): self._document = doc if self.subplots: for plot in self.subplots.values(): if plot is not None: plot.document = doc def __init__(self, *args, **params): super(BokehPlot, self).__init__(*args, **params) self._document = None self.root = None def get_data(self, element, ranges, style): """ Returns the data from an element in the appropriate format for initializing or updating a ColumnDataSource and a dictionary which maps the expected keywords arguments of a glyph to the column in the datasource. """ raise NotImplementedError def _construct_callbacks(self): """ Initializes any callbacks for streams which have defined the plotted object as a source. """ if isinstance(self, GenericOverlayPlot): zorders = [] elif self.batched: zorders = list( range(self.zorder, self.zorder + len(self.hmap.last))) else: zorders = [self.zorder] if isinstance(self, GenericOverlayPlot) and not self.batched: sources = [] elif not self.static or isinstance(self.hmap, DynamicMap): sources = [(i, o) for i, inputs in self.stream_sources.items() for o in inputs if i in zorders] else: sources = [(self.zorder, self.hmap.last)] cb_classes = set() for _, source in sources: streams = Stream.registry.get(id(source), []) registry = Stream._callbacks['bokeh'] cb_classes |= {(registry[type(stream)], stream) for stream in streams if type(stream) in registry and stream.linked} cbs = [] sorted_cbs = sorted(cb_classes, key=lambda x: id(x[0])) for cb, group in groupby(sorted_cbs, lambda x: x[0]): cb_streams = [s for _, s in group] cbs.append(cb(self, cb_streams, source)) return cbs def push(self): """ Pushes updated plot data via the Comm. """ if self.renderer.mode == 'server': return if self.comm is None: raise Exception('Renderer does not have a comm.') msg = self.renderer.diff(self, binary=True) if msg is None: return self.comm.send(msg.header_json) self.comm.send(msg.metadata_json) self.comm.send(msg.content_json) for header, payload in msg.buffers: self.comm.send(json.dumps(header)) self.comm.send(buffers=[payload]) def set_root(self, root): """ Sets the current document on all subplots. """ for plot in self.traverse(lambda x: x): plot.root = root def _init_datasource(self, data): """ Initializes a data source to be passed into the bokeh glyph. """ data = {k: decode_bytes(vs) for k, vs in data.items()} return ColumnDataSource(data=data) def _update_datasource(self, source, data): """ Update datasource with data for a new frame. """ data = {k: decode_bytes(vs) for k, vs in data.items()} if (self.streaming and self.streaming[0].data is self.current_frame.data and self._stream_data): stream = self.streaming[0] if stream._triggering: data = {k: v[-stream._chunk_length:] for k, v in data.items()} source.stream(data, stream.length) else: source.data.update(data) @property def state(self): """ The plotting state that gets updated via the update method and used by the renderer to generate output. """ return self.handles['plot'] @property def current_handles(self): """ Should return a list of plot objects that have changed and should be updated. """ return [] def _fontsize(self, key, label='fontsize', common=True): """ Converts integer fontsizes to a string specifying fontsize in pt. """ size = super(BokehPlot, self)._fontsize(key, label, common) return { k: v if isinstance(v, basestring) else '%spt' % v for k, v in size.items() } def sync_sources(self): """ Syncs data sources between Elements, which draw data from the same object. """ get_sources = lambda x: (id(x.current_frame.data), x) filter_fn = lambda x: (x.shared_datasource and x.current_frame is not None and not isinstance( x.current_frame.data, np.ndarray ) and 'source' in x.handles) data_sources = self.traverse(get_sources, [filter_fn]) grouped_sources = groupby(sorted(data_sources, key=lambda x: x[0]), lambda x: x[0]) shared_sources = [] source_cols = {} plots = [] for _, group in grouped_sources: group = list(group) if len(group) > 1: source_data = {} for _, plot in group: source_data.update(plot.handles['source'].data) new_source = ColumnDataSource(source_data) for _, plot in group: renderer = plot.handles.get('glyph_renderer') for callback in plot.callbacks: callback.reset() if renderer is None: continue elif 'data_source' in renderer.properties(): renderer.update(data_source=new_source) else: renderer.update(source=new_source) if hasattr(renderer, 'view'): renderer.view.update(source=new_source) plot.handles['source'] = new_source plots.append(plot) shared_sources.append(new_source) source_cols[id(new_source)] = [c for c in new_source.data] for plot in plots: for callback in plot.callbacks: callback.initialize() self.handles['shared_sources'] = shared_sources self.handles['source_cols'] = source_cols
class HoloViews(PaneBase): """ HoloViews panes render any HoloViews object to a corresponding Bokeh model while respecting the currently selected backend. """ backend = param.String(default=None, doc=""" The HoloViews backend used to render the plot (if None defaults to the currently selected renderer).""") widget_type = param.ObjectSelector(default='individual', objects=['individual', 'scrubber'], doc=""") Whether to generate individual widgets for each dimension or on global scrubber.""") widgets = param.Dict(default={}, doc=""" A mapping from dimension name to a widget instance which will be used to override the default widgets.""") precedence = 0.8 def __init__(self, object, **params): super(HoloViews, self).__init__(object, **params) self.widget_box = WidgetBox() self._update_widgets() self._plots = {} @param.depends('object', 'widgets', watch=True) def _update_widgets(self): if self.object is None: widgets, values = [] else: widgets, values = self.widgets_from_dimensions( self.object, self.widgets, self.widget_type) self._values = values # Clean up anything models listening to the previous widgets for _, cbs in self._callbacks.items(): for cb in list(cbs): if cb.inst in self.widget_box.objects: print(cb) cb.inst.param.unwatch(cb) cbs.remove(cb) self.widget_box.objects = widgets if widgets and not self.widget_box in self.layout.objects: self.layout.append(self.widget_box) elif not widgets and self.widget_box in self.layout.objects: self.layout.pop(self.widget_box) @classmethod def applies(cls, obj): if 'holoviews' not in sys.modules: return False from holoviews.core.dimension import Dimensioned return isinstance(obj, Dimensioned) def _cleanup(self, root=None, final=False): """ Traverses HoloViews object to find and clean up any streams connected to existing plots. """ if root is not None: old_plot = self._plots.pop(root.ref['id'], None) if old_plot: old_plot.cleanup() super(HoloViews, self)._cleanup(root, final) def _render(self, doc, comm, root): from holoviews import Store, renderer if not Store.renderers: loaded_backend = (self.backend or 'bokeh') renderer(loaded_backend) Store.current_backend = loaded_backend backend = self.backend or Store.current_backend renderer = Store.renderers[backend] if backend == 'bokeh': renderer = renderer.instance( mode='server' if comm is None else 'default') kwargs = {'doc': doc, 'root': root} if backend == 'bokeh' else {} if comm: kwargs['comm'] = comm plot = renderer.get_plot(self.object, **kwargs) ref = root.ref['id'] if ref in self._plots: old_plot = self._plots[ref] old_plot.comm = None old_plot.cleanup() self._plots[root.ref['id']] = plot return plot def _get_model(self, doc, root=None, parent=None, comm=None): """ Should return the Bokeh model to be rendered. """ ref = root.ref['id'] plot = self._render(doc, comm, root) child_pane = Pane(plot.state, _temporary=True) model = child_pane._get_model(doc, root, parent, comm) self._models[ref] = model self._link_object(doc, root, parent, comm) if self.widget_box.objects: self._link_widgets(child_pane, root, comm) return model def _link_widgets(self, pane, root, comm): def update_plot(change): from holoviews.core.util import cross_index from holoviews.plotting.bokeh.plot import BokehPlot widgets = self.widget_box.objects if self.widget_type == 'scrubber': key = cross_index([v for v in self._values.values()], widgets[0].value) else: key = tuple(w.value for w in widgets) plot = self._plots[root.ref['id']] if isinstance(plot, BokehPlot): if comm: plot.update(key) plot.push() else: def update_plot(): plot.update(key) plot.document.add_next_tick_callback(update_plot) else: plot.update(key) pane.object = plot.state ref = root.ref['id'] for w in self.widget_box.objects: watcher = w.param.watch(update_plot, 'value') self._callbacks[ref].append(watcher) @classmethod def widgets_from_dimensions(cls, object, widget_types={}, widgets_type='individual'): from holoviews.core import Dimension from holoviews.core.util import isnumeric, unicode from holoviews.core.traversal import unique_dimkeys from .widgets import Widget, DiscreteSlider, Select, FloatSlider dims, keys = unique_dimkeys(object) if dims == [Dimension('Frame')] and keys == [(0, )]: return [], {} nframes = 1 values = dict(zip(dims, zip(*keys))) dim_values = OrderedDict() widgets = [] for dim in dims: widget_type, widget = None, None vals = dim.values or values.get(dim, None) dim_values[dim.name] = vals if widgets_type == 'scrubber': if not vals: raise ValueError( 'Scrubber widget may only be used if all dimensions define values.' ) nframes *= len(vals) elif dim.name in widget_types: widget = widget_types[dim.name] if isinstance(widget, Widget): widgets.append(widget) continue elif isinstance(widget, type) and issubclass(widget, Widget): widget_type = widget else: raise ValueError('Explicit widget definitions expected ' 'to be a widget instance or type, %s ' 'dimension widget declared as %s.' % (dim, widget)) if vals: if all(isnumeric(v) for v in vals): vals = sorted(vals) labels = [unicode(dim.pprint_value(v)) for v in vals] options = OrderedDict(zip(labels, vals)) widget_type = widget_type or DiscreteSlider else: options = list(vals) widget_type = widget_type or Select default = vals[0] if dim.default is None else dim.default widget = widget_type(name=dim.label, options=options, value=default) elif dim.range != (None, None): default = dim.range[0] if dim.default is None else dim.default step = 0.1 if dim.step is None else dim.step widget_type = widget_type or FloatSlider widget = widget_type(step=step, name=dim.label, start=dim.range[0], end=dim.range[1], value=default) if widget is not None: widgets.append(widget) if widgets_type == 'scrubber': widgets = [Player(length=nframes)] return widgets, dim_values
class GridPlot(CompositePlot, GenericCompositePlot): """ Plot a group of elements in a grid layout based on a GridSpace element object. """ axis_offset = param.Integer(default=50, doc=""" Number of pixels to adjust row and column widths and height by to compensate for shared axes.""") fontsize = param.Parameter(default={'title': '16pt'}, allow_None=True, doc=""" Specifies various fontsizes of the displayed text. Finer control is available by supplying a dictionary where any unmentioned keys reverts to the default sizes, e.g: {'title': '15pt'}""") merge_tools = param.Boolean(default=True, doc=""" Whether to merge all the tools into a single toolbar""") shared_xaxis = param.Boolean(default=False, doc=""" If enabled the x-axes of the GridSpace will be drawn from the objects inside the Grid rather than the GridSpace dimensions.""") shared_yaxis = param.Boolean(default=False, doc=""" If enabled the x-axes of the GridSpace will be drawn from the objects inside the Grid rather than the GridSpace dimensions.""") toolbar = param.ObjectSelector( default='above', objects=["above", "below", "left", "right", None], doc=""" The toolbar location, must be one of 'above', 'below', 'left', 'right', None.""") xaxis = param.ObjectSelector(default=True, objects=['bottom', 'top', None, True, False], doc=""" Whether and where to display the xaxis, supported options are 'bottom', 'top' and None.""") yaxis = param.ObjectSelector(default=True, objects=['left', 'right', None, True, False], doc=""" Whether and where to display the yaxis, supported options are 'left', 'right' and None.""") xrotation = param.Integer(default=0, bounds=(0, 360), doc=""" Rotation angle of the xticks.""") yrotation = param.Integer(default=0, bounds=(0, 360), doc=""" Rotation angle of the yticks.""") plot_size = param.Integer(default=120, doc=""" Defines the width and height of each plot in the grid""") def __init__(self, layout, ranges=None, layout_num=1, keys=None, **params): if not isinstance(layout, GridSpace): raise Exception("GridPlot only accepts GridSpace.") super(GridPlot, self).__init__(layout=layout, layout_num=layout_num, ranges=ranges, keys=keys, **params) self.cols, self.rows = layout.shape self.subplots, self.layout = self._create_subplots(layout, ranges) if self.top_level: self.comm = self.init_comm() self.traverse(lambda x: setattr(x, 'comm', self.comm)) self.traverse(lambda x: attach_streams(self, x.hmap, 2), [GenericElementPlot]) def _create_subplots(self, layout, ranges): subplots = OrderedDict() frame_ranges = self.compute_ranges(layout, None, ranges) frame_ranges = OrderedDict([ (key, self.compute_ranges(layout, key, frame_ranges)) for key in self.keys ]) collapsed_layout = layout.clone(shared_data=False, id=layout.id) for i, coord in enumerate(layout.keys(full_grid=True)): r = i % self.rows c = i // self.rows if not isinstance(coord, tuple): coord = (coord, ) view = layout.data.get(coord, None) # Create subplot if view is not None: vtype = view.type if isinstance(view, HoloMap) else view.__class__ opts = self.lookup_options(view, 'plot').options else: vtype = None # Create axes offset = self.axis_offset kwargs = {} if c == 0 and r != 0: kwargs['xaxis'] = None kwargs['width'] = self.plot_size + offset if c != 0 and r == 0: kwargs['yaxis'] = None kwargs['height'] = self.plot_size + offset if c == 0 and r == 0: kwargs['width'] = self.plot_size + offset kwargs['height'] = self.plot_size + offset if r != 0 and c != 0: kwargs['xaxis'] = None kwargs['yaxis'] = None if 'width' not in kwargs or not self.shared_yaxis: kwargs['width'] = self.plot_size if 'height' not in kwargs or not self.shared_xaxis: kwargs['height'] = self.plot_size if 'border' not in kwargs: kwargs['border'] = 3 kwargs['show_legend'] = False if not self.shared_xaxis: kwargs['xaxis'] = None if not self.shared_yaxis: kwargs['yaxis'] = None # Create subplot plotting_class = Store.registry[self.renderer.backend].get( vtype, None) if plotting_class is None: if view is not None: self.warning("Bokeh plotting class for %s type not found, " "object will not be rendered." % vtype.__name__) else: subplot = plotting_class(view, dimensions=self.dimensions, show_title=False, subplot=True, renderer=self.renderer, ranges=frame_ranges, uniform=self.uniform, keys=self.keys, **dict(opts, **kwargs)) collapsed_layout[coord] = (subplot.layout if isinstance( subplot, GenericCompositePlot) else subplot.hmap) subplots[coord] = subplot return subplots, collapsed_layout def initialize_plot(self, ranges=None, plots=[]): ranges = self.compute_ranges(self.layout, self.keys[-1], None) passed_plots = list(plots) plots = [[None for c in range(self.cols)] for r in range(self.rows)] for i, coord in enumerate(self.layout.keys(full_grid=True)): r = i % self.rows c = i // self.rows subplot = self.subplots.get(wrap_tuple(coord), None) if subplot is not None: plot = subplot.initialize_plot(ranges=ranges, plots=passed_plots) plots[r][c] = plot passed_plots.append(plot) else: passed_plots.append(None) plot = gridplot(plots[::-1], toolbar_position=self.toolbar, merge_tools=self.merge_tools) plot = self._make_axes(plot) title = self._get_title(self.keys[-1]) if title: plot = Column(title, plot) self.handles['title'] = title self._update_callbacks(plot) self.handles['plot'] = plot self.handles['plots'] = plots if self.shared_datasource: self.sync_sources() self.drawn = True return self.handles['plot'] def _make_axes(self, plot): width, height = self.renderer.get_size(plot) x_axis, y_axis = None, None kwargs = dict(sizing_mode=self.sizing_mode) if self.xaxis: flip = self.shared_xaxis rotation = self.xrotation lsize = self._fontsize('xlabel').get('fontsize') tsize = self._fontsize('xticks', common=False).get('fontsize') xfactors = list(unique_iterator(self.layout.dimension_values(0))) x_axis = make_axis('x', width, xfactors, self.layout.kdims[0], flip=flip, rotation=rotation, label_size=lsize, tick_size=tsize) if self.yaxis and self.layout.ndims > 1: flip = self.shared_yaxis rotation = self.yrotation lsize = self._fontsize('ylabel').get('fontsize') tsize = self._fontsize('yticks', common=False).get('fontsize') yfactors = list(unique_iterator(self.layout.dimension_values(1))) y_axis = make_axis('y', height, yfactors, self.layout.kdims[1], flip=flip, rotation=rotation, label_size=lsize, tick_size=tsize) if x_axis and y_axis: plot = filter_toolboxes(plot) r1, r2 = ([y_axis, plot], [None, x_axis]) if self.shared_xaxis: r1, r2 = r2, r1 if self.shared_yaxis: r1, r2 = r1[::-1], r2[::-1] models = layout_padding([r1, r2], self.renderer) plot = gridplot(models, **kwargs) elif y_axis: models = [y_axis, plot] if self.shared_yaxis: models = models[::-1] plot = Row(*models, **kwargs) elif x_axis: models = [plot, x_axis] if self.shared_xaxis: models = models[::-1] plot = Column(*models, **kwargs) return plot @update_shared_sources def update_frame(self, key, ranges=None): """ Update the internal state of the Plot to represent the given key tuple (where integers represent frames). Returns this state. """ ranges = self.compute_ranges(self.layout, key, ranges) for coord in self.layout.keys(full_grid=True): subplot = self.subplots.get(wrap_tuple(coord), None) if subplot is not None: subplot.update_frame(key, ranges) title = self._get_title(key) if title: self.handles['title']
class SankeyPlot(GraphPlot): labels = param.ClassSelector(class_=(str, dim), doc=""" The dimension or dimension value transform used to draw labels from.""" ) show_values = param.Boolean(default=True, doc=""" Whether to show the values.""") label_position = param.ObjectSelector(default='right', objects=['left', 'right'], doc=""" Whether node labels should be placed to the left or right.""") node_width = param.Number(default=15, doc=""" Width of the nodes.""") node_padding = param.Integer(default=None, doc=""" Number of pixels of padding relative to the bounds.""") iterations = param.Integer(default=32, doc=""" Number of iterations to run the layout algorithm.""") node_sort = param.Boolean(default=True, doc=""" Sort nodes in ascending breadth.""") # Deprecated options color_index = param.ClassSelector(default=2, class_=(str, int), allow_None=True, doc=""" Index of the dimension from which the node labels will be drawn""") label_index = param.ClassSelector(default=2, class_=(str, int), allow_None=True, doc=""" Index of the dimension from which the node labels will be drawn""") filled = True style_opts = GraphPlot.style_opts + ['label_text_font_size'] def get_extents(self, element, ranges, range_type='combined'): """ A Chord plot is always drawn on a unit circle. """ if range_type == 'extents': return element.nodes.extents xdim, ydim = element.nodes.kdims[:2] xpad = .05 if self.label_index is None else 0.25 x0, x1 = ranges[xdim.name][range_type] y0, y1 = ranges[ydim.name][range_type] xdiff = (x1 - x0) ydiff = (y1 - y0) if self.label_position == 'right': x0, x1 = x0 - (0.05 * xdiff), x1 + xpad * xdiff else: x0, x1 = x0 - xpad * xdiff, x1 + (0.05 * xdiff) x0, x1 = max_range([xdim.range, (x0, x1)]) y0, y1 = max_range( [ydim.range, (y0 - (0.05 * ydiff), y1 + (0.05 * ydiff))]) return (x0, y0, x1, y1) def get_data(self, element, ranges, style): data, style, axis_kwargs = super().get_data(element, ranges, style) rects, labels = [], [] label_dim = element.nodes.get_dimension(self.label_index) labels = self.labels if label_dim and labels: if self.label_index not in [2, None]: self.param.warning( "Cannot declare style mapping for 'labels' option " "and declare a label_index; ignoring the label_index.") elif label_dim: labels = label_dim if isinstance(labels, str): labels = element.nodes.get_dimension(labels) if labels is None: text = [] if isinstance(labels, dim): text = labels.apply(element, flat=True) else: text = element.nodes.dimension_values(labels) text = [labels.pprint_value(v) for v in text] value_dim = element.vdims[0] text_labels = [] for i, node in enumerate(element._sankey['nodes']): x0, x1, y0, y1 = (node[a + i] for a in 'xy' for i in '01') rect = {'height': y1 - y0, 'width': x1 - x0, 'xy': (x0, y0)} rects.append(rect) if len(text): label = text[i] else: label = '' if self.show_values: value = value_dim.pprint_value(node['value'], print_unit=True) if label: label = '%s - %s' % (label, value) else: label = value if label: x = x1 + ( x1 - x0) / 4. if self.label_position == 'right' else x0 - ( x1 - x0) / 4. text_labels.append((label, (x, (y0 + y1) / 2.))) data['rects'] = rects if text_labels: data['text'] = text_labels return data, style, axis_kwargs def _update_labels(self, ax, data, style): labels = self.handles.get('labels', []) for label in labels: try: label.remove() except: pass if 'text' not in data: return [] fontsize = style.get('label_text_font_size', 8) align = 'left' if self.label_position == 'right' else 'right' labels = [] for text in data['text']: label = ax.annotate(*text, xycoords='data', horizontalalignment=align, fontsize=fontsize, verticalalignment='center', rotation_mode='anchor') labels.append(label) return labels def init_artists(self, ax, plot_args, plot_kwargs): fontsize = plot_kwargs.pop('label_text_font_size', 8) artists = super().init_artists(ax, plot_args, plot_kwargs) groups = [g for g in self._style_groups if g != 'node'] node_opts = filter_styles(plot_kwargs, 'node', groups, ('s', 'node_s')) rects = [Rectangle(**rect) for rect in plot_args['rects']] if 'vmin' in node_opts: node_opts['clim'] = node_opts.pop('vmin'), node_opts.pop('vmax') if 'c' in node_opts: node_opts['array'] = node_opts.pop('c') artists['rects'] = ax.add_collection( PatchCollection(rects, **node_opts)) plot_kwargs['label_text_font_size'] = fontsize artists['labels'] = self._update_labels(ax, plot_args, plot_kwargs) return artists def update_handles(self, key, axis, element, ranges, style): data, style, axis_kwargs = self.get_data(element, ranges, style) self._update_nodes(element, data, style) self._update_edges(element, data, style) self.handles['labels'] = self._update_labels(axis, data, style) rects = self.handles['rects'] paths = [Rectangle(**r) for r in data['rects']] rects.set_paths(paths) if 'node_facecolors' in style: rects.set_facecolors(style['node_facecolors']) return axis_kwargs
class LayoutPlot(CompositePlot, GenericLayoutPlot): shared_axes = param.Boolean(default=True, doc=""" Whether axes should be shared across plots""") shared_datasource = param.Boolean(default=False, doc=""" Whether Elements drawing the data from the same object should share their Bokeh data source allowing for linked brushing and other linked behaviors.""") merge_tools = param.Boolean(default=True, doc=""" Whether to merge all the tools into a single toolbar""") tabs = param.Boolean(default=False, doc=""" Whether to display overlaid plots in separate panes""") toolbar = param.ObjectSelector( default='above', objects=["above", "below", "left", "right", None], doc=""" The toolbar location, must be one of 'above', 'below', 'left', 'right', None.""") def __init__(self, layout, keys=None, **params): super(LayoutPlot, self).__init__(layout, keys=keys, **params) self.layout, self.subplots, self.paths = self._init_layout(layout) if self.top_level: self.comm = self.init_comm() self.traverse(lambda x: setattr(x, 'comm', self.comm)) self.traverse(lambda x: attach_streams(self, x.hmap, 2), [GenericElementPlot]) def _init_layout(self, layout): # Situate all the Layouts in the grid and compute the gridspec # indices for all the axes required by each LayoutPlot. layout_count = 0 collapsed_layout = layout.clone(shared_data=False, id=layout.id) frame_ranges = self.compute_ranges(layout, None, None) frame_ranges = OrderedDict([ (key, self.compute_ranges(layout, key, frame_ranges)) for key in self.keys ]) layout_items = layout.grid_items() layout_dimensions = layout.kdims if isinstance(layout, NdLayout) else None layout_subplots, layouts, paths = {}, {}, {} for r, c in self.coords: # Get view at layout position and wrap in AdjointLayout key, view = layout_items.get((c, r) if self.transpose else (r, c), (None, None)) view = view if isinstance(view, AdjointLayout) else AdjointLayout( [view]) layouts[(r, c)] = view paths[r, c] = key # Compute the layout type from shape layout_lens = {1: 'Single', 2: 'Dual', 3: 'Triple'} layout_type = layout_lens.get(len(view), 'Single') # Get the AdjoinLayout at the specified coordinate positions = AdjointLayoutPlot.layout_dict[layout_type]['positions'] # Create temporary subplots to get projections types # to create the correct subaxes for all plots in the layout layout_key, _ = layout_items.get((r, c), (None, None)) if isinstance(layout, NdLayout) and layout_key: layout_dimensions = OrderedDict( zip(layout_dimensions, layout_key)) # Generate the axes and create the subplots with the appropriate # axis objects, handling any Empty objects. empty = isinstance(view.main, Empty) if empty or view.main is None: continue elif not view.traverse(lambda x: x, [Element]): self.warning('%s is empty, skipping subplot.' % view.main) continue else: layout_count += 1 subplot_data = self._create_subplots( view, positions, layout_dimensions, frame_ranges, num=0 if empty else layout_count) subplots, adjoint_layout = subplot_data # Generate the AdjointLayoutsPlot which will coordinate # plotting of AdjointLayouts in the larger grid plotopts = self.lookup_options(view, 'plot').options layout_plot = AdjointLayoutPlot(adjoint_layout, layout_type, subplots, **plotopts) layout_subplots[(r, c)] = layout_plot if layout_key: collapsed_layout[layout_key] = adjoint_layout return collapsed_layout, layout_subplots, paths def _create_subplots(self, layout, positions, layout_dimensions, ranges, num=0): """ Plot all the views contained in the AdjointLayout Object using axes appropriate to the layout configuration. All the axes are supplied by LayoutPlot - the purpose of the call is to invoke subplots with correct options and styles and hide any empty axes as necessary. """ subplots = {} adjoint_clone = layout.clone(shared_data=False, id=layout.id) main_plot = None for pos in positions: # Pos will be one of 'main', 'top' or 'right' or None element = layout.get(pos, None) if element is None or not element.traverse(lambda x: x, [Element, Empty]): continue subplot_opts = dict(adjoined=main_plot) # Options common for any subplot vtype = element.type if isinstance(element, HoloMap) else element.__class__ plot_type = Store.registry[self.renderer.backend].get(vtype, None) plotopts = self.lookup_options(element, 'plot').options side_opts = {} if pos != 'main': plot_type = AdjointLayoutPlot.registry.get(vtype, plot_type) if pos == 'right': yaxis = 'right-bare' if plot_type and 'bare' in plot_type.yaxis else 'right' width = plot_type.width if plot_type else 0 side_opts = dict(height=main_plot.height, yaxis=yaxis, width=width, invert_axes=True, labelled=['y'], xticks=1, xaxis=main_plot.xaxis) else: xaxis = 'top-bare' if plot_type and 'bare' in plot_type.xaxis else 'top' height = plot_type.height if plot_type else 0 side_opts = dict(width=main_plot.width, xaxis=xaxis, height=height, labelled=['x'], yticks=1, yaxis=main_plot.yaxis) # Override the plotopts as required # Customize plotopts depending on position. plotopts = dict(side_opts, **plotopts) plotopts.update(subplot_opts) if vtype is Empty: subplots[pos] = None continue elif plot_type is None: self.warning( "Bokeh plotting class for %s type not found, object will " "not be rendered." % vtype.__name__) continue num = num if len(self.coords) > 1 else 0 subplot = plot_type(element, keys=self.keys, dimensions=self.dimensions, layout_dimensions=layout_dimensions, ranges=ranges, subplot=True, uniform=self.uniform, layout_num=num, renderer=self.renderer, **dict({'shared_axes': self.shared_axes}, **plotopts)) subplots[pos] = subplot if isinstance(plot_type, type) and issubclass( plot_type, GenericCompositePlot): adjoint_clone[pos] = subplots[pos].layout else: adjoint_clone[pos] = subplots[pos].hmap if pos == 'main': main_plot = subplot return subplots, adjoint_clone def initialize_plot(self, plots=None, ranges=None): ranges = self.compute_ranges(self.layout, self.keys[-1], None) passed_plots = [] if plots is None else plots plots = [[] for _ in range(self.rows)] tab_titles = {} insert_rows, insert_cols = [], [] for r, c in self.coords: subplot = self.subplots.get((r, c), None) if subplot is not None: shared_plots = passed_plots if self.shared_axes else None subplots = subplot.initialize_plot(ranges=ranges, plots=shared_plots) # Computes plotting offsets depending on # number of adjoined plots offset = sum(r >= ir for ir in insert_rows) if len(subplots) > 2: # Add pad column in this position insert_cols.append(c) if r not in insert_rows: # Insert and pad marginal row if none exists plots.insert(r + offset, [None for _ in range(len(plots[r]))]) # Pad previous rows for ir in range(r): plots[ir].insert(c + 1, None) # Add to row offset insert_rows.append(r) offset += 1 # Add top marginal plots[r + offset - 1] += [subplots.pop(-1), None] elif len(subplots) > 1: # Add pad column in this position insert_cols.append(c) # Pad previous rows for ir in range(r): plots[r].insert(c + 1, None) # Pad top marginal if one exists if r in insert_rows: plots[r + offset - 1] += 2 * [None] else: # Pad top marginal if one exists if r in insert_rows: plots[r + offset - 1] += [None] * (1 + (c in insert_cols)) plots[r + offset] += subplots if len(subplots) == 1 and c in insert_cols: plots[r + offset].append(None) passed_plots.append(subplots[0]) if self.tabs: title = subplot.subplots['main']._format_title( self.keys[-1], dimensions=False) if not title: title = ' '.join(self.paths[r, c]) tab_titles[r, c] = title else: plots[r + offset] += [empty_plot(0, 0)] # Replace None types with empty plots # to avoid bokeh bug plots = layout_padding(plots, self.renderer) # Wrap in appropriate layout model kwargs = dict(sizing_mode=self.sizing_mode) if self.tabs: panels = [ Panel(child=child, title=str(tab_titles.get((r, c)))) for r, row in enumerate(plots) for c, child in enumerate(row) if child is not None ] layout_plot = Tabs(tabs=panels) else: plots = filter_toolboxes(plots) plots, width = pad_plots(plots) layout_plot = gridplot(children=plots, width=width, toolbar_position=self.toolbar, merge_tools=self.merge_tools, **kwargs) title = self._get_title(self.keys[-1]) if title: self.handles['title'] = title layout_plot = Column(title, layout_plot, **kwargs) self._update_callbacks(layout_plot) self.handles['plot'] = layout_plot self.handles['plots'] = plots if self.shared_datasource: self.sync_sources() self.drawn = True return self.handles['plot'] @update_shared_sources def update_frame(self, key, ranges=None): """ Update the internal state of the Plot to represent the given key tuple (where integers represent frames). Returns this state. """ ranges = self.compute_ranges(self.layout, key, ranges) for r, c in self.coords: subplot = self.subplots.get((r, c), None) if subplot is not None: subplot.update_frame(key, ranges) title = self._get_title(key) if title: self.handles['title'] = title
class Test(param.Parameterized): a = param.ObjectSelector()
class BasicDashboardComponents(param.Parameterized): # variables to be displayed by widgets and set by the user: # select data column for scatter plot x-axis: X = param.ObjectSelector(default=None, objects=[], label='x axis data') # select data column for scatter plot y-axis: Y = param.ObjectSelector(default=None, objects=[], label='y axis data') # select column by which to filter the dataframe: cutoff_by_column = param.ObjectSelector(default=None, objects=[]) # set cutoff value by which to filter (keeps rows with value >=cutoff_value: cutoff_value = param.Number(default=0, bounds=(0, 120), allow_None=True) # select column for boxplot view: var_to_inspect = param.ObjectSelector(default=None, objects=[]) def __init__(self, df, *args, **kwargs): self.df = df super(type(self), self).__init__(*args, **kwargs) # update parameters values based on instance external inputs: self.X = self.df.columns.tolist()[ 0] # set the default value of the widget self.param.X.objects = self.df.columns.tolist( ) # set the list of objects to select from in the widget self.Y = self.df.columns.tolist()[1] self.param.Y.objects = self.df.columns.tolist() self.cutoff_by_column = self.df.columns.tolist()[0] self.param.cutoff_by_column.objects = self.df.columns.tolist() self.var_to_inspect = self.df.columns.tolist()[0] self.param.var_to_inspect.objects = self.df.columns.tolist() def filter_data_rows(self): ''' filter the dataframe rows by a cutoff_value and a column set by the user. :return: dataframe where the values in column "cutoff_by_column" >= "cutoff_value" ''' data = self.df.copy() data = utils.filter_df_rows(df=data, cutoff_by_column=self.cutoff_by_column, cutoff_value=self.cutoff_value) return data def metrics_table_view(self): ''' :return: dataframe of regression performance metrics (mape, mdape, mse, rmse, R2) wrapped by Panel package ''' data = self.filter_data_rows() metrics_dict = utils.calc_metrics_regression( data['actual'].to_numpy(), data['predicted'].to_numpy()) metrics_df = pd.DataFrame.from_dict(metrics_dict, orient='index').T return pn.pane.DataFrame(metrics_df, width=1350) def scatter_view(self): ''' create a scatter plot using the filtered dataframe (output of self.filter_data_rows) and where x and y axes are dataframe columns set by the user via self.x and self.y :return: a Plotly scatter plot wrapped by Panel package ''' data = self.filter_data_rows() array_x = data[self.X].to_numpy() array_y = data[self.Y].to_numpy() fig = utils.plot_scatter(x=array_x, y=array_y, add_unit_line=True, add_R2=True, layout_kwargs=dict(title='', xaxis_title=self.X, yaxis_title=self.Y)) return pn.pane.Plotly( fig) # pn.pane.Plotly is the Panel wrapper for Plotly figures def error_boxplot_view(self): ''' create a boxplot using the filtered dataframe (output of self.filter_data_rows) and where the data is dataframe column set by the user via self.var_to_inspect. :return: a Plotly boxplot wrapped by Panel package ''' data = self.filter_data_rows() fig = px.box(data, y=self.var_to_inspect, color_discrete_sequence=['green']) return pn.pane.Plotly( fig, width=400 ) # pn.pane.Plotly is the Panel wrapper for Plotly figures def error_summary_stats_table(self): data = self.filter_data_rows() stats_df = data.describe( percentiles=[0.025, 0.2, 0.3, 0.5, 0.7, 0.8, 0.975]).T stats_df = stats_df.round(3) return pn.pane.DataFrame(stats_df, width=1350) def wrap_param_var_to_inspect(self): ''' change the default widget of self.var_to_inspect from drop-down menu to Radio-button. :return: a widget wrapped by Panel package ''' return pn.Param( self.param, parameters=[ 'var_to_inspect' ], # pn.Param is a wrapper of Panel package for Param objects. name='Choose variable to inspect distribution', show_name=True, widgets={'var_to_inspect': { 'type': pn.widgets.RadioButtonGroup }}, width=200) @param.depends('cutoff_by_column', watch=True) def _update_cutoff_value(self): ''' an update method to dynamically change the range of self.cutoff_value based on the user selection of self.cutoff_by_column. This method is triggered each time the chosen 'cutoff_by_column' change ''' self.param['cutoff_value'].bounds = ( self.df[self.cutoff_by_column].min(), self.df[self.cutoff_by_column].max()) self.cutoff_value = self.df[self.cutoff_by_column].min()