from __future__ import unicode_literals
from itertools import product
import numpy as np
from matplotlib import cm
from matplotlib import pyplot as plt
import param
from ..core.options import Store
from ..core import OrderedDict, NdMapping, ViewableElement, CompositeOverlay, HoloMap
from ..core.util import match_spec
from ..element import Scatter, Curve, Histogram, Bars, Points, Raster, VectorField, ErrorBars, Polygons
from .element import ElementPlot
from .plot import Plot
class ChartPlot(ElementPlot):
def __init__(self, data, **params):
super(ChartPlot, self).__init__(data, **params)
key_dim = self.map.last.get_dimension(0)
self.cyclic_range = key_dim.range if key_dim.cyclic else None
def _cyclic_format_x_tick_label(self, x):
if self.relative_labels:
return str(x)
return str(int(np.round(180*x/self.cyclic_range[1])))
def _rotate(self, seq, n=1):
n = n % len(seq) # n=hop interval
return seq[n:] + seq[:n]
def _cyclic_reduce_ticks(self, x_values):
values = []
labels = []
step = self.cyclic_range[1] / (self.xticks - 1)
if self.relative_labels:
labels.append(-90)
label_step = 180 / (self.xticks - 1)
else:
labels.append(x_values[0])
label_step = step
values.append(x_values[0])
for i in range(0, self.xticks - 1):
labels.append(labels[-1] + label_step)
values.append(values[-1] + step)
return values, [self._cyclic_format_x_tick_label(x) for x in labels]
def _cyclic_curves(self, curveview):
"""
Mutate the lines object to generate a rotated cyclic curves.
"""
x_values = list(curveview.data[:, 0])
y_values = list(curveview.data[:, 1])
if self.center_cyclic:
rotate_n = self.peak_argmax+len(x_values)/2
y_values = self._rotate(y_values, n=rotate_n)
ticks = self._rotate(x_values, n=rotate_n)
else:
ticks = list(x_values)
ticks.append(ticks[0])
x_values.append(x_values[0]+self.cyclic_range[1])
y_values.append(y_values[0])
self.xvalues = x_values
return np.vstack([x_values, y_values]).T
[docs]class CurvePlot(ChartPlot):
"""
CurvePlot can plot Curve and ViewMaps of Curve, which can be
displayed as a single frame or animation. Axes, titles and legends
are automatically generated from dim_info.
If the dimension is set to cyclic in the dim_info it will rotate
the curve so that minimum y values are at the minimum x value to
make the plots easier to interpret.
"""
autotick = param.Boolean(default=False, doc="""
Whether to let matplotlib automatically compute tick marks
or to allow the user to control tick marks.""")
center_cyclic = param.Boolean(default=True, doc="""
If enabled and plotted quantity is cyclic will center the
plot around the peak.""")
num_ticks = param.Integer(default=5, doc="""
If autotick is disabled, this number of tickmarks will be drawn.""")
relative_labels = param.Boolean(default=False, doc="""
If plotted quantity is cyclic and center_cyclic is enabled,
will compute tick labels relative to the center.""")
show_frame = param.Boolean(default=False, doc="""
Disabled by default for clarity.""")
show_grid = param.Boolean(default=True, doc="""
Enable axis grid.""")
show_legend = param.Boolean(default=True, doc="""
Whether to show legend for the plot.""")
style_opts = ['alpha', 'color', 'visible', 'linewidth', 'linestyle', 'marker']
def __call__(self, ranges=None):
element = self.map.last
axis = self.handles['axis']
key = self.keys[-1]
ranges = self.compute_ranges(self.map, key, ranges)
ranges = match_spec(element, ranges)
# Create xticks and reorder data if cyclic
xticks = None
data = element.data
if self.cyclic_range is not None:
if self.center_cyclic:
self.peak_argmax = np.argmax(element.data[:, 1])
data = self._cyclic_curves(element)
xticks = self._cyclic_reduce_ticks(self.xvalues)
# Create line segments and apply style
style = self.style[self.cyclic_index]
line_segment = axis.plot(data[:, 0], data[:, 1],
zorder=self.zorder, **style)[0]
self.handles['line_segment'] = line_segment
self.handles['legend_handle'] = line_segment
return self._finalize_axis(self.keys[-1], ranges=ranges, xticks=xticks)
def update_handles(self, axis, view, key, ranges=None):
data = view.data
if self.cyclic_range is not None:
data = self._cyclic_curves(view)
self.handles['line_segment'].set_xdata(data[:, 0])
self.handles['line_segment'].set_ydata(data[:, 1])
[docs]class ErrorPlot(ChartPlot):
"""
ErrorPlot plots the ErrorBar Element type and supporting
both horizontal and vertical error bars via the 'horizontal'
plot option.
"""
horizontal = param.Boolean(default=False, doc="""
Whether to draw horizontal or vertical error bars.""")
style_opts = ['ecolor', 'elinewidth', 'capsize', 'capthick',
'barsabove', 'lolims', 'uplims', 'xlolims',
'errorevery', 'xuplims', 'alpha', 'linestyle',
'linewidth', 'markeredgecolor', 'markeredgewidth',
'markerfacecolor', 'markersize', 'solid_capstyle',
'solid_joinstyle', 'dashes', 'color']
def __call__(self, ranges=None):
element = self.map.last
axis = self.handles['axis']
key = self.keys[-1]
ranges = self.compute_ranges(self.map, key, ranges)
ranges = match_spec(element, ranges)
error_kwargs = dict(self.style[self.cyclic_index], fmt='none',
zorder=self.zorder)
kwarg = 'xerr' if self.horizontal else 'yerr'
error_kwargs[kwarg] = element.data[:, 2:4].T
_, (bottoms, tops), verts = axis.errorbar(element.data[:, 0],
element.data[:, 1],
**error_kwargs)
self.handles['bottoms'] = bottoms
self.handles['tops'] = tops
self.handles['verts'] = verts[0]
return self._finalize_axis(self.keys[-1], ranges=ranges)
def update_handles(self, axis, view, key, ranges=None):
data = view.data
bottoms = self.handles['bottoms']
tops = self.handles['tops']
verts = self.handles['verts']
paths = verts.get_paths()
if self.horizontal:
bdata = data[:, 0] - data[:, 2]
tdata = data[:, 0] + data[:, 3]
tops.set_xdata(bdata)
tops.set_ydata(data[:, 1])
bottoms.set_xdata(tdata)
bottoms.set_ydata(data[:, 1])
for i, path in enumerate(paths):
path.vertices = np.array([[bdata[i], data[i, 1]],
[tdata[i], data[i, 1]]])
else:
bdata = data[:, 1] - data[:, 2]
tdata = data[:, 1] + data[:, 3]
bottoms.set_xdata(data[:, 0])
bottoms.set_ydata(bdata)
tops.set_xdata(data[:, 0])
tops.set_ydata(tdata)
for i, path in enumerate(paths):
path.vertices = np.array([[data[i, 0], bdata[i]],
[data[i, 0], tdata[i]]])
[docs]class HistogramPlot(ChartPlot):
"""
HistogramPlot can plot DataHistograms and ViewMaps of
DataHistograms, which can be displayed as a single frame or
animation.
"""
show_frame = param.Boolean(default=False, doc="""
Disabled by default for clarity.""")
show_grid = param.Boolean(default=False, doc="""
Whether to overlay a grid on the axis.""")
style_opts = ['alpha', 'color', 'align', 'visible', 'facecolor',
'edgecolor', 'log', 'capsize', 'error_kw', 'hatch']
def __init__(self, histograms, **params):
self.center = False
self.cyclic = False
super(HistogramPlot, self).__init__(histograms, **params)
if self.orientation == 'vertical':
self.axis_settings = ['ylabel', 'xlabel', 'yticks']
else:
self.axis_settings = ['xlabel', 'ylabel', 'xticks']
val_dim = self.map.last.get_dimension(1)
self.cyclic_range = val_dim.range if val_dim.cyclic else None
def __call__(self, ranges=None):
hist = self.map.last
key = self.keys[-1]
ranges = self.compute_ranges(self.map, key, ranges)
el_ranges = match_spec(hist, ranges)
# Get plot ranges and values
edges, hvals, widths, lims = self._process_hist(hist)
if self.orientation == 'vertical':
self.offset_linefn = self.handles['axis'].axvline
self.plotfn = self.handles['axis'].barh
else:
self.offset_linefn = self.handles['axis'].axhline
self.plotfn = self.handles['axis'].bar
# Plot bars and make any adjustments
style = self.style[self.cyclic_index]
bars = self.plotfn(edges, hvals, widths, zorder=self.zorder, **style)
self.handles['bars'] = self._update_plot(self.keys[-1], hist, bars, lims, ranges) # Indexing top
self.handles['legend_handle'] = bars
ticks = self._compute_ticks(hist, edges, widths, lims)
ax_settings = self._process_axsettings(hist, lims, ticks)
return self._finalize_axis(self.keys[-1], ranges=el_ranges, **ax_settings)
def _process_hist(self, hist):
"""
Get data from histogram, including bin_ranges and values.
"""
self.cyclic = hist.get_dimension(0).cyclic
edges = hist.edges[:-1]
hist_vals = np.array(hist.values)
widths = [hist._width] * len(hist) if getattr(hist, '_width', None) else np.diff(hist.edges)
lims = hist.range(0) + hist.range(1)
return edges, hist_vals, widths, lims
def _compute_ticks(self, view, edges, widths, lims):
"""
Compute the ticks either as cyclic values in degrees or as roughly
evenly spaced bin centers.
"""
if self.cyclic:
x0, x1, _, _ = lims
xvals = np.linspace(x0, x1, self.xticks)
labels = ["%.0f" % np.rad2deg(x) + '\N{DEGREE SIGN}' for x in xvals]
else:
dim = view.get_dimension(0)
inds = np.linspace(0, len(edges)-1, self.xticks, dtype=np.int)
xvals = [edges[i] for i in inds]
labels = [dim.pprint_value(v) for v in xvals]
return [xvals, labels]
def get_extents(self, view, ranges):
x0, y0, x1, y1 = super(HistogramPlot, self).get_extents(view, ranges)
return (0, x0, y1, x1) if self.orientation == 'vertical' else (x0, 0, x1, y1)
def _process_axsettings(self, hist, lims, ticks):
"""
Get axis settings options including ticks, x- and y-labels
and limits.
"""
axis_settings = dict(zip(self.axis_settings, [None, None, (None if self.overlaid else ticks)]))
return axis_settings
def _update_plot(self, key, hist, bars, lims, ranges):
"""
Process bars can be subclassed to manually adjust bars
after being plotted.
"""
return bars
def _update_artists(self, key, hist, edges, hvals, widths, lims, ranges):
"""
Update all the artists in the histogram. Subclassable to
allow updating of further artists.
"""
plot_vals = zip(self.handles['bars'], edges, hvals, widths)
for bar, edge, height, width in plot_vals:
if self.orientation == 'vertical':
bar.set_y(edge)
bar.set_width(height)
bar.set_height(width)
else:
bar.set_x(edge)
bar.set_height(height)
bar.set_width(width)
[docs] def update_handles(self, axis, view, key, ranges=None):
"""
Update the plot for an animation.
:param axis:
"""
# Process values, axes and style
edges, hvals, widths, lims = self._process_hist(view)
ticks = self._compute_ticks(view, edges, widths, lims)
ax_settings = self._process_axsettings(view, lims, ticks)
self._update_artists(key, view, edges, hvals, widths, lims, ranges)
return ax_settings
class SideHistogramPlot(HistogramPlot):
aspect = param.Parameter(default='auto', doc="""
Aspect ratios on SideHistogramPlot should be determined by the
AdjointLayoutPlot.""")
offset = param.Number(default=0.2, bounds=(0,1), doc="""
Histogram value offset for a colorbar.""")
show_grid = param.Boolean(default=True, doc="""
Whether to overlay a grid on the axis.""")
show_title = param.Boolean(default=False, doc="""
Titles should be disabled on all SidePlots to avoid clutter.""")
show_xlabel = param.Boolean(default=False, doc="""
Whether to show the x-label of the plot. Disabled by default
because plots are often too cramped to fit the title correctly.""")
def _process_hist(self, hist):
"""
Subclassed to offset histogram by defined amount.
"""
edges, hvals, widths, lims = super(SideHistogramPlot, self)._process_hist(hist)
offset = self.offset * lims[3]
hvals *= 1-self.offset
hvals += offset
lims = lims[0:3] + (lims[3] + offset,)
return edges, hvals, widths, lims
def _process_axsettings(self, hist, lims, ticks):
axsettings = super(SideHistogramPlot, self)._process_axsettings(hist, lims, ticks)
if not self.show_xlabel:
axsettings['ylabel' if self.orientation == 'vertical' else 'xlabel'] = ''
return axsettings
def _update_artists(self, n, view, edges, hvals, widths, lims, ranges):
super(SideHistogramPlot, self)._update_artists(n, view, edges, hvals, widths, lims, ranges)
self._update_plot(n, view, self.handles['bars'], lims, ranges)
def _update_plot(self, key, view, bars, lims, ranges):
"""
Process the bars and draw the offset line as necessary. If a
color map is set in the style of the 'main' ViewableElement object, color
the bars appropriately, respecting the required normalization
settings.
"""
hist = view
main = self.adjoined.main
y0, y1 = hist.range(1)
offset = self.offset * y1
hist_dim = hist.get_dimension(0).name
range_item = main
if isinstance(main, HoloMap):
if issubclass(main.type, CompositeOverlay):
range_item = [hm for hm in main.split_overlays()[1]
if hist_dim in hm.dimensions('all', label=True)][0]
else:
range_item = HoloMap({0: main}, kdims=['Frame'])
ranges = match_spec(range_item.last, ranges)
if hist_dim in ranges:
main_range = ranges[hist_dim]
else:
framewise = Store.lookup_options(range_item.last, 'norm').options.get('framewise')
if framewise and range_item.get(key, False):
main_range = range_item.get(key, False).range(hist_dim)
else:
main_range = range_item.range(hist_dim)
if offset and ('offset_line' not in self.handles):
self.handles['offset_line'] = self.offset_linefn(offset,
linewidth=1.0,
color='k')
elif offset:
self._update_separator(offset)
# If .main is an NdOverlay or a HoloMap of Overlays get the correct style
if isinstance(range_item, HoloMap):
range_item = range_item.last
if isinstance(range_item, CompositeOverlay):
range_item = [ov for ov in range_item
if hist_dim in ov.dimensions('value', label=True)][0]
if isinstance(range_item, (Raster, Points, Polygons)):
style = Store.lookup_options(range_item, 'style')[self.cyclic_index]
cmap = cm.get_cmap(style.get('cmap'))
main_range = style.get('clims', main_range)
else:
cmap = None
if cmap is not None:
self._colorize_bars(cmap, bars, main_range)
return bars
def get_extents(self, element, ranges):
x0, _, x1, _ = element.extents
_, y1 = element.range(1)
return (0, x0, y1, x1) if self.orientation == 'vertical' else (x0, 0, x1, y1)
def _colorize_bars(self, cmap, bars, main_range):
"""
Use the given cmap to color the bars, applying the correct
color ranges as necessary.
"""
vertical = (self.orientation == 'vertical')
cmap_range = main_range[1] - main_range[0]
lower_bound = main_range[0]
for bar in bars:
bar_bin = bar.get_y() if vertical else bar.get_x()
width = bar.get_height() if vertical else bar.get_width()
try:
color_val = (bar_bin+width/2.-lower_bound)/cmap_range
except:
color_val = 0
bar.set_facecolor(cmap(color_val))
bar.set_clip_on(False)
def _update_separator(self, offset):
"""
Compute colorbar offset and update separator line
if map is non-zero.
"""
offset_line = self.handles['offset_line']
if offset == 0:
offset_line.set_visible(False)
else:
offset_line.set_visible(True)
if self.orientation == 'vertical':
offset_line.set_xdata(offset)
else:
offset_line.set_ydata(offset)
[docs]class PointPlot(ChartPlot):
"""
Note that the 'cmap', 'vmin' and 'vmax' style arguments control
how point magnitudes are rendered to different colors.
"""
colorbar = param.Boolean(default=False, doc="""
Whether to add a colorbar to the plot.""")
color_index = param.Integer(default=3, doc="""
Index of the dimension from which the color will the drawn""")
size_index = param.Integer(default=2, doc="""
Index of the dimension from which the sizes will the drawn.""")
scaling_factor = param.Number(default=1, bounds=(1, None), doc="""
If values are supplied the area of the points is computed relative
to the marker size. It is then multiplied by scaling_factor to the power
of the ratio between the smallest point and all other points.
For values of 1 scaling by the values is disabled, a factor of 2
allows for linear scaling of the area and a factor of 4 linear
scaling of the point width.""")
show_grid = param.Boolean(default=True, doc="""
Whether to draw grid lines at the tick positions.""")
style_opts = ['alpha', 'color', 'edgecolors', 'facecolors',
'linewidth', 'marker', 'size', 'visible',
'cmap', 'vmin', 'vmax']
def __call__(self, ranges=None):
points = self.map.last
axis = self.handles['axis']
ranges = self.compute_ranges(self.map, self.keys[-1], ranges)
ranges = match_spec(points, ranges)
ndims = points.data.shape[1]
xs = points.data[:, 0] if len(points.data) else []
ys = points.data[:, 1] if len(points.data) else []
sz = points.data[:, self.size_index] if self.size_index < ndims else None
cs = points.data[:, self.color_index] if self.color_index < ndims else None
style = self.style[self.cyclic_index]
if sz is not None and self.scaling_factor > 1:
style['s'] = self._compute_size(sz, style)
color = style.pop('color', None)
if cs is not None:
style['c'] = cs
else:
style['c'] = color
edgecolor = style.pop('edgecolors', 'none')
scatterplot = axis.scatter(xs, ys, zorder=self.zorder, edgecolors=edgecolor, **style)
self.handles['paths'] = scatterplot
self.handles['legend_handle'] = scatterplot
if cs is not None:
val_dim = points.dimensions(label=True)[self.color_index]
clims = ranges.get(val_dim)
scatterplot.set_clim(clims)
if self.colorbar:
self._draw_colorbar(scatterplot)
return self._finalize_axis(self.keys[-1], ranges=ranges)
def _compute_size(self, sizes, opts):
ms = opts.pop('s') if 's' in opts else plt.rcParams['lines.markersize']
sizes = np.ma.array(sizes, mask=sizes<=0)
return (ms*self.scaling_factor**sizes)
def update_handles(self, axis, element, key, ranges=None):
paths = self.handles['paths']
paths.set_offsets(element.data[:, 0:2])
ndims = element.data.shape[1]
if ndims > 2:
sz = element.data[:, self.size_index] if self.size_index < ndims else None
cs = element.data[:, self.color_index] if self.color_index < ndims else None
opts = self.style[0]
if sz is not None and self.scaling_factor > 1:
paths.set_sizes(self._compute_size(sz, opts))
if cs is not None:
val_dim = element.dimensions(label=True)[self.color_index]
ranges = self.compute_ranges(self.map, key, ranges)
ranges = match_spec(element, ranges)
paths.set_clim(ranges[val_dim])
paths.set_array(cs)
if self.colorbar:
self._draw_colorbar(paths)
[docs]class VectorFieldPlot(ElementPlot):
"""
Renders vector fields in sheet coordinates. The vectors are
expressed in polar coordinates and may be displayed according to
angle alone (with some common, arbitrary arrow length) or may be
true polar vectors.
Optionally, the arrows may be colored but this dimension is
redundant with either the specified angle or magnitudes. This
choice is made by setting the color_dim parameter.
Note that the 'cmap' style argument controls the color map used to
color the arrows. The length of the arrows is controlled by the
'scale' style option where a value of 1.0 is such that the largest
arrow shown is no bigger than the smallest sampling distance.
"""
color_dim = param.ObjectSelector(default=None,
objects=['angle', 'magnitude', None], doc="""
Which of the polar vector components is mapped to the color
dimension (if any)""")
arrow_heads = param.Boolean(default=True, doc="""
Whether or not to draw arrow heads. If arrowheads are enabled,
they may be customized with the 'headlength' and
'headaxislength' style options.""")
normalize_lengths = param.Boolean(default=True, doc="""
Whether to normalize vector magnitudes automatically. If False,
it will be assumed that the lengths have already been correctly
normalized.""")
style_opts = ['alpha', 'color', 'edgecolors', 'facecolors',
'linewidth', 'marker', 'visible', 'cmap',
'scale', 'headlength', 'headaxislength', 'pivot']
def __init__(self, *args, **params):
super(VectorFieldPlot, self).__init__(*args, **params)
self._min_dist = self._get_map_info(self.map)
def _get_map_info(self, vmap):
"""
Get the minimum sample distance and maximum magnitude
"""
dists = []
for vfield in vmap:
dists.append(self._get_min_dist(vfield))
return min(dists)
def _get_info(self, vfield, input_scale, ranges):
xs = vfield.data[:, 0] if len(vfield.data) else []
ys = vfield.data[:, 1] if len(vfield.data) else []
radians = vfield.data[:, 2] if len(vfield.data) else []
magnitudes = vfield.data[:, 3] if vfield.data.shape[1]>=4 else np.array([1.0] * len(xs))
colors = magnitudes if self.color_dim == 'magnitude' else radians
if vfield.data.shape[1] >= 4:
magnitude_dim = vfield.get_dimension(3).name
_, max_magnitude = ranges[magnitude_dim]
else:
max_magnitude = 1.0
min_dist = self._min_dist if self._min_dist else self._get_min_dist(vfield)
if self.normalize_lengths and max_magnitude != 0:
magnitudes = magnitudes / max_magnitude
return (xs, ys, list((radians / np.pi) * 180),
magnitudes, colors, input_scale / min_dist)
def _get_min_dist(self, vfield):
"Get the minimum sampling distance."
xys = np.array([complex(x,y) for x,y in zip(vfield.data[:,0],
vfield.data[:,1])])
m, n = np.meshgrid(xys, xys)
distances = abs(m-n)
np.fill_diagonal(distances, np.inf)
return distances.min()
def __call__(self, ranges=None):
vfield = self.map.last
axis = self.handles['axis']
colorized = self.color_dim is not None
kwargs = self.style[self.cyclic_index]
input_scale = kwargs.pop('scale', 1.0)
ranges = self.compute_ranges(self.map, self.keys[-1], ranges)
ranges = match_spec(vfield, ranges)
xs, ys, angles, lens, colors, scale = self._get_info(vfield, input_scale, ranges)
args = (xs, ys, lens, [0.0] * len(vfield.data))
args = args + (colors,) if colorized else args
if not self.arrow_heads:
kwargs['headaxislength'] = 0
if 'pivot' not in kwargs: kwargs['pivot'] = 'mid'
quiver = axis.quiver(*args, zorder=self.zorder, units='x',
scale_units='x', scale = scale, angles = angles ,
**({k:v for k,v in kwargs.items() if k!='color'}
if colorized else kwargs))
if self.color_dim == 'angle':
clims = vfield.get_dimension(2).range
quiver.set_clim(clims)
elif self.color_dim == 'magnitude':
magnitude_dim = vfield.get_dimension(3).name
quiver.set_clim(ranges[magnitude_dim])
self.handles['axis'].add_collection(quiver)
self.handles['quiver'] = quiver
self.handles['legend_handle'] = quiver
self.handles['input_scale'] = input_scale
return self._finalize_axis(self.keys[-1], ranges=ranges)
def update_handles(self, axis, view, key, ranges=None):
self.handles['quiver'].set_offsets(view.data[:,0:2])
input_scale = self.handles['input_scale']
ranges = self.compute_ranges(self.map, key, ranges)
ranges = match_spec(view, ranges)
xs, ys, angles, lens, colors, scale = self._get_info(view, input_scale, ranges)
# Set magnitudes, angles and colors if supplied.
quiver = self.handles['quiver']
quiver.U = lens
quiver.angles = angles
if self.color_dim is not None:
quiver.set_array(colors)
if self.color_dim == 'magnitude':
magnitude_dim = view.get_dimension(3).name
quiver.set_clim(ranges[magnitude_dim])
class BarPlot(ElementPlot):
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.""")
padding = param.Number(default=0.2, doc="""
Defines the padding between groups.""")
color_by = param.List(default=['category'], doc="""
Defines how the Bar elements colored. Valid options include
any permutation of 'group', 'category' and 'stack'.""")
show_legend = param.Boolean(default=True, doc="""
Whether to show legend for the plot.""")
xticks = param.Integer(0, precedence=-1)
style_opts = ['alpha', 'color', 'align', 'visible', 'edgecolor',
'log', 'facecolor', 'capsize', 'error_kw', 'hatch']
_dimensions = OrderedDict([('group', 0),
('category',1),
('stack',2)])
def __init__(self, element, **params):
super(BarPlot, self).__init__(element, **params)
self.values, self.bar_dimensions = self._get_values()
def _get_values(self):
"""
Get unique index value for each bar
"""
gi, ci, si =self.group_index, self.category_index, self.stack_index
ndims = self.map.last.ndims
dims = self.map.last.kdims
dimensions = []
values = {}
for vidx, vtype in zip([gi, ci, si], self._dimensions):
if vidx < ndims:
dim = dims[vidx]
dimensions.append(dim)
vals = self.map.dimension_values(dim.name)
params = dict(kdims=[dim])
else:
dimensions.append(None)
vals = [None]
params = {}
values[vtype] = NdMapping([(v, None) for v in vals],
**params).keys()
return values, dimensions
def _compute_styles(self, element, style_groups):
"""
Computes color and hatch combinations by
any combination of the 'group', 'category'
and 'stack'.
"""
style = Store.lookup_options(element, 'style')[0]
sopts = []
for sopt in ['color', 'hatch']:
if sopt in style:
sopts.append(sopt)
style.pop(sopt, None)
color_groups = []
for sg in style_groups:
color_groups.append(self.values[sg])
style_product = list(product(*color_groups))
wrapped_style = Store.lookup_options(element, 'style').max_cycles(len(style_product))
color_groups = {k:tuple(wrapped_style[n][sopt] for sopt in sopts)
for n,k in enumerate(style_product)}
return style, color_groups, sopts
def get_extents(self, element, ranges):
return 0, 0, len(self.values['group']), np.NaN
def __call__(self, ranges=None):
element = self.map.last
vdim = element.vdims[0]
axis = self.handles['axis']
key = self.keys[-1]
ranges = self.compute_ranges(self.map, key, ranges)
ranges = match_spec(element, ranges)
dims = element.dimensions('key', label=True)
self.handles['bars'], xticks = self._create_bars(axis, element)
self.handles['legend_handle'] = self.handles['bars']
self._set_ticks(axis, dims, xticks)
return self._finalize_axis(key, ranges=ranges, ylabel=str(vdim))
def _set_ticks(self, axis, dims, xticks):
"""
Apply ticks with appropriate offsets.
"""
ndims = len(dims)
idx = self.group_index if self.group_index < ndims else self.category_index
ticks, labels, yalignments = zip(*sorted(xticks, key=lambda x: x[0]))
axis.set_xlabel(dims[idx], labelpad=-0.15 if idx < ndims else -0.05)
axis.set_xticks(ticks)
axis.set_xticklabels(labels)
for t, y in zip(axis.get_xticklabels(), yalignments):
t.set_y(y)
def _create_bars(self, axis, element):
# Get style and dimension information
values = self.values
gi, ci, si = self.group_index, self.category_index, self.stack_index
gdim, cdim, sdim = [element.kdims[i] if i < element.ndims else None
for i in (gi, ci, si) ]
indices = dict(zip(self._dimensions, (gi, ci, si)))
style_groups = [sg for sg in self.color_by if indices[sg] < element.ndims]
style_opts, color_groups, sopts = self._compute_styles(element, style_groups)
dims = element.dimensions('key', label=True)
ndims = len(dims)
# Compute widths
width = (1-(2.*self.padding)) / len(values['category'])
# Initialize variables
xticks = []
val_key = [None] * ndims
style_key = [None] * len(style_groups)
label_key = [None] * len(style_groups)
labels = []
bars = {}
# Iterate over group, category and stack dimension values
# computing xticks and drawing bars and applying styles
for gidx, grp_name in enumerate(values['group']):
if grp_name is not None:
if 'group' in style_groups:
idx = style_groups.index('group')
label_key[idx] = gdim.pprint_value(grp_name)
style_key[idx] = grp_name
val_key[gi] = grp_name
if ci < ndims:
yalign = -0.125
xticks.append((gidx+0.5, dims[ci], -0.05))
else:
yalign = 0
xticks.append((gidx+0.5, grp_name, yalign))
for cidx, cat_name in enumerate(values['category']):
xpos = gidx+self.padding+(cidx*width)
if cat_name is not None:
if 'category' in style_groups:
idx = style_groups.index('category')
label_key[idx] = gdim.pprint_value(cat_name)
style_key[idx] = cat_name
val_key[ci] = cat_name
xticks.append((xpos+width/2., cat_name, 0))
prev = 0
for sidx, stk_name in enumerate(values['stack']):
if stk_name is not None:
if 'stack' in style_groups:
idx = style_groups.index('stack')
label_key[idx] = gdim.pprint_value(stk_name)
style_key[idx] = stk_name
val_key[si] = stk_name
val = element.get(tuple(val_key), (np.NaN,))
label = ', '.join(label_key)
style = dict(style_opts, label='' if label in labels else label,
**dict(zip(sopts, color_groups[tuple(style_key)])))
bar = axis.bar([xpos], val, width=width , bottom=prev,
**style)
# Update variables
bars[tuple(val_key)] = bar
prev += val if np.isfinite(val) else 0
labels.append(label)
title = [str(element.kdims[indices[cg]])
for cg in self.color_by if indices[cg] < ndims]
if any(len(l) for l in labels):
axis.legend(title=', '.join(title))
return bars, xticks
def update_handles(self, axis, element, key, ranges=None):
dims = element.dimensions('key', label=True)
ndims = len(dims)
ci, gi, si = self.category_index, self.group_index, self.stack_index
val_key = [None] * ndims
for g in self.values['group']:
if g is not None: val_key[gi] = g
for c in self.values['category']:
if c is not None: val_key[ci] = c
prev = 0
for s in self.values['stack']:
if s is not None: val_key[si] = s
bar = self.handles['bars'].get(tuple(val_key))
if bar:
height = element.get(tuple(val_key), np.NaN)
height = height if np.isscalar(height) else height[0]
bar[0].set_height(height)
bar[0].set_y(prev)
prev += height if np.isfinite(height) else 0
Store.registry.update({Curve: CurvePlot,
Scatter: PointPlot,
Bars: BarPlot,
Histogram: HistogramPlot,
Points: PointPlot,
VectorField: VectorFieldPlot,
ErrorBars: ErrorPlot})
Plot.sideplots.update({Histogram: SideHistogramPlot})