def test_Instance(self): p = Instance(HasProps) with pytest.raises(ValueError) as e: p.validate("junk") assert not str(e).endswith("ValueError")
class Mixin(HasProps): mixin_num = Int(12) mixin_container = List(String) mixin_child = Instance(HasProps)
class SomeModelInTestClientServer(Model): foo = Int(2) child = Instance(Model)
class DrawTool(Tool): __implementation__ = JS_CODE source = Instance(ColumnDataSource)
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. """ data = Instance(ColumnDataSource) iterable = List(Any, default=None) attrname = 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. """) items = 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`. """) bins = Instance(Bins, help=""" If an attribute spec is binning data, so that we can map one value in the `iterable` to one value in `items`, then this attribute will contain an instance of the Bins stat. This is used to create unique labels for each bin, which is then used for `items` instead of the actual unique values in `columns`. """) 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) if self.default is None and self.iterable is not None: self.default = next(iter(copy(self.iterable))) if self.data is not None and self.columns is not None: if df is None: df = self.data.to_df() self._generate_items(df, columns=self.columns) if self.items is not None and self.iterable is not None: self.attr_map = self._create_attr_map() @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.sort: # TODO (fpliger): this handles pandas API change so users do not experience # the related annoying deprecation warning. This is probably worth # removing when pandas deprecated version (0.16) is "old" enough try: df = df.sort_values(by=columns, ascending=self.ascending) except AttributeError: 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=None, columns=None): """Creates map between unique values and available attributes.""" if df is not None and columns is not None: self._generate_items(df, columns) iterable = self._setup_iterable() return {item: next(iterable) for item in self._item_tuples()} def _item_tuples(self): return [self._ensure_tuple(item) for item in self.items] 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 and self.data 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 update_data(self, data): self.setup(data=data, columns=self.columns) def __getitem__(self, item): """Lookup the attribute to use for the given unique group label.""" if not self.attr_map: 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)] @property def series(self): if not self.attr_map: return pd.Series() else: index = pd.MultiIndex.from_tuples(self._item_tuples(), names=self.columns) return pd.Series(list(self.attr_map.values()), index=index)
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 = Either(Instance(Stat), Enum(*list(stats.keys())), default=Min(), help=""" The stat used to derive the starting point of the composite glyph.""") end_agg = Either(Instance(Stat), Enum(*list(stats.keys())), default=Max(), help=""" The stat used to derive the end point of the composite glyph.""") start = Float(default=0.0) end = Float() def __init__(self, label, values, **kwargs): kwargs['label'] = label kwargs['values'] = values super(Interval, self).__init__(**kwargs) self.setup() def get_start(self): """Get the value for the start of the glyph.""" if len(self.values.index) == 1: self.start_agg = None return self.values[0] elif isinstance(self.start_agg, str): self.start_agg = stats[self.start_agg]() 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.""" if len(self.values.index ) == 1 and not self.values.dtype.name == 'object': self.end_agg = None return self.values[0] elif isinstance(self.end_agg, str): self.end_agg = stats[self.end_agg]() 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.x_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] line_alpha = [self.line_alpha] return dict(x=x, y=y, width=width, height=height, color=color, fill_alpha=fill_alpha, line_color=line_color, line_alpha=line_alpha) @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.x_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.x_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 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 return None 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=self.label, x_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 SomeModelInTestPullDoc(Model): foo = Int(2) child = Instance(Model)
class LuminoDock(HTMLBox): children = List(Either(Tuple(String, Instance(LayoutDOM)), Tuple(String, Instance(LayoutDOM), InsertMode), Tuple(String, Instance(LayoutDOM), InsertMode, Int)), default=[])
def test_Instance(self, detail): p = Instance(HasProps) with pytest.raises(ValueError) as e: p.validate("junk", detail) assert str(e).endswith("ValueError") == (not detail)
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. """ # derived models bins = Instance(BinnedStat, 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.""") density = Bool(False, help=""" Whether to normalize the histogram. If True, the result is the value of the probability *density* function at the bin, normalized such that the *integral* over the range is 1. If False, the result will contain the number of samples in each bin. For more info check :class:`~bokeh.charts.stats.Histogram` documentation. (default: False) """) def __init__(self, values, label=None, color=None, bins=None, **kwargs): if label is not None: kwargs['label'] = label kwargs['values'] = values if color is not None: kwargs['color'] = color # remove width, since this is handled automatically kwargs.pop('width', None) # keep original bins setting private since it just needs to be # delegated to the Histogram stat self._bins = bins 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 return None def build_renderers(self): """Yield a bar glyph for each bin.""" # TODO(fpliger): We should expose the bin stat class so we could let # users specify other bins other the Histogram Stat self.bins = Histogram(values=self.values, bins=self._bins, density=self.density) bars = [] for bin in self.bins.bins: bars.append( BarGlyph(label=bin.label[0], x_label=bin.center, values=bin.values, color=self.color, fill_alpha=self.fill_alpha, agg=bin.stat, width=bin.width)) # provide access to bars as children for bounds properties self.bars = self.children = bars for comp_glyph in self.bars: for renderer in comp_glyph.renderers: yield renderer @property def y_min(self): return 0.0
class Builder(HasProps): """ A prototype class to inherit each new chart Builder type. It provides useful methods to be used by the inherited builder classes, in order to automate most of the charts creation tasks and leave the core customization to specialized builder classes. In that pattern inherited builders just need to provide the following methods: Required: * :meth:`~bokeh.charts.builder.Builder.yield_renderers`: yields the glyphs to be rendered into the plot. Here you should call the :meth:`~bokeh.charts.builder.Builder.add_glyph` method so that the builder can setup the legend for you. * :meth:`~bokeh.charts.builder.Builder.set_ranges`: setup the ranges for the glyphs. This is called after glyph creation, so you are able to inspect the comp_glyphs for their minimum and maximum values. See the :meth:`~bokeh.charts.builder.Builder.create` method for more information on when this is called and how the builder provides the ranges to the containing :class:`Chart` using the :meth:`Chart.add_ranges` method. Optional: * :meth:`~bokeh.charts.builder.Builder.setup`: provides an area where subclasses of builder can introspect properties, setup attributes, or change property values. This is called before :meth:`~bokeh.charts.builder.Builder.process_data`. * :meth:`~bokeh.charts.builder.Builder.process_data`: provides an area where subclasses of builder can manipulate the source data before renderers are created. """ # Optional Inputs x_range = Instance(Range) y_range = Instance(Range) xlabel = String() ylabel = String() xscale = String() yscale = String() palette = List(Color, help="""Optional input to override the default palette used by any color attribute. """) # Dimension Configuration """ The dimension labels that drive the position of the glyphs. Subclasses should implement this so that the Builder base class knows which dimensions it needs to operate on. An example for a builder working with cartesian x and y coordinates would be dimensions = ['x', 'y']. You should then instantiate the x and y dimensions as attributes of the subclass of builder using the :class:`Dimension <bokeh.charts.properties.Dimension>` class. One for x, as x = Dimension(...), and one as y = Dimension(...). """ dimensions = None # None because it MUST be overridden """ The dimension labels that must exist to produce the glyphs. This specifies what are the valid configurations for the chart, with the option of specifying the type of the columns. The :class:`~bokeh.charts.data_source.ChartDataSource` will inspect this property of your subclass of Builder and use this to fill in any required dimensions if no keyword arguments are used. """ req_dimensions = [] # Attribute Configuration attributes = Dict(String, Instance(AttrSpec), help=""" The attribute specs used to group data. This is a mapping between the role of the attribute spec (e.g. 'color') and the :class:`~bokeh.charts.attributes.AttrSpec` class (e.g., :class:`~bokeh.charts.attributes.ColorAttr`). The Builder will use this attributes property during runtime, which will consist of any attribute specs that are passed into the chart creation function (e.g., :class:`~bokeh.charts.Bar`), ones that are created for the user from simple input types (e.g. `Bar(..., color='red')` or `Bar(..., color=<column_name>)`), or lastly, the attribute spec found in the default_attributes configured for the subclass of :class:`~bokeh.charts.builder.Builder`. """) """ The default attribute specs used to group data. This is where the subclass of Builder should specify what the default attributes are that will yield attribute values to each group of data, and any specific configuration. For example, the :class:`ColorAttr` utilizes a default palette for assigning color based on groups of data. If the user doesn't assign a column of the data to the associated attribute spec, then the default attrspec is used, which will yield a constant color value for each group of data. This is by default the first color in the default palette, but can be customized by setting the default color in the ColorAttr. """ default_attributes = None # None because it MUST be overridden # Derived properties (created by Builder at runtime) attribute_columns = List(ColumnLabel, help=""" All columns used for specifying attributes for the Chart. The Builder will set this value on creation so that the subclasses can know the distinct set of columns that are being used to assign attributes. """) comp_glyphs = List(Instance(CompositeGlyph), help=""" A list of composite glyphs, where each represents a unique subset of data. The composite glyph is a helper class that encapsulates all low level :class:`~bokeh.models.glyphs.Glyph`, that represent a higher level group of data. For example, the :class:`BoxGlyph` is a single class that yields each :class:`GlyphRenderer` needed to produce a Box on a :class:`BoxPlot`. The single Box represents a full array of values that are aggregated, and is made up of multiple :class:`~bokeh.models.glyphs.Rect` and :class:`~bokeh.models.glyphs.Segment` glyphs. """) labels = List( String, help="""Represents the unique labels to be used for legends.""") """List of attributes to use for legends.""" label_attributes = [] """ Used to assign columns to dimensions when no selections have been provided. The default behavior is provided by the :class:`OrderedAssigner`, which assigns a single column to each dimension available in the `Builder`'s `dims` property. """ column_selector = OrderedAssigner comp_glyph_types = List(Instance(CompositeGlyph)) sort_dim = Dict(String, Bool, default={}) sort_legend = List(Tuple(String, Bool), help=""" List of tuples to use for sorting the legend, in order that they should be used for sorting. This sorting can be different than the sorting used for the rest of the chart. For example, you might want to sort only on the column assigned to the color attribute, or sort it descending. The order of each tuple is (Column, Ascending). """) legend_sort_field = String(help=""" Attribute that should be used to sort the legend, for example: color, dash, maker, etc. Valid values for this property depend on the type of chart. """) legend_sort_direction = Enum(SortDirection, help=""" Sort direction to apply to :attr:`~bokeh.charts.builder.Builder.sort_legend`. Valid values are: `ascending` or `descending`. """) source = Instance(ColumnDataSource) tooltips = Either(List(Tuple(String, String)), List(String), Bool, default=None, help=""" Tells the builder to add tooltips to the chart by either using the columns specified to the chart attributes (True), or by generating tooltips for each column specified (list(str)), or by explicit specification of the tooltips using the valid input for the `HoverTool` tooltips kwarg. """) __deprecated_attributes__ = ('sort_legend', ) def __init__(self, *args, **kws): """Common arguments to be used by all the inherited classes. Args: data (:ref:`userguide_charts_data_types`): source data for the chart legend (str, bool): the legend of your plot. The legend content is inferred from incoming input.It can be ``top_left``, ``top_right``, ``bottom_left``, ``bottom_right``. It is ``top_right`` is you set it as True. Attributes: source (obj): datasource object for your plot, initialized as a dummy None. x_range (obj): x-associated datarange object for you plot, initialized as a dummy None. y_range (obj): y-associated datarange object for you plot, initialized as a dummy None. groups (list): to be filled with the incoming groups of data. Useful for legend construction. data (dict): to be filled with the incoming data and be passed to the ChartDataSource for each Builder class. attr (list(AttrSpec)): to be filled with the new attributes created after loading the data dict. """ data = None if len(args) != 0 or len(kws) != 0: # chart dimensions can be literal dimensions or attributes attrs = list(self.default_attributes.keys()) dims = self.dimensions + attrs # pop the dimension inputs from kwargs data_args = {} for dim in dims: if dim in kws.keys(): data_args[dim] = kws[dim] # build chart data source from inputs, given the dimension configuration data_args['dims'] = tuple(dims) data_args['required_dims'] = tuple(self.req_dimensions) data_args['attrs'] = attrs data_args['column_assigner'] = self.column_selector data = ChartDataSource.from_data(*args, **data_args) # make sure that the builder dimensions have access to the chart data source for dim in self.dimensions: getattr(getattr(self, dim), 'set_data')(data) # handle input attrs and ensure attrs have access to data attributes = self._setup_attrs(data, kws) # remove inputs handled by dimensions and chart attributes for dim in dims: kws.pop(dim, None) else: attributes = dict() kws['attributes'] = attributes super(Builder, self).__init__(**kws) # collect unique columns used for attributes self.attribute_columns = collect_attribute_columns(**self.attributes) for k in self.__deprecated_attributes__: if k in kws: setattr(self, k, kws[k]) self._data = data self._legends = [] def _setup_attrs(self, data, kws): """Handle overridden attributes and initialize them with data. Makes sure that all attributes have access to the data source, which is used for mapping attributes to groups of data. Returns: None """ source = ColumnDataSource(data.df) attr_names = self.default_attributes.keys() custom_palette = kws.get('palette') attributes = dict() for attr_name in attr_names: attr = kws.pop(attr_name, None) # if given an attribute use it if isinstance(attr, AttrSpec): attributes[attr_name] = attr # if we are given columns, use those elif isinstance(attr, (str, list)): attributes[attr_name] = self.default_attributes[ attr_name]._clone() # override palette if available if isinstance(attributes[attr_name], ColorAttr): if custom_palette is not None: attributes[attr_name].iterable = custom_palette attributes[attr_name].setup(data=source, columns=attr) else: # override palette if available if (isinstance(self.default_attributes[attr_name], ColorAttr) and custom_palette is not None): attributes[attr_name] = self.default_attributes[ attr_name]._clone() attributes[attr_name].iterable = custom_palette else: attributes[attr_name] = self.default_attributes[ attr_name]._clone() # make sure all have access to data source for attr_name in attr_names: attributes[attr_name].update_data(data=source) return attributes def setup(self): """Perform any initial pre-processing, attribute config. Returns: None """ pass def process_data(self): """Make any global data manipulations before grouping. It has to be implemented by any of the inherited class representing each different chart type. It is the place where we make specific calculations for each chart. Returns: None """ pass def yield_renderers(self): """ Generator that yields the glyphs to be draw on the plot It has to be implemented by any of the inherited class representing each different chart type. Yields: :class:`GlyphRenderer` """ raise NotImplementedError( 'Subclasses of %s must implement _yield_renderers.' % self.__class__.__name__) def set_ranges(self): """Calculate and set the x and y ranges. It has to be implemented by any of the subclasses of builder representing each different chart type, and is called after :meth:`yield_renderers`. Returns: None """ raise NotImplementedError( 'Subclasses of %s must implement _set_ranges.' % self.__class__.__name__) def get_dim_extents(self): """Helper method to retrieve maximum extents of all the renderers. Returns: a dict mapping between dimension and value for x_max, y_max, x_min, y_min """ return { 'x_max': max([renderer.x_max for renderer in self.comp_glyphs]), 'y_max': max([renderer.y_max for renderer in self.comp_glyphs]), 'x_min': min([renderer.x_min for renderer in self.comp_glyphs]), 'y_min': min([renderer.y_min for renderer in self.comp_glyphs]) } def add_glyph(self, group, glyph): """Add a composite glyph. Manages the legend, since the builder might not want all attribute types used for the legend. Args: group (:class:`DataGroup`): the data the `glyph` is associated with glyph (:class:`CompositeGlyph`): the glyph associated with the `group` Returns: None """ if isinstance(glyph, list): for sub_glyph in glyph: self.comp_glyphs.append(sub_glyph) else: self.comp_glyphs.append(glyph) # handle cases where builders have specified which attributes to use for labels label = None if len(self.label_attributes) > 0: for attr in self.label_attributes: # this will get the last attribute group label for now if self.attributes[attr].columns is not None: label = self._get_group_label(group, attr=attr) # if no special case for labeling, just use the group label if label is None: label = self._get_group_label(group, attr='label') # add to legend if new and unique label if str(label) not in self.labels and label is not None: self._legends.append((label, glyph.renderers)) self.labels.append(label) def _get_group_label(self, group, attr='label'): """Get the label of the group by the attribute name. Args: group (:attr:`DataGroup`: the group of data attr (str, optional): the attribute name containing the label, defaults to 'label'. Returns: str: the label for the group """ if attr is 'label': label = group.label else: label = group[attr] if isinstance(label, dict): label = tuple(label.values()) return self._get_label(label) @staticmethod def _get_label(raw_label): """Converts a label by string or tuple to a string representation. Args: raw_label (str or tuple(any, any)): a unique identifier for the data group Returns: str: a label that is usable in charts """ # don't convert None type to string so we can test for it later if raw_label is None: return None if (isinstance(raw_label, tuple) or isinstance(raw_label, list)) and \ len(raw_label) == 1: raw_label = raw_label[0] elif isinstance(raw_label, dict): raw_label = label_from_index_dict(raw_label) return str(raw_label) def collect_attr_kwargs(self): if hasattr(super(self.__class__, self), 'default_attributes'): attrs = set(self.default_attributes.keys()) - set( (super(self.__class__, self).default_attributes or {}).keys()) else: attrs = set() return attrs def get_group_kwargs(self, group, attrs): return {attr: group[attr] for attr in attrs} def create(self, chart=None): """Builds the renderers, adding them and other components to the chart. Args: chart (:class:`Chart`, optional): the chart that will contain the glyph renderers that the `Builder` produces. Returns: :class:`Chart` """ # call methods that allow customized setup by subclasses self.setup() self.process_data() # create and add renderers to chart renderers = self.yield_renderers() if chart is None: chart = Chart() chart.add_renderers(self, renderers) # handle ranges after renders, since ranges depend on aggregations # ToDo: should reconsider where this occurs self.set_ranges() chart.add_ranges('x', self.x_range) chart.add_ranges('y', self.y_range) # sort the legend if we are told to self._legends = self._sort_legend(self.legend_sort_field, self.legend_sort_direction, self._legends, self.attributes) # always contribute legends, let Chart sort it out chart.add_legend(self._legends) chart.add_labels('x', self.xlabel) chart.add_labels('y', self.ylabel) chart.add_scales('x', self.xscale) chart.add_scales('y', self.yscale) if self.tooltips is not None: tooltips = build_hover_tooltips(hover_spec=self.tooltips, chart_cols=self.attribute_columns) chart.add_tooltips(tooltips) return chart @classmethod def generate_help(cls): help_str = '' for comp_glyph in cls.comp_glyph_types: help_str += str(comp_glyph.glyph_properties()) return help_str @staticmethod def _sort_legend(legend_sort_field, legend_sort_direction, legends, attributes): """Sort legends sorted by looping though sort_legend items ( see :attr:`Builder.sort_legend` for more details) """ if legend_sort_field: if len(attributes[legend_sort_field].columns) > 0: # TODO(fpliger): attributes should be consistent and not # need any type checking but for # the moment it is not, specially when going # though a process like binning or when data # is built for HeatMap, Scatter, etc... item_order = [ x[0] if isinstance(x, tuple) else x for x in attributes[legend_sort_field].items ] item_order = [ str(x) if not isinstance(x, string_types) else x for x in item_order ] def foo(leg): return item_order.index(leg[0]) reverse = legend_sort_direction == 'descending' return list(sorted(legends, key=foo, reverse=reverse)) return legends @property def sort_legend(self): deprecated((0, 12, 0), 'Chart.sort_legend', 'Chart.legend_sort_field') return [(self.legend_sort_field, self.legend_sort_direction)] @sort_legend.setter def sort_legend(self, value): deprecated((0, 12, 0), 'Chart.sort_legend', 'Chart.legend_sort_field') self.legend_sort_field, direction = value[0] if direction: self.legend_sort_direction = "ascending" else: self.legend_sort_direction = "descending"
class BinnedStat(Stat): """ Base class for shared functionality accross bins and aggregates dimensions for plotting. """ bin_stat = Instance(BinStats, help=""" A mapping between each dimension and associated binning calculations. """) bins = List(Instance(Bin), help=""" A list of the `Bin` instances that were produced as result of the inputs. Iterating over `Bins` will iterate over this list. Each `Bin` can be inspected for metadata about the bin and the values associated with it. """) stat = Instance(Stat, default=Count(), help=""" The statistical operation to be used on the values in each bin. """) bin_column = String() centers_column = String() aggregate = Bool(default=True) bin_values = Bool(default=False) bin_width = Float() def __init__(self, values=None, column=None, bins=None, stat='count', source=None, **properties): if isinstance(stat, str): stat = stats[stat]() properties['column'] = column or 'vals' properties['stat'] = stat properties['values'] = values properties['source'] = source self._bins = bins super(BinnedStat, self).__init__(**properties) def _get_stat(self): stat_kwargs = {} if self.source is not None: stat_kwargs['source'] = self.source stat_kwargs['column'] = self.column elif self.values is not None: stat_kwargs['values'] = self.values stat_kwargs['bins'] = self._bins return BinStats(**stat_kwargs) def update(self): self.bin_stat = self._get_stat() self.bin_stat.update()
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): source = properties.pop('source', None) if source is not None: if isinstance(source, pd.DataFrame): source = ColumnDataSource(source) properties['source'] = source 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): data = ColumnDataSource(data) if isinstance(data, ColumnDataSource): self.source = data if column is not None: self.column = column else: self.values = data self.update() self.calculate() def get_data(self, column=None): """Returns the available columnlabel/source values or column values.""" if self.source is not None and (self.column is not None or column is not None): if column is not None: col = column else: col = self.column return pd.Series(self.source.data[col]) elif self.values is None and self.source is not None: return pd.Series(self.source.to_df().index) 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 DrawTool(Tool): __implementation__ = TypeScript(JS_CODE) source = Instance(ColumnDataSource)
class AggregateGlyph(NestedCompositeGlyph): """A base composite glyph for aggregating an array. Implements default stacking and dodging behavior that other composite glyphs can inherit. """ x_label = String() x_label_value = Any() 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 __init__(self, x_label=None, **kwargs): label = kwargs.get('label') if x_label is not None: kwargs['x_label_value'] = x_label if not isinstance(x_label, str): x_label = str(x_label) kwargs['x_label'] = x_label elif label is not None: kwargs['x_label'] = str(label) super(AggregateGlyph, self).__init__(**kwargs) 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(label_from_index_dict(self.x_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) labels = [getattr(glyph, prop) for glyph in glyphs] labels = [ tuple(label.values()) if isinstance(label, dict) else label for label in labels ] [grouped[label].append(glyph) for label, glyph in zip(labels, glyphs)] labels = pd.Series(labels).drop_duplicates().values return labels, grouped def __stack__(self, glyphs): """Apply relative shifts to the composite glyphs for stacking.""" filtered_glyphs = self.filter_glyphs(glyphs) labels, grouped = self.groupby(filtered_glyphs, 'x_label') for label in labels: group = grouped[label] # 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) labels, 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, label in enumerate(labels): group = grouped[label] for glyph in group: glyph.dodge_shift = step[i + 1] glyph.width = width glyph.refresh()
def test_has_ref(self) -> None: prop = bcpc.Seq(Int) assert not prop.has_ref prop = bcpc.Seq(Instance(_TestModel)) assert prop.has_ref
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(help="""Derived value for 25% of all values.""") q2 = Float(help="""Derived value for 50% of all values.""") q3 = Float(help="""Derived value for 75% of all values.""") 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 kwargs.get( 'bar_color', None) or self.lookup('bar_color').class_default() kwargs['outliers'] = kwargs.pop('outliers', None) or outliers kwargs['label'] = label kwargs['values'] = values x_label = kwargs.get('x_label') kwargs['q2_glyph'] = QuartileGlyph(label=label, x_label=x_label, values=values, interval1=0.25, interval2=0.5, width=width, color=bar_color) kwargs['q3_glyph'] = QuartileGlyph(label=label, x_label=x_label, values=values, interval1=0.5, interval2=0.75, width=width, color=bar_color) super(BoxGlyph, self).__init__(**kwargs) self.setup() def build_renderers(self): """Yields all renderers that make up the BoxGlyph.""" 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(label=self.label, y=outlier_values, x=[self.get_dodge_label()] * len(outlier_values), 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): """Sets all derived stat properties of the BoxGlyph.""" self.q1 = self.q2_glyph.start self.q2 = self.q2_glyph.end self.q3 = self.q3_glyph.end self.iqr = self.q3 - self.q1 mx = Max() mx.set_data(self.values) mn = Min() mn.set_data(self.values) self.w0 = max(self.q1 - (1.5 * self.iqr), mn.value) self.w1 = min(self.q3 + (1.5 * self.iqr), mx.value) def build_source(self): """Calculate stats and builds and returns source for whiskers.""" 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 dict(x0s=x0s, y0s=y0s, x1s=x1s, y1s=y1s) def _set_sources(self): """Set the column data source on the whisker glyphs.""" 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): """Returns list of composite glyphs, excluding the regular glyph renderers.""" 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 OtherModel(Model): child = Instance(Model)
class SomeModel(Model): data = ColumnData(Any, Any, default={}) ref1 = Instance(OtherModel, default=lambda: OtherModel()) ref2 = Instance(OtherModel, default=lambda: OtherModel())
class SomeModelInTestDocument(Model): foo = Int(2) child = Nullable(Instance(Model))
def test_Instance(self) -> None: p = Instance(HasProps) with pytest.raises(ValueError) as e: p.validate("junk") assert matches(str(e.value), r"expected an instance of type HasProps, got junk of type str")
def test_has_ref(self) -> None: prop0 = bcpn.Nullable(Int) assert not prop0.has_ref prop1 = bcpn.Nullable(Instance(_TestModel)) assert prop1.has_ref
class Base(HasProps): num = Int(12) container = List(String) child = Instance(HasProps)
def test_has_ref(self): prop = bcpc.Dict(String, Int) assert not prop.has_ref prop = bcpc.Dict(String, Instance(_TestModel)) assert prop.has_ref
class Sub(Base, Mixin): sub_num = Int(12) sub_container = List(String) sub_child = Instance(HasProps)
def test_has_ref(self): prop = bcpc.Tuple(Int, Int) assert not prop.has_ref prop = bcpc.Tuple(Int, Instance(_TestModel)) assert prop.has_ref
def test_Instance(self, detail) -> None: p = Instance(HasProps) with pytest.raises(ValueError) as e: p.validate("junk", detail) assert (str(e.value) == "") == (not detail)
class BinGlyph(XyGlyph): """Represents a group of data that was aggregated and is represented by a glyph. """ bins = Instance(Bins) column = String() stat = String() glyph_name = String() width = Float() height = Float() def __init__(self, x, y, values, column=None, stat='count', glyph='rect', width=1, height=1, **kwargs): df = pd.DataFrame(dict(x_vals=x, y_vals=y, values_vals=values)) df.drop_duplicates(inplace=True) kwargs['x'] = df.x_vals kwargs['y'] = df.y_vals kwargs['values'] = df.values_vals kwargs['column'] = column kwargs['stat'] = stat kwargs['glyph_name'] = glyph kwargs['height'] = height kwargs['width'] = width if 'glyphs' not in kwargs: kwargs['glyphs'] = {'rect': Rect} super(XyGlyph, self).__init__(**kwargs) self.setup() def build_source(self): return {'x': self.x, 'y': self.y, 'values': self.values} def build_renderers(self): glyph_class = self.glyphs[self.glyph_name] glyph = glyph_class(x='x', y='y', height=self.height, width=self.width, fill_color=self.fill_color, line_color=self.line_color, dilate=True) yield GlyphRenderer(glyph=glyph) @property def x_max(self): return self.get_data_range('x')[1] + self.width / 2.0 @property def x_min(self): return self.get_data_range('x')[0] - self.width / 2.0 @property def y_max(self): return self.get_data_range('y')[1] + self.height / 2.0 @property def y_min(self): return self.get_data_range('y')[0] - self.height / 2.0 def get_data_range(self, col): data = self.source.data[col] if ChartDataSource.is_number(data): return min(data), max(data) else: return 1, len(data.drop_duplicates())
class Bin(Stat): """Represents a single bin of data values and attributes of the bin.""" label = Either(String, List(String)) start = Either(Float, List(Float)) stop = Either(Float, List(Float)) start_label = String() stop_label = String() center = Either(Float, List(Float)) stat = Instance(Stat, default=Count()) width = Float() def __init__(self, bin_label, values=None, source=None, **properties): if isinstance(bin_label, tuple): bin_label = list(bin_label) else: bin_label = [bin_label] properties['label'] = bin_label bounds = self.process_bounds(bin_label) starts, stops = zip(*bounds) centers = [(start + stop) / 2.0 for start, stop in zip(starts, stops)] if len(starts) == 1: starts = starts[0] stops = stops[0] centers = centers[0] else: starts = list(starts) stops = list(stops) centers = list(centers) properties['start'] = starts properties['stop'] = stops properties['center'] = centers 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 process_bounds(self, bin_label): if isinstance(bin_label, list): return [self.binstr_to_list(dim) for dim in bin_label] else: return [self.binstr_to_list(bin_label)] def update(self): self.stat.set_data(self.values) def calculate(self): self.value = self.stat.value