The below probably needs some more explanation, but it's a starting point.
File paths are always given starting from the holoviews/
top level.
There are (at least) three files one needs to change for a new Element:
Element
s, e.g. Points
, themselves are defined in the modules in element/
. These are backend-independent.ElementConversion
s can be put into __init__.py
.
plotting/*
's submodules hold the various plotting routines. These are backend-specific as they specify how the data are transformed and eventually handled by the backend. Here (or most likely in one of the parent classes) you'll the concrete statements with which holoviews draws e.g. into matplotlib axes.
plotting/*/__init__.py
holds the Store.register()
statement, such that Holoviews knows which Element
belongs to which *Plot
routine.
For changing an existing Element, number 2. is sufficient.
*Plot
can have a number of methods. Some of the most important that I've so far come across are:
get_data
_init_glyph
get_extents
These are frequently overwritten when subclassing an existing plotting class.
Parameters to the operation are defined in the class as my_parameter param.[type]; and can be accessed in any method as self.p.my_parameter.
_style_groups
is a dict
where keys are Bokeh glyphs and values are what the respective "part" of the Element is called in the return of the get_data
method.
To see all of this in action, you may have a look at how I implemented the Segments element in the bokeh backend:
SegmentPlot
(this is bokeh-specific).Segments
, which is backend-agnostichv.Store.register({Segments: SegmentPlot}, 'bokeh')
import param
import holoviews as hv
from holoviews.plotting.bokeh.chart import line_properties, fill_properties
from holoviews.plotting.bokeh.element import ColorbarPlot
from holoviews.element.geom import Geometry
from holoviews.core.util import max_range
class SegmentPlot(ColorbarPlot):
"""
Segments are lines in 2D space where each two each dimensions specify a
(x, y) node of the line.
"""
style_opts = line_properties + ['cmap']
_nonvectorized_styles = ['cmap']
_plot_methods = dict(single='segment')
def get_data(self, element, ranges, style):
# Get [x0, y0, x1, y1]
x0idx, y0idx, x1idx, y1idx = (
(1, 0, 3, 2) if self.invert_axes else (0, 1, 2, 3)
)
# Compute segments
x0s, y0s, x1s, y1s = (
element.dimension_values(x0idx),
element.dimension_values(y0idx),
element.dimension_values(x1idx),
element.dimension_values(y1idx)
)
data = {'x0': x0s, 'x1': x1s, 'y0': y0s, 'y1': y1s}
mapping = dict(x0='x0', x1='x1', y0='y0', y1='y1')
return (data, mapping, style)
def get_extents(self, element, ranges, range_type='combined'):
"""
Use first two key dimensions to set names, and all four
to set the data range.
"""
kdims = element.kdims
# loop over start and end points of segments
# simultaneously in each dimension
for kdim0, kdim1 in zip([kdims[i].name for i in range(2)],
[kdims[i].name for i in range(2,4)]):
new_range = {}
for kdim in [kdim0, kdim1]:
# for good measure, update ranges for both start and end kdim
for r in ranges[kdim]:
# combine (x0, x1) and (y0, y1) in range calculation
new_range[r] = max_range([ranges[kd][r]
for kd in [kdim0, kdim1]])
ranges[kdim0] = new_range
ranges[kdim1] = new_range
return super(SegmentPlot, self).get_extents(element, ranges, range_type)
class Segments(Geometry):
"""
Segments represent a collection of lines in 2D space.
"""
group = param.String(default='Segments', constant=True)
kdims = param.List(default=[Dimension('x0'), Dimension('y0'),
Dimension('x1'), Dimension('y1')],
bounds=(4, 4), constant=True, doc="""
Segments represent lines given by x- and y-
coordinates in 2D space.""")
hv.Store.register({Segments: SegmentPlot}, 'bokeh')
hv.Store.set_current_backend('bokeh')