Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bokeh charts #339

Merged
merged 24 commits into from
Dec 20, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
5e533e5
Added initial ChartPlot implementation
philippjfr Dec 7, 2015
10fea80
Added initial bokeh BarPlot implementation
philippjfr Dec 7, 2015
d259fce
Added BoxWhisker Element and corresponding plot types
philippjfr Dec 7, 2015
37414ab
Added support for ranges on Bokeh ChartPlots
philippjfr Dec 7, 2015
6e18c55
Added control over BarPlot stacking and grouping
philippjfr Dec 7, 2015
1534e03
Fixed BoxWhisker docstring
philippjfr Dec 7, 2015
736699a
Merge branch 'master' into bokeh_charts
philippjfr Dec 19, 2015
734ea36
Added documentation and styles to Bokeh ChartPlots
philippjfr Dec 19, 2015
e362823
Fixes for styling of BoxPlots
philippjfr Dec 19, 2015
9464d6c
Added support for 1D BoxWhisker Elements
philippjfr Dec 19, 2015
d4d15ba
Fixes for matplotlib Bar and BoxWhisker Plots
philippjfr Dec 19, 2015
4c860d2
Added BoxWhisker Example to Elements Tutorial
philippjfr Dec 19, 2015
c9b3231
Added BoxWhisker TableConversion method
philippjfr Dec 19, 2015
ab7a070
Made border offsets for adjoined plots dependent on type
philippjfr Dec 19, 2015
4b6c969
Added SideBoxPlot for matplotlib
philippjfr Dec 19, 2015
3340820
Fixes for matplotlib AdjointLayout aspects
philippjfr Dec 20, 2015
e17cd0d
BoxWhisker is now 1D by default
philippjfr Dec 20, 2015
99f81e2
Updated Elements Tutorial after adjoine plot changes
philippjfr Dec 20, 2015
de29ed6
Updated reference_data submodule reference
philippjfr Dec 20, 2015
c26d3be
Added BoxWhisker Element comparison
philippjfr Dec 20, 2015
b1b2ff3
Made bokeh BarPlot plot options consistent with matplotlib
philippjfr Dec 20, 2015
5b1d64f
Added support for 1D BoxWhisker in bokeh
philippjfr Dec 20, 2015
7d4cc7f
Updating bokeh Charts plot properties
philippjfr Dec 20, 2015
d303e8a
Increased default Bokeh border width
philippjfr Dec 20, 2015
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 68 additions & 2 deletions doc/Tutorials/Elements.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
" <dt>[``Spread``](#Spread)</dt><dd>Just like ErrorBars, Spread is a collection of x-/y-coordinates with associated symmetric or asymmetric errors.</dd>\n",
" <dt>[``Bars``](#Bars)</dt><dd>Data collected and binned into categories.</dd>\n",
" <dt>[``Histogram``](#Histogram)</dt><dd>Data collected and binned in a continuous space using specified bin edges.</dd>\n",
" <dt>[``BoxWhisker``](#BoxWhisker)</dt><dd>Distributions of data varying by 0-N key dimensions.</dd>\n",
" <dt>[``Scatter``](#Scatter)</dt><dd>Discontinuous collection of points indexed over a single dimension.</dd>\n",
" <dt>[``Points``](#Points)</dt><dd>Discontinuous collection of points indexed over two dimensions.</dd>\n",
" <dt>[``VectorField``](#VectorField)</dt><dd>Cyclic variable (and optional auxiliary data) distributed over two-dimensional space.</dd>\n",
Expand Down Expand Up @@ -327,6 +328,71 @@
" kdims=['Group', 'Category', 'Stack'], vdims=['Count'])"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### ``BoxWhisker`` <a id='BoxWhisker'></a>"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The ``BoxWhisker`` Element allows representing distribution of data varying by 0-N key dimensions. To represent the distribution of a single variable we can create a BoxWhisker Element with no key dimensions and a single value dimension:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": [
"hv.BoxWhisker(np.random.randn(200), kdims=[], vdims=['Value'])"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"BoxWhisker Elements support any number of dimensions and may also be inverted. To style the boxes and whiskers supply boxprops, whiskerprops and flierprops."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": [
"%%opts BoxWhisker [fig_size=200 invert_axes=True] (boxprops=dict(color='gray') whiskerprops=dict(color='indianred'))\n",
"groups = [chr(65+g) for g in np.random.randint(0, 3, 200)]\n",
"hv.BoxWhisker((groups, np.random.randint(0, 5, 200), np.random.randn(200)),\n",
" kdims=['Group', 'Category'], vdims=['Value']).sort()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"``BoxWhisker`` Elements may also be used to represent a distribution as a marginal plot by adjoining it."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": [
"points = hv.Points(np.random.randn(500, 2))\n",
"points << hv.BoxWhisker(points['y']) << hv.BoxWhisker(points['x'])"
]
},
{
"cell_type": "markdown",
"metadata": {},
Expand Down Expand Up @@ -546,7 +612,7 @@
},
"outputs": [],
"source": [
"%%opts Spikes (alpha=0.05) [spike_length=0.5] AdjointLayout [border_size=0]\n",
"%%opts Spikes (alpha=0.05) [spike_length=1]\n",
"points = hv.Points(np.random.randn(500, 2))\n",
"points << hv.Spikes(points['y']) << hv.Spikes(points['x'])"
]
Expand Down Expand Up @@ -1390,7 +1456,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython2",
"version": "2.7.10"
"version": "2.7.11"
}
},
"nbformat": 4,
Expand Down
2 changes: 1 addition & 1 deletion doc/reference_data
Submodule reference_data updated 182 files
15 changes: 15 additions & 0 deletions holoviews/element/chart.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,21 @@ class Bars(Columns):



class BoxWhisker(Chart):
"""
BoxWhisker represent data as a distributions highlighting
the median, mean and various percentiles.
"""

group = param.String(default='BoxWhisker', constant=True)

kdims = param.List(default=[], bounds=(0,None))

vdims = param.List(default=[Dimension('y')], bounds=(1,1))

_1d = True


class Histogram(Element2D):
"""
Histogram contains a number of bins, which are defined by the
Expand Down
7 changes: 6 additions & 1 deletion holoviews/element/comparison.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,12 +155,13 @@ def register(cls):
cls.equality_type_funcs[Histogram] = cls.compare_histogram
cls.equality_type_funcs[Bars] = cls.compare_bars
cls.equality_type_funcs[Spikes] = cls.compare_spikes
cls.equality_type_funcs[BoxWhisker] = cls.compare_boxwhisker
cls.equality_type_funcs[VectorField] = cls.compare_vectorfield

# Tables
cls.equality_type_funcs[ItemTable] = cls.compare_itemtables
cls.equality_type_funcs[Table] = cls.compare_tables
cls.equality_type_funcs[Points] = cls.compare_points
cls.equality_type_funcs[VectorField] = cls.compare_vectorfield

# Pandas DFrame objects
cls.equality_type_funcs[DataFrameView] = cls.compare_dframe
Expand Down Expand Up @@ -505,6 +506,10 @@ def compare_bars(cls, el1, el2, msg='Bars'):
def compare_spikes(cls, el1, el2, msg='Spikes'):
cls.compare_columns(el1, el2, msg)

@classmethod
def compare_boxwhisker(cls, el1, el2, msg='BoxWhisker'):
cls.compare_columns(el1, el2, msg)

#=========#
# Rasters #
#=========#
Expand Down
4 changes: 4 additions & 0 deletions holoviews/element/tabular.py
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,10 @@ def bars(self, kdims=None, vdims=None, mdims=None, **kwargs):
from .chart import Bars
return self._conversion(kdims, vdims, mdims, Bars, **kwargs)

def box(self, kdims=None, vdims=None, mdims=None, **kwargs):
from .chart import BoxWhisker
return self._conversion(kdims, vdims, mdims, BoxWhisker, **kwargs)

def bivariate(self, kdims=None, vdims=None, mdims=None, **kwargs):
from ..interface.seaborn import Bivariate
return self._convert(kdims, vdims, mdims, Bivariate, **kwargs)
Expand Down
8 changes: 5 additions & 3 deletions holoviews/plotting/bokeh/__init__.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
from ...core import (Store, Overlay, NdOverlay, Layout, AdjointLayout,
GridSpace, NdElement, Columns, GridMatrix, NdLayout)
from ...element import (Curve, Points, Scatter, Image, Raster, Path,
RGB, Histogram, Spread, HeatMap, Contours,
Path, Box, Bounds, Ellipse, Polygons,
RGB, Histogram, Spread, HeatMap, Contours, Bars,
Path, Box, Bounds, Ellipse, Polygons, BoxWhisker,
ErrorBars, Text, HLine, VLine, Spline, Spikes,
Table, ItemTable, Surface, Scatter3D, Trisurface)
from ...core.options import Options, Cycle, OptionTree
Expand All @@ -14,7 +14,7 @@
from .callbacks import Callbacks
from .element import OverlayPlot, BokehMPLWrapper, BokehMPLRawWrapper
from .chart import (PointPlot, CurvePlot, SpreadPlot, ErrorPlot, HistogramPlot,
SideHistogramPlot, SpikesPlot, SideSpikesPlot)
SideHistogramPlot, BoxPlot, BarPlot, SpikesPlot, SideSpikesPlot)
from .path import PathPlot, PolygonPlot
from .plot import GridPlot, LayoutPlot, AdjointLayoutPlot
from .raster import RasterPlot, RGBPlot, HeatmapPlot
Expand All @@ -38,6 +38,8 @@
ErrorBars: ErrorPlot,
Spread: SpreadPlot,
Spikes: SpikesPlot,
BoxWhisker: BoxPlot,
Bars: BarPlot,

# Rasters
Image: RasterPlot,
Expand Down
174 changes: 172 additions & 2 deletions holoviews/plotting/bokeh/chart.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import numpy as np
from bokeh.models import Circle
from bokeh.charts import Bar, BoxPlot as BokehBoxPlot
from bokeh.models import Circle, GlyphRenderer, ColumnDataSource, Range1d
import param

from ...core import Dimension
from ...core.util import max_range
from ...element import Chart, Raster, Points, Polygons, Spikes
from ..util import compute_sizes, get_sideplot_ranges
from ..util import compute_sizes, get_sideplot_ranges, match_spec
from .element import ElementPlot, line_properties, fill_properties
from .path import PathPlot, PolygonPlot
from .util import map_colors, get_cmap, mpl_to_bokeh
Expand Down Expand Up @@ -311,3 +312,172 @@ class SideSpikesPlot(SpikesPlot):
height = param.Integer(default=80, doc="Height of plot")

width = param.Integer(default=80, doc="Width of plot")



class ChartPlot(ElementPlot):
"""
ChartPlot creates and updates Bokeh high-level Chart instances.
The current implementation requires creating a new Chart for each
frame and updating the existing Chart. Once Bokeh supports updating
Charts directly this workaround will no longer be required.
"""

def initialize_plot(self, ranges=None, plot=None, plots=None, source=None):
"""
Initializes a new plot object with the last available frame.
"""
# Get element key and ranges for frame
element = self.hmap.last
key = self.keys[-1]
ranges = self.compute_ranges(self.hmap, key, ranges)
ranges = match_spec(element, ranges)
self.current_ranges = ranges
self.current_frame = element
self.current_key = key

# Initialize plot, source and glyph
if plot is not None:
raise Exception("Can't overlay Bokeh Charts based plot properties")

init_element = element.clone(element.interface.concat(self.hmap.values()))
properties = self.style[self.cyclic_index]
plot = self._init_chart(init_element, ranges)

self.handles['plot'] = plot
self.handles['glyph_renderers'] = [r for r in plot.renderers
if isinstance(r, GlyphRenderer)]
self._update_chart(key, element, ranges)

# Update plot, source and glyph
self.drawn = True

return plot


def update_frame(self, key, ranges=None, plot=None, element=None):
"""
Updates an existing plot with data corresponding
to the key.
"""
element = self._get_frame(key)
if not element:
if self.dynamic and self.overlaid:
self.current_key = key
element = self.current_frame
else:
element = self._get_frame(key)
else:
self.current_key = key
self.current_frame = element

self.style = self.lookup_options(element, 'style')
self.set_param(**self.lookup_options(element, 'plot').options)
ranges = self.compute_ranges(self.hmap, key, ranges)
ranges = match_spec(element, ranges)
self.current_ranges = ranges

self._update_chart(key, element, ranges)


def _update_chart(self, key, element, ranges):
new_chart = self._init_chart(element, ranges)
old_chart = self.handles['plot']
old_renderers = old_chart.select(type=GlyphRenderer)
new_renderers = new_chart.select(type=GlyphRenderer)

old_chart.y_range.update(**new_chart.y_range.properties_with_values())
updated = []
for new_r in new_renderers:
for old_r in old_renderers:
if type(old_r.glyph) == type(new_r.glyph):
old_renderers.pop(old_renderers.index(old_r))
new_props = new_r.properties_with_values()
source = new_props.pop('data_source')
old_r.glyph.update(**new_r.glyph.properties_with_values())
old_r.update(**new_props)
old_r.data_source.data.update(source.data)
updated.append(old_r)
break

for old_r in old_renderers:
if old_r not in updated:
emptied = {k: [] for k in old_r.data_source.data}
old_r.data_source.data.update(emptied)

properties = self._plot_properties(key, old_chart, element)
old_chart.update(**properties)


@property
def current_handles(self):
plot = self.handles['plot']
sources = plot.select(type=ColumnDataSource)
return sources


class BoxPlot(ChartPlot):
"""
BoxPlot generates a box and whisker plot from a BoxWhisker
Element. This allows plotting the median, mean and various
percentiles. Displaying outliers is currently not supported
as they cannot be consistently updated.
"""

style_opts = ['color', 'whisker_color'] + line_properties

def _init_chart(self, element, ranges):
properties = self.style[self.cyclic_index]
dframe = element.dframe()
label = element.dimensions('key', True)
if len(element.dimensions()) == 1:
dframe[''] = ''
label = ['']
plot = BokehBoxPlot(dframe, label=label,
values=element.dimensions('value', True)[0],
**properties)

# Disable outliers for now as they cannot be consistently updated.
plot.renderers = [r for r in plot.renderers
if not (isinstance(r, GlyphRenderer) and
isinstance(r.glyph, Circle))]
return plot


class BarPlot(ChartPlot):
"""
BarPlot allows generating single- or multi-category
bar Charts, by selecting which key dimensions are
mapped onto separate groups, categories and stacks.
"""

group_index = param.Integer(default=0, doc="""
Index of the dimension in the supplied Bars
Element, which will be laid out into groups.""")

category_index = param.Integer(default=1, doc="""
Index of the dimension in the supplied Bars
Element, which will be laid out into categories.""")

stack_index = param.Integer(default=2, doc="""
Index of the dimension in the supplied Bars
Element, which will stacked.""")

style_opts = ['bar_width', 'max_height', 'color', 'fill_alpha']

def _init_chart(self, element, ranges):
kdims = element.dimensions('key', True)
vdim = element.dimensions('value', True)[0]

kwargs = self.style[self.cyclic_index]
if self.group_index < element.ndims:
kwargs['label'] = kdims[self.group_index]
if self.category_index < element.ndims:
kwargs['group'] = kdims[self.category_index]
if self.stack_index < element.ndims:
kwargs['stack'] = kdims[self.stack_index]
crange = Range1d(*ranges.get(vdim))
plot = Bar(element.dframe(), values=vdim,
continuous_range=crange, **kwargs)
return plot

2 changes: 1 addition & 1 deletion holoviews/plotting/bokeh/element.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ class ElementPlot(BokehPlot, GenericElementPlot):
bgcolor = param.Parameter(default='white', doc="""
Background color of the plot.""")

border = param.Number(default=2, doc="""
border = param.Number(default=10, doc="""
Minimum border around plot.""")

fontsize = param.Parameter(default={'title': '12pt'}, allow_None=True, doc="""
Expand Down
Loading