import matplotlib as mpl
import numpy as np
import pandas as pd
import param
from matplotlib import patches
from matplotlib.lines import Line2D
from ...core.options import abbreviated_exception
from ...core.util import match_spec
from ...element import HLines, HSpans, VLines, VSpans
from .element import ColorbarPlot, ElementPlot
from .plot import mpl_rc_context
[docs]class ABLine2D(Line2D):
"""
Draw a line based on its slope and y-intercept. Additional arguments are
passed to the <matplotlib.lines.Line2D> constructor.
"""
def __init__(self, slope, intercept, *args, **kwargs):
ax = kwargs['axes']
# init the line, add it to the axes
super().__init__([], [], *args, **kwargs)
self._slope = slope
self._intercept = intercept
ax.add_line(self)
# cache the renderer, draw the line for the first time
ax.figure.canvas.draw()
self._update_lim(None)
# connect to axis callbacks
self.axes.callbacks.connect('xlim_changed', self._update_lim)
self.axes.callbacks.connect('ylim_changed', self._update_lim)
def _update_lim(self, event):
""" called whenever axis x/y limits change """
x = np.array(self.axes.get_xbound())
y = (self._slope * x) + self._intercept
self.set_data(x, y)
self.axes.draw_artist(self)
[docs]class AnnotationPlot(ElementPlot):
"""
AnnotationPlot handles the display of all annotation elements.
"""
show_legend = param.Boolean(default=False, doc="""
Whether to show legend for the plot.""")
def __init__(self, annotation, **params):
self._annotation = annotation
super().__init__(annotation, **params)
self.handles['annotations'] = []
@mpl_rc_context
def initialize_plot(self, ranges=None):
annotation = self.hmap.last
key = self.keys[-1]
ranges = self.compute_ranges(self.hmap, key, ranges)
ranges = match_spec(annotation, ranges)
axis = self.handles['axis']
opts = self.style[self.cyclic_index]
with abbreviated_exception():
handles = self.draw_annotation(axis, annotation.data, opts)
self.handles['annotations'] = handles
return self._finalize_axis(key, element=annotation, ranges=ranges)
[docs] def update_handles(self, key, axis, annotation, ranges, style):
# Clear all existing annotations
for element in self.handles['annotations']:
element.remove()
with abbreviated_exception():
self.handles['annotations'] = self.draw_annotation(axis, annotation.data, style)
[docs]class VLinePlot(AnnotationPlot):
"Draw a vertical line on the axis"
style_opts = ['alpha', 'color', 'linewidth', 'linestyle', 'visible']
def draw_annotation(self, axis, position, opts):
if self.invert_axes:
return [axis.axhline(position, **opts)]
else:
return [axis.axvline(position, **opts)]
[docs]class HLinePlot(AnnotationPlot):
"Draw a horizontal line on the axis"
style_opts = ['alpha', 'color', 'linewidth', 'linestyle', 'visible']
[docs] def draw_annotation(self, axis, position, opts):
"Draw a horizontal line on the axis"
if self.invert_axes:
return [axis.axvline(position, **opts)]
else:
return [axis.axhline(position, **opts)]
[docs]class VSpanPlot(AnnotationPlot):
"Draw a vertical span on the axis"
style_opts = ['alpha', 'color', 'facecolor', 'edgecolor',
'linewidth', 'linestyle', 'visible']
[docs] def draw_annotation(self, axis, positions, opts):
"Draw a vertical span on the axis"
if self.invert_axes:
return [axis.axhspan(*positions, **opts)]
else:
return [axis.axvspan(*positions, **opts)]
[docs]class HSpanPlot(AnnotationPlot):
"Draw a horizontal span on the axis"
style_opts = ['alpha', 'color', 'facecolor', 'edgecolor',
'linewidth', 'linestyle', 'visible']
[docs] def draw_annotation(self, axis, positions, opts):
"Draw a horizontal span on the axis"
if self.invert_axes:
return [axis.axvspan(*positions, **opts)]
else:
return [axis.axhspan(*positions, **opts)]
[docs]class SlopePlot(AnnotationPlot):
style_opts = ['alpha', 'color', 'linewidth', 'linestyle', 'visible']
[docs] def draw_annotation(self, axis, position, opts):
"Draw a horizontal line on the axis"
gradient, intercept = position
if self.invert_axes:
if gradient == 0:
gradient = np.inf, np.inf
else:
gradient, intercept = 1/gradient, -(intercept/gradient)
artist = ABLine2D(*position, axes=axis, **opts)
return [artist]
[docs]class TextPlot(AnnotationPlot):
"Draw the Text annotation object"
style_opts = ['alpha', 'color', 'family', 'weight', 'visible']
def draw_annotation(self, axis, data, opts):
(x,y, text, fontsize,
horizontalalignment, verticalalignment, rotation) = data
if self.invert_axes: x, y = y, x
opts['fontsize'] = fontsize
return [axis.text(x,y, text,
horizontalalignment = horizontalalignment,
verticalalignment = verticalalignment,
rotation=rotation, **opts)]
[docs]class LabelsPlot(ColorbarPlot):
color_index = param.ClassSelector(default=None, class_=(str, int),
allow_None=True, doc="""
Index of the dimension from which the color will the drawn""")
xoffset = param.Number(default=None, doc="""
Amount of offset to apply to labels along x-axis.""")
yoffset = param.Number(default=None, doc="""
Amount of offset to apply to labels along x-axis.""")
style_opts = ['alpha', 'color', 'family', 'weight', 'size', 'visible',
'horizontalalignment', 'verticalalignment', 'cmap', 'rotation']
_nonvectorized_styles = ['cmap']
_plot_methods = dict(single='annotate')
def get_data(self, element, ranges, style):
with abbreviated_exception():
style = self._apply_transforms(element, ranges, style)
xs, ys = (element.dimension_values(i) for i in range(2))
tdim = element.get_dimension(2)
text = [tdim.pprint_value(v) for v in element.dimension_values(tdim)]
positions = (ys, xs) if self.invert_axes else (xs, ys)
if self.xoffset is not None:
xs += self.xoffset
if self.yoffset is not None:
ys += self.yoffset
cs = None
cdim = element.get_dimension(self.color_index)
if cdim:
self._norm_kwargs(element, ranges, style, cdim)
cs = element.dimension_values(cdim)
if 'c' in style:
cs = style.pop('c')
if 'size' in style: style['fontsize'] = style.pop('size')
if 'horizontalalignment' not in style: style['horizontalalignment'] = 'center'
if 'verticalalignment' not in style: style['verticalalignment'] = 'center'
return positions + (text, cs), style, {}
[docs] def init_artists(self, ax, plot_args, plot_kwargs):
if plot_args[-1] is not None:
cmap = plot_kwargs.pop('cmap', None)
colors = list(np.unique(plot_args[-1]))
vmin, vmax = plot_kwargs.pop('vmin'), plot_kwargs.pop('vmax')
else:
cmap = None
plot_args = plot_args[:-1]
vectorized = {k: v for k, v in plot_kwargs.items() if isinstance(v, np.ndarray)}
texts = []
for i, item in enumerate(zip(*plot_args)):
x, y, text = item[:3]
if len(item) == 4 and cmap is not None:
color = item[3]
if plot_args[-1].dtype.kind in 'if':
color = (color - vmin) / (vmax-vmin)
plot_kwargs['color'] = cmap(color)
else:
color = colors.index(color) if color in colors else np.nan
plot_kwargs['color'] = cmap(color)
kwargs = dict(plot_kwargs, **{k: v[i] for k, v in vectorized.items()})
texts.append(ax.text(x, y, text, **kwargs))
return {'artist': texts}
[docs] def teardown_handles(self):
if 'artist' in self.handles:
for artist in self.handles['artist']:
artist.remove()
[docs]class ArrowPlot(AnnotationPlot):
"Draw an arrow using the information supplied to the Arrow annotation"
_arrow_style_opts = ['alpha', 'color', 'lw', 'linewidth', 'visible']
_text_style_opts = TextPlot.style_opts + ['textsize', 'fontsize']
style_opts = sorted(set(_arrow_style_opts + _text_style_opts))
def draw_annotation(self, axis, data, opts):
x, y, text, direction, points, arrowstyle = data
if self.invert_axes: x, y = y, x
direction = direction.lower()
arrowprops = dict({'arrowstyle':arrowstyle},
**{k: opts[k] for k in self._arrow_style_opts if k in opts})
textopts = {k: opts[k] for k in self._text_style_opts if k in opts}
if direction in ['v', '^']:
xytext = (0, points if direction=='v' else -points)
elif direction in ['>', '<']:
xytext = (points if direction=='<' else -points, 0)
if 'fontsize' in textopts:
self.param.warning('Arrow fontsize style option is deprecated, '
'use textsize option instead.')
if 'textsize' in textopts:
textopts['fontsize'] = textopts.pop('textsize')
return [axis.annotate(text, xy=(x, y), textcoords='offset points',
xytext=xytext, ha="center", va="center",
arrowprops=arrowprops, **textopts)]
[docs]class SplinePlot(AnnotationPlot):
"Draw the supplied Spline annotation (see Spline docstring)"
style_opts = ['alpha', 'edgecolor', 'linewidth', 'linestyle', 'visible']
def draw_annotation(self, axis, data, opts):
verts, codes = data
if not len(verts):
return []
patch = patches.PathPatch(mpl.path.Path(verts, codes),
facecolor='none', **opts)
axis.add_patch(patch)
return [patch]
class _SyntheticAnnotationPlot(AnnotationPlot):
apply_ranges = param.Boolean(default=True, doc="""
Whether to include the annotation in axis range calculations.""")
style_opts = ['alpha', 'color', 'facecolor', 'edgecolor',
'linewidth', 'linestyle', 'visible']
def draw_annotation(self, axis, positions, opts):
if isinstance(positions, np.ndarray):
values = positions
else:
size = len(self.hmap.last.kdims)
first_keys = list(positions)[:size]
values = zip(*[positions[n] for n in first_keys])
fn = getattr(axis, self._methods[self.invert_axes])
return [fn(*val, **opts) for val in values]
def get_extents(self, element, ranges=None, range_type='combined', **kwargs):
extents = super().get_extents(element, ranges, range_type)
if isinstance(element, HLines):
extents = np.nan, extents[0], np.nan, extents[2]
elif isinstance(element, VLines):
extents = extents[0], np.nan, extents[2], np.nan
elif isinstance(element, HSpans):
extents = pd.array(extents)
extents = np.nan, extents[:2].min(), np.nan, extents[2:].max()
elif isinstance(element, VSpans):
extents = pd.array(extents)
extents = extents[:2].min(), np.nan, extents[2:].max(), np.nan
return extents
def initialize_plot(self, ranges=None):
figure = super().initialize_plot(ranges=ranges)
labels = "yx" if self.invert_axes else "xy"
if isinstance(figure, mpl.axes.Axes):
figure.set_xlabel(labels[0])
figure.set_ylabel(labels[1])
else:
figure.axes[0].set_xlabel(labels[0])
figure.axes[0].set_ylabel(labels[1])
return figure
[docs]class HLinesAnnotationPlot(_SyntheticAnnotationPlot):
_methods = ("axhline", "axvline")
[docs]class VLinesAnnotationPlot(_SyntheticAnnotationPlot):
_methods = ("axvline", "axhline")
[docs]class HSpansAnnotationPlot(_SyntheticAnnotationPlot):
_methods = ("axhspan", "axvspan")
[docs]class VSpansAnnotationPlot(_SyntheticAnnotationPlot):
_methods = ("axvspan", "axhspan")