'''
'''
from __future__ import absolute_import
import pandas as pd
from six import iteritems
from bokeh.core.has_props import HasProps
from bokeh.models.renderers import GlyphRenderer
from bokeh.models.sources import ColumnDataSource
from bokeh.core.properties import Any, Color, Dict, Either, Float, Instance, List, String
from .properties import Column, ColumnLabel
[docs]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 singular 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 = Either(String, Dict(String, Any), default='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.
Most composite glyphs add their own representation of one or more values-like
columns/arrays that they receive as inputs. These are compiled together for
generating `source`, `data`, and `df` by the individual composite glyphs.
""")
# 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))
glyphs = Dict(String, Any) # where we expect a Glyph class as Value
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.""")
fill_color = Color(default="gray")
line_color = Color(default='black', help="""A default outline color for contained
glyphs.""")
fill_alpha = Float(default=0.8)
line_alpha = Float(default=1.0)
left_buffer = Float(default=0.0)
right_buffer = Float(default=0.0)
top_buffer = Float(default=0.0)
bottom_buffer = Float(default=0.0)
[docs] def __init__(self, **properties):
vals = properties.get('values')
if String().is_valid(vals) or Float().is_valid(vals):
properties['values'] = [vals]
super(CompositeGlyph, self).__init__(**properties)
self.setup()
[docs] 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()
[docs] def refresh(self):
"""Update the GlyphRenderers.
.. note:
this method would be called after data is added.
"""
if self.renderers is not None:
data = self.build_source()
if data is not None:
if isinstance(data, dict):
source = ColumnDataSource(data)
if not isinstance(source, ColumnDataSource) and source is not None:
raise TypeError('build_source must return dict or ColumnDataSource.')
else:
self.source = self.add_chart_index(source)
self._set_sources()
@property
def data(self):
if self.source is not None:
return self.source.data
else:
return {}
@property
def df(self):
if self.data:
return pd.DataFrame(self.data)
else:
return pd.DataFrame()
[docs] def add_chart_index(self, data):
"""Add identifier of the data group as a column for each row.
Args:
data (dict or `ColumnDataSource`): can be the type of data used internally
to ColumnDataSource, or a ColumnDataSource.
Returns:
dict or `ColumnDataSource`: returns the same type of data provided
"""
if isinstance(data, ColumnDataSource):
source = data
data = source.data
else:
source = None
# add chart index to data
if 'chart_index' not in data and len(list(data.keys())) > 0:
n_rows = len(list(data.values())[0])
# add composite chart index as column
data['chart_index'] = [self.label] * n_rows
# add constant value for each column in chart index
if isinstance(self.label, dict):
for col, val in iteritems(self.label):
data[col] = [val] * n_rows
if source is not None:
source.data = data
return source
else:
return data
[docs] def build_renderers(self):
yield GlyphRenderer()
[docs] def build_source(self):
data = {}
if self.values is not None:
data = {'values': self.values}
return data
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
[docs] def apply_operations(self):
pass
@classmethod
[docs] def glyph_properties(cls):
props = {}
for name, glyph in iteritems(cls.glyphs):
props[name] = glyph.class_properties(withbases=True)
return props
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__)