Skip to content

Commit

Permalink
Add ArrowPlot implementation for bokeh (#1608)
Browse files Browse the repository at this point in the history
* Small fix for CompositeElementPlot

* Added bokeh ArrowPlot
  • Loading branch information
philippjfr authored and jlstevens committed Jun 26, 2017
1 parent 9ca3190 commit f33580c
Show file tree
Hide file tree
Showing 3 changed files with 91 additions and 8 deletions.
6 changes: 4 additions & 2 deletions holoviews/plotting/bokeh/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
GridSpace, GridMatrix, NdLayout, config)
from ...element import (Curve, Points, Scatter, Image, Raster, Path,
RGB, Histogram, Spread, HeatMap, Contours, Bars,
Box, Bounds, Ellipse, Polygons, BoxWhisker,
Box, Bounds, Ellipse, Polygons, BoxWhisker, Arrow,
ErrorBars, Text, HLine, VLine, Spline, Spikes,
Table, ItemTable, Area, HSV, QuadMesh, VectorField)
from ...core.options import Options, Cycle, Palette
Expand All @@ -17,7 +17,7 @@
except:
DFrame = None

from .annotation import TextPlot, LineAnnotationPlot, SplinePlot
from .annotation import TextPlot, LineAnnotationPlot, SplinePlot, ArrowPlot
from .callbacks import Callback # noqa (API import)
from .element import OverlayPlot, ElementPlot
from .chart import (PointPlot, CurvePlot, SpreadPlot, ErrorPlot, HistogramPlot,
Expand Down Expand Up @@ -79,6 +79,7 @@
VLine: LineAnnotationPlot,
Text: TextPlot,
Spline: SplinePlot,
Arrow: ArrowPlot,

# Tabular
Table: TablePlot,
Expand Down Expand Up @@ -150,6 +151,7 @@ def colormap_generator(palette):
# Annotations
options.HLine = Options('style', color=Cycle(), line_width=3, alpha=1)
options.VLine = Options('style', color=Cycle(), line_width=3, alpha=1)
options.Arrow = Options('style', arrow_size=10)

# Define composite defaults
options.GridMatrix = Options('plot', shared_xaxis=True, shared_yaxis=True,
Expand Down
84 changes: 82 additions & 2 deletions holoviews/plotting/bokeh/annotation.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
from collections import defaultdict

import numpy as np
from bokeh.models import Span
from bokeh.models import Span, Arrow
try:
from bokeh.models.arrow_heads import TeeHead, NormalHead
arrow_start = {'<->': NormalHead, '<|-|>': NormalHead}
arrow_end = {'->': NormalHead, '-[': TeeHead, '-|>': NormalHead, '-': None}
except:
from bokeh.models.arrow_heads import OpenHead, NormalHead
arrow_start = {'<->': NormalHead, '<|-|>': NormalHead}
arrow_end = {'->': NormalHead, '-[': OpenHead, '-|>': NormalHead, '-': None}

from ...element import HLine
from .element import ElementPlot, text_properties, line_properties
from .element import ElementPlot, CompositeElementPlot, text_properties, line_properties


class TextPlot(ElementPlot):
Expand Down Expand Up @@ -104,3 +112,75 @@ def get_data(self, element, ranges=None, empty=False):
'unsupported splines were skipped during plotting.')
data = {da: data[da] for da in data_attrs}
return (data, dict(zip(data_attrs, data_attrs)))



class ArrowPlot(CompositeElementPlot):

style_opts = (['arrow_%s' % p for p in line_properties+['size']] + text_properties)

_style_groups = {'arrow': 'arrow', 'label': 'text'}

_update_handles = ['glyph']

_plot_methods = dict(single='text')

def get_data(self, element, ranges=None, empty=False):
plot = self.state
label_mapping = dict(x='x', y='y', text='text')

# Compute arrow
x1, y1 = element.x, element.y
axrange = plot.x_range if self.invert_axes else plot.y_range
span = (axrange.end - axrange.start) / 6.
if element.direction == '^':
x2, y2 = x1, y1-span
label_mapping['text_baseline'] = 'top'
elif element.direction == '<':
x2, y2 = x1+span, y1
label_mapping['text_align'] = 'left'
label_mapping['text_baseline'] = 'middle'
elif element.direction == '>':
x2, y2 = x1-span, y1
label_mapping['text_align'] = 'right'
label_mapping['text_baseline'] = 'middle'
else:
x2, y2 = x1, y1+span
label_mapping['text_baseline'] = 'bottom'
arrow_opts = {'x_end': x1, 'y_end': y1,
'x_start': x2, 'y_start': y2}

# Define arrowhead
arrow_opts['arrow_start'] = arrow_start.get(element.arrowstyle, None)
arrow_opts['arrow_end'] = arrow_end.get(element.arrowstyle, NormalHead)

# Compute label
if self.invert_axes:
label_data = dict(x=[y2], y=[x2])
else:
label_data = dict(x=[x2], y=[y2])
label_data['text'] = [element.text]
return ({'label': label_data},
{'arrow': arrow_opts, 'label': label_mapping})

def _init_glyph(self, plot, mapping, properties, key):
"""
Returns a Bokeh glyph object.
"""
properties.pop('legend')
if key == 'arrow':
properties.pop('source')
arrow_end = mapping.pop('arrow_end')
arrow_start = mapping.pop('arrow_start')
start = arrow_start(**properties) if arrow_start else None
end = arrow_end(**properties) if arrow_end else None
glyph = Arrow(start=start, end=end, **dict(**mapping))
else:
properties = {p if p == 'source' else 'text_'+p: v
for p, v in properties.items()}
glyph, _ = super(ArrowPlot, self)._init_glyph(plot, mapping, properties, 'text')
plot.renderers.append(glyph)
return None, glyph

def get_extents(self, element, ranges=None):
return None, None, None, None
9 changes: 5 additions & 4 deletions holoviews/plotting/bokeh/element.py
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,7 @@ def _hover_opts(self, element):
dims += element.dimensions()
return list(util.unique_iterator(dims)), {}


def _init_tools(self, element, callbacks=[]):
"""
Processes the list of tools to be supplied to the plot.
Expand Down Expand Up @@ -960,13 +961,13 @@ def _init_glyphs(self, plot, element, ranges, source):
current_id = element._plot_id

self.handles['previous_id'] = current_id
for key, gdata in data.items():
source = self._init_datasource(gdata)
for key in dict(mapping, **data):
source = self._init_datasource(data.get(key, {}))
self.handles[key+'_source'] = source
properties = self._glyph_properties(plot, element, source, ranges)
properties = self._process_properties(key, properties)
with abbreviated_exception():
renderer, glyph = self._init_glyph(plot, mapping[key], properties, key)
renderer, glyph = self._init_glyph(plot, mapping.get(key, {}), properties, key)
self.handles[key+'_glyph'] = glyph
if isinstance(renderer, Renderer):
self.handles[key+'glyph_renderer'] = renderer
Expand All @@ -975,7 +976,7 @@ def _init_glyphs(self, plot, element, ranges, source):

# Update plot, source and glyph
with abbreviated_exception():
self._update_glyph(renderer, properties, mapping[key], glyph)
self._update_glyph(renderer, properties, mapping.get(key, {}), glyph)


def _process_properties(self, key, properties):
Expand Down

0 comments on commit f33580c

Please sign in to comment.