Skip to content

Commit

Permalink
Support column oriented measure input for line. Build out more of the…
Browse files Browse the repository at this point in the history
… special columns functionality. Add ability to stack measurements. Simplify getting group values. Add example for line and line with datetime column. Rename scatter glyph to point glyph to setup combining point and line glyphs. Have builders provide scales to chart.
  • Loading branch information
nroth-dealnews committed Sep 9, 2015
1 parent f4cc1d7 commit ded044e
Show file tree
Hide file tree
Showing 10 changed files with 235 additions and 145 deletions.
17 changes: 16 additions & 1 deletion bokeh/charts/_attributes.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@
from itertools import cycle
from copy import copy

from bokeh.properties import HasProps, String, List, Instance, Either, Any, Dict, Color
from bokeh.properties import HasProps, String, List, Instance, Either, Any, Dict, Color, Enum
from bokeh.models.sources import ColumnDataSource
from bokeh.charts import DEFAULT_PALETTE
from bokeh.charts._properties import ColumnLabel
from bokeh.charts.utils import marker_types
from bokeh.enums import DashPattern


class AttrSpec(HasProps):
Expand Down Expand Up @@ -144,6 +145,20 @@ def __init__(self, **kwargs):
super(MarkerAttr, self).__init__(**kwargs)


dashes = DashPattern._values


class DashAttr(AttrSpec):
name = 'dash'
iterable = List(String, default=dashes)

def __init__(self, **kwargs):
iterable = kwargs.pop('dash', None)
if iterable is not None:
kwargs['iterable'] = iterable
super(DashAttr, self).__init__(**kwargs)


class GroupAttr(AttrSpec):
name = 'nest'

Expand Down
24 changes: 18 additions & 6 deletions bokeh/charts/_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,9 @@ class Builder(HasProps):
xlabel = String()
ylabel = String()

xscale = String()
yscale = String()

# Dimensional Modeling
dimensions = List(String, help="""The dimension
labels that drive the position of the glyphs.""")
Expand Down Expand Up @@ -256,6 +259,9 @@ def create(self, chart=None):
chart.add_labels('x', self.xlabel)
chart.add_labels('y', self.ylabel)

chart.add_scales('x', self.xscale)
chart.add_scales('y', self.yscale)

return chart


Expand All @@ -270,34 +276,40 @@ class XYBuilder(Builder):
['y'],
['x', 'y']]

default_attributes = {'color': ColorAttr(),
'marker': MarkerAttr()}
default_attributes = {'color': ColorAttr()}

def _set_ranges(self):
"""Calculate and set the x and y ranges."""
# ToDo: handle when only single dimension is provided

endx = self.x.max
startx = self.x.min
self.x_range = self._get_range(self.x.data[self.x.selection], startx, endx)
self.x_range = self._get_range('x', startx, endx)

endy = self.y.max
starty = self.y.min
self.y_range = self._get_range(self.y.data[self.y.selection], starty, endy)
self.y_range = self._get_range('y', starty, endy)

@staticmethod
def _get_range(values, start, end):
def _get_range(self, dim, start, end):

values = getattr(self, dim).data
dtype = values.dtype.name
if dtype == 'object':
factors = values.drop_duplicates()
factors.sort(inplace=True)
setattr(self, dim + 'scale', 'categorical')
return FactorRange(factors=factors.tolist())
elif 'datetime' in dtype:
setattr(self, dim + 'scale', 'datetime')
return Range1d(start=start, end=end)
else:

diff = end - start
if diff == 0:
setattr(self, dim + 'scale', 'categorical')
return FactorRange(factors=['None'])
else:
setattr(self, dim + 'scale', 'linear')
return Range1d(start=start - 0.1 * diff, end=end + 0.1 * diff)


Expand Down
8 changes: 6 additions & 2 deletions bokeh/charts/_chart.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ def __init__(self):
self._renderer_map = []
self._ranges = defaultdict(list)
self._labels = defaultdict(list)
self._scales = defaultdict(list)

# Add to document and session if server output is asked
_doc = None
Expand Down Expand Up @@ -143,15 +144,18 @@ def add_ranges(self, dim, range):
def add_labels(self, dim, label):
self._labels[dim].append(label)

def add_scales(self, dim, scale):
self._scales[dim].append(scale)

def _get_labels(self, dim):
if not getattr(self._options, dim + 'label') and len(self._labels[dim]) > 0:
return self._labels[dim][0]
else:
return getattr(self._options, dim + 'label')

def create_axes(self):
self._xaxis = self.make_axis('x', "below", self._options.xscale, self._get_labels('x'))
self._yaxis = self.make_axis('y', "left", self._options.yscale, self._get_labels('y'))
self._xaxis = self.make_axis('x', "below", self._scales['x'][0], self._get_labels('x'))
self._yaxis = self.make_axis('y', "left", self._scales['y'][0], self._get_labels('y'))

def create_grids(self, xgrid=True, ygrid=True):
if xgrid:
Expand Down
42 changes: 33 additions & 9 deletions bokeh/charts/_data_source.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,14 +57,36 @@ def gen_column_names(n):
return col_names


def get_index(data):
return pd.Series(data.index.values)


def get_unity(data, value=1):
data_copy = data.copy()
data_copy['_charts_ones'] = value
return data_copy['_charts_ones']

special_columns = {'index': get_index,
'unity': get_unity}


class DataGroup(object):
"""Contains subset of data and metadata about it."""

def __init__(self, label, data, attr_specs):
self.label = label
self.data = data
self.data = data.reset_index()
self.attr_specs = attr_specs

def get_values(self, selection):
if selection in list(special_columns.keys()):
return special_columns[selection](self.data)
elif isinstance(selection, str) or \
isinstance(selection, list):
return self.data[selection]
else:
return None

@property
def source(self):
return ColumnDataSource(self.data)
Expand All @@ -86,7 +108,7 @@ def groupby(df, **specs):

# if there was any input for chart attributes, which require grouping
if spec_cols:
df = df.sort(columns=spec_cols)
#df = df.sort(columns=spec_cols)

for name, data in df.groupby(spec_cols):

Expand Down Expand Up @@ -138,11 +160,6 @@ def __init__(self, df, dims=('x', 'y'), required_dims=('x',), selections=None, *
self._selections = self.get_selections(selections, **kwargs)
self.meta = self.collect_metadata(df)
self._validate_selections()
self._add_chart_columns()

def _add_chart_columns(self):
# ToDo: reconsider how to get these values into each group
self._data['_charts_ones'] = 1

def get_selections(self, selections, **kwargs):
"""Maps chart dimensions to selections and checks that required dim requirements are met."""
Expand Down Expand Up @@ -180,10 +197,17 @@ def __getitem__(self, dim):
e.g. dim='x'
"""
if self._selections[dim] is not None:
if dim in self._selections.keys():
return self._selections[dim]
else:
return '_charts_ones'
return None

def stack_measures(self, measures, ids=None):
for dim in self._dims:
# find the dimension the measures are associated with
if measures == self._selections[dim]:
self._selections[dim] = 'value'
self._data = pd.melt(self._data, id_vars=ids, value_vars=measures)

def groupby(self, **specs):
"""Iterable of chart attribute specifications, associated with columns.
Expand Down
14 changes: 7 additions & 7 deletions bokeh/charts/_properties.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

from bokeh.properties import (HasProps, Either, String, Int, List, Dict,
Bool, PrimitiveProperty, bokeh_integer_types, Array)
from bokeh.charts._data_source import special_columns


class Column(Array):
Expand Down Expand Up @@ -141,19 +142,18 @@ def data(self):
if self._data.empty or self.selection is None:
return pd.Series(1)
else:
if not isinstance(self.selection, list):
select = [self.selection]
else:
select = self.selection
return self._data[select]
# 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
if self.columns is None:
self.columns = list(self._data.columns.values)
self.columns = list(self._data.columns.values)

@property
def min(self):
Expand Down
111 changes: 46 additions & 65 deletions bokeh/charts/builder/line_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,21 +17,17 @@
#-----------------------------------------------------------------------------
from __future__ import absolute_import

from six import string_types
import numpy as np

from ..utils import cycle_colors
from .._builder import Builder, create_and_build
from ...models import ColumnDataSource, DataRange1d, GlyphRenderer, Range1d
from ...models.glyphs import Line as LineGlyph
from ...properties import Any
from .._builder import XYBuilder, create_and_build
from ..glyphs import LineGlyph
from .._attributes import DashAttr, ColorAttr
from ...models.sources import ColumnDataSource

#-----------------------------------------------------------------------------
# Classes and functions
#-----------------------------------------------------------------------------


def Line(values, index=None, **kws):
def Line(data, x=None, y=None, **kws):
""" Create a line chart using :class:`LineBuilder <bokeh.charts.builder.line_builder.LineBuilder>` to
render the geometry from values and index.
Expand Down Expand Up @@ -68,10 +64,17 @@ def Line(values, index=None, **kws):
show(line)
"""
return create_and_build(LineBuilder, values, index=index, **kws)
if x is None and y is not None:
x = 'index'
elif x is not None and y is None:
y = 'index'

kws['x'] = x
kws['y'] = y
return create_and_build(LineBuilder, data, **kws)


class LineBuilder(Builder):
class LineBuilder(XYBuilder):
"""This is the Line class and it is in charge of plotting
Line charts in an easy and intuitive way.
Essentially, we provide a way to ingest the data, make the proper
Expand All @@ -80,60 +83,38 @@ class LineBuilder(Builder):
And finally add the needed lines taking the references from the source.
"""

index = Any(help="""
An index to be used for all data series as follows:
- A 1d iterable of any sort that will be used as
series common index
- As a string that corresponds to the key of the
mapping to be used as index (and not as data
series) if area.values is a mapping (like a dict,
an OrderedDict or a pandas DataFrame)
""")

def _process_data(self):
"""Calculate the chart properties accordingly from line.values.
Then build a dict containing references to all the points to be
used by the line glyph inside the ``_yield_renderers`` method.
"""
self._data = dict()
# list to save all the attributes we are going to create
self._attr = []
xs = self._values_index
self.set_and_get("x", "", np.array(xs))
for col, values in self._values.items():
if isinstance(self.index, string_types) and col == self.index:
continue

# save every new group we find
self._groups.append(col)
self.set_and_get("y_", col, values)

def _set_ranges(self):
"""
Push the Line data into the ColumnDataSource and calculate the
proper ranges.
"""
self._source = ColumnDataSource(self._data)
self.x_range = DataRange1d()

y_names = self._attr[1:]
endy = max(max(self._data[i]) for i in y_names)
starty = min(min(self._data[i]) for i in y_names)
self.y_range = Range1d(
start=starty - 0.1 * (endy - starty),
end=endy + 0.1 * (endy - starty)
)
default_attributes = {'color': ColorAttr(),
'dash': DashAttr()}

def _setup(self):

# if we were given no colors and a list of measurements, stack them
if self.attributes['color'].columns is None:
if isinstance(self.y.selection, list):
self._stack_measures(dim='y', ids=self.x.selection)
elif isinstance(self.x.selection, list):
self._stack_measures(dim='x', ids=self.y.selection)

def _stack_measures(self, dim, ids):
dim_prop = getattr(self, dim)

# transform our data by stacking the measurements into one column
self._data.stack_measures(measures=dim_prop.selection, ids=ids)

# update our dimension with the updated data
dim_prop.set_data(self._data)

# color by the name of each variable
self.attributes['color'] = ColorAttr(columns='variable',
data=ColumnDataSource(self._data.df))

def _yield_renderers(self):
"""Use the line glyphs to connect the xy points in the Line.
Takes reference points from the data loaded at the ColumnDataSource.
"""
colors = cycle_colors(self._attr, self.palette)
for i, duplet in enumerate(self._attr[1:], start=1):
glyph = LineGlyph(x='x', y=duplet, line_color=colors[i - 1])
renderer = GlyphRenderer(data_source=self._source, glyph=glyph)
self.legends.append((self._groups[i-1], [renderer]))
for group in self._data.groupby(**self.attributes):
glyph = LineGlyph(x=group.get_values(self.x.selection),
y=group.get_values(self.y.selection),
line_color=group['color'],
dash=group['dash'])
renderer = glyph.renderers[0]
self._legends.append((str(group.label), [renderer]))

yield renderer

0 comments on commit ded044e

Please sign in to comment.