class MyModel(VBoxModelForm): """Input Widgets, define the fields you want to read from the input here as bokeh properties input_specs is a list of dictionary, specifying how the kind of input widget you want for each property. the name field must match one of the properties, for example here, we use names of offset and scale. You can also specify title, if you want a different label in the generated form """ offset = Float(1.0) scale = Float(1.0) title = String(default="my sin wave") input_specs = [ { "widget": TextInput, "name": "title", "value": "my sin wave" }, { "widget": Slider, "name": "offset", "value": 1.0, "start": 0.0, "end": 5.0 }, { "widget": Slider, "name": "scale", "value": 1.0, "start": -5.0, "end": 5.0 }, ]
class XyGlyph(CompositeGlyph): """Composite glyph that plots in cartesian coordinates.""" x = EitherColumn(String, Column(Float), Column(String), Column(Datetime), Column(Bool)) y = EitherColumn(String, Column(Float), Column(String), Column(Datetime), Column(Bool)) line_color = String(default=DEFAULT_PALETTE[0]) line_alpha = Float(default=1.0) def build_source(self): if self.x is None: x = [self.label] * len(self.y) data = dict(x_values=x, y_values=self.y) elif self.y is None: y = [self.label] * len(self.x) data = dict(x_values=self.x, y_values=y) else: data = dict(x_values=self.x, y_values=self.y) return ColumnDataSource(data) @property def x_max(self): return max(self.source._data['x_values']) @property def x_min(self): return min(self.source._data['x_values']) @property def y_max(self): return max(self.source._data['y_values']) @property def y_min(self): return min(self.source._data['y_values'])
class DatetimeAxis(LinearAxis): type = String("datetime_axis") axis_label = String("date") scale = String("time") num_labels = Int(8) char_width = Int(10) fill_ratio = Float(0.3) formats = Dict({"days": ["%m/%d/%Y"]})
class Quantile(Stat): """Produces the cutpoint that divides the input data by the interval. Quartiles are a special case of quartiles that divide a dataset into four equal-size groups. (https://en.wikipedia.org/wiki/Quantile) """ interval = Float(default=0.5) def calculate(self): self.value = self.get_data().quantile(self.interval)
def test_Float(self): prop = Float() 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.assertTrue(prop.is_valid(0.0)) self.assertTrue(prop.is_valid(1.0)) self.assertFalse(prop.is_valid(1.0 + 1.0j)) self.assertFalse(prop.is_valid("")) self.assertFalse(prop.is_valid(())) self.assertFalse(prop.is_valid([])) self.assertFalse(prop.is_valid({})) self.assertFalse(prop.is_valid(Foo()))
class Stat(HasProps): column = ColumnLabel() source = ColumnDataSource() values = EitherColumn(Column(Float), Column(Int), Column(String), Column(Date), Column(Datetime), Column(Bool), default=None) value = Float() def __init__(self, **properties): super(Stat, self).__init__(**properties) self._refresh() def _refresh(self): """Lazy update of properties, used for initial transform init.""" if self.get_data() is not None: self.update() self.calculate() def set_data(self, data, column=None): """Set data properties and update all dependent properties.""" if isinstance(data, pd.DataFrame): self.source = ColumnDataSource(data) if column is None: raise ValueError( 'When providing a table of data, you must also provide a column label' ) else: self.column = column else: self.values = data self.update() self.calculate() def get_data(self): """Returns the available columnlabel/source values or column values.""" if self.source is not None and self.column is not None: return self.source._data[self.column] elif self.values is not None: return self.values else: return None def calculate(self): """Return transformed value from column label/source or column-like data.""" raise NotImplementedError( 'You must implement the calculate method for each stat type.') def update(self): pass
class Bin(Stat): """Represents a single bin of data values and attributes of the bin.""" label = String() start = Float() stop = Float() start_label = String() stop_label = String() center = Float() stat = Instance(Stat, default=Count()) def __init__(self, bin_label, values, **properties): properties['label'] = bin_label properties['start'], properties['stop'] = self.binstr_to_list( bin_label) properties['center'] = (properties['start'] + properties['stop']) / 2.0 properties['values'] = values super(Bin, self).__init__(**properties) @staticmethod def binstr_to_list(bins): """Produce a consistent display of a bin of data.""" value_chunks = bins.split(',') value_chunks = [ val.replace('[', '').replace(']', '').replace('(', '').replace(')', '') for val in value_chunks ] bin_values = [float(value) for value in value_chunks] return bin_values[0], bin_values[1] def update(self): self.stat.set_data(self.values) def calculate(self): self.value = self.stat.value
class PointGlyph(XyGlyph): """A set of glyphs placed in x,y coordinates with the same attributes.""" fill_color = Color(default=DEFAULT_PALETTE[1]) fill_alpha = Float(default=0.7) marker = String(default='circle') size = Float(default=8) def __init__(self, x=None, y=None, line_color=None, fill_color=None, marker=None, size=None, **kwargs): kwargs['x'] = x kwargs['y'] = y kwargs['line_color'] = line_color or self.line_color kwargs['fill_color'] = fill_color or self.fill_color kwargs['marker'] = marker or self.marker kwargs['size'] = size or self.size super(PointGlyph, self).__init__(**kwargs) self.setup() def get_glyph(self): return marker_types[self.marker] def build_renderers(self): glyph_type = self.get_glyph() glyph = glyph_type(x='x_values', y='y_values', line_color=self.line_color, fill_color=self.fill_color, size=self.size, fill_alpha=self.fill_alpha, line_alpha=self.line_alpha) yield GlyphRenderer(glyph=glyph)
def test_Float(self): prop = Float() 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.assertTrue(prop.is_valid(0.0)) self.assertTrue(prop.is_valid(1.0)) self.assertFalse(prop.is_valid(1.0+1.0j)) self.assertFalse(prop.is_valid("")) self.assertFalse(prop.is_valid(())) self.assertFalse(prop.is_valid([])) self.assertFalse(prop.is_valid({})) self.assertFalse(prop.is_valid(Foo()))
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 AggregateGlyph(NestedCompositeGlyph): """A base composite glyph for aggregating an array. Implements default stacking and dodging behavior that other composite glyphs can inherit. """ stack_label = String() stack_shift = Float(default=0.0) dodge_label = String( help="""Where on the scale the glyph should be placed.""") dodge_shift = Float(default=None) agg = Instance(Stat, default=Sum()) span = Float(help="""The range of values represented by the aggregate.""") def get_dodge_label(self, shift=0.0): """Generate the label defining an offset in relation to a position on a scale.""" if self.dodge_shift is None: shift_str = ':' + str(0.5 + shift) elif self.dodge_shift is not None: shift_str = ':' + str(self.dodge_shift + shift) else: shift_str = '' return str(self.label) + shift_str def filter_glyphs(self, glyphs): """Return only the glyphs that are of the same class.""" return [glyph for glyph in glyphs if isinstance(glyph, self.__class__)] @staticmethod def groupby(glyphs, prop): """Returns a dict of `CompositeGlyph`s, grouped by unique values of prop. For example, if all glyphs had a value of 'a' or 'b' for glyph.prop, the dict would contain two keys, 'a' and 'b', where each value is a list of the glyphs that had each of the values. """ grouped = defaultdict(list) [grouped[getattr(glyph, prop)].append(glyph) for glyph in glyphs] return grouped def __stack__(self, glyphs): """Apply relative shifts to the composite glyphs for stacking.""" if self.stack_label is not None: filtered_glyphs = self.filter_glyphs(glyphs) grouped = self.groupby(filtered_glyphs, 'label') for index, group in iteritems(grouped): # separate the negative and positive aggregates into separate groups neg_group = [glyph for glyph in group if glyph.span < 0] pos_group = [glyph for glyph in group if glyph.span >= 0] # apply stacking to each group separately for group in [neg_group, pos_group]: shift = [] for i, glyph in enumerate(group): # save off the top of each rect's height shift.append(glyph.span) if i > 0: glyph.stack_shift = sum(shift[0:i]) glyph.refresh() def __dodge__(self, glyphs): """Apply relative shifts to the composite glyphs for dodging.""" if self.dodge_label is not None: filtered_glyphs = self.filter_glyphs(glyphs) grouped = self.groupby(filtered_glyphs, 'dodge_label') # calculate transformations step = np.linspace(0, 1.0, len(grouped.keys()) + 1, endpoint=False) width = min(0.2, (1. / len(grouped.keys()))**1.1) # set bar attributes and re-aggregate for i, (index, group) in enumerate(iteritems(grouped)): for glyph in group: glyph.dodge_shift = step[i + 1] glyph.width = width glyph.refresh()
class Quantile(Stat): interval = Float(default=0.5) def calculate(self): self.value = self.get_data().quantile(self.interval)
class DataRange1d(DataRange): """ Represents a range in a scalar dimension """ sources = List(ColumnsRef, has_ref=True) rangepadding = Float(0.1) start = Float end = Float
class Range1d(PlotObject): start = Float() end = Float()
class HistogramGlyph(AggregateGlyph): """Depicts the distribution of values using rectangles created by binning. The histogram represents a distribution, so will likely include other options for displaying it, such as KDE and cumulative density. """ bins = Instance(Bins) bin_count = Float() bars = List(Instance(BarGlyph)) centers = List(Float) bin_width = Float() def __init__(self, values, label=None, color=None, bin_count=None, **kwargs): if label is not None: kwargs['label'] = label kwargs['values'] = values kwargs['bin_count'] = bin_count kwargs['color'] = color or self.color # remove width, since this is handled automatically kwargs.pop('width', None) super(HistogramGlyph, self).__init__(**kwargs) def _set_sources(self): pass def build_source(self): pass def build_renderers(self): self.bins = Bins(values=self.values, bin_count=self.bin_count) self.centers = [bin.center for bin in self.bins.bins] self.bin_width = self.centers[1] - self.centers[0] bars = [] for bin in self.bins.bins: bars.append( BarGlyph(label=bin.center, values=bin.values, color=self.color, fill_alpha=self.fill_alpha, agg=bin.stat, width=self.bin_width)) self.bars = bars self.children = self.bars for comp_glyph in self.bars: for renderer in comp_glyph.renderers: yield renderer @property def y_min(self): return 0.0
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)
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 HistogramGlyph(AggregateGlyph): """Depicts the distribution of values using rectangles created by binning. The histogram represents a distribution, so will likely include other options for displaying it, such as KDE and cumulative density. """ # input properties bin_width = Float() bin_count = Float( help="""Provide a manually specified number of bins to use.""") # derived models bins = Instance(Bins, help="""A stat used to calculate the bins. The bins stat includes attributes about each composite bin.""") bars = List(Instance(BarGlyph), help="""The histogram is comprised of many BarGlyphs that are derived from the values.""") def __init__(self, values, label=None, color=None, bin_count=None, **kwargs): if label is not None: kwargs['label'] = label kwargs['values'] = values kwargs['bin_count'] = bin_count if color is not None: kwargs['color'] = color # remove width, since this is handled automatically kwargs.pop('width', None) super(HistogramGlyph, self).__init__(**kwargs) self.setup() def _set_sources(self): # No need to set sources, since composite glyphs handle this pass def build_source(self): # No need to build source, since composite glyphs handle this pass def build_renderers(self): """Yield a bar glyph for each bin.""" self.bins = Bins(values=self.values, bin_count=self.bin_count) centers = [bin.center for bin in self.bins.bins] self.bin_width = centers[1] - centers[0] bars = [] for bin in self.bins.bins: bars.append( BarGlyph(label=bin.center, values=bin.values, color=self.color, fill_alpha=self.fill_alpha, agg=bin.stat, width=self.bin_width)) # provide access to bars as children for bounds properties self.bars = bars self.children = self.bars for comp_glyph in self.bars: for renderer in comp_glyph.renderers: yield renderer @property def y_min(self): return 0.0
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 Base(HasProps): num = NumberSpec(12) not_a_dataspec = Float(10)
def test_Float(self): prop = Float() 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.assertTrue(prop.is_valid(0.0)) self.assertTrue(prop.is_valid(1.0)) self.assertFalse(prop.is_valid(1.0+1.0j)) self.assertFalse(prop.is_valid("")) self.assertFalse(prop.is_valid(())) self.assertFalse(prop.is_valid([])) self.assertFalse(prop.is_valid({})) self.assertFalse(prop.is_valid(Foo())) try: import numpy as np # TODO: self.assertFalse(prop.is_valid(np.bool8(False))) # TODO: self.assertFalse(prop.is_valid(np.bool8(True))) self.assertTrue(prop.is_valid(np.int8(0))) self.assertTrue(prop.is_valid(np.int8(1))) self.assertTrue(prop.is_valid(np.int16(0))) self.assertTrue(prop.is_valid(np.int16(1))) self.assertTrue(prop.is_valid(np.int32(0))) self.assertTrue(prop.is_valid(np.int32(1))) self.assertTrue(prop.is_valid(np.int64(0))) self.assertTrue(prop.is_valid(np.int64(1))) self.assertTrue(prop.is_valid(np.uint8(0))) self.assertTrue(prop.is_valid(np.uint8(1))) self.assertTrue(prop.is_valid(np.uint16(0))) self.assertTrue(prop.is_valid(np.uint16(1))) self.assertTrue(prop.is_valid(np.uint32(0))) self.assertTrue(prop.is_valid(np.uint32(1))) self.assertTrue(prop.is_valid(np.uint64(0))) self.assertTrue(prop.is_valid(np.uint64(1))) self.assertTrue(prop.is_valid(np.float16(0))) self.assertTrue(prop.is_valid(np.float16(1))) self.assertTrue(prop.is_valid(np.float32(0))) self.assertTrue(prop.is_valid(np.float32(1))) self.assertTrue(prop.is_valid(np.float64(0))) self.assertTrue(prop.is_valid(np.float64(1))) self.assertFalse(prop.is_valid(np.complex64(1.0+1.0j))) self.assertFalse(prop.is_valid(np.complex128(1.0+1.0j))) self.assertFalse(prop.is_valid(np.complex256(1.0+1.0j))) except ImportError: pass
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 Stat(HasProps): """Represents a statistical operation to summarize a column of data. Can be computed from either a ColumnLabel with a ColumnDataSource, *or*, a discrete column of data. """ # inputs column = ColumnLabel( help="""A column to use for the stat calculation. Required when providing a ColumnDataSource as input.""") source = Instance(ColumnDataSource, help="""One option for providing the data source for stat calculation.""") values = EitherColumn(Column(Float), Column(Int), Column(String), Column(Date), Column(Datetime), Column(Bool), default=None, help=""" Second option for providing values for stat calculation is by passing the actual column of data.""") # output value = Float( help="""The value calculated for the stat. Some stats could use multiple properties to provide the calculation if required.""") def __init__(self, **properties): super(Stat, self).__init__(**properties) self._refresh() def _refresh(self): """Lazy update of properties, used for initial transform init.""" if self.get_data() is not None: self.update() self.calculate() def set_data(self, data, column=None): """Set data properties and update all dependent properties.""" if isinstance(data, pd.DataFrame): self.source = ColumnDataSource(data) if column is None: raise ValueError('When providing a table of data, ' 'you must also provide a column label') else: self.column = column else: self.values = data self.update() self.calculate() def get_data(self): """Returns the available columnlabel/source values or column values.""" if self.source is not None and self.column is not None: return self.source._data[self.column] elif self.values is not None: return self.values else: return None def calculate(self): """Return transformed value from column label/source or column-like data.""" raise NotImplementedError('You must implement the calculate method ' 'for each stat type.') def update(self): """Perform any initial work before the actual calculation is performed.""" pass
class Child(Base): z = Float(3.14)
class AggregateGlyph(NestedCompositeGlyph): """A base composite glyph for aggregating an array. Implements default stacking and dodging behavior that other composite glyphs can inherit. """ stack_label = String() stack_shift = Float(default=0.0) dodge_label = String() dodge_shift = Float(default=None) agg = Instance(Stat, default=Sum()) span = Float() def get_dodge_label(self, shift=0.0): if self.dodge_shift is None: shift_str = ':' + str(0.5 + shift) elif self.dodge_shift is not None: shift_str = ':' + str(self.dodge_shift + shift) else: shift_str = '' return str(self.label) + shift_str def filter_glyphs(self, glyphs): return [glyph for glyph in glyphs if isinstance(glyph, self.__class__)] @staticmethod def groupby(glyphs, prop): grouped = defaultdict(list) [grouped[getattr(glyph, prop)].append(glyph) for glyph in glyphs] return grouped def __stack__(self, glyphs): if self.stack_label is not None: filtered_glyphs = self.filter_glyphs(glyphs) grouped = self.groupby(filtered_glyphs, 'label') for index, group in iteritems(grouped): group = sorted(group, key=lambda x: x.stack_label) shift = [] for i, glyph in enumerate(group): # save off the top of each rect's height shift.append(glyph.span) if i > 0: glyph.stack_shift = sum(shift[0:i]) glyph.refresh() def __dodge__(self, glyphs): if self.dodge_label is not None: filtered_glyphs = self.filter_glyphs(glyphs) grouped = self.groupby(filtered_glyphs, 'dodge_label') # calculate transformations step = np.linspace(0, 1.0, len(grouped.keys()) + 1, endpoint=False) width = min(0.2, (1. / len(grouped.keys()))**1.1) # set bar attributes and re-aggregate for i, (index, group) in enumerate(iteritems(grouped)): for glyph in group: glyph.dodge_shift = step[i + 1] glyph.width = width glyph.refresh()