class CollisionModifier(HasProps): """Models an special type of operation that alters how glyphs interact. Used to handle the manipulation of glyphs for operations, such as stacking. The list of `CompositeGlyph`s can either be input into the `CollisionModifier` as keyword args, or added individually with the `add_glyph` method. """ comp_glyphs = List(Instance(CompositeGlyph), help="""A list of composite glyphs, to apply the modification to.""") name = String(help="""The name of the collision modifier.""") method_name = String( help="""The name of the method that will be utilized on the composite glyphs. This method must exist on all `comp_glyphs`.""") columns = Either(ColumnLabel, List(ColumnLabel), help="""Some collision modifiers might require column labels to apply the operation in relation to.""") def add_glyph(self, comp_glyph): self.comp_glyphs.append(comp_glyph) def apply(self, renderers=None): if len(self.comp_glyphs) == 0: self.comp_glyphs = renderers if len(self.comp_glyphs) > 0: # the first renderer's operation method is applied to the rest getattr(self.comp_glyphs[0], self.method_name)(self.comp_glyphs) else: raise AttributeError( '%s must be applied to available renderers, none found.' % self.__class__.__name__)
class LinearAxis(GuideRenderer): type = String("linear_axis") dimension = Int(0) location = Either(String('min'), Float) bounds = String('auto') axis_label = String axis_label_standoff = Int axis_label_props = Include(TextProps, prefix="axis_label") major_label_standoff = Int major_label_orientation = Either(Enum("horizontal", "vertical"), Int) major_label_props = Include(TextProps, prefix="major_label") # Line props axis_props = Include(LineProps, prefix="axis") tick_props = Include(LineProps, prefix="major_tick") major_tick_in = Int major_tick_out = Int
class Bins(Stat): """A set of many individual Bin stats. Bin counts using: https://en.wikipedia.org/wiki/Freedman%E2%80%93Diaconis_rule """ bin_count = Either(Int, Float) bin_width = Float(default=None, help='Use Freedman-Diaconis rule if None.') bins = List(Instance(Bin)) q1 = Quantile(interval=0.25) q3 = Quantile(interval=0.75) labels = List(String) def __init__(self, values=None, column=None, bins=None, **properties): properties['values'] = values properties['column'] = column properties['bins'] = bins super(Bins, self).__init__(**properties) def update(self): values = self.get_data() self.q1.set_data(values) self.q3.set_data(values) if self.bin_count is None: self.calc_num_bins(values) def calculate(self): binned, bin_edges = pd.cut(self.get_data(), self.bin_count, retbins=True, precision=0) df = pd.DataFrame(dict(values=self.get_data(), bins=binned)) bins = [] for name, group in df.groupby('bins'): bins.append(Bin(bin_label=name, values=group['values'])) self.bins = bins def calc_num_bins(self, values): iqr = self.q3.value - self.q1.value self.bin_width = 2 * iqr * (len(values)**-(1. / 3.)) self.bin_count = np.ceil( (self.values.max() - self.values.min()) / self.bin_width)
class CollisionModifier(HasProps): renderers = List(Instance(CompositeGlyph)) name = String() method_name = String() columns = Either(ColumnLabel, List(ColumnLabel)) def add_renderer(self, renderer): self.renderers.append(renderer) def apply(self, renderers=None): if len(self.renderers) == 0: self.renderers = renderers if len(self.renderers) > 0: # the first renderer's operation method is applied to the rest getattr(self.renderers[0], self.method_name)(self.renderers) else: raise AttributeError( '%s must be applied to available renderers, none found.' % self.__class__.__name__)
class Foo(PlotObject): """ This is a Foo model. """ index = Either(Auto, Enum('abc', 'def', 'xzy'), help="doc for index") value = Tuple(Float, Float, help="doc for value")
def test_Either(self): with self.assertRaises(TypeError): prop = Either() prop = Either(Range(Int, 0, 100), Regex("^x*$"), List(Int)) self.assertTrue(prop.is_valid(None)) # TODO: self.assertFalse(prop.is_valid(False)) # TODO: self.assertFalse(prop.is_valid(True)) self.assertTrue(prop.is_valid(0)) self.assertTrue(prop.is_valid(1)) self.assertFalse(prop.is_valid(0.0)) self.assertFalse(prop.is_valid(1.0)) self.assertFalse(prop.is_valid(1.0 + 1.0j)) self.assertTrue(prop.is_valid("")) self.assertFalse(prop.is_valid(())) self.assertTrue(prop.is_valid([])) self.assertFalse(prop.is_valid({})) self.assertFalse(prop.is_valid(Foo())) self.assertTrue(prop.is_valid(100)) self.assertFalse(prop.is_valid(-100)) self.assertTrue(prop.is_valid("xxx")) self.assertFalse(prop.is_valid("yyy")) self.assertTrue(prop.is_valid([1, 2, 3])) self.assertFalse(prop.is_valid([1, 2, ""]))
def test_Either(self): with self.assertRaises(TypeError): prop = Either() prop = Either(Interval(Int, 0, 100), Regex("^x*$"), List(Int)) self.assertTrue(prop.is_valid(None)) # TODO: self.assertFalse(prop.is_valid(False)) # TODO: self.assertFalse(prop.is_valid(True)) self.assertTrue(prop.is_valid(0)) self.assertTrue(prop.is_valid(1)) self.assertFalse(prop.is_valid(0.0)) self.assertFalse(prop.is_valid(1.0)) self.assertFalse(prop.is_valid(1.0+1.0j)) self.assertTrue(prop.is_valid("")) self.assertFalse(prop.is_valid(())) self.assertTrue(prop.is_valid([])) self.assertFalse(prop.is_valid({})) self.assertFalse(prop.is_valid(Foo())) self.assertTrue(prop.is_valid(100)) self.assertFalse(prop.is_valid(-100)) self.assertTrue(prop.is_valid("xxx")) self.assertFalse(prop.is_valid("yyy")) self.assertTrue(prop.is_valid([1, 2, 3])) self.assertFalse(prop.is_valid([1, 2, ""]))
class BoxGlyph(AggregateGlyph): """Summarizes the distribution with a collection of glyphs. A box glyph produces one "box" for a given array of vales. The box is made up of multiple other child composite glyphs (intervals, scatter) and directly produces glyph renderers for the whiskers, as well. """ q1 = Float() q2 = Float() q3 = Float() iqr = Float() w0 = Float(help='Lower whisker') w1 = Float(help='Upper whisker') q2_glyph = Instance(QuartileGlyph) q3_glyph = Instance(QuartileGlyph) whisker_glyph = Instance(GlyphRenderer) outliers = Either(Bool, Instance(PointGlyph)) marker = String(default='circle') whisker_width = Float(default=0.3) whisker_line_width = Float(default=2) whisker_span_line_width = Float(default=2) whisker_color = String(default='black') outlier_fill_color = String(default='red') outlier_line_color = String(default='red') outlier_size = Float(default=5) bar_color = String(default='DimGrey') def __init__(self, label, values, outliers=True, **kwargs): width = kwargs.pop('width', None) bar_color = kwargs.pop('color', None) or self.bar_color kwargs['outliers'] = kwargs.pop('outliers', None) or outliers kwargs['label'] = label kwargs['values'] = values kwargs['q2_glyph'] = QuartileGlyph(label=label, values=values, interval1=0.25, interval2=0.5, width=width, color=bar_color) kwargs['q3_glyph'] = QuartileGlyph(label=label, values=values, interval1=0.5, interval2=0.75, width=width, color=bar_color) super(BoxGlyph, self).__init__(**kwargs) def build_renderers(self): self.calc_quartiles() outlier_values = self.values[((self.values < self.w0) | (self.values > self.w1))] self.whisker_glyph = GlyphRenderer( glyph=Segment(x0='x0s', y0='y0s', x1='x1s', y1='y1s', line_width=self.whisker_line_width, line_color=self.whisker_color)) if len(outlier_values) > 0 and self.outliers: self.outliers = PointGlyph(y=outlier_values, label=self.get_dodge_label(), line_color=self.outlier_line_color, fill_color=self.outlier_fill_color, size=self.outlier_size, marker=self.marker) for comp_glyph in self.composite_glyphs: for renderer in comp_glyph.renderers: yield renderer yield self.whisker_glyph def calc_quartiles(self): self.q1 = self.q2_glyph.start self.q2 = self.q2_glyph.end self.q3 = self.q3_glyph.end self.iqr = self.q3 - self.q1 self.w0 = self.q1 - (1.5 * self.iqr) self.w1 = self.q3 + (1.5 * self.iqr) def build_source(self): self.calc_quartiles() x_label = self.get_dodge_label() x_w0_label = self.get_dodge_label(shift=(self.whisker_width / 2.0)) x_w1_label = self.get_dodge_label(shift=-(self.whisker_width / 2.0)) # span0, whisker bar0, span1, whisker bar1 x0s = [x_label, x_w0_label, x_label, x_w0_label] y0s = [self.w0, self.w0, self.q3, self.w1] x1s = [x_label, x_w1_label, x_label, x_w1_label] y1s = [self.q1, self.w0, self.w1, self.w1] return ColumnDataSource(dict(x0s=x0s, y0s=y0s, x1s=x1s, y1s=y1s)) def _set_sources(self): self.whisker_glyph.data_source = self.source def get_extent(self, func, prop_name): return func([ getattr(renderer, prop_name) for renderer in self.composite_glyphs ]) @property def composite_glyphs(self): comp_glyphs = [self.q2_glyph, self.q3_glyph] if isinstance(self.outliers, PointGlyph): comp_glyphs.append(self.outliers) return comp_glyphs @property def x_max(self): return self.get_extent(max, 'x_max') + self.right_buffer @property def x_min(self): return self.get_extent(min, 'x_min') - self.left_buffer @property def y_max(self): return max(self.w1, self.get_extent(max, 'y_max')) + self.top_buffer @property def y_min(self): return min(self.w0, self.get_extent(min, 'y_min')) - self.bottom_buffer
class Interval(AggregateGlyph): """A rectangle representing aggregated values. The interval is a rect glyph where two of the parallel sides represent a summary of values. Each of the two sides is derived from a separate aggregation of the values provided to the interval. Note: A bar is a special case interval where one side is pinned and used to communicate a value relative to it. """ width = Float(default=0.8) start_agg = Instance(Stat, default=Min()) end_agg = Instance(Stat, default=Max()) start = Float(default=0.0) end = Float() label_value = Either(String, Float, Datetime, Bool, default=None) def __init__(self, label, values, **kwargs): if not isinstance(label, str): label_value = label label = str(label) else: label_value = None kwargs['label'] = label kwargs['label_value'] = label_value kwargs['values'] = values super(Interval, self).__init__(**kwargs) def get_start(self): self.start_agg.set_data(self.values) return self.start_agg.value def get_end(self): self.end_agg.set_data(self.values) return self.end_agg.value def get_span(self): return self.end - self.start def build_source(self): # ToDo: Handle rotation self.start = self.get_start() self.end = self.get_end() self.span = self.get_span() width = [self.width] if self.dodge_shift is not None: x = [self.get_dodge_label()] else: x = [self.label_value or self.label] height = [self.span] y = [self.stack_shift + (self.span / 2.0) + self.start] color = [self.color] fill_alpha = [self.fill_alpha] return ColumnDataSource( dict(x=x, y=y, width=width, height=height, color=color, fill_alpha=fill_alpha)) @property def x_max(self): return (self.dodge_shift or self.label_value) + (self.width / 2.0) @property def x_min(self): return (self.dodge_shift or self.label_value) - (self.width / 2.0) @property def y_max(self): return self.stack_shift + self.span + self.start @property def y_min(self): return self.stack_shift + self.start def build_renderers(self): glyph = Rect(x='x', y='y', width='width', height='height', fill_color='color', fill_alpha='fill_alpha') yield GlyphRenderer(glyph=glyph)
class Dimension(HasProps): """Configures valid Chart column selections. A dimension is Chart property that is assigned one or more columns names or indices. Each column can match one or more column types, which are important to charts, because the type of column selection can greatly affect the behavior of generalized Charts. The Dimension also provides convenient utilities for accessing information about the current provided configuration at the global, non-grouped level. """ name = String() alt_names = Either(String, List(String), default=None) columns = Either(ColumnLabel, List(ColumnLabel), default=None) valid = Either(PrimitiveProperty, List(PrimitiveProperty), default=None) invalid = Either(PrimitiveProperty, List(PrimitiveProperty), default=None) selection = Either(ColumnLabel, List(ColumnLabel), default=None) def __init__(self, name, **properties): properties['name'] = name super(Dimension, self).__init__(**properties) self._data = pd.DataFrame() self._chart_source = None def get_valid_types(self, col_data): """Returns all property types that are matched.""" valid_types = list(self.valid) matches = [] # validate each type on the provided column for valid_type in valid_types: prop = valid_type() # if valid, append to the output try: prop.validate(col_data) matches.append(valid_type) except ValueError: pass return matches @property def data(self): """The data selected for the Dimension. Returns pd.Series(1) if data is empty or no selection. """ if self._data.empty or self.selection is None: return pd.Series(1) else: # return special column type if available if self.selection in list(special_columns.keys()): return special_columns[self.selection](self._data) return self._data[self.selection] def set_data(self, data): """Builder must provide data so that builder has access to configuration metadata.""" self.selection = data[self.name] self._chart_source = data self._data = data.df self.columns = list(self._data.columns.values) @property def min(self): """The minimum of one to many column selections.""" if isinstance(self.data, pd.Series): return self.data.min() else: return self.data.min(axis=1).min() @property def max(self): """The maximum of one to many column selections.""" if isinstance(self.data, pd.Series): return self.data.max() else: return self.data.max(axis=1).max() @property def dtype(self): if isinstance(self.data, pd.DataFrame): return self.data.dtypes[self.selection[0]] else: return self.data.dtype @property def computed(self): if self._chart_source is None: return False else: return self._chart_source.is_computed(self.selection) @property def selected_title(self): """A title formatted representation of selected columns.""" return title_from_columns(self.selection)
class CompositeGlyph(HasProps): """Represents a subset of data. A collection of hetero or homogeneous glyph renderers which represent a subset of data. The purpose of the composite glyph is to abstract away the details of constructing glyphs, based on the details of a subset of data, from the grouping operations that a generalized builder must implement. In general, the Builder operates at the full column oriented data source level, segmenting and assigning attributes from a large selection, while the composite glyphs will typically be passed an array-like structures with one or more singlular attributes to apply. Another way to explain the concept is that the Builder operates as the groupby, as in pandas, while the CompositeGlyph operates as the apply. What is the responsibility of the Composite Glyph? - Produce GlyphRenderers - Apply any aggregations - Tag the GlyphRenderers with the group label - Apply transforms due to chart operations - Operations require implementation of special methods """ label = String('All', help='Identifies the subset of data.') values = Either(Column(Float), Column(String), help='Array-like values.') color = Color(default='gray') fill_alpha = Float(default=0.8) source = Instance(ColumnDataSource) operations = List(Any) renderers = List(Instance(GlyphRenderer)) left_buffer = Float(default=0.0) right_buffer = Float(default=0.0) top_buffer = Float(default=0.0) bottom_buffer = Float(default=0.0) def __init__(self, **kwargs): label = kwargs.pop('label', None) if label is not None: if not isinstance(label, str): label = str(label) kwargs['label'] = label super(CompositeGlyph, self).__init__(**kwargs) self.setup() def setup(self): self.renderers = [renderer for renderer in self.build_renderers()] if self.renderers is not None: self.refresh() def refresh(self): if self.renderers is not None: self.source = self.build_source() self._set_sources() def build_renderers(self): raise NotImplementedError('You must return list of renderers.') def build_source(self): raise NotImplementedError('You must return ColumnDataSource.') def _set_sources(self): """Store reference to source in each glyph renderer.""" for renderer in self.renderers: renderer.data_source = self.source def __stack__(self, glyphs): pass def __jitter__(self, glyphs): pass def __dodge__(self, glyphs): pass def __overlay__(self, glyphs): pass def apply_operations(self): pass
class AttrSpec(HasProps): """A container for assigning attributes to values and retrieving them as needed. A special function this provides is automatically handling cases where the provided iterator is too short compared to the distinct values provided. Once created as attr_spec, you can do attr_spec[data_label], where data_label must be a one dimensional tuple of values, representing the unique group in the data. See the :meth:`AttrSpec.setup` method for the primary way to provide an existing AttrSpec with data and column values and update all derived property values. """ id = Any() data = Instance(ColumnDataSource) name = String(help='Name of the attribute the spec provides.') columns = Either(ColumnLabel, List(ColumnLabel), help=""" The label or list of column labels that correspond to the columns that will be used to find all distinct values (single column) or combination of values ( multiple columns) to then assign a unique attribute to. If not enough unique attribute values are found, then the attribute values will be cycled. """) default = Any(default=None, help=""" The default value for the attribute, which is used if no column is assigned to the attribute for plotting. If the default value is not provided, the first value in the `iterable` property is used. """) attr_map = Dict(Any, Any, help=""" Created by the attribute specification when `iterable` and `data` are available. The `attr_map` will include a mapping between the distinct value(s) found in `columns` and the attribute value that has been assigned. """) iterable = List(Any, default=None, help=""" The iterable of attribute values to assign to the distinct values found in `columns` of `data`. """) items = List(Any, default=None, help=""" The attribute specification calculates this list of distinct values that are found in `columns` of `data`. """) sort = Bool(default=True, help=""" A boolean flag to tell the attribute specification to sort `items`, when it is calculated. This affects which value of `iterable` is assigned to each distinct value in `items`. """) ascending = Bool(default=True, help=""" A boolean flag to tell the attribute specification how to sort `items` if the `sort` property is set to `True`. The default setting for `ascending` is `True`. """) def __init__(self, columns=None, df=None, iterable=None, default=None, items=None, **properties): """Create a lazy evaluated attribute specification. Args: columns: a list of column labels df(:class:`~pandas.DataFrame`): the data source for the attribute spec. iterable: an iterable of distinct attribute values default: a value to use as the default attribute when no columns are passed items: the distinct values in columns. If items is provided as input, then the values provided are used instead of being calculated. This can be used to force a specific order for assignment. **properties: other properties to pass to parent :class:`HasProps` """ properties['columns'] = self._ensure_list(columns) if df is not None: properties['data'] = ColumnDataSource(df) if default is None and iterable is not None: default_iter = copy(iterable) properties['default'] = next(iter(default_iter)) elif default is not None: properties['default'] = default if iterable is not None: properties['iterable'] = iterable if items is not None: properties['items'] = items super(AttrSpec, self).__init__(**properties) @staticmethod def _ensure_list(attr): """Always returns a list with the provided value. Returns the value if a list.""" if isinstance(attr, str): return [attr] elif isinstance(attr, tuple): return list(attr) else: return attr @staticmethod def _ensure_tuple(attr): """Return tuple with the provided value. Returns the value if a tuple.""" if not isinstance(attr, tuple): return (attr, ) else: return attr def _setup_default(self): """Stores the first value of iterable into `default` property.""" self.default = next(self._setup_iterable()) def _setup_iterable(self): """Default behavior is to copy and cycle the provided iterable.""" return cycle(copy(self.iterable)) def _generate_items(self, df, columns): """Produce list of unique tuples that identify each item.""" if self.items is None or len(self.items) == 0: if self.sort: df = df.sort(columns=columns, ascending=self.ascending) items = df[columns].drop_duplicates() self.items = [tuple(x) for x in items.to_records(index=False)] def _create_attr_map(self, df, columns): """Creates map between unique values and available attributes.""" self._generate_items(df, columns) iterable = self._setup_iterable() iter_map = {} for item in self.items: item = self._ensure_tuple(item) iter_map[item] = next(iterable) return iter_map def set_columns(self, columns): """Set columns property and update derived properties as needed.""" columns = self._ensure_list(columns) if all([col in self.data.column_names for col in columns]): self.columns = columns else: # we have input values other than columns # assume this is now the iterable at this point self.iterable = columns self._setup_default() def setup(self, data=None, columns=None): """Set the data and update derived properties as needed.""" if data is not None: self.data = data if columns is not None: self.set_columns(columns) if self.columns is not None and self.data is not None: self.attr_map = self._create_attr_map(self.data.to_df(), self.columns) def __getitem__(self, item): """Lookup the attribute to use for the given unique group label.""" if not self.columns or not self.data or item is None: return self.default elif self._ensure_tuple(item) not in self.attr_map.keys(): # make sure we have attr map self.setup() return self.attr_map[self._ensure_tuple(item)]
class AttrSpec(HasProps): """A container for assigning attributes to values and retrieving them as needed. A special function this provides is automatically handling cases where the provided iterator is too short compared to the distinct values provided. Once created as attr_spec, you can do attr_spec[data_label], where data_label must be a one dimensional tuple of values, representing the unique group in the data. """ id = Any() data = Instance(ColumnDataSource) name = String(help='Name of the attribute the spec provides.') columns = Either(ColumnLabel, List(ColumnLabel)) default = Any(default=None) attr_map = Dict(Any, Any) iterable = List(Any, default=None) items = List(Any) def __init__(self, columns=None, df=None, iterable=None, default=None, **properties): properties['columns'] = self._ensure_list(columns) if df is not None: properties['data'] = ColumnDataSource(df) if default is None and iterable is not None: default_iter = copy(iterable) properties['default'] = next(iter(default_iter)) elif default is not None: properties['default'] = default if iterable is not None: properties['iterable'] = iterable super(AttrSpec, self).__init__(**properties) @staticmethod def _ensure_list(attr): if isinstance(attr, str): return [attr] elif isinstance(attr, tuple): return list(attr) else: return attr @staticmethod def _ensure_tuple(attr): if not isinstance(attr, tuple): return (attr, ) else: return attr def _setup_default(self): self.default = next(self._setup_iterable()) def _setup_iterable(self): """Default behavior is to copy and cycle the provided iterable.""" return cycle(copy(self.iterable)) def _generate_items(self, df, columns): """Produce list of unique tuples that identify each item.""" df = df.sort(columns=columns) items = df[columns].drop_duplicates() self.items = [tuple(x) for x in items.to_records(index=False)] def _create_attr_map(self, df, columns): """Creates map between unique values and available attributes.""" self._generate_items(df, columns) iterable = self._setup_iterable() iter_map = {} for item in self.items: item = self._ensure_tuple(item) iter_map[item] = next(iterable) return iter_map def set_columns(self, columns): columns = self._ensure_list(columns) if all([col in self.data.column_names for col in columns]): self.columns = columns else: # we have input values other than columns # assume this is now the iterable at this point self.iterable = columns self._setup_default() def setup(self, data=None, columns=None): if data is not None: self.data = data if columns is not None: self.set_columns(columns) if self.columns is not None and self.data is not None: self.attr_map = self._create_attr_map(self.data.to_df(), self.columns) def __getitem__(self, item): """Lookup the attribute to use for the given unique group label.""" if not self.columns or not self.data or item is None: return self.default elif self._ensure_tuple(item) not in self.attr_map.keys(): # make sure we have attr map self.setup() return self.attr_map[self._ensure_tuple(item)]
class CompositeGlyph(HasProps): """Represents a subset of data. A collection of hetero or homogeneous glyph renderers which represent a subset of data. The purpose of the composite glyph is to abstract away the details of constructing glyphs, based on the details of a subset of data, from the grouping operations that a generalized builders must implement. In general, the Builder operates at the full column oriented data source level, segmenting and assigning attributes from a large selection, while the composite glyphs will typically be passed an array-like structures with one or more singlular attributes to apply. Another way to explain the concept is that the Builder operates as the groupby, as in pandas, while the CompositeGlyph operates as the function used in the apply. What is the responsibility of the Composite Glyph? - Produce GlyphRenderers - Apply any aggregations - Tag the GlyphRenderers with the group label - Apply transforms due to chart operations - Note: Operations require implementation of special methods """ # composite glyph inputs label = String('None', help='Identifies the subset of data.') values = Either(Column(Float), Column(String), help="""Array-like values, which are used as the input to the composite glyph.""") # derived from inputs source = Instance(ColumnDataSource, help="""The data source used for the contained glyph renderers. Simple glyphs part of the composite glyph might not use the column data source.""") renderers = List(Instance(GlyphRenderer)) operations = List(Any, help="""A list of chart operations that can be applied to manipulate their visual depiction.""") color = Color(default='gray', help="""A high level color. Some glyphs will implement more specific color attributes for parts or specific glyphs.""" ) line_color = Color(default='black', help="""A default outline color for contained glyphs.""") fill_alpha = Float(default=0.8) left_buffer = Float(default=0.0) right_buffer = Float(default=0.0) top_buffer = Float(default=0.0) bottom_buffer = Float(default=0.0) def __init__(self, **kwargs): label = kwargs.pop('label', None) if label is not None: if not isinstance(label, str): label = str(label) kwargs['label'] = label super(CompositeGlyph, self).__init__(**kwargs) def setup(self): """Build renderers and data source and set sources on renderers.""" self.renderers = [renderer for renderer in self.build_renderers()] if self.renderers is not None: self.refresh() def refresh(self): """Update the GlyphRenderers. .. note: this method would be called after data is added. """ if self.renderers is not None: self.source = self.build_source() self._set_sources() def build_renderers(self): raise NotImplementedError('You must return list of renderers.') def build_source(self): raise NotImplementedError('You must return ColumnDataSource.') def _set_sources(self): """Store reference to source in each GlyphRenderer. .. note:: if the glyphs that are part of the composite glyph differ, you may have to override this method and handle the sources manually. """ for renderer in self.renderers: renderer.data_source = self.source def __stack__(self, glyphs): """A special method the `stack` function applies to composite glyphs.""" pass def __jitter__(self, glyphs): """A special method the `jitter` function applies to composite glyphs.""" pass def __dodge__(self, glyphs): """A special method the `dodge` function applies to composite glyphs.""" pass def __overlay__(self, glyphs): """A special method the `overlay` function applies to composite glyphs.""" pass def apply_operations(self): pass
class Interval(AggregateGlyph): """A rectangle representing aggregated values. The interval is a rect glyph where two of the parallel sides represent a summary of values. Each of the two sides is derived from a separate aggregation of the values provided to the interval. .. note:: A bar is a special case interval where one side is pinned and used to communicate a value relative to it. """ width = Float(default=0.8) start_agg = Instance(Stat, default=Min(), help="""The stat used to derive the starting point of the composite glyph.""") end_agg = Instance(Stat, default=Max(), help="""The stat used to derive the end point of the composite glyph.""") start = Float(default=0.0) end = Float() label_value = Either(String, Float, Datetime, Bool, default=None) def __init__(self, label, values, **kwargs): if not isinstance(label, str): label_value = label label = str(label) else: label_value = None kwargs['label'] = label kwargs['label_value'] = label_value kwargs['values'] = values super(Interval, self).__init__(**kwargs) self.setup() def get_start(self): """Get the value for the start of the glyph.""" self.start_agg.set_data(self.values) return self.start_agg.value def get_end(self): """Get the value for the end of the glyph.""" self.end_agg.set_data(self.values) return self.end_agg.value def get_span(self): """The total range between the start and end.""" return self.end - self.start def build_source(self): # ToDo: Handle rotation self.start = self.get_start() self.end = self.get_end() self.span = self.get_span() width = [self.width] if self.dodge_shift is not None: x = [self.get_dodge_label()] else: x = [self.label_value or self.label] height = [self.span] y = [self.stack_shift + (self.span / 2.0) + self.start] color = [self.color] fill_alpha = [self.fill_alpha] line_color = [self.line_color] return ColumnDataSource( dict(x=x, y=y, width=width, height=height, color=color, fill_alpha=fill_alpha, line_color=line_color)) @property def x_max(self): """The maximum extent of the glyph in x. .. note:: Dodging the glyph can affect the value. """ return (self.dodge_shift or self.label_value) + (self.width / 2.0) @property def x_min(self): """The maximum extent of the glyph in y. .. note:: Dodging the glyph can affect the value. """ return (self.dodge_shift or self.label_value) - (self.width / 2.0) @property def y_max(self): """Maximum extent of all `Glyph`s. How much we are stacking + the height of the interval + the base of the interval .. note:: the start and end of the glyph can swap between being associated with the min and max when the glyph end represents a negative value. """ return max(self.bottom, self.top) @property def y_min(self): """The minimum extent of all `Glyph`s in y. .. note:: the start and end of the glyph can swap between being associated with the min and max when the glyph end represents a negative value. """ return min(self.bottom, self.top) @property def bottom(self): """The value associated with the start of the stacked glyph.""" return self.stack_shift + self.start @property def top(self): """The value associated with the end of the stacked glyph.""" return self.stack_shift + self.span + self.start def build_renderers(self): """Yields a `GlyphRenderer` associated with a `Rect` glyph.""" glyph = Rect(x='x', y='y', width='width', height='height', fill_color='color', fill_alpha='fill_alpha', line_color='line_color') yield GlyphRenderer(glyph=glyph)