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.""") show_legend = param.Boolean(default=False, doc=""" Adds a legend based on the entries of the middle-right plot""") 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.ClassSelector(default=120, class_=(int, tuple), doc=""" Defines the width and height of each plot in the grid, either as a tuple specifying width and height or an integer for a square plot.""") 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.traverse(lambda x: attach_streams(self, x.hmap, 2), [GenericElementPlot]) if 'axis_offset' in params: self.param.warning("GridPlot axis_offset option is deprecated " "since 1.12.0 since subplots are now sized " "correctly and therefore no longer require " "an offset.") def _create_subplots(self, layout, ranges): if isinstance(self.plot_size, tuple): width, height = self.plot_size else: width, height = self.plot_size, self.plot_size subplots = OrderedDict() frame_ranges = self.compute_ranges(layout, None, ranges) keys = self.keys[:1] if self.dynamic else self.keys frame_ranges = OrderedDict([ (key, self.compute_ranges(layout, key, frame_ranges)) for key in 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 if type(view) in (Layout, NdLayout): raise SkipRendering("Cannot plot nested Layouts.") if not displayable(view): view = collate(view) # Create axes kwargs = {} if width is not None: kwargs['frame_width'] = width if height is not None: kwargs['frame_height'] = height if c == 0: kwargs['align'] = 'end' if c == 0 and r != 0: kwargs['xaxis'] = None if c != 0 and r == 0: kwargs['yaxis'] = None if r != 0 and c != 0: kwargs['xaxis'] = None kwargs['yaxis'] = None if 'border' not in kwargs: kwargs['border'] = 3 if self.show_legend and c == (self.cols - 1) and r == (self.rows - 1): kwargs['show_legend'] = True kwargs['legend_position'] = 'right' else: 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.param.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], merge_tools=self.merge_tools, sizing_mode=self.sizing_mode, toolbar_location=self.toolbar) plot = self._make_axes(plot) title = self._get_title_div(self.keys[-1]) if title: plot = Column(title, plot) self.handles['title'] = title self.handles['plot'] = plot self.handles['plots'] = plots self._update_callbacks(plot) if self.shared_datasource: self.sync_sources() if self.top_level: self.init_links() 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 keys = self.layout.keys(full_grid=True) 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([wrap_tuple(k)[0] for k in keys])) 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([k[1] for k in keys])) 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: x_axis.margin = (0, 0, 0, 50) r1, r2 = r1[::-1], r2[::-1] plot = gridplot([r1, r2]) elif y_axis: models = [y_axis, plot] if self.shared_yaxis: models = models[::-1] plot = Row(*models) elif x_axis: models = [plot, x_axis] if self.shared_xaxis: models = models[::-1] plot = Column(*models) 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_div(key) if title: self.handles['title']
class MPLPlot(DimensionedPlot): """ An MPLPlot object draws a matplotlib figure object when called or indexed but can also return a matplotlib animation object as appropriate. MPLPlots take element objects such as Image, Contours or Points as inputs and plots them in the appropriate format using matplotlib. As HoloMaps are supported, all plots support animation via the anim() method. """ renderer = MPLRenderer sideplots = {} fig_alpha = param.Number(default=1.0, bounds=(0, 1), doc=""" Alpha of the overall figure background.""") fig_bounds = param.NumericTuple(default=(0.15, 0.15, 0.85, 0.85), doc=""" The bounds of the overall figure as a 4-tuple of the form (left, bottom, right, top), defining the size of the border around the subplots.""") fig_inches = param.Parameter(default=4, doc=""" The overall matplotlib figure size in inches. May be set as an integer in which case it will be used to autocompute a size. Alternatively may be set with an explicit tuple or list, in which case it will be applied directly after being scaled by fig_size. If either the width or height is set to None, it will be computed automatically.""") fig_latex = param.Boolean(default=False, doc=""" Whether to use LaTeX text in the overall figure.""") fig_rcparams = param.Dict(default={}, doc=""" matplotlib rc parameters to apply to the overall figure.""") fig_size = param.Integer(default=100, bounds=(1, None), doc=""" Size relative to the supplied overall fig_inches in percent.""") finalize_hooks = param.HookList(default=[], doc=""" Optional list of hooks called when finalizing an axis. The hook is passed the full set of plot handles and the displayed object.""") sublabel_format = param.String(default=None, allow_None=True, doc=""" Allows labeling the subaxes in each plot with various formatters including {Alpha}, {alpha}, {numeric} and {roman}.""") sublabel_position = param.NumericTuple(default=(-0.35, 0.85), doc=""" Position relative to the plot for placing the optional subfigure label.""" ) sublabel_size = param.Number(default=18, doc=""" Size of optional subfigure label.""") projection = param.ObjectSelector(default=None, objects=['3d', 'polar', None], doc=""" The projection of the plot axis, default of None is equivalent to 2D plot, 3D and polar plots are also supported.""") show_frame = param.Boolean(default=True, doc=""" Whether or not to show a complete frame around the plot.""") fontsize = param.Parameter(default=None, allow_None=True, doc=""" Specifies various fontsizes of the displayed text. By default, the fontsize is determined by matplotlib (via rcparams) but if set to an integer, this is the fontsize of all text except for tick labels (and subfigure labels in Layouts). Finer control is available by supplying a dictionary where any unmentioned keys reverts to the default sizes, e.g: {'ticks':20, 'title':15, 'ylabel':5, 'xlabel':5}""") def __init__(self, fig=None, axis=None, **params): self._create_fig = True super(MPLPlot, self).__init__(**params) # List of handles to matplotlib objects for animation update scale = self.fig_size / 100. if isinstance(self.fig_inches, (tuple, list)): self.fig_inches = [ None if i is None else i * scale for i in self.fig_inches ] else: self.fig_inches *= scale fig, axis = self._init_axis(fig, axis) self.handles['fig'] = fig self.handles['axis'] = axis def _fontsize(self, key, label='fontsize', common=True): """ To be used as kwargs e.g: **self._fontsize('title') """ if not self.fontsize: return {} if isinstance(self.fontsize, dict): if key not in self.fontsize: return {} else: return {label: self.fontsize[key]} return {label: self.fontsize} if common else {} def _init_axis(self, fig, axis): """ Return an axis which may need to be initialized from a new figure. """ if not fig and self._create_fig: rc_params = self.fig_rcparams if self.fig_latex: rc_params['text.usetex'] = True with mpl.rc_context(rc=rc_params): fig = plt.figure() l, b, r, t = self.fig_bounds inches = self.fig_inches fig.subplots_adjust(left=l, bottom=b, right=r, top=t) fig.patch.set_alpha(self.fig_alpha) if isinstance(inches, (tuple, list)): inches = list(inches) if inches[0] is None: inches[0] = inches[1] elif inches[1] is None: inches[1] = inches[0] fig.set_size_inches(list(inches)) else: fig.set_size_inches([inches, inches]) axis = fig.add_subplot(111, projection=self.projection) axis.set_aspect('auto') return fig, axis def _subplot_label(self, axis): layout_num = self.layout_num if self.subplot else 1 if self.sublabel_format and not self.adjoined and layout_num > 0: from mpl_toolkits.axes_grid1.anchored_artists import AnchoredText labels = {} if '{Alpha}' in self.sublabel_format: labels['Alpha'] = int_to_alpha(layout_num - 1) elif '{alpha}' in self.sublabel_format: labels['alpha'] = int_to_alpha(layout_num - 1, upper=False) elif '{numeric}' in self.sublabel_format: labels['numeric'] = self.layout_num elif '{Roman}' in self.sublabel_format: labels['Roman'] = int_to_roman(layout_num) elif '{roman}' in self.sublabel_format: labels['roman'] = int_to_roman(layout_num).lower() at = AnchoredText(self.sublabel_format.format(**labels), loc=3, bbox_to_anchor=self.sublabel_position, frameon=False, prop=dict(size=self.sublabel_size, weight='bold'), bbox_transform=axis.transAxes) at.patch.set_visible(False) axis.add_artist(at) def _finalize_axis(self, key): """ General method to finalize the axis and plot. """ if 'title' in self.handles: self.handles['title'].set_visible(self.show_title) self.drawn = True if self.subplot: return self.handles['axis'] else: fig = self.handles['fig'] plt.close(fig) return fig @property def state(self): return self.handles['fig'] def anim(self, start=0, stop=None, fps=30): """ Method to return a matplotlib animation. The start and stop frames may be specified as well as the fps. """ figure = self.initialize_plot() anim = animation.FuncAnimation(figure, self.update_frame, frames=self.keys, interval=1000.0 / fps) # Close the figure handle plt.close(figure) return anim def update(self, key): rc_params = self.fig_rcparams if self.fig_latex: rc_params['text.usetex'] = True mpl.rcParams.update(rc_params) if len(self) == 1 and key == 0 and not self.drawn: return self.initialize_plot() return self.__getitem__(key)
class LayoutPlot(GenericLayoutPlot, CompositePlot): """ A LayoutPlot accepts either a Layout or a NdLayout and displays the elements in a cartesian grid in scanline order. """ aspect_weight = param.Number(default=0, doc=""" Weighting of the individual aspects when computing the Layout grid aspects and overall figure size.""") fig_bounds = param.NumericTuple(default=(0.05, 0.05, 0.95, 0.95), doc=""" The bounds of the figure as a 4-tuple of the form (left, bottom, right, top), defining the size of the border around the subplots.""") tight = param.Boolean(default=False, doc=""" Tightly fit the axes in the layout within the fig_bounds and tight_padding.""") tight_padding = param.Parameter(default=3, doc=""" Integer or tuple specifying the padding in inches in a tight layout.""") hspace = param.Number(default=0.5, doc=""" Specifies the space between horizontally adjacent elements in the grid. Default value is set conservatively to avoid overlap of subplots.""") vspace = param.Number(default=0.1, doc=""" Specifies the space between vertically adjacent elements in the grid. Default value is set conservatively to avoid overlap of subplots.""") fontsize = param.Parameter(default={'title': 16}, allow_None=True) def __init__(self, layout, **params): super(LayoutPlot, self).__init__(layout=layout, **params) self.subplots, self.subaxes, self.layout = self._compute_gridspec( layout) def _compute_gridspec(self, layout): """ Computes the tallest and widest cell for each row and column by examining the Layouts in the GridSpace. The GridSpec is then instantiated and the LayoutPlots are configured with the appropriate embedded layout_types. The first element of the returned tuple is a dictionary of all the LayoutPlots indexed by row and column. The second dictionary in the tuple supplies the grid indicies needed to instantiate the axes for each LayoutPlot. """ layout_items = layout.grid_items() layout_dimensions = layout.kdims if isinstance(layout, NdLayout) else None layouts = {} row_heightratios, col_widthratios = {}, {} col_aspects, row_aspects = defaultdict(lambda: [0, 0]), defaultdict( lambda: [0, 0]) for (r, c) in self.coords: # Get view at layout position and wrap in AdjointLayout _, view = layout_items.get((r, c), (None, None)) layout_view = view if isinstance( view, AdjointLayout) else AdjointLayout([view]) layouts[(r, c)] = layout_view # Compute shape of AdjointLayout element layout_lens = {1: 'Single', 2: 'Dual', 3: 'Triple'} layout_type = layout_lens[len(layout_view)] hidx = 0 # Get aspects main = layout_view.main main = main.last if isinstance(main, HoloMap) else main main_options = self.lookup_options(main, 'plot').options if main else {} if main and not isinstance(main_options.get('aspect', 1), basestring): main_aspect = main_options.get('aspect', 1) main_aspect = self.aspect_weight * main_aspect + 1 - self.aspect_weight else: main_aspect = 1 if layout_type == 'Triple': row_aspect = [0.25, 1. / main_aspect] else: row_aspect = [1. / main_aspect, 0] if layout_type in ['Dual', 'Triple']: col_aspect = [main_aspect, 0.25] else: col_aspect = [main_aspect, 0] # Compute width and height ratios width_ratios = AdjointLayoutPlot.layout_dict[layout_type][ 'width_ratios'][:] height_ratios = AdjointLayoutPlot.layout_dict[layout_type][ 'height_ratios'][:] if not isinstance(main_aspect, (basestring, type(None))): width_ratios[0] = (width_ratios[0] * main_aspect) height_ratios[0] = (height_ratios[hidx] * 1. / main_aspect) layout_shape = (len(width_ratios), len(height_ratios)) # For each row and column record the width and height ratios # of the LayoutPlot with the most horizontal or vertical splits # and largest aspect if layout_shape[1] > row_heightratios.get(r, (0, None))[0]: row_heightratios[r] = [layout_shape[1], height_ratios] if height_ratios[hidx] > row_heightratios[r][1][hidx]: row_heightratios[r][1][hidx] = height_ratios[hidx] if layout_shape[0] > col_widthratios.get(c, (0, None))[0]: col_widthratios[c] = (layout_shape[0], width_ratios) if width_ratios[0] > col_widthratios[c][1][0]: col_widthratios[c][1][0] = width_ratios[0] for i in range(2): if col_aspect[i] > col_aspects.get(c, [0, 0])[i]: col_aspects[c][i] = col_aspect[i] if row_aspect[i] > row_aspects.get(r, [0, 0])[i]: row_aspects[r][i] = row_aspect[i] # In order of row/column collect the largest width and height ratios height_ratios = [v[1] for k, v in sorted(row_heightratios.items())] width_ratios = [v[1] for k, v in sorted(col_widthratios.items())] col_aspect_ratios = [v for k, v in sorted(col_aspects.items())] row_aspect_ratios = [v for k, v in sorted(row_aspects.items())] # Compute the number of rows and cols cols = np.sum([len(wr) for wr in width_ratios]) rows = np.sum([len(hr) for hr in height_ratios]) # Flatten the width and height ratio lists wr_list = [wr for wrs in width_ratios for wr in wrs] hr_list = [hr for hrs in height_ratios for hr in hrs] # Compute and set the plot size if not explicitly supplied col_ars = [ar for ars in col_aspect_ratios for ar in ars] row_ars = [ar for ars in row_aspect_ratios for ar in ars] width = len(col_ars[::2]) + sum(col_ars[1::2]) yscale = sum(col_ars) / sum(row_ars) xinches, yinches = None, None if not isinstance(self.fig_inches, (tuple, list)): xinches = self.fig_inches * width yinches = xinches / yscale elif self.fig_inches[0] is None: xinches = self.fig_inches[1] * yscale yinches = self.fig_inches[1] elif self.fig_inches[1] is None: xinches = self.fig_inches[0] yinches = self.fig_inches[0] / yscale if xinches and yinches: self.handles['fig'].set_size_inches([xinches, yinches]) self.gs = gridspec.GridSpec(rows, cols, width_ratios=wr_list, height_ratios=hr_list, wspace=self.hspace, hspace=self.vspace) # Situate all the Layouts in the grid and compute the gridspec # indices for all the axes required by each LayoutPlot. gidx = 0 layout_count = 0 tight = self.tight 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_subplots, layout_axes = {}, {} for r, c in self.coords: # Compute the layout type from shape wsplits = len(width_ratios[c]) hsplits = len(height_ratios[r]) if (wsplits, hsplits) == (1, 1): layout_type = 'Single' elif (wsplits, hsplits) == (2, 1): layout_type = 'Dual' elif (wsplits, hsplits) == (1, 2): layout_type = 'Embedded Dual' elif (wsplits, hsplits) == (2, 2): layout_type = 'Triple' # Get the AdjoinLayout at the specified coordinate view = layouts[(r, c)] 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 _, _, projs = self._create_subplots(layouts[(r, c)], positions, None, frame_ranges, create=False) gidx, gsinds = self.grid_situate(gidx, layout_type, cols) 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. obj = layouts[(r, c)] empty = isinstance(obj.main, Empty) if empty: obj = AdjointLayout([]) else: layout_count += 1 subaxes = [ plt.subplot(self.gs[ind], projection=proj) for ind, proj in zip(gsinds, projs) ] subplot_data = self._create_subplots( obj, positions, layout_dimensions, frame_ranges, dict(zip(positions, subaxes)), num=0 if empty else layout_count) subplots, adjoint_layout, _ = subplot_data layout_axes[(r, c)] = subaxes # 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, subaxes, subplots, fig=self.handles['fig'], **plotopts) layout_subplots[(r, c)] = layout_plot tight = not any( type(p) is GridPlot for p in layout_plot.subplots.values()) and tight if layout_key: collapsed_layout[layout_key] = adjoint_layout # Apply tight layout if enabled and incompatible # GridPlot isn't present. if tight: if isinstance(self.tight_padding, (tuple, list)): wpad, hpad = self.tight_padding padding = dict(w_pad=wpad, h_pad=hpad) else: padding = dict(w_pad=self.tight_padding, h_pad=self.tight_padding) self.gs.tight_layout(self.handles['fig'], rect=self.fig_bounds, **padding) # Create title handle if self.show_title and len(self.coords) > 1: title = self.handles['fig'].suptitle('', **self._fontsize('title')) self.handles['title'] = title return layout_subplots, layout_axes, collapsed_layout def grid_situate(self, current_idx, layout_type, subgrid_width): """ Situate the current AdjointLayoutPlot in a LayoutPlot. The LayoutPlot specifies a layout_type into which the AdjointLayoutPlot must be embedded. This enclosing layout is guaranteed to have enough cells to display all the views. Based on this enforced layout format, a starting index supplied by LayoutPlot (indexing into a large gridspec arrangement) is updated to the appropriate embedded value. It will also return a list of gridspec indices associated with the all the required layout axes. """ # Set the layout configuration as situated in a NdLayout if layout_type == 'Single': start, inds = current_idx + 1, [current_idx] elif layout_type == 'Dual': start, inds = current_idx + 2, [current_idx, current_idx + 1] bottom_idx = current_idx + subgrid_width if layout_type == 'Embedded Dual': bottom = ((current_idx + 1) % subgrid_width) == 0 grid_idx = (bottom_idx if bottom else current_idx) + 1 start, inds = grid_idx, [current_idx, bottom_idx] elif layout_type == 'Triple': bottom = ((current_idx + 2) % subgrid_width) == 0 grid_idx = (bottom_idx if bottom else current_idx) + 2 start, inds = grid_idx, [ current_idx, current_idx + 1, bottom_idx, bottom_idx + 1 ] return start, inds def _create_subplots(self, layout, positions, layout_dimensions, ranges, axes={}, num=1, create=True): """ 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 = {} projections = [] adjoint_clone = layout.clone(shared_data=False, id=layout.id) subplot_opts = dict(show_title=False, adjoined=layout) for pos in positions: # Pos will be one of 'main', 'top' or 'right' or None view = layout.get(pos, None) ax = axes.get(pos, None) if view is None: projections.append(None) continue # Determine projection type for plot components = view.traverse(lambda x: x) projs = [ '3d' if isinstance(c, Element3D) else self.lookup_options( c, 'plot').options.get('projection', None) for c in components ] projs = [p for p in projs if p is not None] if len(set(projs)) > 1: raise Exception( "A single axis may only be assigned one projection type") elif projs: projections.append(projs[0]) else: projections.append(None) if not create: continue # Customize plotopts depending on position. plotopts = self.lookup_options(view, 'plot').options # Options common for any subplot override_opts = {} sublabel_opts = {} if pos == 'main': own_params = self.get_param_values(onlychanged=True) sublabel_opts = { k: v for k, v in own_params if 'sublabel_' in k } if not isinstance(view, GridSpace): override_opts = dict(aspect='square') elif pos == 'right': right_opts = dict(orientation='vertical', xaxis=None, yaxis='left') override_opts = dict(subplot_opts, **right_opts) elif pos == 'top': top_opts = dict(xaxis='bottom', yaxis=None) override_opts = dict(subplot_opts, **top_opts) # Override the plotopts as required plotopts = dict(sublabel_opts, **plotopts) plotopts.update(override_opts, fig=self.handles['fig']) vtype = view.type if isinstance(view, HoloMap) else view.__class__ if isinstance(view, GridSpace): plotopts['create_axes'] = ax is not None if pos == 'main': plot_type = Store.registry['matplotlib'][vtype] else: plot_type = MPLPlot.sideplots[vtype] num = num if len(self.coords) > 1 else 0 subplots[pos] = plot_type(view, axis=ax, keys=self.keys, dimensions=self.dimensions, layout_dimensions=layout_dimensions, ranges=ranges, subplot=True, uniform=self.uniform, layout_num=num, **plotopts) if isinstance(view, (Element, HoloMap, CompositeOverlay)): adjoint_clone[pos] = subplots[pos].map else: adjoint_clone[pos] = subplots[pos].layout return subplots, adjoint_clone, projections def update_handles(self, axis, view, key, ranges=None): """ Should be called by the update_frame class to update any handles on the plot. """ if self.show_title and 'title' in self.handles and len( self.coords) > 1: self.handles['title'].set_text(self._format_title(key)) def initialize_plot(self): axis = self.handles['axis'] self.update_handles(axis, None, self.keys[-1]) ranges = self.compute_ranges(self.layout, self.keys[-1], None) for subplot in self.subplots.values(): subplot.initialize_plot(ranges=ranges) return self._finalize_axis(None)
class Dimension(param.Parameterized): """ Dimension objects are used to specify some important general features that may be associated with a collection of values. For instance, a Dimension may specify that a set of numeric values actually correspond to 'Height' (dimension name), in units of meters, with a descriptive label 'Height of adult males'. All dimensions object have a name that identifies them and a label containing a suitable description. If the label is not explicitly specified it matches the name. These two parameters define the core identity of the dimension object and must match if two dimension objects are to be considered equivalent. All other parameters are considered optional metadata and are not used when testing for equality. Unlike all the other parameters, these core parameters can be used to construct a Dimension object from a tuple. This format is sufficient to define an identical Dimension: Dimension('a', label='Dimension A') == Dimension(('a', 'Dimension A')) Everything else about a dimension is considered to reflect non-semantic preferences. Examples include the default value (which may be used in a visualization to set an initial slider position), how the value is to rendered as text (which may be used to specify the printed floating point precision) or a suitable range of values to consider for a particular analysis. Units ----- Full unit support with automated conversions are on the HoloViews roadmap. Once rich unit objects are supported, the unit (or more specifically the type of unit) will be part of the core dimension specification used to establish equality. Until this feature is implemented, there are two auxiliary parameters that hold some partial information about the unit: the name of the unit and whether or not it is cyclic. The name of the unit is used as part of the pretty-printed representation and knowing whether it is cyclic is important for certain operations. """ name = param.String(doc=""" Short name associated with the Dimension, such as 'height' or 'weight'. Valid Python identifiers make good names, because they can be used conveniently as a keyword in many contexts.""") label = param.String(default=None, doc=""" Unrestricted label used to describe the dimension. A label should succinctly describe the dimension and may contain any characters, including Unicode and LaTeX expression.""") cyclic = param.Boolean(default=False, doc=""" Whether the range of this feature is cyclic such that the maximum allowed value (defined by the range parameter) is continuous with the minimum allowed value.""") value_format = param.Callable(default=None, doc=""" Formatting function applied to each value before display.""") range = param.Tuple(default=(None, None), doc=""" Specifies the minimum and maximum allowed values for a Dimension. None is used to represent an unlimited bound.""") soft_range = param.Tuple(default=(None, None), doc=""" Specifies a minimum and maximum reference value, which may be overridden by the data.""") type = param.Parameter(default=None, doc=""" Optional type associated with the Dimension values. The type may be an inbuilt constructor (such as int, str, float) or a custom class object.""") step = param.Number(default=None, doc=""" Optional floating point step specifying how frequently the underlying space should be sampled. May be used to define a discrete sampling of over the range.""") unit = param.String(default=None, allow_None=True, doc=""" Optional unit string associated with the Dimension. For instance, the string 'm' may be used represent units of meters and 's' to represent units of seconds.""") values = param.List(default=[], doc=""" Optional specification of the allowed value set for the dimension that may also be used to retain a categorical ordering.""") # Defines default formatting by type type_formatters = {} unit_format = ' ({unit})' presets = {} # A dictionary-like mapping name, (name,) or # (name, unit) to a preset Dimension object def __init__(self, spec, **params): """ Initializes the Dimension object with the given name. """ if 'name' in params: raise KeyError('Dimension name must only be passed as the positional argument') if isinstance(spec, Dimension): existing_params = dict(spec.get_param_values()) elif (spec, params.get('unit', None)) in self.presets.keys(): preset = self.presets[(str(spec), str(params['unit']))] existing_params = dict(preset.get_param_values()) elif spec in self.presets: existing_params = dict(self.presets[spec].get_param_values()) elif (spec,) in self.presets: existing_params = dict(self.presets[(spec,)].get_param_values()) else: existing_params = {} all_params = dict(existing_params, **params) if isinstance(spec, tuple): name, label = spec all_params['name'] = name all_params['label'] = label if 'label' in params and (label != params['label']): if params['label'] != label: self.warning('Using label as supplied by keyword ({!r}), ignoring ' 'tuple value {!r}'.format(params['label'], label)) all_params['label'] = params['label'] elif isinstance(spec, basestring): all_params['name'] = spec all_params['label'] = params.get('label', spec) if all_params['name'] == '': raise ValueError('Dimension name cannot be the empty string') if all_params['label'] in ['', None]: raise ValueError('Dimension label cannot be None or the empty string') values = params.get('values', []) if isinstance(values, basestring) and values == 'initial': self.warning("The 'initial' string for dimension values is no longer supported.") values = [] all_params['values'] = list(unique_array(values)) super(Dimension, self).__init__(**all_params) @property def spec(self): "Returns the corresponding tuple specification" return (self.name, self.label) def __call__(self, spec=None, **overrides): "Aliased to clone method. To be deprecated in 2.0" return self.clone(spec=spec, **overrides) def clone(self, spec=None, **overrides): """ Derive a new Dimension that inherits existing parameters except for the supplied, explicit overrides """ settings = dict(self.get_param_values(onlychanged=True), **overrides) if spec is None: spec = (self.name, overrides.get('label', self.label)) if 'label' in overrides and isinstance(spec, basestring) : spec = (spec, overrides['label']) elif 'label' in overrides and isinstance(spec, tuple) : if overrides['label'] != spec[1]: self.warning('Using label as supplied by keyword ({!r}), ignoring ' 'tuple value {!r}'.format(overrides['label'], spec[1])) spec = (spec[0], overrides['label']) return self.__class__(spec, **{k:v for k,v in settings.items() if k not in ['name', 'label']}) def __hash__(self): """ The hash allows Dimension objects to be used as dictionary keys in Python 3. """ return hash(self.spec) def __setstate__(self, d): """ Compatibility for pickles before alias attribute was introduced. """ super(Dimension, self).__setstate__(d) self.label = self.name def __eq__(self, other): "Implements equals operator including sanitized comparison." if isinstance(other, Dimension): return self.spec == other.spec # For comparison to strings. Name may be sanitized. return other in [self.name, self.label, dimension_sanitizer(self.name)] def __ne__(self, other): "Implements not equal operator including sanitized comparison." return not self.__eq__(other) def __lt__(self, other): "Dimensions are sorted alphanumerically by name" return self.name < other.name if isinstance(other, Dimension) else self.name < other def __str__(self): return self.name def __repr__(self): return self.pprint() @property def pprint_label(self): "The pretty-printed label string for the Dimension" unit = ('' if self.unit is None else type(self.unit)(self.unit_format).format(unit=self.unit)) return bytes_to_unicode(self.label) + bytes_to_unicode(unit) def pprint(self): changed = dict(self.get_param_values(onlychanged=True)) if len(set([changed.get(k, k) for k in ['name','label']])) == 1: return 'Dimension({spec})'.format(spec=repr(self.name)) ordering = sorted( sorted(changed.keys()), key=lambda k: (- float('inf') if self.params(k).precedence is None else self.params(k).precedence)) kws = ", ".join('%s=%r' % (k, changed[k]) for k in ordering if k != 'name') return 'Dimension({spec}, {kws})'.format(spec=repr(self.name), kws=kws) def pprint_value(self, value): """ Applies the defined formatting to the value. """ own_type = type(value) if self.type is None else self.type formatter = (self.value_format if self.value_format else self.type_formatters.get(own_type)) if formatter: if callable(formatter): return formatter(value) elif isinstance(formatter, basestring): if isinstance(value, dt.datetime): return value.strftime(formatter) elif isinstance(value, np.datetime64): return dt64_to_dt(value).strftime(formatter) elif re.findall(r"\{(\w+)\}", formatter): return formatter.format(value) else: return formatter % value return unicode(bytes_to_unicode(value)) def pprint_value_string(self, value): """ Pretty prints the dimension name and value using the global title_format variable, including the unit string (if set). Numeric types are printed to the stated rounding level. """ unit = '' if self.unit is None else ' ' + bytes_to_unicode(self.unit) value = self.pprint_value(value) return title_format.format(name=bytes_to_unicode(self.label), val=value, unit=unit)
class GeoPlot(ProjectionPlot, ElementPlot): """ Plotting baseclass for geographic plots with a cartopy projection. """ apply_ranges = param.Boolean(default=False, doc=""" Do not use ranges to compute plot extents by default.""") projection = param.Parameter(default=ccrs.PlateCarree()) def __init__(self, element, **params): if 'projection' not in params: el = element.last if isinstance(element, HoloMap) else element params['projection'] = el.crs super(GeoPlot, self).__init__(element, **params) plot_opts = self.lookup_options(self.hmap.last, 'plot').options self.geographic = is_geographic(self.hmap.last) if 'aspect' not in plot_opts: self.aspect = 'equal' if self.geographic else 'square' def get_extents(self, element, ranges): """ Subclasses the get_extents method using the GeoAxes set_extent method to project the extents to the Elements coordinate reference system. """ ax = self.handles['axis'] extents = super(GeoPlot, self).get_extents(element, ranges) x0, y0, x1, y1 = extents if not self.geographic: return extents # If extent can't be determined autoscale, otherwise use # set_extent to convert coordinates to native coordinate # system if (None in extents or any(not np.isfinite(e) for e in extents) or x0 == x1 or y0 == y1): extents = None else: try: extents = project_extents((x0, y0, x1, y1), element.crs, self.projection) except: extents = (np.NaN, ) * 4 if extents: l, b, r, t = extents else: ax.autoscale_view() (l, r), (b, t) = ax.get_xlim(), ax.get_ylim() return l, b, r, t def teardown_handles(self): """ Delete artist handle so it can be redrawn. """ try: self.handles['artist'].remove() except ValueError: pass
class GenesVsCounts(OperationInterface): hue = param.Parameter(default=None) mode = param.ObjectSelector(default="scatter", objects=["scatter"]) @staticmethod def genewise_scatter(data, hue=None, gene_dim="Gene", sample_dim="Sample", count_dim="counts"): hvds = hv.Dataset(data.to_dataframe().reset_index()) kdims = [gene_dim, count_dim] vdims = [sample_dim] if hue: vdims += [hue] scatter = hv.Scatter(hvds, kdims=kdims, vdims=vdims) if hue is not None: overlays = { key: scatter.select(**{hue: key}) for key in scatter.data[hue].unique() } return hv.NdOverlay(overlays) return scatter # @staticmethod # def genewise_violin(data, hue=None, # gene_dim="Gene", sample_dim="Sample", count_dim="counts"): # hvds = hv.Dataset(data.to_dataframe().reset_index()) # # kdims = [gene_dim] # vdims = [count_dim] # # if hue: # kdims += [hue] # # violin = hv.Violin(hvds, kdims=kdims, vdims=vdims) # # if hue is not None: # overlays = {key: violin.select(**{hue: key}) # for key in violin.data[hue].unique()} # return hv.NdOverlay(overlays) # # return violin def process(self): self.set_param(gene_set_mode="intersection") if self.gene_set_collection is not None: self.set_param(selected_gene_sets=list( self.gene_set_collection.gene_sets.keys())) plotting_dims = [self.count_variable] if self.hue: plotting_dims += [self.hue] selected_subset = self.gem.data[plotting_dims].sel( self.get_selection_indexes()) modes = { "scatter": self.genewise_scatter, # "violin": self.genewise_violin, } return modes[self.mode](selected_subset, self.hue)
class DETRApp(param.Parameterized): "A Panel App for object detection using DE:TR:" title = param.String("DE:TR: Object Detection App") progress = param.Parameter() input_image_url = param.String(config.DEFAULT_URL, label="Input Image URL") run_detr = param.Action(label="Run DE:TR:") set_random_image = param.Action(label="Random Image") plot = param.Parameter() suppression_enabled = param.Boolean(config.SUPPRESSION_ENABLED, label="Enabled") suppression = param.Number( config.SUPPRESSION_DEFAULT, bounds=config.SUPPRESSION_BOUNDS, label="Non-maximum suppression (IoU)", ) confidence = param.Number(config.CONFIDENCE_DEFAULT, bounds=config.CONFIDENCE_BOUNDS, label="Confidence Treshold") view = param.Parameter() def __init__(self, **params): params["progress"], params["plot"], params["view"] = self._get_view() params["set_random_image"] = self._set_random_image params["run_detr"] = self._update_plot super().__init__(**params) self.transform, self.detr, self.device = get_transform_detr_and_device( ) self.run_detr() # pylint: disable=not-callable def _get_view(self): style = pn.pane.HTML(config.STYLE, width=0, height=0, margin=0, sizing_mode="fixed") description = pn.pane.Markdown(__doc__) progress = pn.widgets.Progress(bar_color="secondary", width=285, sizing_mode="fixed", margin=(0, 5, 10, 5)) progress.active = False app_bar = pn.Row( pn.pane.Markdown("# " + self.title, sizing_mode="stretch_width", margin=(None, None, None, 25)), sizing_mode="stretch_width", margin=(25, 5, 0, 5), css_classes=["app-bar"], ) top_selections = pn.Row( pn.Param( self, parameters=["input_image_url"], default_layout=pn.Row, show_name=False, ), pn.Param( self, parameters=["run_detr", "set_random_image"], widgets={"set_random_image": { "button_type": "success" }}, default_layout=pn.Row, show_name=False, width=300, margin=(14, 5, 5, 5), sizing_mode="fixed", ), ) top_selections = pn.Row( pn.Param( self, parameters=["input_image_url", "run_detr", "set_random_image"], widgets={ "set_random_image": { "button_type": "success", "align": "end", "width": 125, "sizing_mode": "fixed", }, "run_detr": { "align": "end", "width": 125, "sizing_mode": "fixed" }, }, default_layout=pn.Row, show_name=False, width=900, )) bottom_selections = pn.Column( pn.Param( self, parameters=["suppression", "confidence"], default_layout=pn.Row, show_name=False, ), pn.Param( self, parameters=["suppression_enabled"], show_name=False, ), ) plot = pn.pane.Plotly(height=600, config={"responsive": True}) app_view = pn.Column( style, description, app_bar, pn.Row(pn.Spacer(), progress), top_selections, plot, bottom_selections, ) return progress, plot, app_view def _set_random_image(self, _=None): urls = config.RANDOM_URLS current_url = self.input_image_url new_url = current_url while current_url == new_url: new_url = random.choice(urls) self.input_image_url = new_url self._update_plot() def _update_plot(self, _=None): self.progress.active = True self.plot.object = get_figure( apply_nms=self.suppression_enabled, iou=self.suppression, confidence=self.confidence, url=self.input_image_url, transform=self.transform, detr=self.detr, device=self.device, ) self.progress.active = False
class FileSelector(CompositeWidget): directory = param.String(default=os.getcwd(), doc=""" The directory to explore.""") file_pattern = param.String(default='*', doc=""" A glob-like pattern to filter the files.""") only_files = param.Boolean(default=False, doc=""" Whether to only allow selecting files.""") margin = param.Parameter(default=(5, 10, 20, 10), doc=""" Allows to create additional space around the component. May be specified as a two-tuple of the form (vertical, horizontal) or a four-tuple (top, right, bottom, left).""") show_hidden = param.Boolean(default=False, doc=""" Whether to show hidden files and directories (starting with a period).""") size = param.Integer(default=10, doc=""" The number of options shown at once (note this is the only way to control the height of this widget)""") refresh_period = param.Integer(default=None, doc=""" If set to non-None value indicates how frequently to refresh the directory contents in milliseconds.""") root_directory = param.String(default=None, doc=""" If set, overrides directory parameter as the root directory beyond which users cannot navigate.""") value = param.List(default=[], doc=""" List of selected files.""") _composite_type = Column def __init__(self, directory=None, **params): from ..pane import Markdown if directory is not None: params['directory'] = os.path.abspath( os.path.expanduser(directory)) if 'root_directory' in params: root = params['root_directory'] params['root_directory'] = os.path.abspath( os.path.expanduser(root)) if params.get('width') and params.get( 'height') and 'sizing_mode' not in params: params['sizing_mode'] = None super().__init__(**params) # Set up layout layout = { p: getattr(self, p) for p in Layoutable.param if p not in ('name', 'height', 'margin') and getattr(self, p) is not None } sel_layout = dict(layout, sizing_mode='stretch_both', height=None, margin=0) self._selector = CrossSelector(filter_fn=lambda p, f: fnmatch(f, p), size=self.size, **sel_layout) self._back = Button(name='◀', width=25, margin=(5, 10, 0, 0), disabled=True) self._forward = Button(name='▶', width=25, margin=(5, 10), disabled=True) self._up = Button(name='⬆', width=25, margin=(5, 10), disabled=True) self._directory = TextInput(value=self.directory, margin=(5, 10), width_policy='max') self._go = Button(name='⬇', disabled=True, width=25, margin=(5, 10, 0, 0)) self._reload = Button(name='↻', width=25, margin=(5, 15, 0, 10)) self._nav_bar = Row( self._back, self._forward, self._up, self._directory, self._go, self._reload, **dict(layout, width=None, margin=0, width_policy='max')) self._composite[:] = [self._nav_bar, Divider(margin=0), self._selector] self._selector._selected.insert( 0, Markdown('### Selected files', margin=0)) self._selector._unselected.insert( 0, Markdown('### File Browser', margin=0)) self.link(self._selector, size='size') # Set up state self._stack = [] self._cwd = None self._position = -1 self._update_files(True) # Set up callback self.link(self._directory, directory='value') self._selector.param.watch(self._update_value, 'value') self._go.on_click(self._update_files) self._reload.on_click(self._update_files) self._up.on_click(self._go_up) self._back.on_click(self._go_back) self._forward.on_click(self._go_forward) self._directory.param.watch(self._dir_change, 'value') self._selector._lists[False].param.watch(self._select, 'value') self._selector._lists[False].param.watch(self._filter_blacklist, 'options') self._periodic = PeriodicCallback(callback=self._refresh, period=self.refresh_period or 0) self.param.watch(self._update_periodic, 'refresh_period') if self.refresh_period: self._periodic.start() def _update_periodic(self, event): if event.new: self._periodic.period = event.new if not self._periodic.running: self._periodic.start() elif self._periodic.running: self._periodic.stop() @property def _root_directory(self): return self.root_directory or self.directory def _update_value(self, event): value = [ v for v in event.new if not self.only_files or os.path.isfile(v) ] self._selector.value = value self.value = value def _dir_change(self, event): path = os.path.abspath(os.path.expanduser(self._directory.value)) if not path.startswith(self._root_directory): self._directory.value = self._root_directory return elif path != self._directory.value: self._directory.value = path self._go.disabled = path == self._cwd def _refresh(self): self._update_files(refresh=True) def _update_files(self, event=None, refresh=False): path = os.path.abspath(self._directory.value) refresh = refresh or (event and getattr(event, 'obj', None) is self._reload) if refresh: path = self._cwd elif not os.path.isdir(path): self._selector.options = ['Entered path is not valid'] self._selector.disabled = True return elif event is not None and (not self._stack or path != self._stack[-1]): self._stack.append(path) self._position += 1 self._cwd = path if not refresh: self._go.disabled = True self._up.disabled = path == self._root_directory if self._position == len(self._stack) - 1: self._forward.disabled = True if 0 <= self._position and len(self._stack) > 1: self._back.disabled = False selected = self.value dirs, files = scan_path(path, self.file_pattern) for s in selected: check = os.path.realpath(s) if os.path.islink(s) else s if os.path.isdir(check): dirs.append(s) elif os.path.isfile(check): files.append(s) paths = [ p for p in sorted(dirs) + sorted(files) if self.show_hidden or not os.path.basename(p).startswith('.') ] abbreviated = [ ('📁' if f in dirs else '') + os.path.relpath(f, self._cwd) for f in paths ] options = OrderedDict(zip(abbreviated, paths)) self._selector.options = options self._selector.value = selected def _filter_blacklist(self, event): """ Ensure that if unselecting a currently selected path and it is not in the current working directory then it is removed from the blacklist. """ dirs, files = scan_path(self._cwd, self.file_pattern) paths = [('📁' if p in dirs else '') + os.path.relpath(p, self._cwd) for p in dirs + files] blacklist = self._selector._lists[False] options = OrderedDict(self._selector._items) self._selector.options.clear() self._selector.options.update([(k, v) for k, v in options.items() if k in paths or v in self.value]) blacklist.options = [o for o in blacklist.options if o in paths] def _select(self, event): if len(event.new) != 1: self._directory.value = self._cwd return relpath = event.new[0].replace('📁', '') sel = os.path.abspath(os.path.join(self._cwd, relpath)) if os.path.isdir(sel): self._directory.value = sel else: self._directory.value = self._cwd def _go_back(self, event): self._position -= 1 self._directory.value = self._stack[self._position] self._update_files() self._forward.disabled = False if self._position == 0: self._back.disabled = True def _go_forward(self, event): self._position += 1 self._directory.value = self._stack[self._position] self._update_files() def _go_up(self, event=None): path = self._cwd.split(os.path.sep) self._directory.value = os.path.sep.join(path[:-1]) or os.path.sep self._update_files(True)
class Layoutable(param.Parameterized): align = param.ObjectSelector(default='start', objects=['start', 'end', 'center'], doc=""" Whether the object should be aligned with the start, end or center of its container""") aspect_ratio = param.Parameter(default=None, doc=""" Describes the proportional relationship between component's width and height. This works if any of component's dimensions are flexible in size. If set to a number, ``width / height = aspect_ratio`` relationship will be maintained. Otherwise, if set to ``"auto"``, component's preferred width and height will be used to determine the aspect (if not set, no aspect will be preserved).""") background = param.Parameter(default=None, doc=""" Background color of the component.""") css_classes = param.List(default=None, doc=""" CSS classes to apply to the layout.""") width = param.Integer(default=None, bounds=(0, None), doc=""" The width of the component (in pixels). This can be either fixed or preferred width, depending on width sizing policy.""") height = param.Integer(default=None, bounds=(0, None), doc=""" The height of the component (in pixels). This can be either fixed or preferred height, depending on height sizing policy.""") min_width = param.Integer(default=None, bounds=(0, None), doc=""" Minimal width of the component (in pixels) if width is adjustable.""") min_height = param.Integer(default=None, bounds=(0, None), doc=""" Minimal height of the component (in pixels) if height is adjustable.""" ) max_width = param.Integer(default=None, bounds=(0, None), doc=""" Minimal width of the component (in pixels) if width is adjustable.""") max_height = param.Integer(default=None, bounds=(0, None), doc=""" Minimal height of the component (in pixels) if height is adjustable.""" ) margin = param.Parameter(default=5, doc=""" Allows to create additional space around the component. May be specified as a two-tuple of the form (vertical, horizontal) or a four-tuple (top, right, bottom, left).""") width_policy = param.ObjectSelector( default="auto", objects=['auto', 'fixed', 'fit', 'min', 'max'], doc=""" Describes how the component should maintain its width. ``"auto"`` Use component's preferred sizing policy. ``"fixed"`` Use exactly ``width`` pixels. Component will overflow if it can't fit in the available horizontal space. ``"fit"`` Use component's preferred width (if set) and allow it to fit into the available horizontal space within the minimum and maximum width bounds (if set). Component's width neither will be aggressively minimized nor maximized. ``"min"`` Use as little horizontal space as possible, not less than the minimum width (if set). The starting point is the preferred width (if set). The width of the component may shrink or grow depending on the parent layout, aspect management and other factors. ``"max"`` Use as much horizontal space as possible, not more than the maximum width (if set). The starting point is the preferred width (if set). The width of the component may shrink or grow depending on the parent layout, aspect management and other factors. """) height_policy = param.ObjectSelector( default="auto", objects=['auto', 'fixed', 'fit', 'min', 'max'], doc=""" Describes how the component should maintain its height. ``"auto"`` Use component's preferred sizing policy. ``"fixed"`` Use exactly ``height`` pixels. Component will overflow if it can't fit in the available vertical space. ``"fit"`` Use component's preferred height (if set) and allow to fit into the available vertical space within the minimum and maximum height bounds (if set). Component's height neither will be aggressively minimized nor maximized. ``"min"`` Use as little vertical space as possible, not less than the minimum height (if set). The starting point is the preferred height (if set). The height of the component may shrink or grow depending on the parent layout, aspect management and other factors. ``"max"`` Use as much vertical space as possible, not more than the maximum height (if set). The starting point is the preferred height (if set). The height of the component may shrink or grow depending on the parent layout, aspect management and other factors. """) sizing_mode = param.ObjectSelector(default=None, objects=[ 'fixed', 'stretch_width', 'stretch_height', 'stretch_both', 'scale_width', 'scale_height', 'scale_both', None ], doc=""" How the component should size itself. This is a high-level setting for maintaining width and height of the component. To gain more fine grained control over sizing, use ``width_policy``, ``height_policy`` and ``aspect_ratio`` instead (those take precedence over ``sizing_mode``). ``"fixed"`` Component is not responsive. It will retain its original width and height regardless of any subsequent browser window resize events. ``"stretch_width"`` Component will responsively resize to stretch to the available width, without maintaining any aspect ratio. The height of the component depends on the type of the component and may be fixed or fit to component's contents. ``"stretch_height"`` Component will responsively resize to stretch to the available height, without maintaining any aspect ratio. The width of the component depends on the type of the component and may be fixed or fit to component's contents. ``"stretch_both"`` Component is completely responsive, independently in width and height, and will occupy all the available horizontal and vertical space, even if this changes the aspect ratio of the component. ``"scale_width"`` Component will responsively resize to stretch to the available width, while maintaining the original or provided aspect ratio. ``"scale_height"`` Component will responsively resize to stretch to the available height, while maintaining the original or provided aspect ratio. ``"scale_both"`` Component will responsively resize to both the available width and height, while maintaining the original or provided aspect ratio. """) def __init__(self, **params): if (params.get('width', None) is not None and params.get('height', None) is not None and params.get('width_policy') is None and params.get('height_policy') is None and 'sizing_mode' not in params): params['sizing_mode'] = 'fixed' elif (not (self.param.sizing_mode.constant or self.param.sizing_mode.readonly) and type(self).sizing_mode is None): params['sizing_mode'] = params.get('sizing_mode', config.sizing_mode) super(Layoutable, self).__init__(**params)
class annotate(param.ParameterizedFunction): """ The annotate function allows drawing, editing and annotating any given Element (if it is supported). The annotate function returns a Layout of the editable plot and an Overlay of table(s), which allow editing the data of the element. The edited and annotated data may be accessed using the element and selected properties. """ annotator = param.Parameter(doc="""The current Annotator instance.""") annotations = param.ClassSelector(default=[], class_=(dict, list), doc=""" Annotations to associate with each object.""") edit_vertices = param.Boolean(default=True, doc=""" Whether to add tool to edit vertices.""") empty_value = param.Parameter(default=None, doc=""" The value to insert on annotation columns when drawing a new element.""") num_objects = param.Integer(default=None, bounds=(0, None), doc=""" The maximum number of objects to draw.""") show_vertices = param.Boolean(default=True, doc=""" Whether to show vertices when drawing the Path.""") table_transforms = param.HookList(default=[], doc=""" Transform(s) to apply to element when converting data to Table. The functions should accept the Annotator and the transformed element as input.""") table_opts = param.Dict(default={ 'editable': True, 'width': 400 }, doc=""" Opts to apply to the editor table(s).""") vertex_annotations = param.ClassSelector(default=[], class_=(dict, list), doc=""" Columns to annotate the Polygons with.""") vertex_style = param.Dict(default={'nonselection_alpha': 0.5}, doc=""" Options to apply to vertices during drawing and editing.""") _annotator_types = OrderedDict() @property def annotated(self): annotated = self.annotator.object if Store.current_backend == 'bokeh': return annotated.opts(clone=True, tools=['hover']) @property def selected(self): selected = self.annotator.selected if Store.current_backend == 'bokeh': return selected.opts(clone=True, tools=['hover']) @classmethod def compose(cls, *annotators): """Composes multiple annotator layouts and elements The composed Layout will contain all the elements in the supplied annotators and an overlay of all editor tables. Args: annotators: Annotator layouts or elements to compose Returns: A new layout consisting of the overlaid plots and tables """ layers = [] tables = [] for annotator in annotators: if isinstance(annotator, Layout): l, ts = annotator layers.append(l) tables += ts elif isinstance(annotator, annotate): layers.append(annotator.plot) tables += [t[0].object for t in annotator.editor] elif isinstance(annotator, (HoloMap, ViewableElement)): layers.append(annotator) else: raise ValueError("Cannot compose %s type with annotators." % type(annotator).__name__) tables = Overlay(tables, group='Annotator') return (Overlay(layers).collate() + tables) def __call__(self, element, **params): overlay = element if isinstance(element, Overlay) else [element] layers = [] annotator_type = None for element in overlay: matches = [] for eltype, atype in self._annotator_types.items(): if isinstance(element, eltype): matches.append( (getmro(type(element)).index(eltype), atype)) if matches: if annotator_type is not None: msg = ( 'An annotate call may only annotate a single element. ' 'If you want to annotate multiple elements call annotate ' 'on each one separately and then use the annotate.compose ' 'method to combine them into a single layout.') raise ValueError(msg) annotator_type = sorted(matches)[0][1] self.annotator = annotator_type(element, **params) tables = Overlay([t[0].object for t in self.annotator.editor], group='Annotator') layout = (self.annotator.plot + tables) layers.append(layout) else: layers.append(element) if annotator_type is None: obj = overlay if isinstance(overlay, Overlay) else element raise ValueError('Could not find an Element to annotate on' '%s object.' % type(obj).__name__) if len(layers) == 1: return layers[0] return self.compose(*layers)
class Vega(PaneBase): """ Vega panes allow rendering Vega plots and traces. For efficiency any array objects found inside a Figure are added to a ColumnDataSource which allows using binary transport to sync the figure on bokeh server and via Comms. """ margin = param.Parameter(default=(5, 5, 30, 5), doc=""" Allows to create additional space around the component. May be specified as a two-tuple of the form (vertical, horizontal) or a four-tuple (top, right, bottom, left).""") priority = 0.8 _updates = True @classmethod def is_altair(cls, obj): if 'altair' in sys.modules: import altair as alt return isinstance(obj, alt.api.TopLevelMixin) return False @classmethod def applies(cls, obj): if isinstance(obj, dict) and 'vega' in obj.get('$schema', '').lower(): return True return cls.is_altair(obj) @classmethod def _to_json(cls, obj): if isinstance(obj, dict): json = dict(obj) if 'data' in json: data = json['data'] if isinstance(data, dict): json['data'] = dict(data) elif isinstance(data, list): json['data'] = [dict(d) for d in data] return json return obj.to_dict() def _get_sources(self, json, sources): datasets = json.get('datasets', {}) for name in list(datasets): if name in sources or isinstance(datasets[name], dict): continue data = datasets.pop(name) if isinstance(data, list) and any( isinstance(d, dict) and 'geometry' in d for d in data): # Handle geometry records types datasets[name] = data continue columns = set(data[0]) if data else [] if self.is_altair(self.object): import altair as alt if (not isinstance( self.object.data, (alt.Data, alt.UrlData, type(alt.Undefined))) and columns == set(self.object.data)): data = ColumnDataSource.from_df(self.object.data) else: data = ds_as_cds(data) sources[name] = ColumnDataSource(data=data) else: sources[name] = ColumnDataSource(data=ds_as_cds(data)) data = json.get('data', {}) if isinstance(data, dict): data = data.pop('values', {}) if data: sources['data'] = ColumnDataSource(data=ds_as_cds(data)) elif isinstance(data, list): for d in data: if 'values' in d: sources[d['name']] = ColumnDataSource( data=ds_as_cds(d.pop('values'))) @classmethod def _get_dimensions(cls, json, props): if json is None: return if 'config' in json and 'view' in json['config']: size_config = json['config']['view'] else: size_config = json view = {} for w in ('width', 'continuousWidth'): if w in size_config: view['width'] = size_config[w] for h in ('height', 'continuousHeight'): if h in size_config: view['height'] = size_config[h] for p in ('width', 'height'): if p not in view or isinstance(view[p], string_types): continue if props.get(p) is None or p in view and props.get(p) < view[p]: v = view[p] props[p] = v + 22 if isinstance(v, int) else v responsive_height = json.get('height') == 'container' responsive_width = json.get('width') == 'container' if responsive_height and responsive_width: props['sizing_mode'] = 'stretch_both' elif responsive_width: props['sizing_mode'] = 'stretch_width' elif responsive_height: props['sizing_mode'] = 'stretch_height' def _get_model(self, doc, root=None, parent=None, comm=None): VegaPlot = lazy_load('panel.models.vega', 'VegaPlot', isinstance(comm, JupyterComm)) sources = {} if self.object is None: json = None else: json = self._to_json(self.object) self._get_sources(json, sources) props = self._process_param_change(self._init_params()) self._get_dimensions(json, props) model = VegaPlot(data=json, data_sources=sources, **props) if root is None: root = model self._models[root.ref['id']] = (model, parent) return model def _update(self, ref=None, model=None): if self.object is None: json = None else: json = self._to_json(self.object) self._get_sources(json, model.data_sources) props = { p: getattr(self, p) for p in list(Layoutable.param) if getattr(self, p) is not None } self._get_dimensions(json, props) props['data'] = json model.update(**props)
class Annotator(PaneBase): """ An Annotator allows drawing, editing and annotating a specific type of element. Each Annotator consists of the `plot` to draw and edit the element and the `editor`, which contains a list of tables, which make it possible to annotate each object in the element with additional properties defined in the `annotations`. """ annotations = param.ClassSelector(default=[], class_=(dict, list), doc=""" Annotations to associate with each object.""") default_opts = param.Dict(default={ 'responsive': True, 'min_height': 400, 'padding': 0.1, 'framewise': True }, doc=""" Opts to apply to the element.""") empty_value = param.Parameter(default=None, doc=""" The value to insert on annotation columns when drawing a new element.""") object = param.ClassSelector(class_=Element, doc=""" The Element to edit and annotate.""") num_objects = param.Integer(default=None, bounds=(0, None), doc=""" The maximum number of objects to draw.""") table_transforms = param.HookList(default=[], doc=""" Transform(s) to apply to element when converting data to Table. The functions should accept the Annotator and the transformed element as input.""") table_opts = param.Dict(default={ 'editable': True, 'width': 400 }, doc=""" Opts to apply to the editor table(s).""") # Once generic editing tools are merged into bokeh this could # include snapshot, restore and clear tools _tools = [] # Allows patching on custom behavior _extra_opts = {} # Triggers for updates to the table _triggers = ['annotations', 'object', 'table_opts'] # Links between plot and table _link_type = DataLink _selection_link_type = SelectionLink priority = 0.7 @classmethod def applies(cls, obj): if 'holoviews' not in sys.modules: return False return isinstance(obj, cls.param.object.class_) @property def _element_type(self): return self.param.object.class_ @property def _object_name(self): return self._element_type.__name__ def __init__(self, object=None, **params): super(Annotator, self).__init__(None, **params) self.object = self._process_element(object) self._table_row = Row() self.editor = Tabs(('%s' % param_name(self.name), self._table_row)) self.plot = DynamicMap(self._get_plot) self.plot.callback.inputs[:] = [self.object] self._tables = [] self._init_stream() self._stream.add_subscriber(self._update_object, precedence=0.1) self._selection = Selection1D(source=self.plot) self._update_table() self._update_links() self.param.watch(self._update, self._triggers) self.layout[:] = [self.plot, self.editor] @param.depends('annotations', 'object', 'default_opts') def _get_plot(self): return self._process_element(self.object) def _get_model(self, doc, root=None, parent=None, comm=None): return self.layout._get_model(doc, root, parent, comm) @preprocess def _update(self, event=None): if event and event.name == 'object': with param.discard_events(self): self.object = self._process_element(event.new) self._update_table() def _update_links(self): if hasattr(self, '_link'): self._link.unlink() self._link = self._link_type(self.plot, self._table) if self._selection_link_type: if hasattr(self, '_selection_link'): self._selection_link.unlink() self._selection_link = SelectionLink(self.plot, self._table) def _update_object(self, data=None): with param.discard_events(self): if len(self._stream.source) == 0: self.plot[()] self.object = self._stream.element def _update_table(self): object = self.object for transform in self.table_transforms: object = transform(object) self._table = Table(object, label=param_name(self.name)).opts( show_title=False, **self.table_opts) self._update_links() self._table_row[:] = [self._table] def select(self, selector=None): return self.layout.select(selector) @classmethod def compose(cls, *annotators): """Composes multiple Annotator instances and elements The composed Panel will contain all the elements in the supplied Annotators and Tabs containing all editors. Args: annotators: Annotator objects or elements to compose Returns: A new Panel consisting of the overlaid plots and tables """ layers, tables = [], [] for a in annotators: if isinstance(a, Annotator): layers.append(a.plot) tables += a.tables elif isinstance(a, Element): layers.append(a) return Row(Overlay(layers).collate(), Tabs(*tables)) @property def tables(self): return list(zip(self.editor._names, self.editor)) @property def selected(self): return self.object.iloc[self._selection.index]
class Param(PaneBase): """ Param panes render a Parameterized class to a set of widgets which are linked to the parameter values on the class. """ display_threshold = param.Number(default=0, precedence=-10, doc=""" Parameters with precedence below this value are not displayed.""") default_layout = param.ClassSelector(default=Column, class_=Panel, is_instance=False) default_precedence = param.Number(default=1e-8, precedence=-10, doc=""" Precedence value to use for parameters with no declared precedence. By default, zero predecence is available for forcing some parameters to the top of the list, and other values above the default_precedence values can be used to sort or group parameters arbitrarily.""") expand = param.Boolean(default=False, doc=""" Whether parameterized subobjects are expanded or collapsed on instantiation.""") expand_button = param.Boolean(default=None, doc=""" Whether to add buttons to expand and collapse sub-objects.""") expand_layout = param.Parameter(default=Column, doc=""" Layout to expand sub-objects into.""") height = param.Integer(default=None, bounds=(0, None), doc=""" Height of widgetbox the parameter widgets are displayed in.""") initializer = param.Callable(default=None, doc=""" User-supplied function that will be called on initialization, usually to update the default Parameter values of the underlying parameterized object.""") name = param.String(default='', doc=""" Title of the pane.""") parameters = param.List(default=[], allow_None=True, doc=""" If set this serves as a whitelist of parameters to display on the supplied Parameterized object.""") show_labels = param.Boolean(default=True, doc=""" Whether to show labels for each widget""") show_name = param.Boolean(default=True, doc=""" Whether to show the parameterized object's name""") width = param.Integer(default=300, allow_None=True, bounds=(0, None), doc=""" Width of widgetbox the parameter widgets are displayed in.""") widgets = param.Dict(doc=""" Dictionary of widget overrides, mapping from parameter name to widget class.""") priority = 0.1 _unpack = True _mapping = { param.Action: Button, param.Boolean: Checkbox, param.CalendarDate: DatePicker, param.Color: ColorPicker, param.Date: DatetimeInput, param.DateRange: DateRangeSlider, param.CalendarDateRange: DateRangeSlider, param.DataFrame: DataFrame, param.Dict: LiteralInputTyped, param.FileSelector: SingleFileSelector, param.Filename: TextInput, param.Foldername: TextInput, param.Integer: IntSlider, param.List: LiteralInputTyped, param.MultiFileSelector: FileSelector, param.ListSelector: MultiSelect, param.Number: FloatSlider, param.ObjectSelector: Select, param.Parameter: LiteralInputTyped, param.Range: RangeSlider, param.Selector: Select, param.String: TextInput, } if hasattr(param, 'Event'): _mapping[param.Event] = Button _rerender_params = [] def __init__(self, object=None, **params): if isinstance(object, param.Parameter): if not 'show_name' in params: params['show_name'] = False params['parameters'] = [object.name] object = object.owner if isinstance(object, param.parameterized.Parameters): object = object.cls if object.self is None else object.self if 'parameters' not in params and object is not None: params['parameters'] = [p for p in object.param if p != 'name'] self._explicit_parameters = False else: self._explicit_parameters = object is not None if object and 'name' not in params: params['name'] = param_name(object.name) super().__init__(object, **params) self._updating = [] # Construct Layout kwargs = {p: v for p, v in self.param.get_param_values() if p in Layoutable.param and v is not None} self._widget_box = self.default_layout(**kwargs) layout = self.expand_layout if isinstance(layout, Panel): self._expand_layout = layout self.layout = self._widget_box elif isinstance(self._widget_box, layout): self.layout = self._expand_layout = self._widget_box elif isinstance(layout, type) and issubclass(layout, Panel): self.layout = self._expand_layout = layout(self._widget_box, **kwargs) else: raise ValueError('expand_layout expected to be a panel.layout.Panel' 'type or instance, found %s type.' % type(layout).__name__) self.param.watch(self._update_widgets, [ 'object', 'parameters', 'name', 'display_threshold', 'expand_button', 'expand', 'expand_layout', 'widgets', 'show_labels', 'show_name']) self._update_widgets() def __repr__(self, depth=0): cls = type(self).__name__ obj_cls = type(self.object).__name__ params = [] if self.object is None else list(self.object.param) parameters = [k for k in params if k != 'name'] params = [] for p, v in sorted(self.param.get_param_values()): if v is self.param[p].default: continue elif v is None: continue elif isinstance(v, string_types) and v == '': continue elif p == 'object' or (p == 'name' and (v.startswith(obj_cls) or v.startswith(cls))): continue elif p == 'parameters' and v == parameters: continue try: params.append('%s=%s' % (p, abbreviated_repr(v))) except RuntimeError: params.append('%s=%s' % (p, '...')) obj = 'None' if self.object is None else '%s' % type(self.object).__name__ template = '{cls}({obj}, {params})' if params else '{cls}({obj})' return template.format(cls=cls, params=', '.join(params), obj=obj) #---------------------------------------------------------------- # Callback API #---------------------------------------------------------------- @property def _synced_params(self): ignored_params = ['default_layout', 'loading'] return [p for p in Layoutable.param if p not in ignored_params] def _update_widgets(self, *events): parameters = [] for event in sorted(events, key=lambda x: x.name): if event.name == 'object': if isinstance(event.new, param.parameterized.Parameters): # Setting object will trigger this method a second time self.object = event.new.cls if event.new.self is None else event.new.self return if self._explicit_parameters: parameters = self.parameters elif event.new is None: parameters = [] else: parameters = [p for p in event.new.param if p != 'name'] self.name = param_name(event.new.name) if event.name == 'parameters': if event.new is None: self._explicit_parameters = False if self.object is not None: parameters = [p for p in self.object.param if p != 'name'] else: self._explicit_parameters = True parameters = [] if event.new == [] else event.new if parameters != [] and parameters != self.parameters: # Setting parameters will trigger this method a second time self.parameters = parameters return for cb in list(self._callbacks): if cb.inst in self._widget_box.objects: cb.inst.param.unwatch(cb) self._callbacks.remove(cb) # Construct widgets if self.object is None: self._widgets = {} else: self._widgets = self._get_widgets() alias = {'_title': 'name'} widgets = [widget for p, widget in self._widgets.items() if (self.object.param[alias.get(p, p)].precedence is None) or (self.object.param[alias.get(p, p)].precedence >= self.display_threshold)] self._widget_box.objects = widgets if not (self.expand_button == False and not self.expand): self._link_subobjects() def _link_subobjects(self): for pname, widget in self._widgets.items(): widgets = [widget] if isinstance(widget, Widget) else widget if not any(is_parameterized(getattr(w, 'value', None)) or any(is_parameterized(o) for o in getattr(w, 'options', [])) for w in widgets): continue if (isinstance(widgets, Row) and isinstance(widgets[1], Toggle)): selector, toggle = (widgets[0], widgets[1]) else: selector, toggle = (widget, None) def toggle_pane(change, parameter=pname): "Adds or removes subpanel from layout" parameterized = getattr(self.object, parameter) existing = [p for p in self._expand_layout.objects if isinstance(p, Param) and p.object in recursive_parameterized(parameterized)] if not change.new: self._expand_layout[:] = [ e for e in self._expand_layout.objects if e not in existing ] elif change.new: kwargs = {k: v for k, v in self.param.get_param_values() if k not in ['name', 'object', 'parameters']} pane = Param(parameterized, name=parameterized.name, **kwargs) if isinstance(self._expand_layout, Tabs): title = self.object.param[pname].label pane = (title, pane) self._expand_layout.append(pane) def update_pane(change, parameter=pname): "Adds or removes subpanel from layout" layout = self._expand_layout existing = [p for p in layout.objects if isinstance(p, Param) and p.object is change.old] if toggle: toggle.disabled = not is_parameterized(change.new) if not existing: return elif is_parameterized(change.new): parameterized = change.new kwargs = {k: v for k, v in self.param.get_param_values() if k not in ['name', 'object', 'parameters']} pane = Param(parameterized, name=parameterized.name, **kwargs) layout[layout.objects.index(existing[0])] = pane else: layout.pop(existing[0]) watchers = [selector.param.watch(update_pane, 'value')] if toggle: watchers.append(toggle.param.watch(toggle_pane, 'value')) self._callbacks += watchers if self.expand: if self.expand_button: toggle.value = True else: toggle_pane(namedtuple('Change', 'new')(True)) def widget(self, p_name): """Get widget for param_name""" p_obj = self.object.param[p_name] kw_widget = {} widget_class_overridden = True if self.widgets is None or p_name not in self.widgets: widget_class_overridden = False widget_class = self.widget_type(p_obj) elif isinstance(self.widgets[p_name], dict): kw_widget = dict(self.widgets[p_name]) if 'widget_type' in self.widgets[p_name]: widget_class = kw_widget.pop('widget_type') elif 'type' in self.widgets[p_name]: widget_class = kw_widget.pop('type') else: widget_class_overridden = False widget_class = self.widget_type(p_obj) else: widget_class = self.widgets[p_name] if not self.show_labels and not issubclass(widget_class, _ButtonBase): label = '' else: label = p_obj.label kw = dict(disabled=p_obj.constant, name=label) value = getattr(self.object, p_name) if value is not None: kw['value'] = value # Update kwargs kw.update(kw_widget) if hasattr(p_obj, 'get_range'): options = p_obj.get_range() if not options and value is not None: options = [value] kw['options'] = options if hasattr(p_obj, 'get_soft_bounds'): bounds = p_obj.get_soft_bounds() if bounds[0] is not None: kw['start'] = bounds[0] if bounds[1] is not None: kw['end'] = bounds[1] if ('start' not in kw or 'end' not in kw): # Do not change widget class if _mapping was overridden if not widget_class_overridden: if (isinstance(p_obj, param.Number) and not isinstance(p_obj, (param.Date, param.CalendarDate))): widget_class = FloatInput if isinstance(p_obj, param.Integer): widget_class = IntInput elif not issubclass(widget_class, LiteralInput): widget_class = LiteralInput if hasattr(widget_class, 'step') and getattr(p_obj, 'step', None): kw['step'] = p_obj.step kwargs = {k: v for k, v in kw.items() if k in widget_class.param} if isinstance(widget_class, type) and issubclass(widget_class, Button): kwargs.pop('value', None) if isinstance(widget_class, Widget): widget = widget_class else: widget = widget_class(**kwargs) widget._param_pane = self widget._param_name = p_name watchers = self._callbacks def link_widget(change): if p_name in self._updating: return try: self._updating.append(p_name) self.object.param.set_param(**{p_name: change.new}) finally: self._updating.remove(p_name) if hasattr(param, 'Event') and isinstance(p_obj, param.Event): def event(change): self.object.param.trigger(p_name) watcher = widget.param.watch(event, 'clicks') elif isinstance(p_obj, param.Action): def action(change): value(self.object) watcher = widget.param.watch(action, 'clicks') elif kw_widget.get('throttled', False) and hasattr(widget, 'value_throttled'): watcher = widget.param.watch(link_widget, 'value_throttled') else: watcher = widget.param.watch(link_widget, 'value') watchers.append(watcher) def link(change, watchers=[watcher]): updates = {} if p_name not in self._widgets: return widget = self._widgets[p_name] if change.what == 'constant': updates['disabled'] = change.new elif change.what == 'precedence': if change.new is change.old: return elif change.new is None: self._rerender() elif (change.new < self.display_threshold and widget in self._widget_box.objects): self._widget_box.pop(widget) elif change.new >= self.display_threshold: self._rerender() return elif change.what == 'objects': updates['options'] = p_obj.get_range() elif change.what == 'bounds': start, end = p_obj.get_soft_bounds() supports_bounds = hasattr(widget, 'start') if start is None or end is None: rerender = supports_bounds else: rerender = not supports_bounds if supports_bounds: updates['start'] = start updates['end'] = end if rerender: self._rerender_widget(p_name) return elif change.what == 'step': updates['step'] = p_obj.step elif change.what == 'label': updates['name'] = p_obj.label elif p_name in self._updating: return elif hasattr(param, 'Event') and isinstance(p_obj, param.Event): return elif isinstance(p_obj, param.Action): prev_watcher = watchers[0] widget.param.unwatch(prev_watcher) def action(event): change.new(self.object) watchers[0] = widget.param.watch(action, 'clicks') idx = self._callbacks.index(prev_watcher) self._callbacks[idx] = watchers[0] return elif kw_widget.get('throttled', False) and hasattr(widget, 'value_throttled'): updates['value_throttled'] = change.new elif isinstance(widget, Row) and len(widget) == 2: updates['value'] = change.new widget = widget[0] else: updates['value'] = change.new try: self._updating.append(p_name) if change.type == 'triggered': with discard_events(widget): widget.param.set_param(**updates) widget.param.trigger(*updates) else: widget.param.set_param(**updates) finally: self._updating.remove(p_name) # Set up links to parameterized object watchers.append(self.object.param.watch(link, p_name, 'constant')) watchers.append(self.object.param.watch(link, p_name, 'precedence')) watchers.append(self.object.param.watch(link, p_name, 'label')) if hasattr(p_obj, 'get_range'): watchers.append(self.object.param.watch(link, p_name, 'objects')) if hasattr(p_obj, 'get_soft_bounds'): watchers.append(self.object.param.watch(link, p_name, 'bounds')) if 'step' in kw: watchers.append(self.object.param.watch(link, p_name, 'step')) watchers.append(self.object.param.watch(link, p_name)) options = kwargs.get('options', []) if isinstance(options, dict): options = options.values() if ((is_parameterized(value) or any(is_parameterized(o) for o in options)) and (self.expand_button or (self.expand_button is None and not self.expand))): widget.margin = (5, 0, 5, 10) toggle = Toggle(name='\u22EE', button_type='primary', disabled=not is_parameterized(value), max_height=30, max_width=20, height_policy='fit', align='end', margin=(0, 0, 5, 10)) widget.width = self._widget_box.width-60 return Row(widget, toggle, width_policy='max', margin=0) else: return widget @property def _ordered_params(self): params = [(p, pobj) for p, pobj in self.object.param.objects('existing').items() if p in self.parameters or p == 'name'] key_fn = lambda x: x[1].precedence if x[1].precedence is not None else self.default_precedence sorted_precedence = sorted(params, key=key_fn) filtered = [(k, p) for k, p in sorted_precedence] groups = itertools.groupby(filtered, key=key_fn) # Params preserve definition order in Python 3.6+ dict_ordered_py3 = (sys.version_info.major == 3 and sys.version_info.minor >= 6) dict_ordered = dict_ordered_py3 or (sys.version_info.major > 3) ordered_groups = [list(grp) if dict_ordered else sorted(grp) for (_, grp) in groups] ordered_params = [el[0] for group in ordered_groups for el in group if (el[0] != 'name' or el[0] in self.parameters)] return ordered_params #---------------------------------------------------------------- # Model API #---------------------------------------------------------------- def _rerender(self): precedence = lambda k: self.object.param['name' if k == '_title' else k].precedence params = self._ordered_params if self.show_name: params.insert(0, '_title') widgets = [] for k in params: if precedence(k) is None or precedence(k) >= self.display_threshold: widgets.append(self._widgets[k]) self._widget_box.objects = widgets def _rerender_widget(self, p_name): watchers = [] for w in self._callbacks: if w.inst is self._widgets[p_name]: w.inst.param.unwatch(w) else: watchers.append(w) self._widgets[p_name] = self.widget(p_name) self._rerender() def _get_widgets(self): """Return name,widget boxes for all parameters (i.e., a property sheet)""" # Format name specially if self.expand_layout is Tabs: widgets = [] elif self.show_name: widgets = [('_title', StaticText(value='<b>{0}</b>'.format(self.name)))] else: widgets = [] widgets += [(pname, self.widget(pname)) for pname in self._ordered_params] return OrderedDict(widgets) def _get_model(self, doc, root=None, parent=None, comm=None): model = self.layout._get_model(doc, root, parent, comm) self._models[root.ref['id']] = (model, parent) return model def _cleanup(self, root): self.layout._cleanup(root) super()._cleanup(root) #---------------------------------------------------------------- # Public API #---------------------------------------------------------------- @classmethod def applies(cls, obj): return (is_parameterized(obj) or isinstance(obj, param.parameterized.Parameters) or (isinstance(obj, param.Parameter) and obj.owner is not None)) @classmethod def widget_type(cls, pobj): ptype = type(pobj) for t in classlist(ptype)[::-1]: if t in cls._mapping: if isinstance(cls._mapping[t], types.FunctionType): return cls._mapping[t](pobj) return cls._mapping[t] def select(self, selector=None): """ Iterates over the Viewable and any potential children in the applying the Selector. Arguments --------- selector: type or callable or None The selector allows selecting a subset of Viewables by declaring a type or callable function to filter by. Returns ------- viewables: list(Viewable) """ return super().select(selector) + self.layout.select(selector) def get_root(self, doc=None, comm=None, preprocess=True): """ Returns the root model and applies pre-processing hooks Arguments --------- doc: bokeh.Document Bokeh document the bokeh model will be attached to. comm: pyviz_comms.Comm Optional pyviz_comms when working in notebook preprocess: boolean (default=True) Whether to run preprocessing hooks Returns ------- Returns the bokeh model corresponding to this panel object """ doc = init_doc(doc) root = self.layout.get_root(doc, comm, preprocess) ref = root.ref['id'] self._models[ref] = (root, None) state._views[ref] = (self, root, doc, comm) return root
class DiscreteSlider(CompositeWidget, _SliderBase): options = param.ClassSelector(default=[], class_=(dict, list)) value = param.Parameter() formatter = param.String(default='%.3g') _rename = {'formatter': None} _supports_embed = True _text_link = """ var labels = {labels} target.text = labels[source.value] """ def __init__(self, **params): self._syncing = False super(DiscreteSlider, self).__init__(**params) if 'formatter' not in params and all( isinstance(v, (int, np.int_)) for v in self.values): self.formatter = '%d' if self.value is None and None not in self.values and self.options: self.value = self.values[0] elif self.value not in self.values: raise ValueError('Value %s not a valid option, ' 'ensure that the supplied value ' 'is one of the declared options.' % self.value) self._text = StaticText(margin=(5, 0, 0, 5), style={'white-space': 'nowrap'}) self._slider = IntSlider() self._composite = Column(self._text, self._slider) self._update_options() self.param.watch(self._update_options, ['options', 'formatter']) self.param.watch(self._update_value, ['value']) def _update_options(self, *events): values, labels = self.values, self.labels if self.value not in values: value = 0 self.value = values[0] else: value = values.index(self.value) self._slider = IntSlider(start=0, end=len(self.options) - 1, value=value, tooltips=False, show_value=False, margin=(0, 5, 5, 5), _supports_embed=False) js_code = self._text_link.format(labels=repr(self.labels)) self._jslink = self._slider.jslink(self._text, code={'value': js_code}) self._slider.param.watch(self._sync_value, 'value') self._text.value = labels[value] self._composite[1] = self._slider def _update_value(self, event): values = self.values if self.value not in values: self.value = values[0] return index = self.values.index(self.value) if self._syncing: return try: self._syncing = True self._slider.value = index finally: self._syncing = False def _sync_value(self, event): if self._syncing: return try: self._syncing = True self.value = self.values[event.new] finally: self._syncing = False def _get_embed_state(self, root, max_opts=3): model = self._composite[1]._models[root.ref['id']][0] return self, model, self.values, lambda x: x.value, 'value', 'cb_obj.value' @property def labels(self): title = (self.name + ': ' if self.name else '') if isinstance(self.options, dict): return [title + ('<b>%s</b>' % o) for o in self.options] else: return [ title + ('<b>%s</b>' % (o if isinstance(o, string_types) else (self.formatter % o))) for o in self.options ] @property def values(self): return list(self.options.values()) if isinstance( self.options, dict) else self.options
class DataFrame(HTML): """ DataFrame renders pandas, dask and streamz DataFrame types using their custom HTML repr. In the case of a streamz DataFrame the rendered data will update periodically. """ bold_rows = param.Boolean(default=True, doc=""" Make the row labels bold in the output.""") border = param.Integer(default=0, doc=""" A ``border=border`` attribute is included in the opening `<table>` tag.""") classes = param.List(default=['panel-df'], doc=""" CSS class(es) to apply to the resulting html table.""") col_space = param.ClassSelector(default=None, class_=(str, int), doc=""" The minimum width of each column in CSS length units. An int is assumed to be px units.""") decimal = param.String(default='.', doc=""" Character recognized as decimal separator, e.g. ',' in Europe.""") float_format = param.Callable(default=None, doc=""" Formatter function to apply to columns' elements if they are floats. The result of this function must be a unicode string.""") formatters = param.ClassSelector(default=None, class_=(dict, list), doc=""" Formatter functions to apply to columns' elements by position or name. The result of each function must be a unicode string.""") header = param.Boolean(default=True, doc=""" Whether to print column labels.""") index = param.Boolean(default=True, doc=""" Whether to print index (row) labels.""") index_names = param.Boolean(default=True, doc=""" Prints the names of the indexes.""") justify = param.ObjectSelector(default=None, allow_None=True, objects=[ 'left', 'right', 'center', 'justify', 'justify-all', 'start', 'end', 'inherit', 'match-parent', 'initial', 'unset'], doc=""" How to justify the column labels.""") max_rows = param.Integer(default=None, doc=""" Maximum number of rows to display.""") max_cols = param.Integer(default=None, doc=""" Maximum number of columns to display.""") na_rep = param.String(default='NaN', doc=""" String representation of NAN to use.""") render_links = param.Boolean(default=False, doc=""" Convert URLs to HTML links.""") show_dimensions = param.Boolean(default=False, doc=""" Display DataFrame dimensions (number of rows by number of columns).""") sparsify = param.Boolean(default=True, doc=""" Set to False for a DataFrame with a hierarchical index to print every multi-index key at each row.""") _object = param.Parameter(default=None, doc="""Hidden parameter.""") _rerender_params = [ 'object', '_object', 'bold_rows', 'border', 'classes', 'col_space', 'decimal', 'float_format', 'formatters', 'header', 'index', 'index_names', 'justify', 'max_rows', 'max_cols', 'na_rep', 'render_links', 'show_dimensions', 'sparsify', 'sizing_mode' ] _dask_params = ['max_rows'] def __init__(self, object=None, **params): super(DataFrame, self).__init__(object, **params) self._stream = None self._setup_stream() @classmethod def applies(cls, obj): module = getattr(obj, '__module__', '') name = type(obj).__name__ if (any(m in module for m in ('pandas', 'dask', 'streamz')) and name in ('DataFrame', 'Series', 'Random', 'DataFrames', 'Seriess')): return 0.3 else: return False def _set_object(self, object): self._object = object @param.depends('object', watch=True) def _setup_stream(self): if not self._models or not hasattr(self.object, 'stream'): return elif self._stream: self._stream.destroy() self._stream = None self._stream = self.object.stream.latest().rate_limit(0.5).gather() self._stream.sink(self._set_object) def _get_model(self, doc, root=None, parent=None, comm=None): model = super(DataFrame, self)._get_model(doc, root, parent, comm) self._setup_stream() return model def _cleanup(self, model): super(DataFrame, self)._cleanup(model) if not self._models and self._stream: self._stream.destroy() self._stream = None def _get_properties(self): properties = DivPaneBase._get_properties(self) if self._stream: df = self._object else: df = self.object if hasattr(df, 'to_frame'): df = df.to_frame() module = getattr(df, '__module__', '') if hasattr(df, 'to_html'): if 'dask' in module: html = df.to_html(max_rows=self.max_rows).replace('border="1"', '') else: kwargs = {p: getattr(self, p) for p in self._rerender_params if p not in DivPaneBase.param and p != '_object'} html = df.to_html(**kwargs) else: html = '' return dict(properties, text=escape(html))
class LiteralInput(Widget): """ LiteralInput allows declaring Python literals using a text input widget. Optionally a type may be declared. """ serializer = param.ObjectSelector(default='ast', objects=['ast', 'json'], doc=""" The serialization (and deserialization) method to use. 'ast' uses ast.literal_eval and 'json' uses json.loads and json.dumps. """) type = param.ClassSelector(default=None, class_=(type, tuple), is_instance=True) value = param.Parameter(default=None) _rename = {'name': 'title', 'type': None, 'serializer': None} _source_transforms = {'value': """JSON.parse(value.replace(/'/g, '"'))"""} _target_transforms = { 'value': """JSON.stringify(value).replace(/,/g, ", ").replace(/:/g, ": ")""" } _widget_type = _BkTextInput def __init__(self, **params): super(LiteralInput, self).__init__(**params) self._state = '' self._validate(None) self._callbacks.append(self.param.watch(self._validate, 'value')) def _validate(self, event): if self.type is None: return new = self.value if not isinstance(new, self.type) and new is not None: if event: self.value = event.old types = repr(self.type) if isinstance( self.type, tuple) else self.type.__name__ raise ValueError('LiteralInput expected %s type but value %s ' 'is of type %s.' % (types, new, type(new).__name__)) def _process_property_change(self, msg): msg = super(LiteralInput, self)._process_property_change(msg) new_state = '' if 'value' in msg: value = msg.pop('value') try: if self.serializer == 'json': value = json.loads(value) else: value = ast.literal_eval(value) except Exception: new_state = ' (invalid)' value = self.value else: if self.type and not isinstance(value, self.type): vtypes = self.type if isinstance(self.type, tuple) else (self.type, ) typed_value = None for vtype in vtypes: try: typed_value = vtype(value) except Exception: pass else: break if typed_value is None and value is not None: new_state = ' (wrong type)' value = self.value else: value = typed_value msg['value'] = value msg['name'] = msg.get('title', self.name).replace(self._state, '') + new_state self._state = new_state self.param.trigger('name') return msg def _process_param_change(self, msg): msg = super(LiteralInput, self)._process_param_change(msg) if 'value' in msg: value = '' if msg['value'] is None else msg['value'] if isinstance(value, string_types): value = repr(value) elif self.serializer == 'json': value = json.dumps(value, sort_keys=True) else: value = as_unicode(value) msg['value'] = value msg['title'] = self.name return msg
class HoloViews(PaneBase): """ HoloViews panes render any HoloViews object to a corresponding Bokeh model while respecting the currently selected backend. """ backend = param.ObjectSelector( default=None, objects=['bokeh', 'plotly', 'matplotlib'], doc=""" The HoloViews backend used to render the plot (if None defaults to the currently selected renderer).""") center = param.Boolean(default=False, doc=""" Whether to center the plot.""") linked_axes = param.Boolean(default=True, doc=""" Whether to use link the axes of bokeh plots inside this pane across a panel layout.""") renderer = param.Parameter(default=None, doc=""" Explicit renderer instance to use for rendering the HoloViews plot. Overrides the backend.""") theme = param.ClassSelector(default=None, class_=(Theme, str), allow_None=True, doc=""" Bokeh theme to apply to the HoloViews plot.""") widget_location = param.ObjectSelector(default='right_top', objects=[ 'left', 'bottom', 'right', 'top', 'top_left', 'top_right', 'bottom_left', 'bottom_right', 'left_top', 'left_bottom', 'right_top', 'right_bottom'], doc=""" The layout of the plot and the widgets. The value refers to the position of the widgets relative to the plot.""") widget_layout = param.ObjectSelector( objects=[WidgetBox, Row, Column], constant=True, default=WidgetBox, doc=""" The layout object to display the widgets in.""") 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.""") priority = 0.8 _panes = {'bokeh': Bokeh, 'matplotlib': Matplotlib, 'plotly': Plotly} _rename = { 'backend': None, 'center': None, 'linked_axes': None, 'renderer': None, 'theme': None, 'widgets': None, 'widget_layout': None, 'widget_location': None, 'widget_type': None } _rerender_params = ['object', 'backend'] def __init__(self, object=None, **params): super().__init__(object, **params) self._initialized = False self._responsive_content = False self._restore_plot = None self.widget_box = self.widget_layout() self._widget_container = [] self._update_widgets() self._plots = {} self.param.watch(self._update_widgets, self._rerender_params) self._initialized = True @param.depends('center', 'widget_location', watch=True) def _update_layout(self): loc = self.widget_location if not len(self.widget_box): widgets = [] elif loc in ('left', 'right'): widgets = Column(VSpacer(), self.widget_box, VSpacer()) elif loc in ('top', 'bottom'): widgets = Row(HSpacer(), self.widget_box, HSpacer()) elif loc in ('top_left', 'bottom_left'): widgets = Row(self.widget_box, HSpacer()) elif loc in ('top_right', 'bottom_right'): widgets = Row(HSpacer(), self.widget_box) elif loc in ('left_top', 'right_top'): widgets = Column(self.widget_box, VSpacer()) elif loc in ('left_bottom', 'right_bottom'): widgets = Column(VSpacer(), self.widget_box) center = self.center and not self._responsive_content self._widget_container = widgets if not widgets: if center: components = [HSpacer(), self, HSpacer()] else: components = [self] elif center: if loc.startswith('left'): components = [widgets, HSpacer(), self, HSpacer()] elif loc.startswith('right'): components = [HSpacer(), self, HSpacer(), widgets] elif loc.startswith('top'): components = [HSpacer(), Column(widgets, Row(HSpacer(), self, HSpacer())), HSpacer()] elif loc.startswith('bottom'): components = [HSpacer(), Column(Row(HSpacer(), self, HSpacer()), widgets), HSpacer()] else: if loc.startswith('left'): components = [widgets, self] elif loc.startswith('right'): components = [self, widgets] elif loc.startswith('top'): components = [Column(widgets, self)] elif loc.startswith('bottom'): components = [Column(self, widgets)] self.layout[:] = components #---------------------------------------------------------------- # Callback API #---------------------------------------------------------------- @param.depends('theme', watch=True) def _update_theme(self, *events): if self.theme is None: return for (model, _) in self._models.values(): if model.document: model.document.theme = self.theme @param.depends('widget_type', 'widgets', watch=True) def _update_widgets(self, *events): 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 cb in list(self._callbacks): if cb.inst in self.widget_box.objects: cb.inst.param.unwatch(cb) self._callbacks.remove(cb) # Add new widget callbacks for widget in widgets: watcher = widget.param.watch(self._widget_callback, 'value') self._callbacks.append(watcher) self.widget_box[:] = widgets if ((widgets and self.widget_box not in self._widget_container) or (not widgets and self.widget_box in self._widget_container) or not self._initialized): self._update_layout() def _update_plot(self, plot, pane): from holoviews.core.util import cross_index, wrap_tuple_streams widgets = self.widget_box.objects if not widgets: return elif 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) if plot.dynamic: widget_dims = [w.name for w in widgets] key = [key[widget_dims.index(kdim)] if kdim in widget_dims else None for kdim in plot.dimensions] key = wrap_tuple_streams(tuple(key), plot.dimensions, plot.streams) if plot.backend == 'bokeh': if plot.comm or state._unblocked(plot.document): with unlocked(): plot.update(key) if plot.comm and 'embedded' not in plot.root.tags: plot.push() else: if plot.document.session_context: plot.document.add_next_tick_callback(partial(plot.update, key)) else: plot.update(key) else: plot.update(key) if hasattr(plot.renderer, 'get_plot_state'): pane.object = plot.renderer.get_plot_state(plot) else: # Compatibility with holoviews<1.13.0 pane.object = plot.state def _widget_callback(self, event): for _, (plot, pane) in self._plots.items(): self._update_plot(plot, pane) #---------------------------------------------------------------- # Model API #---------------------------------------------------------------- def _get_model(self, doc, root=None, parent=None, comm=None): from holoviews.plotting.plot import Plot if root is None: return self.get_root(doc, comm) ref = root.ref['id'] if self.object is None: model = _BkSpacer() self._models[ref] = (model, parent) return model if self._restore_plot is not None: plot = self._restore_plot self._restore_plot = None elif isinstance(self.object, Plot): plot = self.object else: plot = self._render(doc, comm, root) plot.pane = self backend = plot.renderer.backend if hasattr(plot.renderer, 'get_plot_state'): state = plot.renderer.get_plot_state(plot) else: # Compatibility with holoviews<1.13.0 state = plot.state # Ensure rerender if content is responsive but layout is centered if (backend == 'bokeh' and self.center and state.sizing_mode not in ('fixed', None) and not self._responsive_content): self._responsive_content = True self._update_layout() self._restore_plot = plot raise RerenderError() else: self._responsive_content = False kwargs = {p: v for p, v in self.param.get_param_values() if p in Layoutable.param and p != 'name'} child_pane = self._panes.get(backend, Pane)(state, **kwargs) self._update_plot(plot, child_pane) model = child_pane._get_model(doc, root, parent, comm) if ref in self._plots: old_plot, old_pane = self._plots[ref] old_plot.comm = None # Ensures comm does not get cleaned up old_plot.cleanup() self._plots[ref] = (plot, child_pane) self._models[ref] = (model, parent) return model def _render(self, doc, comm, root): import holoviews as hv from holoviews import Store, renderer as load_renderer if self.renderer: renderer = self.renderer backend = renderer.backend else: if not Store.renderers: loaded_backend = (self.backend or 'bokeh') load_renderer(loaded_backend) Store.current_backend = loaded_backend backend = self.backend or Store.current_backend renderer = Store.renderers[backend] mode = 'server' if comm is None else 'default' if backend == 'bokeh': params = {} if self.theme is not None: params['theme'] = self.theme if mode != renderer.mode: params['mode'] = mode if params: renderer = renderer.instance(**params) kwargs = {'margin': self.margin} if backend == 'bokeh' or LooseVersion(str(hv.__version__)) >= str('1.13.0'): kwargs['doc'] = doc kwargs['root'] = root if comm: kwargs['comm'] = comm return renderer.get_plot(self.object, **kwargs) def _cleanup(self, root): """ Traverses HoloViews object to find and clean up any streams connected to existing plots. """ old_plot, old_pane = self._plots.pop(root.ref['id'], (None, None)) if old_plot: old_plot.cleanup() if old_pane: old_pane._cleanup(root) super()._cleanup(root) #---------------------------------------------------------------- # Public API #---------------------------------------------------------------- @classmethod def applies(cls, obj): if 'holoviews' not in sys.modules: return False from holoviews.core.dimension import Dimensioned from holoviews.plotting.plot import Plot return isinstance(obj, Dimensioned) or isinstance(obj, Plot) @classmethod def widgets_from_dimensions(cls, object, widget_types=None, widgets_type='individual'): from holoviews.core import Dimension, DynamicMap from holoviews.core.options import SkipRendering from holoviews.core.util import isnumeric, unicode, datetime_types, unique_iterator from holoviews.core.traversal import unique_dimkeys from holoviews.plotting.plot import Plot, GenericCompositePlot from holoviews.plotting.util import get_dynamic_mode from ..widgets import Widget, DiscreteSlider, Select, FloatSlider, DatetimeInput, IntSlider if widget_types is None: widget_types = {} if isinstance(object, GenericCompositePlot): object = object.layout elif isinstance(object, Plot): object = object.hmap if isinstance(object, DynamicMap) and object.unbounded: dims = ', '.join('%r' % dim for dim in object.unbounded) msg = ('DynamicMap cannot be displayed without explicit indexing ' 'as {dims} dimension(s) are unbounded. ' '\nSet dimensions bounds with the DynamicMap redim.range ' 'or redim.values methods.') raise SkipRendering(msg.format(dims=dims)) dynamic, bounded = get_dynamic_mode(object) dims, keys = unique_dimkeys(object) if ((dims == [Dimension('Frame')] and keys == [(0,)]) or (not dynamic and len(keys) == 1)): return [], {} nframes = 1 values = dict() if dynamic else dict(zip(dims, zip(*keys))) dim_values = OrderedDict() widgets = [] dims = [d for d in dims if values.get(d) is not None or d.values or d.range != (None, None)] for i, dim in enumerate(dims): widget_type, widget, widget_kwargs = None, None, {} if widgets_type == 'individual': if i == 0 and i == (len(dims)-1): margin = (20, 20, 20, 20) elif i == 0: margin = (20, 20, 5, 20) elif i == (len(dims)-1): margin = (5, 20, 20, 20) else: margin = (0, 20, 5, 20) kwargs = {'margin': margin, 'width': 250} else: kwargs = {} vals = dim.values or values.get(dim, None) if vals is not None: vals = list(unique_iterator(vals)) 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): widget.param.set_param(**kwargs) if not widget.name: widget.name = dim.label widgets.append(widget) continue elif isinstance(widget, dict): widget_type = widget.get('type', widget_type) widget_kwargs = dict(widget) 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)) widget_kwargs.update(kwargs) if vals: if all(isnumeric(v) or isinstance(v, datetime_types) for v in vals) and len(vals) > 1: 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_kwargs = dict(dict(name=dim.label, options=options, value=default), **widget_kwargs) widget = widget_type(**widget_kwargs) elif dim.range != (None, None): start, end = dim.range if start == end: continue default = start if dim.default is None else dim.default if widget_type is not None: pass elif all(isinstance(v, int) for v in (start, end, default)): widget_type = IntSlider step = 1 if dim.step is None else dim.step elif isinstance(default, datetime_types): widget_type = DatetimeInput else: widget_type = FloatSlider step = 0.1 if dim.step is None else dim.step widget_kwargs = dict(dict(step=step, name=dim.label, start=dim.range[0], end=dim.range[1], value=default), **widget_kwargs) widget = widget_type(**widget_kwargs) if widget is not None: widgets.append(widget) if widgets_type == 'scrubber': widgets = [Player(length=nframes, width=550)] return widgets, dim_values
class SpikesPlot(SpikesMixin, PathPlot, ColorbarPlot): aspect = param.Parameter(default='square', doc=""" The aspect ratio mode of the plot. Allows setting an explicit aspect ratio as width/height as well as 'square' and 'equal' options.""") color_index = param.ClassSelector(default=None, allow_None=True, class_=(str, int), doc=""" Index of the dimension from which the color will the drawn""") spike_length = param.Number(default=0.1, doc=""" The length of each spike if Spikes object is one dimensional.""") padding = param.ClassSelector(default=(0, 0.1), class_=(int, float, tuple)) position = param.Number(default=0., doc=""" The position of the lower end of each spike.""") style_opts = PathPlot.style_opts + ['cmap'] def init_artists(self, ax, plot_args, plot_kwargs): if 'c' in plot_kwargs: plot_kwargs['array'] = plot_kwargs.pop('c') if 'vmin' in plot_kwargs and 'vmax' in plot_kwargs: plot_kwargs['clim'] = plot_kwargs.pop('vmin'), plot_kwargs.pop( 'vmax') if not 'array' in plot_kwargs and 'cmap' in plot_kwargs: del plot_kwargs['cmap'] line_segments = LineCollection(*plot_args, **plot_kwargs) ax.add_collection(line_segments) return {'artist': line_segments} def get_data(self, element, ranges, style): dimensions = element.dimensions(label=True) ndims = len(dimensions) opts = self.lookup_options(element, 'plot').options pos = self.position if ndims > 1 and 'spike_length' not in opts: data = element.columns([0, 1]) xs, ys = data[dimensions[0]], data[dimensions[1]] data = [[(x, pos), (x, pos + y)] for x, y in zip(xs, ys)] else: xs = element.array([0]) height = self.spike_length data = [[(x[0], pos), (x[0], pos + height)] for x in xs] if self.invert_axes: data = [(line[0][::-1], line[1][::-1]) for line in data] dims = element.dimensions() clean_spikes = [] for spike in data: xs, ys = zip(*spike) cols = [] for i, vs in enumerate((xs, ys)): vs = np.array(vs) if isdatetime(vs): dt_format = Dimension.type_formatters.get( type(vs[0]), Dimension.type_formatters[np.datetime64]) vs = date2num(vs) dims[i] = dims[i].clone( value_format=DateFormatter(dt_format)) cols.append(vs) clean_spikes.append(np.column_stack(cols)) cdim = element.get_dimension(self.color_index) color = style.get('color', None) if cdim and ((isinstance(color, str) and color in element) or isinstance(color, dim)): self.param.warning( "Cannot declare style mapping for 'color' option and " "declare a color_index; ignoring the color_index.") cdim = None if cdim: style['array'] = element.dimension_values(cdim) self._norm_kwargs(element, ranges, style, cdim) if 'spike_length' in opts: axis_dims = (element.dimensions()[0], None) elif len(element.dimensions()) == 1: axis_dims = (element.dimensions()[0], None) else: axis_dims = (element.dimensions()[0], element.dimensions()[1]) with abbreviated_exception(): style = self._apply_transforms(element, ranges, style) return (clean_spikes, ), style, {'dimensions': axis_dims} def update_handles(self, key, axis, element, ranges, style): artist = self.handles['artist'] (data, ), kwargs, axis_kwargs = self.get_data(element, ranges, style) artist.set_paths(data) artist.set_visible(style.get('visible', True)) if 'color' in kwargs: artist.set_edgecolors(kwargs['color']) if 'array' in kwargs or 'c' in kwargs: artist.set_array(kwargs.get('array', kwargs.get('c'))) if 'vmin' in kwargs: artist.set_clim((kwargs['vmin'], kwargs['vmax'])) if 'norm' in kwargs: artist.norm = kwargs['norm'] if 'linewidth' in kwargs: artist.set_linewidths(kwargs['linewidth']) return axis_kwargs
class Vega(PaneBase): """ Vega panes allow rendering Vega plots and traces. For efficiency any array objects found inside a Figure are added to a ColumnDataSource which allows using binary transport to sync the figure on bokeh server and via Comms. """ debounce = param.ClassSelector(default=20, class_=(int, dict), doc=""" Declares the debounce time in milliseconds either for all events or if a dictionary is provided for individual events.""") margin = param.Parameter(default=(5, 5, 30, 5), doc=""" Allows to create additional space around the component. May be specified as a two-tuple of the form (vertical, horizontal) or a four-tuple (top, right, bottom, left).""") selection = param.ClassSelector(class_=Selection, doc=""" The Selection object reflects any selections available on the supplied vega plot into Python.""") show_actions = param.Boolean(default=False, doc=""" Whether to show Vega actions.""") theme = param.ObjectSelector(default=None, allow_None=True, objects=[ 'excel', 'ggplot2', 'quartz', 'vox', 'fivethirtyeight', 'dark', 'latimes', 'urbaninstitute', 'googlecharts' ]) priority = 0.8 _rename = {'selection': None, 'debounce': None} _updates = True def __init__(self, object=None, **params): super().__init__(object, **params) self.param.watch(self._update_selections, ['object']) self._update_selections() @property def _selections(self): return _get_selections(self.object) @property def _throttle(self): default = self.param.debounce.default if isinstance(self.debounce, dict): throttle = { sel: self.debounce.get(sel, default) for sel in self._selections } else: throttle = { sel: self.debounce or default for sel in self._selections } return throttle def _update_selections(self, *args): self.selection = Selection() for e in self._selections: self.selection.param._add_parameter(e, param.Dict()) @classmethod def is_altair(cls, obj): if 'altair' in sys.modules: import altair as alt return isinstance(obj, alt.api.TopLevelMixin) return False @classmethod def applies(cls, obj): if isinstance(obj, dict) and 'vega' in obj.get('$schema', '').lower(): return True return cls.is_altair(obj) @classmethod def _to_json(cls, obj): if isinstance(obj, dict): json = dict(obj) if 'data' in json: data = json['data'] if isinstance(data, dict): json['data'] = dict(data) elif isinstance(data, list): json['data'] = [dict(d) for d in data] return json return obj.to_dict() def _get_sources(self, json, sources): datasets = json.get('datasets', {}) for name in list(datasets): if name in sources or isinstance(datasets[name], dict): continue data = datasets.pop(name) if isinstance(data, list) and any( isinstance(d, dict) and 'geometry' in d for d in data): # Handle geometry records types datasets[name] = data continue columns = set(data[0]) if data else [] if self.is_altair(self.object): import altair as alt if (not isinstance( self.object.data, (alt.Data, alt.UrlData, type(alt.Undefined))) and columns == set(self.object.data)): data = ColumnDataSource.from_df(self.object.data) else: data = ds_as_cds(data) sources[name] = ColumnDataSource(data=data) else: sources[name] = ColumnDataSource(data=ds_as_cds(data)) data = json.get('data', {}) if isinstance(data, dict): data = data.pop('values', {}) if data: sources['data'] = ColumnDataSource(data=ds_as_cds(data)) elif isinstance(data, list): for d in data: if 'values' in d: sources[d['name']] = ColumnDataSource( data=ds_as_cds(d.pop('values'))) @classmethod def _get_dimensions(cls, json, props): if json is None: return if 'config' in json and 'view' in json['config']: size_config = json['config']['view'] else: size_config = json view = {} for w in ('width', 'continuousWidth'): if w in size_config: view['width'] = size_config[w] for h in ('height', 'continuousHeight'): if h in size_config: view['height'] = size_config[h] for p in ('width', 'height'): if p not in view or isinstance(view[p], str): continue if props.get(p) is None or p in view and props.get(p) < view[p]: v = view[p] props[p] = v + 22 if isinstance(v, int) else v responsive_height = json.get('height') == 'container' responsive_width = json.get('width') == 'container' if responsive_height and responsive_width: props['sizing_mode'] = 'stretch_both' elif responsive_width: props['sizing_mode'] = 'stretch_width' elif responsive_height: props['sizing_mode'] = 'stretch_height' def _process_event(self, event): self.selection.param.update( **{event.data['type']: event.data['value']}) def _get_model(self, doc, root=None, parent=None, comm=None): VegaPlot = lazy_load('panel.models.vega', 'VegaPlot', isinstance(comm, JupyterComm), root) sources = {} if self.object is None: json = None else: json = self._to_json(self.object) self._get_sources(json, sources) props = self._process_param_change(self._init_params()) self._get_dimensions(json, props) model = VegaPlot(data=json, data_sources=sources, events=self._selections, throttle=self._throttle, **props) if comm: model.on_event('vega_event', self._comm_event) else: model.on_event('vega_event', partial(self._server_event, doc)) if root is None: root = model self._models[root.ref['id']] = (model, parent) return model def _update(self, ref=None, model=None): if self.object is None: json = None else: json = self._to_json(self.object) self._get_sources(json, model.data_sources) props = { p: getattr(self, p) for p in list(Layoutable.param) if getattr(self, p) is not None } props['throttle'] = self._throttle props['events'] = self._selections self._get_dimensions(json, props) props['data'] = json model.update(**props)
class SideHistogramPlot(AdjoinedPlot, HistogramPlot): bgcolor = param.Parameter(default=(1, 1, 1, 0), doc=""" Make plot background invisible.""") offset = param.Number(default=0.2, bounds=(0, 1), doc=""" Histogram value offset for a colorbar.""") show_grid = param.Boolean(default=False, doc=""" Whether to overlay a grid on the axis.""") def _process_hist(self, hist): """ Subclassed to offset histogram by defined amount. """ edges, hvals, widths, lims, isdatetime = super()._process_hist(hist) offset = self.offset * lims[3] hvals = hvals * (1 - self.offset) hvals += offset lims = lims[0:3] + (lims[3] + offset, ) return edges, hvals, widths, lims, isdatetime def _update_artists(self, n, element, edges, hvals, widths, lims, ranges): super()._update_artists(n, element, edges, hvals, widths, lims, ranges) self._update_plot(n, element, self.handles['artist'], lims, ranges) def _update_plot(self, key, element, bars, lims, ranges): """ Process the bars and draw the offset line as necessary. If a color map is set in the style of the 'main' ViewableElement object, color the bars appropriately, respecting the required normalization settings. """ main = self.adjoined.main _, y1 = element.range(1) offset = self.offset * y1 range_item, main_range, dim = get_sideplot_ranges( self, element, main, ranges) # Check if plot is colormapped plot_type = Store.registry['matplotlib'].get(type(range_item)) if isinstance(plot_type, PlotSelector): plot_type = plot_type.get_plot_class(range_item) opts = self.lookup_options(range_item, 'plot') if plot_type and issubclass(plot_type, ColorbarPlot): cidx = opts.options.get('color_index', None) if cidx is None: opts = self.lookup_options(range_item, 'style') cidx = opts.kwargs.get('color', None) if cidx not in range_item: cidx = None cdim = None if cidx is None else range_item.get_dimension(cidx) else: cdim = None # Get colormapping options if isinstance(range_item, (HeatMap, Raster)) or (cdim and cdim in element): style = self.lookup_options(range_item, 'style')[self.cyclic_index] cmap = cm.get_cmap(style.get('cmap')) main_range = style.get('clims', main_range) else: cmap = None if offset and ('offset_line' not in self.handles): self.handles['offset_line'] = self.offset_linefn(offset, linewidth=1.0, color='k') elif offset: self._update_separator(offset) if cmap is not None and main_range and (None not in main_range): self._colorize_bars(cmap, bars, element, main_range, dim) return bars def _colorize_bars(self, cmap, bars, element, main_range, dim): """ Use the given cmap to color the bars, applying the correct color ranges as necessary. """ cmap_range = main_range[1] - main_range[0] lower_bound = main_range[0] colors = np.array(element.dimension_values(dim)) colors = (colors - lower_bound) / (cmap_range) for c, bar in zip(colors, bars): bar.set_facecolor(cmap(c)) bar.set_clip_on(False) def _update_separator(self, offset): """ Compute colorbar offset and update separator line if map is non-zero. """ offset_line = self.handles['offset_line'] if offset == 0: offset_line.set_visible(False) else: offset_line.set_visible(True) if self.invert_axes: offset_line.set_xdata(offset) else: offset_line.set_ydata(offset)
class PaneBase(Reactive): """ PaneBase is the abstract baseclass for all atomic displayable units in the panel library. Pane defines an extensible interface for wrapping arbitrary objects and transforming them into Bokeh models. Panes are reactive in the sense that when the object they are wrapping is changed any dashboard containing the pane will update in response. To define a concrete Pane type subclass this class and implement the applies classmethod and the _get_model private method. """ default_layout = param.ClassSelector(default=Row, class_=(Panel), is_instance=False, doc=""" Defines the layout the model(s) returned by the pane will be placed in.""") object = param.Parameter(default=None, doc=""" The object being wrapped, which will be converted to a Bokeh model.""") # When multiple Panes apply to an object, the one with the highest # numerical priority is selected. The default is an intermediate value. # If set to None, applies method will be called to get a priority # value for a specific object type. priority = 0.5 # Whether applies requires full set of keywords _applies_kw = False # Whether the Pane layout can be safely unpacked _unpack = True # Declares whether Pane supports updates to the Bokeh model _updates = False # List of parameters that trigger a rerender of the Bokeh model _rerender_params = ['object'] __abstract = True def __init__(self, object=None, **params): applies = self.applies(object, **(params if self._applies_kw else {})) if (isinstance(applies, bool) and not applies) and object is not None: self._type_error(object) super(PaneBase, self).__init__(object=object, **params) kwargs = {k: v for k, v in params.items() if k in Layoutable.param} self.layout = self.default_layout(self, **kwargs) watcher = self.param.watch(self._update_pane, self._rerender_params) self._callbacks.append(watcher) def _type_error(self, object): raise ValueError("%s pane does not support objects of type '%s'." % (type(self).__name__, type(object).__name__)) def __repr__(self, depth=0): cls = type(self).__name__ params = param_reprs(self, ['object']) obj = 'None' if self.object is None else type(self.object).__name__ template = '{cls}({obj}, {params})' if params else '{cls}({obj})' return template.format(cls=cls, params=', '.join(params), obj=obj) def __getitem__(self, index): """ Allows pane objects to behave like the underlying layout """ return self.layout[index] #---------------------------------------------------------------- # Callback API #---------------------------------------------------------------- @property def _linkable_params(self): return [ p for p in self._synced_params() if self._rename.get(p, False) is not None ] def _synced_params(self): ignored_params = ['name', 'default_layout'] + self._rerender_params return [p for p in self.param if p not in ignored_params] def _init_properties(self): return { k: v for k, v in self.param.get_param_values() if v is not None and k not in ['default_layout', 'object'] } def _update_object(self, ref, doc, root, parent, comm): old_model = self._models[ref][0] if self._updates: self._update(ref, old_model) else: new_model = self._get_model(doc, root, parent, comm) try: if isinstance(parent, _BkGridBox): indexes = [ i for i, child in enumerate(parent.children) if child[0] is old_model ] if indexes: index = indexes[0] else: raise ValueError new_model = (new_model, ) + parent.children[index][1:] else: index = parent.children.index(old_model) except ValueError: self.warning('%s pane model %s could not be replaced ' 'with new model %s, ensure that the ' 'parent is not modified at the same ' 'time the panel is being updated.' % (type(self).__name__, old_model, new_model)) else: parent.children[index] = new_model from ..io import state ref = root.ref['id'] if ref in state._views: state._views[ref][0]._preprocess(root) def _update_pane(self, *events): for ref, (_, parent) in self._models.items(): if ref not in state._views or ref in state._fake_roots: continue viewable, root, doc, comm = state._views[ref] if comm or state._unblocked(doc): with unlocked(): self._update_object(ref, doc, root, parent, comm) if comm and 'embedded' not in root.tags: push(doc, comm) else: cb = partial(self._update_object, ref, doc, root, parent, comm) if doc.session_context: doc.add_next_tick_callback(cb) else: cb() def _update(self, ref=None, model=None): """ If _updates=True this method is used to update an existing Bokeh model instead of replacing the model entirely. The supplied model should be updated with the current state. """ raise NotImplementedError #---------------------------------------------------------------- # Public API #---------------------------------------------------------------- @classmethod def applies(cls, obj): """ Given the object return a boolean indicating whether the Pane can render the object. If the priority of the pane is set to None, this method may also be used to define a priority depending on the object being rendered. """ return None def clone(self, object=None, **params): """ Makes a copy of the Pane sharing the same parameters. Arguments --------- params: Keyword arguments override the parameters on the clone. Returns ------- Cloned Pane object """ params = dict(self.param.get_param_values(), **params) old_object = params.pop('object') if object is None: object = old_object return type(self)(object, **params) def get_root(self, doc=None, comm=None): """ Returns the root model and applies pre-processing hooks Arguments --------- doc: bokeh.Document Bokeh document the bokeh model will be attached to. comm: pyviz_comms.Comm Optional pyviz_comms when working in notebook Returns ------- Returns the bokeh model corresponding to this panel object """ doc = doc or _curdoc() if self._updates: root = self._get_model(doc, comm=comm) else: root = self.layout._get_model(doc, comm=comm) self._preprocess(root) ref = root.ref['id'] state._views[ref] = (self, root, doc, comm) return root @classmethod def get_pane_type(cls, obj, **kwargs): """ Returns the applicable Pane type given an object by resolving the precedence of all types whose applies method declares that the object is supported. Arguments --------- obj (object): The object type to return a Pane for Returns ------- The applicable Pane type with the highest precedence. """ if isinstance(obj, Viewable): return type(obj) descendents = [] for p in param.concrete_descendents(PaneBase).values(): if p.priority is None: applies = True try: priority = p.applies(obj, **(kwargs if p._applies_kw else {})) except Exception: priority = False else: applies = None priority = p.priority if isinstance(priority, bool) and priority: raise ValueError('If a Pane declares no priority ' 'the applies method should return a ' 'priority value specific to the ' 'object type or False, but the %s pane ' 'declares no priority.' % p.__name__) elif priority is None or priority is False: continue descendents.append((priority, applies, p)) pane_types = reversed(sorted(descendents, key=lambda x: x[0])) for _, applies, pane_type in pane_types: if applies is None: try: applies = pane_type.applies( obj, **(kwargs if pane_type._applies_kw else {})) except Exception: applies = False if not applies: continue return pane_type raise TypeError('%s type could not be rendered.' % type(obj).__name__)
class PatternGenerator(param.Parameterized): """ A class hierarchy for callable objects that can generate 2D patterns. Once initialized, PatternGenerators can be called to generate a value or a matrix of values from a 2D function, typically accepting at least x and y. A PatternGenerator's Parameters can make use of Parameter's precedence attribute to specify the order in which they should appear, e.g. in a GUI. The precedence attribute has a nominal range of 0.0 to 1.0, with ordering going from 0.0 (first) to 1.0 (last), but any value is allowed. The orientation and layout of the pattern matrices is defined by the SheetCoordinateSystem class, which see. Note that not every parameter defined for a PatternGenerator will be used by every subclass. For instance, a Constant pattern will ignore the x, y, orientation, and size parameters, because the pattern does not vary with any of those parameters. However, those parameters are still defined for all PatternGenerators, even Constant patterns, to allow PatternGenerators to be scaled, rotated, translated, etc. uniformly. """ __abstract = True bounds = BoundingRegionParameter( default=BoundingBox(points=((-0.5, -0.5), (0.5, 0.5))), precedence=-1, doc="BoundingBox of the area in which the pattern is generated.") xdensity = param.Number(default=10, bounds=(0, None), precedence=-1, doc=""" Density (number of samples per 1.0 length) in the x direction.""") ydensity = param.Number(default=10, bounds=(0, None), precedence=-1, doc=""" Density (number of samples per 1.0 length) in the y direction. Typically the same as the xdensity.""") x = param.Number(default=0.0, softbounds=(-1.0, 1.0), precedence=0.20, doc=""" X-coordinate location of pattern center.""") y = param.Number(default=0.0, softbounds=(-1.0, 1.0), precedence=0.21, doc=""" Y-coordinate location of pattern center.""") position = param.Composite(attribs=['x', 'y'], precedence=-1, doc=""" Coordinates of location of pattern center. Provides a convenient way to set the x and y parameters together as a tuple (x,y), but shares the same actual storage as x and y (and thus only position OR x and y need to be specified).""") orientation = param.Number(default=0.0, softbounds=(0.0, 2 * pi), precedence=0.40, doc=""" Polar angle of pattern, i.e., the orientation in the Cartesian coordinate system, with zero at 3 o'clock and increasing counterclockwise.""") size = param.Number(default=1.0, bounds=(0.0, None), softbounds=(0.0, 6.0), precedence=0.30, doc="""Determines the overall size of the pattern.""") scale = param.Number(default=1.0, softbounds=(0.0, 2.0), precedence=0.10, doc=""" Multiplicative strength of input pattern, defaulting to 1.0""") offset = param.Number(default=0.0, softbounds=(-1.0, 1.0), precedence=0.11, doc=""" Additive offset to input pattern, defaulting to 0.0""") mask = param.Parameter(default=None, precedence=-1, doc=""" Optional object (expected to be an array) with which to multiply the pattern array after it has been created, before any output_fns are applied. This can be used to shape the pattern.""") # Note that the class type is overridden to PatternGenerator below mask_shape = param.ClassSelector(param.Parameterized, default=None, precedence=0.06, doc=""" Optional PatternGenerator used to construct a mask to be applied to the pattern.""") output_fns = param.HookList(default=[], class_=TransferFn, precedence=0.08, doc=""" Optional function(s) to apply to the pattern array after it has been created. Can be used for normalization, thresholding, etc.""") def __init__(self, **params): super(PatternGenerator, self).__init__(**params) self.set_matrix_dimensions(self.bounds, self.xdensity, self.ydensity) def __call__(self, **params_to_override): """ Call the subclass's 'function' method on a rotated and scaled coordinate system. Creates and fills an array with the requested pattern. If called without any params, uses the values for the Parameters as currently set on the object. Otherwise, any params specified override those currently set on the object. """ p = ParamOverrides(self, params_to_override) # CEBERRORALERT: position parameter is not currently # supported. We should delete the position parameter or fix # this. # # position=params_to_override.get('position',None) if position # is not None: x,y = position self._setup_xy(p.bounds, p.xdensity, p.ydensity, p.x, p.y, p.orientation) fn_result = self.function(p) self._apply_mask(p, fn_result) result = p.scale * fn_result + p.offset for of in p.output_fns: of(result) return result def _setup_xy(self, bounds, xdensity, ydensity, x, y, orientation): """ Produce pattern coordinate matrices from the bounds and density (or rows and cols), and transforms them according to x, y, and orientation. """ self.debug( lambda: "bounds=%s, xdensity=%s, ydensity=%s, x=%s, y=%s, orientation=%s" % (bounds, xdensity, ydensity, x, y, orientation)) # Generate vectors representing coordinates at which the pattern # will be sampled. # CB: note to myself - use slice_._scs if supplied? x_points, y_points = SheetCoordinateSystem( bounds, xdensity, ydensity).sheetcoordinates_of_matrixidx() # Generate matrices of x and y sheet coordinates at which to # sample pattern, at the correct orientation self.pattern_x, self.pattern_y = self._create_and_rotate_coordinate_arrays( x_points - x, y_points - y, orientation) def function(self, p): """ Function to draw a pattern that will then be scaled and rotated. Instead of implementing __call__ directly, PatternGenerator subclasses will typically implement this helper function used by __call__, because that way they can let __call__ handle the scaling and rotation for them. Alternatively, __call__ itself can be reimplemented entirely by a subclass (e.g. if it does not need to do any scaling or rotation), in which case this function will be ignored. """ raise NotImplementedError def _create_and_rotate_coordinate_arrays(self, x, y, orientation): """ Create pattern matrices from x and y vectors, and rotate them to the specified orientation. """ # Using this two-liner requires that x increase from left to # right and y decrease from left to right; I don't think it # can be rewritten in so little code otherwise - but please # prove me wrong. pattern_y = subtract.outer(cos(orientation) * y, sin(orientation) * x) pattern_x = add.outer(sin(orientation) * y, cos(orientation) * x) return pattern_x, pattern_y def _apply_mask(self, p, mat): """Create (if necessary) and apply the mask to the given matrix mat.""" mask = p.mask ms = p.mask_shape if ms is not None: mask = ms(x=p.x + p.size * (ms.x * cos(p.orientation) - ms.y * sin(p.orientation)), y=p.y + p.size * (ms.x * sin(p.orientation) + ms.y * cos(p.orientation)), orientation=ms.orientation + p.orientation, size=ms.size * p.size, bounds=p.bounds, ydensity=p.ydensity, xdensity=p.xdensity) if mask is not None: mat *= mask def set_matrix_dimensions(self, bounds, xdensity, ydensity): """ Change the dimensions of the matrix into which the pattern will be drawn. Users of this class should call this method rather than changing the bounds, xdensity, and ydensity parameters directly. Subclasses can override this method to update any internal data structures that may depend on the matrix dimensions. """ self.bounds = bounds self.xdensity = xdensity self.ydensity = ydensity
class Card(Column): """ A Card layout allows arranging multiple panel objects in a collapsible, vertical container with a header bar. """ active_header_background = param.String(doc=""" A valid CSS color for the header background when not collapsed.""") button_css_classes = param.List(['card-button'], doc=""" CSS classes to apply to the button element.""") collapsible = param.Boolean(default=True, doc=""" Whether the Card should be expandable and collapsible.""") collapsed = param.Boolean(default=False, doc=""" Whether the contents of the Card are collapsed.""") css_classes = param.List(['card'], doc=""" CSS classes to apply to the overall Card.""") header = param.Parameter(doc=""" A Panel component to display in the header bar of the Card. Will override the given title if defined.""") header_background = param.String(doc=""" A valid CSS color for the header background.""") header_color = param.String(doc=""" A valid CSS color to apply to the header text.""") header_css_classes = param.List(['card-header'], doc=""" CSS classes to apply to the header element.""") title_css_classes = param.List(['card-title'], doc=""" CSS classes to apply to the header title.""") margin = param.Parameter(default=5) title = param.String(doc=""" A title to be displayed in the Card header, will be overridden by the header if defined.""") _bokeh_model = BkCard _rename = dict(Column._rename, title=None, header=None, title_css_classes=None) def __init__(self, *objects, **params): self._header_layout = Row(css_classes=['card-header-row'], sizing_mode='stretch_width') super(Card, self).__init__(*objects, **params) self.param.watch(self._update_header, ['title', 'header', 'title_css_classes']) self._update_header() def _cleanup(self, root): super(Card, self)._cleanup(root) self._header_layout._cleanup(root) def _process_param_change(self, params): scroll = params.pop('scroll', None) css_classes = self.css_classes or [] if scroll: params['css_classes'] = css_classes + ['scrollable'] elif scroll == False: params['css_classes'] = css_classes return super(ListPanel, self)._process_param_change(params) def _update_header(self, *events): from ..pane import HTML, panel if self.header is None: item = HTML('%s' % (self.title or "​"), css_classes=self.title_css_classes, sizing_mode='stretch_width', margin=(2, 5)) else: item = panel(self.header) self._header_layout[:] = [item] def _get_objects(self, model, old_objects, doc, root, comm=None): ref = root.ref['id'] if ref in self._header_layout._models: header = self._header_layout._models[ref][0] else: header = self._header_layout._get_model(doc, root, model, comm) objects = super(Card, self)._get_objects(model, old_objects, doc, root) return [header] + objects
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.""") 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.""") logz = param.Boolean(default=False, doc=""" Whether to apply log scaling to the y-axis of the Chart.""") xformatter = param.ClassSelector( default=None, class_=(util.basestring, ticker.Formatter, FunctionType), doc=""" Formatter for ticks along the x-axis.""") yformatter = param.ClassSelector( default=None, class_=(util.basestring, ticker.Formatter, FunctionType), doc=""" Formatter for ticks along the y-axis.""") zformatter = param.ClassSelector( default=None, class_=(util.basestring, ticker.Formatter, FunctionType), doc=""" Formatter for ticks along the z-axis.""") zaxis = param.Boolean(default=True, doc=""" Whether to display the z-axis.""") 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 is not None 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(element) self._execute_hooks(element) 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, self.xformatter) if ydim: self._set_axis_formatter(axis.yaxis, ydim, self.yformatter) if self.projection == '3d': zdim = dimensions[2] if ndims > 2 else None if zdim or self.zformatter is not None: self._set_axis_formatter(axis.zaxis, zdim, self.zformatter) 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) axes_str = 'xy' axes_list = [axis.xaxis, axis.yaxis] if hasattr(axis, 'zaxis'): axes_str += 'z' axes_list.append(axis.zaxis) for ax, ax_obj in zip(axes_str, axes_list): 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, formatter): """ Set axis formatter based on dimension formatter. """ if isinstance(dim, list): dim = dim[0] if formatter is not None: pass elif 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 get_aspect(self, xspan, yspan): """ Computes the aspect ratio of the plot """ if isinstance(self.aspect, (int, float)): return self.aspect elif self.aspect == 'square': return 1 elif self.aspect == 'equal': return xspan/yspan return 1 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 in [None, False]: axis.set_visible(False) for pos in positions: axes.spines[pos].set_visible(False) else: if option is True: option = positions[0] 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) self.current_ranges = ranges self.current_frame = element self.current_key = key 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 FileDownload(Widget): auto = param.Boolean(default=True, doc=""" Whether to download on the initial click or allow for right-click save as.""") button_type = param.ObjectSelector( default='default', objects=['default', 'primary', 'success', 'warning', 'danger']) callback = param.Callable(default=None, doc=""" A callable that returns the file path or file-like object.""") data = param.String(default=None, doc=""" The data being transferred.""") embed = param.Boolean(default=False, doc=""" Whether to embed the file on initialization.""") file = param.Parameter(default=None, doc=""" The file, file-like object or file contents to transfer. If the file is not pointing to a file on disk a filename must also be provided.""") filename = param.String(default=None, doc=""" A filename which will also be the default name when downloading the file.""") label = param.String(default="Download file", doc=""" The label of the download button""") _clicks = param.Integer(default=0) _transfers = param.Integer(default=0) _mime_types = { 'application': { 'pdf': 'pdf', 'zip': 'zip' }, 'audio': { 'mp3': 'mp3', 'ogg': 'ogg', 'wav': 'wav', 'webm': 'webm' }, 'image': { 'apng': 'apng', 'bmp': 'bmp', 'gif': 'gif', 'ico': 'x-icon', 'cur': 'x-icon', 'jpg': 'jpeg', 'jpeg': 'jpeg', 'png': 'png', 'svg': 'svg+xml', 'tif': 'tiff', 'tiff': 'tiff', 'webp': 'webp' }, 'text': { 'css': 'css', 'csv': 'plain;charset=UTF-8', 'js': 'javascript', 'html': 'html', 'txt': 'plain;charset=UTF-8' }, 'video': { 'mp4': 'mp4', 'ogg': 'ogg', 'webm': 'webm' } } _widget_type = _BkFileDownload _rename = { 'callback': None, 'embed': None, 'file': None, '_clicks': 'clicks', 'name': 'title' } def __init__(self, file=None, **params): self._default_label = 'label' not in params self._synced = False super().__init__(file=file, **params) if self.embed: self._transfer() self._update_label() @param.depends('label', watch=True) def _update_default(self): self._default_label = False @param.depends('file', watch=True) def _update_filename(self): if isinstance(self.file, str): self.filename = os.path.basename(self.file) @param.depends('auto', 'file', 'filename', watch=True) def _update_label(self): label = 'Download' if self._synced or self.auto else 'Transfer' if self._default_label: if self.file is None and self.callback is None: label = 'No file set' else: try: filename = self.filename or os.path.basename(self.file) except TypeError: raise ValueError('Must provide filename if file-like ' 'object is provided.') label = '%s %s' % (label, filename) self.label = label self._default_label = True @param.depends('embed', 'file', 'callback', watch=True) def _update_embed(self): if self.embed: self._transfer() @param.depends('_clicks', watch=True) def _transfer(self): if self.file is None and self.callback is None: if self.embed: raise ValueError('Must provide a file or a callback ' 'if it is to be embedded.') return from ..param import ParamFunction if self.callback is None: fileobj = self.file else: fileobj = ParamFunction.eval(self.callback) filename = self.filename if isinstance(fileobj, str): if not os.path.isfile(fileobj): raise FileNotFoundError('File "%s" not found.' % fileobj) with open(fileobj, 'rb') as f: b64 = b64encode(f.read()).decode("utf-8") if filename is None: filename = os.path.basename(fileobj) elif hasattr(fileobj, 'read'): bdata = fileobj.read() if not isinstance(bdata, bytes): bdata = bdata.encode("utf-8") b64 = b64encode(bdata).decode("utf-8") if filename is None: raise ValueError('Must provide filename if file-like ' 'object is provided.') else: raise ValueError('Cannot transfer unknown object of type %s' % type(fileobj).__name__) ext = filename.split('.')[-1] for mtype, subtypes in self._mime_types.items(): stype = None if ext in subtypes: stype = subtypes[ext] break if stype is None: mime = 'application/octet-stream' else: mime = '{type}/{subtype}'.format(type=mtype, subtype=stype) data = "data:{mime};base64,{b64}".format(mime=mime, b64=b64) self._synced = True self.param.set_param(data=data, filename=filename) self._update_label() self._transfers += 1
class ColorbarPlot(ElementPlot): colorbar = param.Boolean(default=False, doc=""" Whether to draw a colorbar.""") color_levels = param.ClassSelector(default=None, class_=(int, list), doc=""" Number of discrete colors to use when colormapping or a set of color intervals defining the range of values to map each color to.""") clipping_colors = param.Dict(default={}, doc=""" Dictionary to specify colors for clipped values, allows setting color for NaN values and for values above and below the min and max value. The min, max or NaN color may specify an RGB(A) color as a color hex string of the form #FFFFFF or #FFFFFFFF or a length 3 or length 4 tuple specifying values in the range 0-1 or a named HTML color.""") cbar_padding = param.Number(default=0.01, doc=""" Padding between colorbar and other plots.""") cbar_ticks = param.Parameter(default=None, doc=""" Ticks along colorbar-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.""") cbar_width = param.Number(default=0.05, doc=""" Width of the colorbar as a fraction of the main plot""") symmetric = param.Boolean(default=False, doc=""" Whether to make the colormap symmetric around zero.""") _colorbars = {} _default_nan = '#8b8b8b' def __init__(self, *args, **kwargs): super(ColorbarPlot, self).__init__(*args, **kwargs) self._cbar_extend = 'neither' def _adjust_cbar(self, cbar, label, dim): noalpha = math.floor(self.style[self.cyclic_index].get('alpha', 1)) == 1 if (cbar.solids and noalpha): cbar.solids.set_edgecolor("face") cbar.set_label(label) if isinstance(self.cbar_ticks, ticker.Locator): cbar.ax.yaxis.set_major_locator(self.cbar_ticks) elif self.cbar_ticks == 0: cbar.set_ticks([]) elif isinstance(self.cbar_ticks, int): locator = ticker.MaxNLocator(self.cbar_ticks) cbar.ax.yaxis.set_major_locator(locator) elif isinstance(self.cbar_ticks, list): if all(isinstance(t, tuple) for t in self.cbar_ticks): ticks, labels = zip(*self.cbar_ticks) else: ticks, labels = zip(*[(t, dim.pprint_value(t)) for t in self.cbar_ticks]) cbar.set_ticks(ticks) cbar.set_ticklabels(labels) def _finalize_artist(self, element): artist = self.handles.get('artist', None) if artist and self.colorbar: color_dim = element.get_dimension(getattr(self, 'color_index', None)) self._draw_colorbar(color_dim) def _draw_colorbar(self, dim=None, redraw=True): element = self.hmap.last artist = self.handles.get('artist', None) fig = self.handles['fig'] axis = self.handles['axis'] ax_colorbars, position = ColorbarPlot._colorbars.get(id(axis), ([], None)) specs = [spec[:2] for _, _, spec, _ in ax_colorbars] spec = util.get_spec(element) if position is None or not redraw: if redraw: fig.canvas.draw() bbox = axis.get_position() l, b, w, h = bbox.x0, bbox.y0, bbox.width, bbox.height else: l, b, w, h = position # Get colorbar label dim = element.get_dimension(dim) if dim: label = dim.pprint_label elif element.vdims: label = element.vdims[0].pprint_label elif dim is None: label = '' padding = self.cbar_padding width = self.cbar_width if spec[:2] not in specs: offset = len(ax_colorbars) scaled_w = w*width cax = fig.add_axes([l+w+padding+(scaled_w+padding+w*0.15)*offset, b, scaled_w, h]) cbar = fig.colorbar(artist, cax=cax, ax=axis, extend=self._cbar_extend) self._adjust_cbar(cbar, label, dim) self.handles['cax'] = cax self.handles['cbar'] = cbar ylabel = cax.yaxis.get_label() self.handles['bbox_extra_artists'] += [cax, ylabel] ax_colorbars.append((artist, cax, spec, label)) for i, (artist, cax, spec, label) in enumerate(ax_colorbars): scaled_w = w*width cax.set_position([l+w+padding+(scaled_w+padding+w*0.15)*i, b, scaled_w, h]) ColorbarPlot._colorbars[id(axis)] = (ax_colorbars, (l, b, w, h)) def _norm_kwargs(self, element, ranges, opts, vdim, prefix=''): """ Returns valid color normalization kwargs to be passed to matplotlib plot function. """ clim = opts.pop(prefix+'clims', None) values = np.asarray(element.dimension_values(vdim)) if clim is None: if not len(values): clim = (0, 0) categorical = False elif values.dtype.kind in 'uif': if vdim.name in ranges: clim = ranges[vdim.name]['combined'] else: clim = element.range(vdim) if self.logz: # Lower clim must be >0 when logz=True # Choose the maximum between the lowest non-zero value # and the overall range if clim[0] == 0: clim = (values[values!=0].min(), clim[1]) if self.symmetric: clim = -np.abs(clim).max(), np.abs(clim).max() categorical = False else: clim = (0, len(np.unique(values))-1) categorical = True else: categorical = values.dtype.kind not in 'uif' if self.logz: if self.symmetric: norm = mpl_colors.SymLogNorm(vmin=clim[0], vmax=clim[1], linthresh=clim[1]/np.e) else: norm = mpl_colors.LogNorm(vmin=clim[0], vmax=clim[1]) opts[prefix+'norm'] = norm opts[prefix+'vmin'] = clim[0] opts[prefix+'vmax'] = clim[1] cmap = opts.get(prefix+'cmap', 'viridis') if values.dtype.kind not in 'OSUM': ncolors = None if isinstance(self.color_levels, int): ncolors = self.color_levels elif isinstance(self.color_levels, list): ncolors = len(self.color_levels) - 1 if isinstance(cmap, list) and len(cmap) != ncolors: raise ValueError('The number of colors in the colormap ' 'must match the intervals defined in the ' 'color_levels, expected %d colors found %d.' % (ncolors, len(cmap))) try: el_min, el_max = np.nanmin(values), np.nanmax(values) except ValueError: el_min, el_max = -np.inf, np.inf else: ncolors = clim[-1]+1 el_min, el_max = -np.inf, np.inf vmin = -np.inf if opts[prefix+'vmin'] is None else opts[prefix+'vmin'] vmax = np.inf if opts[prefix+'vmax'] is None else opts[prefix+'vmax'] if el_min < vmin and el_max > vmax: self._cbar_extend = 'both' elif el_min < vmin: self._cbar_extend = 'min' elif el_max > vmax: self._cbar_extend = 'max' # Define special out-of-range colors on colormap colors = {} for k, val in self.clipping_colors.items(): if val == 'transparent': colors[k] = {'color': 'w', 'alpha': 0} elif isinstance(val, tuple): colors[k] = {'color': val[:3], 'alpha': val[3] if len(val) > 3 else 1} elif isinstance(val, util.basestring): color = val alpha = 1 if color.startswith('#') and len(color) == 9: alpha = int(color[-2:], 16)/255. color = color[:-2] colors[k] = {'color': color, 'alpha': alpha} if not isinstance(cmap, mpl_colors.Colormap): if isinstance(cmap, dict): factors = np.unique(values) palette = [cmap.get(f, colors.get('NaN', {'color': self._default_nan})['color']) for f in factors] else: palette = process_cmap(cmap, ncolors, categorical=categorical) if isinstance(self.color_levels, list): palette, (vmin, vmax) = color_intervals(palette, self.color_levels, clip=(vmin, vmax)) cmap = mpl_colors.ListedColormap(palette) if 'max' in colors: cmap.set_over(**colors['max']) if 'min' in colors: cmap.set_under(**colors['min']) if 'NaN' in colors: cmap.set_bad(**colors['NaN']) opts[prefix+'cmap'] = cmap
class GridPlot(CompositePlot): """ Plot a group of elements in a grid layout based on a GridSpace element object. """ aspect = param.Parameter(default='equal', doc=""" Aspect ratios on GridPlot should be automatically determined.""") padding = param.Number(default=0.1, doc=""" The amount of padding as a fraction of the total Grid size""") 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.""") show_frame = param.Boolean(default=False, doc=""" Whether to draw a frame around the Grid.""") show_legend = param.Boolean(default=False, doc=""" Legends add to much clutter in a grid and are disabled by default.""") tick_format = param.String(default="%.2f", doc=""" Formatting string for the GridPlot ticklabels.""") xaxis = param.ObjectSelector(default='bottom', objects=['bottom', 'top', None], doc=""" Whether and where to display the xaxis.""") yaxis = param.ObjectSelector(default='left', objects=['left', 'right', None], doc=""" Whether and where to display the yaxis.""") 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 xticks.""") def __init__(self, layout, axis=None, create_axes=True, ranges=None, keys=None, dimensions=None, layout_num=1, **params): if not isinstance(layout, GridSpace): raise Exception("GridPlot only accepts GridSpace.") self.layout = layout self.cols, self.rows = layout.shape self.layout_num = layout_num extra_opts = self.lookup_options(layout, 'plot').options if not keys or not dimensions: dimensions, keys = traversal.unique_dimkeys(layout) if 'uniform' not in params: params['uniform'] = traversal.uniform(layout) super(GridPlot, self).__init__(keys=keys, dimensions=dimensions, **dict(extra_opts, **params)) # Compute ranges layoutwise grid_kwargs = {} if axis is not None: bbox = axis.get_position() l, b, w, h = bbox.x0, bbox.y0, bbox.width, bbox.height grid_kwargs = { 'left': l, 'right': l + w, 'bottom': b, 'top': b + h } self.position = (l, b, w, h) self.fig_inches = self._get_size() self._layoutspec = gridspec.GridSpec(self.rows, self.cols, **grid_kwargs) self.subplots, self.subaxes, self.layout = self._create_subplots( layout, axis, ranges, create_axes) def _get_size(self): max_dim = max(self.layout.shape) # Reduce plot size as GridSpace gets larger shape_factor = 1. / max_dim # Expand small grids to a sensible viewing size expand_factor = 1 + (max_dim - 1) * 0.1 scale_factor = expand_factor * shape_factor cols, rows = self.layout.shape if isinstance(self.fig_inches, (tuple, list)): fig_inches = list(self.fig_inches) if fig_inches[0] is None: fig_inches[0] = fig_inches[1] * (cols / rows) if fig_inches[1] is None: fig_inches[1] = fig_inches[0] * (rows / cols) return fig_inches else: fig_inches = (self.fig_inches, ) * 2 return (scale_factor * cols * fig_inches[0], scale_factor * rows * fig_inches[1]) def _create_subplots(self, layout, axis, ranges, create_axes): layout = layout.map(Compositor.collapse_element, [CompositeOverlay], clone=False) norm_opts = self._deep_options(layout, 'norm', ['axiswise'], [Element]) axiswise = all(v.get('axiswise', False) for v in norm_opts.values()) if not ranges: self.handles['fig'].set_size_inches(self.fig_inches) subplots, subaxes = OrderedDict(), 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) r, c = (0, 0) for coord in layout.keys(full_grid=True): 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 # Create axes kwargs = {} if create_axes: threed = issubclass(vtype, Element3D) subax = plt.subplot(self._layoutspec[r, c], projection='3d' if threed else None) if not axiswise and self.shared_xaxis and self.xaxis is not None: self.xaxis = 'top' if not axiswise and self.shared_yaxis and self.yaxis is not None: self.yaxis = 'right' # Disable subplot axes depending on shared axis options # and the position in the grid if (self.shared_xaxis or self.shared_yaxis) and not axiswise: if c == 0 and r != 0: subax.xaxis.set_ticks_position('none') kwargs['xaxis'] = 'bottom-bare' if c != 0 and r == 0 and not layout.ndims == 1: subax.yaxis.set_ticks_position('none') kwargs['yaxis'] = 'left-bare' if r != 0 and c != 0: kwargs['xaxis'] = 'bottom-bare' kwargs['yaxis'] = 'left-bare' if not self.shared_xaxis: kwargs['xaxis'] = 'bottom-bare' if not self.shared_yaxis: kwargs['yaxis'] = 'left-bare' else: kwargs['xaxis'] = 'bottom-bare' kwargs['yaxis'] = 'left-bare' subaxes[(r, c)] = subax else: subax = None # Create subplot if view is not None: plotting_class = Store.registry['matplotlib'][vtype] subplot = plotting_class(view, fig=self.handles['fig'], axis=subax, dimensions=self.dimensions, show_title=False, subplot=not create_axes, ranges=frame_ranges, uniform=self.uniform, keys=self.keys, show_legend=False, **dict(opts, **kwargs)) collapsed_layout[coord] = subplot.layout if isinstance( subplot, CompositePlot) else subplot.map subplots[(r, c)] = subplot else: subax.set_visible(False) if r != self.rows - 1: r += 1 else: r = 0 c += 1 if create_axes: self.handles['axis'] = self._layout_axis(layout, axis) self._adjust_subplots(self.handles['axis'], subaxes) return subplots, subaxes, collapsed_layout def initialize_plot(self, ranges=None): # Get the extent of the layout elements (not the whole layout) key = self.keys[-1] axis = self.handles['axis'] subplot_kwargs = dict() ranges = self.compute_ranges(self.layout, key, ranges) for subplot in self.subplots.values(): subplot.initialize_plot(ranges=ranges, **subplot_kwargs) if self.show_title: title = axis.set_title(self._format_title(key), **self._fontsize('title')) self.handles['title'] = title self._readjust_axes(axis) self.drawn = True if self.subplot: return self.handles['axis'] plt.close(self.handles['fig']) return self.handles['fig'] def _readjust_axes(self, axis): if self.subplot: axis.set_position(self.position) if self.aspect == 'equal': axis.set_aspect(float(self.rows) / self.cols) self.handles['fig'].canvas.draw() self._adjust_subplots(self.handles['axis'], self.subaxes) def update_handles(self, axis, view, key, ranges=None): """ Should be called by the update_frame class to update any handles on the plot. """ if self.show_title: title = axis.set_title(self._format_title(key), **self._fontsize('title')) self.handles['title'] = title def _layout_axis(self, layout, axis): fig = self.handles['fig'] axkwargs = {'gid': str(self.position)} if axis else {} layout_axis = fig.add_subplot(1, 1, 1, **axkwargs) if axis: axis.set_visible(False) layout_axis.set_position(self.position) layout_axis.patch.set_visible(False) tick_fontsize = self._fontsize('ticks', 'labelsize', common=False) if tick_fontsize: layout_axis.tick_params(**tick_fontsize) # Set labels layout_axis.set_xlabel(str(layout.kdims[0]), **self._fontsize('xlabel')) if layout.ndims == 2: layout_axis.set_ylabel(str(layout.kdims[1]), **self._fontsize('ylabel')) # Compute and set x- and y-ticks dims = layout.kdims keys = layout.keys() if layout.ndims == 1: dim1_keys = keys dim2_keys = [0] layout_axis.get_yaxis().set_visible(False) else: dim1_keys, dim2_keys = zip(*keys) layout_axis.set_ylabel(str(dims[1])) layout_axis.set_aspect(float(self.rows) / self.cols) # Process ticks plot_width = (1.0 - self.padding) / self.cols border_width = self.padding / (self.cols - 1) xticks = [(plot_width / 2) + (r * (plot_width + border_width)) for r in range(self.cols)] plot_height = (1.0 - self.padding) / self.rows border_height = self.padding / (self.rows - 1) if layout.ndims > 1 else 0 yticks = [(plot_height / 2) + (r * (plot_height + border_height)) for r in range(self.rows)] layout_axis.set_xticks(xticks) layout_axis.set_xticklabels( self._process_ticklabels(sorted(set(dim1_keys)), dims[0])) for tick in layout_axis.get_xticklabels(): tick.set_rotation(self.xrotation) ydim = dims[1] if layout.ndims > 1 else None layout_axis.set_yticks(yticks) layout_axis.set_yticklabels( self._process_ticklabels(sorted(set(dim2_keys)), ydim)) for tick in layout_axis.get_yticklabels(): tick.set_rotation(self.yrotation) if not self.show_frame: layout_axis.spines['right' if self.yaxis == 'left' else 'left'].set_visible(False) layout_axis.spines['bottom' if self.xaxis == 'top' else 'top'].set_visible(False) axis = layout_axis if self.xaxis is not None: axis.xaxis.set_ticks_position(self.xaxis) axis.xaxis.set_label_position(self.xaxis) else: axis.xaxis.set_visible(False) if self.yaxis is not None: axis.yaxis.set_ticks_position(self.yaxis) axis.yaxis.set_label_position(self.yaxis) else: axis.yaxis.set_visible(False) for pos in ['left', 'right', 'top', 'bottom']: axis.spines[pos].set_visible(False) return layout_axis def _process_ticklabels(self, labels, dim): formatted_labels = [] for k in labels: if dim and dim.formatter: k = dim.formatter(k) elif not isinstance(k, (str, type(None))): k = self.tick_format % k elif k is None: k = '' formatted_labels.append(k) return formatted_labels def _adjust_subplots(self, axis, subaxes): bbox = axis.get_position() l, b, w, h = bbox.x0, bbox.y0, bbox.width, bbox.height if self.padding: width_padding = w / (1. / self.padding) height_padding = h / (1. / self.padding) else: width_padding, height_padding = 0, 0 if self.cols == 1: b_w = 0 else: b_w = width_padding / (self.cols - 1) if self.rows == 1: b_h = 0 else: b_h = height_padding / (self.rows - 1) ax_w = (w - (width_padding if self.cols > 1 else 0)) / self.cols ax_h = (h - (height_padding if self.rows > 1 else 0)) / self.rows r, c = (0, 0) for ax in subaxes.values(): xpos = l + (c * ax_w) + (c * b_w) ypos = b + (r * ax_h) + (r * b_h) if r != self.rows - 1: r += 1 else: r = 0 c += 1 if not ax is None: ax.set_position([xpos, ypos, ax_w, ax_h])
class DynamicMap(HoloMap): """ A DynamicMap is a type of HoloMap where the elements are dynamically generated by a callback which may be either a callable or a generator. A DynamicMap supports two different modes depending on the type of callable supplied and the dimension declarations. The 'closed' mode is used when the limits of the parameter space are known upon declaration (as specified by the ranges on the key dimensions) or 'open' which allows the continual generation of elements (e.g as data output by a simulator over an unbounded simulated time dimension). Generators always imply open mode but a callable that has any key dimension unbounded in any direction will also be in open mode. Closed mode only applied to callables where all the key dimensions are fully bounded. """ _sorted = False # Declare that callback is a positional parameter (used in clone) __pos_params = ['callback'] callback = param.Parameter(doc=""" The callable or generator used to generate the elements. In the simplest case where all key dimensions are bounded, this can be a callable that accepts the key dimension values as arguments (in the declared order) and returns the corresponding element. For open mode where there is an unbounded key dimension, the return type can specify a key as well as element as the tuple (key, element). If no key is supplied, a simple counter is used instead. If the callback is a generator, open mode is used and next() is simply called. If the callback is callable and in open mode, the element counter value will be supplied as the single argument. This can be used to avoid issues where multiple elements in a Layout each call next() leading to uncontrolled changes in simulator state (the counter can be used to indicate simulation time across the layout). """) cache_size = param.Integer(default=500, doc=""" The number of entries to cache for fast access. This is an LRU cache where the least recently used item is overwritten once the cache is full.""") cache_interval = param.Integer(default=1, doc=""" When the element counter modulo the cache_interval is zero, the element will be cached and therefore accessible when casting to a HoloMap. Applicable in open mode only.""") sampled = param.Boolean(default=False, doc=""" Allows defining a DynamicMap in closed mode without defining the dimension bounds or values. The DynamicMap may then be explicitly sampled via getitem or the sampling is determined during plotting by a HoloMap with fixed sampling. """) def __init__(self, callback, initial_items=None, **params): super(DynamicMap, self).__init__(initial_items, callback=callback, **params) self.counter = 0 if self.callback is None: raise Exception("A suitable callback must be " "declared to create a DynamicMap") self.call_mode = self._validate_mode() self.mode = 'closed' if self.call_mode == 'key' else 'open' def _initial_key(self): """ Construct an initial key for closed mode based on the lower range bounds or values on the key dimensions. """ key = [] for kdim in self.kdims: if kdim.values: key.append(kdim.values[0]) elif kdim.range: key.append(kdim.range[0]) return tuple(key) def _validate_mode(self): """ Check the key dimensions and callback to determine the calling mode. """ isgenerator = isinstance(self.callback, types.GeneratorType) if isgenerator: if self.sampled: raise ValueError("Cannot set DynamicMap containing generator " "to sampled") return 'generator' if self.sampled: return 'key' # Any unbounded kdim (any direction) implies open mode for kdim in self.kdims: if (kdim.values) and kdim.range != (None, None): raise Exception( 'Dimension cannot have both values and ranges.') elif kdim.values: continue if None in kdim.range: return 'counter' return 'key' def _validate_key(self, key): """ Make sure the supplied key values are within the bounds specified by the corresponding dimension range and soft_range. """ key = util.wrap_tuple(key) assert len(key) == len(self.kdims) for ind, val in enumerate(key): kdim = self.kdims[ind] low, high = util.max_range([kdim.range, kdim.soft_range]) if low is not np.NaN: if val < low: raise StopIteration("Key value %s below lower bound %s" % (val, low)) if high is not np.NaN: if val > high: raise StopIteration("Key value %s above upper bound %s" % (val, high)) def _style(self, retval): """ Use any applicable OptionTree of the DynamicMap to apply options to the return values of the callback. """ if self.id not in Store.custom_options(): return retval spec = StoreOptions.tree_to_dict(Store.custom_options()[self.id]) return retval(spec) def _execute_callback(self, *args): """ Execute the callback, validating both the input key and output key where applicable. """ if self.call_mode == 'key': self._validate_key(args) # Validate input key if self.call_mode == 'generator': retval = next(self.callback) else: retval = self.callback(*args) if self.call_mode == 'key': return self._style(retval) if isinstance(retval, tuple): self._validate_key(retval[0]) # Validated output key return self._style(retval) else: self._validate_key((self.counter, )) return (self.counter, self._style(retval)) def clone(self, data=None, shared_data=True, new_type=None, *args, **overrides): """ Clone method to adapt the slightly different signature of DynamicMap that also overrides Dimensioned clone to avoid checking items if data is unchanged. """ if data is None and shared_data: data = self.data return super(UniformNdMapping, self).clone(overrides.pop('callback', self.callback), shared_data, new_type, *(data, ) + args, **overrides) def reset(self): """ Return a cleared dynamic map with a cleared cached and a reset counter. """ if self.call_mode == 'generator': raise Exception("Cannot reset generators.") self.counter = 0 self.data = OrderedDict() return self def _cross_product(self, tuple_key, cache): """ Returns a new DynamicMap if the key (tuple form) expresses a cross product, otherwise returns None. The cache argument is a dictionary (key:element pairs) of all the data found in the cache for this key. Each key inside the cross product is looked up in the cache (self.data) to check if the appropriate element is available. Oherwise the element is computed accordingly. """ if self.mode != 'closed': return None if not any(isinstance(el, (list, set)) for el in tuple_key): return None if len(tuple_key) == 1: product = tuple_key[0] else: args = [ set(el) if isinstance(el, (list, set)) else set([el]) for el in tuple_key ] product = itertools.product(*args) data = [] for inner_key in product: key = util.wrap_tuple(inner_key) if key in cache: val = cache[key] else: val = self._execute_callback(*key) data.append((key, val)) return self.clone(data) def __getitem__(self, key): """ Return an element for any key chosen key (in'closed mode') or for a previously generated key that is still in the cache (for one of the 'open' modes) """ tuple_key = util.wrap_tuple(key) # Validation for closed mode if self.mode == 'closed': # DynamicMap(...)[:] returns a new DynamicMap with the same cache if key == slice(None, None, None): return self.clone(self) if any(isinstance(el, slice) for el in tuple_key): raise Exception( "Slices not supported by DynamicMap in closed mode " "except for the global slice [:] to create a clone.") # Cache lookup try: cache = super(DynamicMap, self).__getitem__(key) # Return selected cache items in a new DynamicMap if isinstance(cache, DynamicMap) and self.mode == 'open': cache = self.clone(cache) except KeyError as e: cache = None if self.mode == 'open' and len(self.data) > 0: raise KeyError( str(e) + " Note: Cannot index outside " "available cache in open interval mode.") # If the key expresses a cross product, compute the elements and return product = self._cross_product(tuple_key, cache.data if cache else {}) if product is not None: return product # Not a cross product and nothing cached so compute element. if cache: return cache val = self._execute_callback(*tuple_key) if self.call_mode == 'counter': val = val[1] self._cache(tuple_key, val) return val def _cache(self, key, val): """ Request that a key/value pair be considered for caching. """ if self.mode == 'open' and (self.counter % self.cache_interval) != 0: return if len(self) >= self.cache_size: first_key = next(self.data.iterkeys()) self.data.pop(first_key) self.data[key] = val def next(self): """ Interface for 'open' mode. For generators, this simply calls the next() method. For callables callback, the counter is supplied as a single argument. """ if self.mode == 'closed': raise Exception("The next() method should only be called in " "one of the open modes.") args = () if self.call_mode == 'generator' else (self.counter, ) retval = self._execute_callback(*args) (key, val) = (retval if isinstance(retval, tuple) else (self.counter, retval)) key = util.wrap_tuple(key) if len(key) != len(self.key_dimensions): raise Exception( "Generated key does not match the number of key dimensions") self._cache(key, val) self.counter += 1 return val # For Python 2 and 3 compatibility __next__ = next
class DataFrame(Widget): aggregators = param.Dict(default={}, doc=""" A dictionary mapping from index name to an aggregator to be used for hierarchical multi-indexes (valid aggregators include 'min', 'max', 'mean' and 'sum'). If separate aggregators for different columns are required the dictionary may be nested as `{index_name: {column_name: aggregator}}`""") auto_edit = param.Boolean(default=False, doc=""" Whether clicking on a table cell automatically starts edit mode.""") autosize_mode = param.ObjectSelector( default='force_fit', objects=["none", "fit_columns", "fit_viewport", "force_fit"], doc=""" Describes the column autosizing mode with one of the following options: ``"fit_columns"`` Compute columns widths based on cell contents but ensure the table fits into the available viewport. This results in no horizontal scrollbar showing up, but data can get unreadable if there is not enough space available. ``"fit_viewport"`` Adjust the viewport size after computing columns widths based on cell contents. ``"force_fit"`` Fit columns into available space dividing the table width across the columns equally (equivalent to `fit_columns=True`). This results in no horizontal scrollbar showing up, but data can get unreadable if there is not enough space available. ``"none"`` Do not automatically compute column widths.""") editors = param.Dict(default={}, doc=""" Bokeh CellEditor to use for a particular column (overrides the default chosen based on the type).""") hierarchical = param.Boolean(default=False, constant=True, doc=""" Whether to generate a hierachical index.""") formatters = param.Dict(default={}, doc=""" Bokeh CellFormatter to use for a particular column (overrides the default chosen based on the type).""") fit_columns = param.Boolean(default=None, doc=""" Whether columns should expand to the available width. This results in no horizontal scrollbar showing up, but data can get unreadable if there is no enough space available.""") frozen_columns = param.Integer(default=None, doc=""" Integer indicating the number of columns to freeze. If set the first N columns will be frozen which prevents them from scrolling out of frame.""") frozen_rows = param.Integer(default=None, doc=""" Integer indicating the number of rows to freeze. If set the first N rows will be frozen which prevents them from scrolling out of frame, if set to a negative value last N rows will be frozen.""") reorderable = param.Boolean(default=True, doc=""" Allows the reordering of a table's columns. To reorder a column, click and drag a table's header to the desired location in the table. The columns on either side will remain in their previous order.""") selection = param.List(default=[], doc=""" The currently selected rows of the table.""") show_index = param.Boolean(default=True, doc=""" Whether to show the index column.""") sortable = param.Boolean(default=True, doc=""" Allows to sort table's contents. By default natural order is preserved. To sort a column, click on it's header. Clicking one more time changes sort direction. Use Ctrl + click to return to natural order. Use Shift + click to sort multiple columns simultaneously.""") row_height = param.Integer(default=25, doc=""" The height of each table row.""") widths = param.ClassSelector(default={}, class_=(dict, int), doc=""" A mapping from column name to column width or a fixed column width.""") value = param.Parameter(default=None) _rename = { 'editors': None, 'formatters': None, 'widths': None, 'disabled': None, 'show_index': None } _manual_params = [ 'value', 'editors', 'formatters', 'selection', 'widths', 'aggregators', 'show_index' ] _aggregators = { 'sum': SumAggregator, 'max': MaxAggregator, 'min': MinAggregator, 'mean': AvgAggregator } def __init__(self, value=None, **params): super(DataFrame, self).__init__(value=value, **params) self.param.watch(self._validate, 'value') self._validate(None) self._renamed_cols = {} self._updating = False def _validate(self, event): if self.value is None: return cols = self.value.columns if len(cols) != len(cols.drop_duplicates()): raise ValueError('Cannot display a pandas.DataFrame with ' 'duplicate column names.') @property def indexes(self): import pandas as pd if self.value is None or not self.show_index: return [] elif isinstance(self.value.index, pd.MultiIndex): return list(self.value.index.names) return [self.value.index.name or 'index'] def _get_columns(self): if self.value is None: return [] indexes = self.indexes col_names = list(self.value.columns) if not self.hierarchical or len(indexes) == 1: col_names = indexes + col_names else: col_names = indexes[-1:] + col_names df = self.value.reset_index() if len(indexes) > 1 else self.value columns = [] for col in col_names: if col in df.columns: data = df[col] else: data = df.index kind = data.dtype.kind if kind == 'i': formatter = NumberFormatter() editor = IntEditor() elif kind == 'f': formatter = NumberFormatter(format='0,0.0[00000]') editor = NumberEditor() elif isdatetime(data) or kind == 'M': formatter = DateFormatter(format='%Y-%m-%d %H:%M:%S') editor = DateEditor() else: formatter = StringFormatter() editor = StringEditor() if col in self.editors: editor = self.editors[col] if col in indexes or editor is None: editor = CellEditor() if col in self.formatters: formatter = self.formatters[col] if str(col) != col: self._renamed_cols[str(col)] = col if isinstance(self.widths, int): width = self.widths else: width = self.widths.get(str(col)) title = str(col) if col in indexes and len(indexes) > 1 and self.hierarchical: title = 'Index: %s' % ' | '.join(indexes) column = TableColumn(field=str(col), title=title, editor=editor, formatter=formatter, width=width) columns.append(column) return columns def _get_groupings(self): if self.value is None: return [] groups = [] for group, agg_group in zip(self.indexes[:-1], self.indexes[1:]): if str(group) != group: self._renamed_cols[str(group)] = group aggs = self._get_aggregators(agg_group) groups.append(GroupingInfo(getter=str(group), aggregators=aggs)) return groups def _get_aggregators(self, group): numeric_cols = list(self.value.select_dtypes(include='number').columns) aggs = self.aggregators.get(group, []) if not isinstance(aggs, list): aggs = [aggs] expanded_aggs = [] for col_aggs in aggs: if not isinstance(col_aggs, dict): col_aggs = {col: col_aggs for col in numeric_cols} for col, agg in col_aggs.items(): if isinstance(agg, str): agg = self._aggregators.get(agg) if issubclass(agg, RowAggregator): expanded_aggs.append(agg(field_=str(col))) return expanded_aggs def _get_properties(self): props = { p: getattr(self, p) for p in list(Layoutable.param) if getattr(self, p) is not None } df = self.value.reset_index() if len(self.indexes) > 1 else self.value if df is None: data = {} else: data = { k if isinstance(k, str) else str(k): v for k, v in ColumnDataSource.from_df(df).items() } if props.get('height', None) is None: length = max([len(v) for v in data.values()]) if data else 0 props['height'] = length * self.row_height + 30 if self.hierarchical: props['target'] = ColumnDataSource( data=dict(row_indices=[], labels=[])) props['grouping'] = self._get_groupings() props['source'] = ColumnDataSource(data=data) props['columns'] = self._get_columns() props['index_position'] = None props['fit_columns'] = self.fit_columns if 'autosize_mode' in DataTable.properties(): props['frozen_columns'] = self.frozen_columns props['frozen_rows'] = self.frozen_rows props['autosize_mode'] = self.autosize_mode props['auto_edit'] = self.auto_edit props['row_height'] = self.row_height props['editable'] = not self.disabled and len(self.indexes) <= 1 props['sortable'] = self.sortable props['reorderable'] = self.reorderable return props def _process_param_change(self, msg): if 'disabled' in msg: msg['editable'] = not msg.pop('disabled') and len( self.indexes) <= 1 return super(DataFrame, self)._process_param_change(msg) def _get_model(self, doc, root=None, parent=None, comm=None): model_type = DataCube if self.hierarchical else DataTable model = model_type(**self._get_properties()) if root is None: root = model self._link_props(model.source, ['data', ('patching', 'data')], doc, root, comm) self._link_props(model.source.selected, ['indices'], doc, root, comm) self._models[root.ref['id']] = (model, parent) return model def _manual_update(self, events, model, doc, root, parent, comm): self._validate(None) for event in events: if event.type == 'triggered' and self._updating: continue elif event.name in ('value', 'show_index'): cds = model.source data = { k if isinstance(k, str) else str(k): v for k, v in ColumnDataSource.from_df(self.value).items() } cds.data = data model.columns = self._get_columns() if isinstance(model, DataCube): model.groupings = self._get_groupings() elif event.name == 'selection': model.source.selected.indices = self.selection elif event.name == 'aggregators': for g in model.grouping: group = self._renamed_cols.get(g.getter, g.getter) index = self.indexes[self.indexes.index(group) + 1] g.aggregators = self._get_aggregators(index) else: for col in model.columns: if col.name in self.editors: col.editor = self.editors[col.name] if col.name in self.formatters: col.formatter = self.formatters[col.name] if col.name in self.widths: col.width = self.widths[col.name] def _process_events(self, events): if 'data' in events: data = events.pop('data') updated = False for k, v in data.items(): if k in self.indexes: continue k = self._renamed_cols.get(k, k) if isinstance(v, dict): v = [ v for _, v in sorted(v.items(), key=lambda it: int(it[0])) ] try: isequal = (self.value[k].values == np.asarray(v)).all() except Exception: isequal = False if not isequal: self.value[k] = v updated = True if updated: self._updating = True try: self.param.trigger('value') finally: self._updating = False if 'indices' in events: self.selection = events.pop('indices') super(DataFrame, self)._process_events(events) @property def selected_dataframe(self): """ Returns a DataFrame of the currently selected rows. """ if not self.selection: return self.value return self.value.iloc[self.selection]
class CompositePlot(BokehPlot): """ CompositePlot is an abstract baseclass for plot types that draw render multiple axes. It implements methods to add an overall title to such a plot. """ sizing_mode = param.ObjectSelector(default=None, objects=[ 'fixed', 'stretch_width', 'stretch_height', 'stretch_both', 'scale_width', 'scale_height', 'scale_both', None ], doc=""" How the component should size itself. * "fixed" : Component is not responsive. It will retain its original width and height regardless of any subsequent browser window resize events. * "stretch_width" Component will responsively resize to stretch to the available width, without maintaining any aspect ratio. The height of the component depends on the type of the component and may be fixed or fit to component's contents. * "stretch_height" Component will responsively resize to stretch to the available height, without maintaining any aspect ratio. The width of the component depends on the type of the component and may be fixed or fit to component's contents. * "stretch_both" Component is completely responsive, independently in width and height, and will occupy all the available horizontal and vertical space, even if this changes the aspect ratio of the component. * "scale_width" Component will responsively resize to stretch to the available width, while maintaining the original or provided aspect ratio. * "scale_height" Component will responsively resize to stretch to the available height, while maintaining the original or provided aspect ratio. * "scale_both" Component will responsively resize to both the available width and height, while maintaining the original or provided aspect ratio. """) fontsize = param.Parameter(default={'title': '15pt'}, 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'}""") def _link_dimensioned_streams(self): """ Should perform any linking required to update titles when dimensioned streams change. """ streams = [ s for s in self.streams if any(k in self.dimensions for k in s.contents) ] for s in streams: s.add_subscriber(self._stream_update, 1) def _stream_update(self, **kwargs): contents = [k for s in self.streams for k in s.contents] key = tuple(None if d in contents else k for d, k in zip(self.dimensions, self.current_key)) key = wrap_tuple_streams(key, self.dimensions, self.streams) self._get_title_div(key) @property def current_handles(self): """ Should return a list of plot objects that have changed and should be updated. """ return [self.handles['title']] if 'title' in self.handles else []