/
element.py
1257 lines (1076 loc) · 52.1 KB
/
element.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
import copy
import math
import warnings
from types import FunctionType
import matplotlib.colors as mpl_colors
import numpy as np
import param
from matplotlib import ticker
from matplotlib.dates import date2num
from matplotlib.image import AxesImage
from packaging.version import Version
from ...core import (
CompositeOverlay,
Dataset,
DynamicMap,
Element,
Element3D,
NdOverlay,
util,
)
from ...core.options import Keywords, abbreviated_exception
from ...element import Graph, Path
from ...streams import Stream
from ...util.transform import dim
from ..plot import GenericElementPlot, GenericOverlayPlot
from ..util import color_intervals, dim_range_key, process_cmap
from .plot import MPLPlot, mpl_rc_context
from .util import EqHistNormalize, mpl_version, validate, wrap_formatter
class ElementPlot(GenericElementPlot, MPLPlot):
apply_ticks = param.Boolean(default=True, doc="""
Whether to apply custom ticks.""")
aspect = param.Parameter(default='square', doc="""
The aspect ratio mode of the plot. By default, a plot may
select its own appropriate aspect ratio but sometimes it may
be necessary to force a square aspect ratio (e.g. to display
the plot as an element of a grid). The modes 'auto' and
'equal' correspond to the axis modes of the same name in
matplotlib, a numeric value specifying the ratio between plot
width and height may also be passed. To control the aspect
ratio between the axis scales use the data_aspect option
instead.""")
data_aspect = param.Number(default=None, doc="""
Defines the aspect of the axis scaling, i.e. the ratio of
y-unit to x-unit.""")
invert_zaxis = param.Boolean(default=False, doc="""
Whether to invert the plot z-axis.""")
labelled = param.List(default=['x', 'y'], doc="""
Whether to plot the 'x' and 'y' labels.""")
logz = param.Boolean(default=False, doc="""
Whether to apply log scaling to the y-axis of the Chart.""")
xformatter = param.ClassSelector(
default=None, class_=(str, ticker.Formatter, FunctionType), doc="""
Formatter for ticks along the x-axis.""")
yformatter = param.ClassSelector(
default=None, class_=(str, ticker.Formatter, FunctionType), doc="""
Formatter for ticks along the y-axis.""")
zformatter = param.ClassSelector(
default=None, class_=(str, ticker.Formatter, FunctionType), doc="""
Formatter for ticks along the z-axis.""")
zaxis = param.Boolean(default=True, doc="""
Whether to display the z-axis.""")
zlabel = param.String(default=None, doc="""
An explicit override of the z-axis label, if set takes precedence
over the dimension label.""")
zrotation = param.Integer(default=0, bounds=(0, 360), doc="""
Rotation angle of the zticks.""")
zticks = param.Parameter(default=None, doc="""
Ticks along z-axis specified as an integer, explicit list of
tick locations, list of tuples containing the locations and
labels or a matplotlib tick locator object. If set to None
default matplotlib ticking behavior is applied.""")
# Element Plots should declare the valid style options for matplotlib call
style_opts = []
# No custom legend options
_legend_opts = {}
# Declare which styles cannot be mapped to a non-scalar dimension
_nonvectorized_styles = ['marker', 'alpha', 'cmap', 'angle', 'visible']
# Whether plot has axes, disables setting axis limits, labels and ticks
_has_axes = True
def __init__(self, element, **params):
super().__init__(element, **params)
check = self.hmap.last
if isinstance(check, CompositeOverlay):
check = check.values()[0] # Should check if any are 3D plots
if isinstance(check, Element3D):
self.projection = '3d'
for hook in self.initial_hooks:
try:
hook(self, element)
except Exception as e:
self.param.warning(f"Plotting hook {hook!r} could not be "
f"applied:\n\n {e}")
def _finalize_axis(self, key, element=None, title=None, dimensions=None, ranges=None, xticks=None,
yticks=None, zticks=None, xlabel=None, ylabel=None, zlabel=None):
"""
Applies all the axis settings before the axis or figure is returned.
Only plots with zorder 0 get to apply their settings.
When the number of the frame is supplied as n, this method looks
up and computes the appropriate title, axis labels and axis bounds.
"""
if element is None:
element = self._get_frame(key)
self.current_frame = element
if not dimensions and element and not self.subplots:
el = element.traverse(lambda x: x, [Element])
if el:
el = el[0]
dimensions = el.nodes.dimensions() if isinstance(el, Graph) else el.dimensions()
axis = self.handles['axis']
subplots = list(self.subplots.values()) if self.subplots else []
if self.zorder == 0 and key is not None:
if self.bgcolor:
if mpl_version <= Version('1.5.9'):
axis.set_axis_bgcolor(self.bgcolor)
else:
axis.set_facecolor(self.bgcolor)
# Apply title
title = self._format_title(key)
if self.show_title and title is not None:
fontsize = self._fontsize('title')
if 'title' in self.handles:
self.handles['title'].set_text(title)
else:
self.handles['title'] = axis.set_title(title, **fontsize)
# Apply subplot label
self._subplot_label(axis)
# Apply axis options if axes are enabled
if element is not None and not any(not sp._has_axes for sp in [self] + subplots):
# Set axis labels
if dimensions:
self._set_labels(axis, dimensions, xlabel, ylabel, zlabel)
else:
if self.xlabel is not None:
axis.set_xlabel(self.xlabel)
if self.ylabel is not None:
axis.set_ylabel(self.ylabel)
if self.zlabel is not None and hasattr(axis, 'set_zlabel'):
axis.set_zlabel(self.zlabel)
if not subplots:
legend = axis.get_legend()
if legend:
legend.set_visible(self.show_legend)
self.handles["bbox_extra_artists"] += [legend]
axis.xaxis.grid(self.show_grid)
axis.yaxis.grid(self.show_grid)
# Apply log axes
if self.logx:
axis.set_xscale('log')
if self.logy:
axis.set_yscale('log')
if not (isinstance(self.projection, str) and self.projection == '3d'):
self._set_axis_position(axis, 'x', self.xaxis)
self._set_axis_position(axis, 'y', self.yaxis)
# Apply ticks
if self.apply_ticks:
self._finalize_ticks(axis, dimensions, xticks, yticks, zticks)
# Set axes limits
self._set_axis_limits(axis, element, subplots, ranges)
# Apply aspects
if self.aspect is not None and self.projection != 'polar' and not self.adjoined:
self._set_aspect(axis, self.aspect)
if not subplots and not self.drawn:
self._finalize_artist(element)
self._execute_hooks(element)
return super()._finalize_axis(key)
def _execute_hooks(self, element):
super()._execute_hooks(element)
self._update_backend_opts()
def _finalize_ticks(self, axis, dimensions, xticks, yticks, zticks):
"""
Finalizes the ticks on the axes based on the supplied ticks
and Elements. Sets the axes position as well as tick positions,
labels and fontsize.
"""
ndims = len(dimensions) if dimensions else 0
xdim = dimensions[0] if ndims else None
ydim = dimensions[1] if ndims > 1 else None
# Tick formatting
if xdim:
self._set_axis_formatter(axis.xaxis, xdim, self.xformatter)
if ydim:
self._set_axis_formatter(axis.yaxis, ydim, self.yformatter)
if isinstance(self.projection, str) and self.projection == '3d':
zdim = dimensions[2] if ndims > 2 else None
if zdim or self.zformatter is not None:
self._set_axis_formatter(axis.zaxis, zdim, self.zformatter)
xticks = xticks if xticks else self.xticks
self._set_axis_ticks(axis.xaxis, xticks, log=self.logx,
rotation=self.xrotation)
yticks = yticks if yticks else self.yticks
self._set_axis_ticks(axis.yaxis, yticks, log=self.logy,
rotation=self.yrotation)
if isinstance(self.projection, str) and self.projection == '3d':
zticks = zticks if zticks else self.zticks
self._set_axis_ticks(axis.zaxis, zticks, log=self.logz,
rotation=self.zrotation)
axes_str = 'xy'
axes_list = [axis.xaxis, axis.yaxis]
if hasattr(axis, 'zaxis'):
axes_str += 'z'
axes_list.append(axis.zaxis)
for ax, ax_obj in zip(axes_str, axes_list):
tick_fontsize = self._fontsize(f'{ax}ticks','labelsize',common=False)
if tick_fontsize: ax_obj.set_tick_params(**tick_fontsize)
def _update_backend_opts(self):
plot = self.handles["fig"]
model_accessor_aliases = {
"figure": "fig",
"axes": "axis",
"ax": "axis",
"colorbar": "cbar",
}
for opt, val in self.backend_opts.items():
parsed_opt = self._parse_backend_opt(
opt, plot, model_accessor_aliases)
if parsed_opt is None:
continue
model, attr_accessor = parsed_opt
if not attr_accessor.startswith("set_"):
attr_accessor = f"set_{attr_accessor}"
if not isinstance(model, list):
# to reduce the need for many if/else; cast to list
# to do the same thing for both single and multiple models
models = [model]
else:
models = model
try:
for m in models:
getattr(m, attr_accessor)(val)
except AttributeError as exc:
valid_options = [attr for attr in dir(models[0]) if attr.startswith("set_")]
kws = Keywords(values=valid_options)
matches = sorted(kws.fuzzy_match(attr_accessor))
self.param.warning(
f"Encountered error: {exc}, or could not find "
f"{attr_accessor!r} method on {type(models[0]).__name__!r} "
f"model. Ensure the custom option spec {opt!r} you provided references a "
f"valid method on the specified model. Similar options include {matches!r}"
)
def _finalize_artist(self, element):
"""
Allows extending the _finalize_axis method with Element
specific options.
"""
def _set_labels(self, axes, dimensions, xlabel=None, ylabel=None, zlabel=None):
"""
Sets the labels of the axes using the supplied list of dimensions.
Optionally explicit labels may be supplied to override the dimension
label.
"""
xlabel, ylabel, zlabel = self._get_axis_labels(dimensions, xlabel, ylabel, zlabel)
if self.invert_axes:
xlabel, ylabel = ylabel, xlabel
if xlabel and self.xaxis and 'x' in self.labelled:
axes.set_xlabel(xlabel, **self._fontsize('xlabel'))
if ylabel and self.yaxis and 'y' in self.labelled:
axes.set_ylabel(ylabel, **self._fontsize('ylabel'))
if zlabel and self.zaxis and 'z' in self.labelled:
axes.set_zlabel(zlabel, **self._fontsize('zlabel'))
def _set_axis_formatter(self, axis, dim, formatter):
"""
Set axis formatter based on dimension formatter.
"""
if isinstance(dim, list): dim = dim[0]
if formatter is not None or dim is None:
pass
elif dim.value_format:
formatter = dim.value_format
elif dim.type in dim.type_formatters:
formatter = dim.type_formatters[dim.type]
if formatter:
axis.set_major_formatter(wrap_formatter(formatter))
def get_aspect(self, xspan, yspan):
"""
Computes the aspect ratio of the plot
"""
if isinstance(self.aspect, (int, float)):
return self.aspect
elif self.aspect == 'square':
return 1
elif self.aspect == 'equal':
return xspan/yspan
return 1
def _set_aspect(self, axes, aspect):
"""
Set the aspect on the axes based on the aspect setting.
"""
if isinstance(self.projection, str) and self.projection == '3d':
return
if ((isinstance(aspect, str) and aspect != 'square') or
self.data_aspect):
data_ratio = self.data_aspect or aspect
else:
(x0, x1), (y0, y1) = axes.get_xlim(), axes.get_ylim()
xsize = np.log(x1) - np.log(x0) if self.logx else x1-x0
ysize = np.log(y1) - np.log(y0) if self.logy else y1-y0
xsize = max(abs(xsize), 1e-30)
ysize = max(abs(ysize), 1e-30)
data_ratio = 1./(ysize/xsize)
if aspect != 'square':
data_ratio = data_ratio/aspect
axes.set_aspect(data_ratio)
def _set_axis_limits(self, axis, view, subplots, ranges):
"""
Compute extents for current view and apply as axis limits
"""
# Extents
extents = self.get_extents(view, ranges)
if not extents or self.overlaid:
axis.autoscale_view(scalex=True, scaley=True)
return
valid_lim = lambda c: util.isnumeric(c) and not np.isnan(c)
coords = [coord if isinstance(coord, np.datetime64) or np.isreal(coord) else np.nan for coord in extents]
coords = [date2num(util.dt64_to_dt(c)) if isinstance(c, np.datetime64) else c
for c in coords]
if (isinstance(self.projection, str) and self.projection == '3d') or len(extents) == 6:
l, b, zmin, r, t, zmax = coords
if self.invert_zaxis or any(p.invert_zaxis for p in subplots):
zmin, zmax = zmax, zmin
if zmin != zmax:
if valid_lim(zmin):
axis.set_zlim(bottom=zmin)
if valid_lim(zmax):
axis.set_zlim(top=zmax)
elif isinstance(self.projection, str) and self.projection == "polar":
_, b, _, t = coords
l = 0
r = 2 * np.pi
else:
l, b, r, t = coords
if self.invert_axes:
l, b, r, t = b, l, t, r
invertx = self.invert_xaxis or any(p.invert_xaxis for p in subplots)
xlim, scalex = self._compute_limits(l, r, self.logx, invertx, 'left', 'right')
inverty = self.invert_yaxis or any(p.invert_yaxis for p in subplots)
ylim, scaley = self._compute_limits(b, t, self.logy, inverty, 'bottom', 'top')
if xlim:
axis.set_xlim(**xlim)
if ylim:
axis.set_ylim(**ylim)
axis.autoscale_view(scalex=scalex, scaley=scaley)
def _compute_limits(self, low, high, log, invert, low_key, high_key):
scale = True
lims = {}
valid_lim = lambda c: util.isnumeric(c) and not np.isnan(c)
if not isinstance(low, util.datetime_types) and log and (low is None or low <= 0):
low = 0.01 if high < 0.01 else 10**(np.log10(high)-2)
self.param.warning(
"Logarithmic axis range encountered value less "
"than or equal to zero, please supply explicit "
"lower-bound to override default of %.3f." % low)
if invert:
high, low = low, high
if isinstance(low, util.cftime_types) or low != high:
if valid_lim(low):
lims[low_key] = low
scale = False
if valid_lim(high):
lims[high_key] = high
scale = False
return lims, scale
def _set_axis_position(self, axes, axis, option):
"""
Set the position and visibility of the xaxis or yaxis by
supplying the axes object, the axis to set, i.e. 'x' or 'y'
and an option to specify the position and visibility of the axis.
The option may be None, 'bare' or positional, i.e. 'left' and
'right' for the yaxis and 'top' and 'bottom' for the xaxis.
May also combine positional and 'bare' into for example 'left-bare'.
"""
positions = {'x': ['bottom', 'top'], 'y': ['left', 'right']}[axis]
axis = axes.xaxis if axis == 'x' else axes.yaxis
if option in [None, False]:
axis.set_visible(False)
for pos in positions:
axes.spines[pos].set_visible(False)
else:
if option is True:
option = positions[0]
if 'bare' in option:
axis.set_ticklabels([])
axis.set_label_text('')
if option != 'bare':
option = option.split('-')[0]
axis.set_ticks_position(option)
axis.set_label_position(option)
if not self.overlaid and not self.show_frame and self.projection != 'polar':
pos = (positions[1] if (option and (option == 'bare' or positions[0] in option))
else positions[0])
axes.spines[pos].set_visible(False)
def _set_axis_ticks(self, axis, ticks, log=False, rotation=0):
"""
Allows setting the ticks for a particular axis either with
a tuple of ticks, a tick locator object, an integer number
of ticks, a list of tuples containing positions and labels
or a list of positions. Also supports enabling log ticking
if an integer number of ticks is supplied and setting a
rotation for the ticks.
"""
if isinstance(ticks, np.ndarray):
ticks = list(ticks)
if isinstance(ticks, (list, tuple)) and all(isinstance(l, list) for l in ticks):
axis.set_ticks(ticks[0])
axis.set_ticklabels(ticks[1])
elif isinstance(ticks, ticker.Locator):
axis.set_major_locator(ticks)
elif ticks is not None and not ticks:
axis.set_ticks([])
elif isinstance(ticks, int):
if log:
locator = ticker.LogLocator(numticks=ticks,
subs=range(1,10))
else:
locator = ticker.MaxNLocator(ticks)
axis.set_major_locator(locator)
elif isinstance(ticks, (list, tuple)):
labels = None
if all(isinstance(t, tuple) for t in ticks):
ticks, labels = zip(*ticks)
axis.set_ticks(ticks)
if labels:
axis.set_ticklabels(labels)
for tick in axis.get_ticklabels():
tick.set_rotation(rotation)
@mpl_rc_context
def update_frame(self, key, ranges=None, element=None):
"""
Set the plot(s) to the given frame number. Operates by
manipulating the matplotlib objects held in the self._handles
dictionary.
If n is greater than the number of available frames, update
using the last available frame.
"""
reused = isinstance(self.hmap, DynamicMap) and self.overlaid
self.prev_frame = self.current_frame
if not reused and element is None:
element = self._get_frame(key)
elif element is not None:
self.current_key = key
self.current_frame = element
if element is not None:
self.param.update(**self.lookup_options(element, 'plot').options)
axis = self.handles['axis']
axes_visible = element is not None or self.overlaid
axis.xaxis.set_visible(axes_visible and self.xaxis)
axis.yaxis.set_visible(axes_visible and self.yaxis)
axis.patch.set_alpha(np.min([int(axes_visible), 1]))
for hname, handle in self.handles.items():
hideable = hasattr(handle, 'set_visible')
if hname not in ['axis', 'fig'] and hideable:
handle.set_visible(element is not None)
if element is None:
return
ranges = self.compute_ranges(self.hmap, key, ranges)
ranges = util.match_spec(element, ranges)
max_cycles = self.style._max_cycles
style = self.lookup_options(element, 'style')
self.style = style.max_cycles(max_cycles) if max_cycles else style
labels = getattr(self, 'legend_labels', {})
label = element.label if self.show_legend else ''
style = dict(label=labels.get(label, label), zorder=self.zorder, **self.style[self.cyclic_index])
axis_kwargs = self.update_handles(key, axis, element, ranges, style)
self._finalize_axis(key, element=element, ranges=ranges,
**(axis_kwargs if axis_kwargs else {}))
def render_artists(self, element, ranges, style, ax):
plot_data, plot_kwargs, axis_kwargs = self.get_data(element, ranges, style)
legend = plot_kwargs.pop('cat_legend', None)
with abbreviated_exception():
handles = self.init_artists(ax, plot_data, plot_kwargs)
if legend and 'artist' in handles and hasattr(handles['artist'], 'legend_elements'):
legend_handles, _ = handles['artist'].legend_elements()
leg = ax.legend(legend_handles, legend['factors'],
title=legend['title'], **self._legend_opts)
ax.add_artist(leg)
return handles, axis_kwargs
@mpl_rc_context
def initialize_plot(self, ranges=None):
element = self.hmap.last
ax = self.handles['axis']
key = list(self.hmap.data.keys())[-1]
dim_map = dict(zip((d.name for d in self.hmap.kdims), key))
key = tuple(dim_map.get(d.name, None) for d in self.dimensions)
ranges = self.compute_ranges(self.hmap, key, ranges)
self.current_ranges = ranges
self.current_frame = element
self.current_key = key
ranges = util.match_spec(element, ranges)
style = dict(zorder=self.zorder, **self.style[self.cyclic_index])
if self.show_legend:
style['label'] = element.label
handles, axis_kwargs = self.render_artists(element, ranges, style, ax)
self.handles.update(handles)
trigger = self._trigger
self._trigger = []
Stream.trigger(trigger)
return self._finalize_axis(self.keys[-1], element=element, ranges=ranges,
**axis_kwargs)
def init_artists(self, ax, plot_args, plot_kwargs):
"""
Initializes the artist based on the plot method declared on
the plot.
"""
plot_method = self._plot_methods.get('batched' if self.batched else 'single')
plot_fn = getattr(ax, plot_method)
if 'norm' in plot_kwargs: # vmin/vmax should now be exclusively in norm
plot_kwargs.pop('vmin', None)
plot_kwargs.pop('vmax', None)
with warnings.catch_warnings():
# scatter have a default cmap and with an empty array will emit this warning
warnings.filterwarnings('ignore', "No data for colormapping provided via 'c'")
artist = plot_fn(*plot_args, **plot_kwargs)
return {'artist': artist[0] if isinstance(artist, list) and
len(artist) == 1 else artist}
def update_handles(self, key, axis, element, ranges, style):
"""
Update the elements of the plot.
"""
self.teardown_handles()
handles, axis_kwargs = self.render_artists(element, ranges, style, axis)
self.handles.update(handles)
return axis_kwargs
def _apply_transforms(self, element, ranges, style):
new_style = dict(style)
for k, v in style.items():
if isinstance(v, str):
if validate(k, v) == True:
continue
elif v in element or (isinstance(element, Graph) and v in element.nodes):
v = dim(v)
elif any(d==v for d in self.overlay_dims):
v = dim(next(d for d in self.overlay_dims if d==v))
if not isinstance(v, dim):
continue
elif (not v.applies(element) and v.dimension not in self.overlay_dims):
new_style.pop(k)
self.param.warning(
f'Specified {k} dim transform {v!r} could not be '
'applied, as not all dimensions could be resolved.')
continue
if v.dimension in self.overlay_dims:
ds = Dataset({d.name: v for d, v in self.overlay_dims.items()},
list(self.overlay_dims))
val = v.apply(ds, ranges=ranges, flat=True)[0]
elif type(element) is Path:
val = np.concatenate([v.apply(el, ranges=ranges, flat=True)
for el in element.split()])
else:
val = v.apply(element, ranges)
if (not np.isscalar(val) and len(util.unique_array(val)) == 1 and
("color" not in k or validate('color', val))):
val = val[0]
if not np.isscalar(val) and k in self._nonvectorized_styles:
element = type(element).__name__
raise ValueError(f'Mapping a dimension to the "{k}" '
'style option is not supported by the '
f'{element} element using the {self.renderer.backend} '
f'backend. To map the "{v.dimension}" dimension '
f'to the {k} use a groupby operation '
'to overlay your data along the dimension.')
style_groups = getattr(self, '_style_groups', [])
groups = [sg for sg in style_groups if k.startswith(sg)]
group = groups[0] if groups else None
prefix = '' if group is None else group+'_'
if (k in (prefix+'c', prefix+'color') and isinstance(val, util.arraylike_types)
and not validate('color', val)):
new_style.pop(k)
self._norm_kwargs(element, ranges, new_style, v, val, prefix)
if val.dtype.kind in 'OSUM':
range_key = dim_range_key(v)
if range_key in ranges and 'factors' in ranges[range_key]:
factors = ranges[range_key]['factors']
else:
factors = util.unique_array(val)
val = util.search_indices(val, factors)
labels = getattr(self, 'legend_labels', {})
factors = [labels.get(f, f) for f in factors]
new_style['cat_legend'] = {
'title': v.dimension, 'prop': 'c', 'factors': factors
}
k = prefix+'c'
new_style[k] = val
for k, val in list(new_style.items()):
# If mapped to color/alpha override static fill/line style
if k == 'c':
new_style.pop('color', None)
style_groups = getattr(self, '_style_groups', [])
groups = [sg for sg in style_groups if k.startswith(sg)]
group = groups[0] if groups else None
prefix = '' if group is None else group+'_'
# Check if element supports fill and line style
supports_fill = (
(prefix != 'edge' or getattr(self, 'filled', True))
and any(o.startswith(prefix+'face') for o in self.style_opts))
if k in (prefix+'c', prefix+'color') and isinstance(val, util.arraylike_types):
fill_style = new_style.get(prefix+'facecolor')
if fill_style and validate('color', fill_style):
new_style.pop('facecolor')
line_style = new_style.get(prefix+'edgecolor')
# If glyph has fill and line style is set overriding line color
if supports_fill and line_style is not None:
continue
if line_style and validate('color', line_style):
new_style.pop('edgecolor')
elif k == 'facecolors' and not isinstance(new_style.get('color', new_style.get('c')), np.ndarray):
# Color overrides facecolors if defined
new_style.pop('color', None)
new_style.pop('c', None)
return new_style
def teardown_handles(self):
"""
If no custom update_handles method is supplied this method
is called to tear down any previous handles before replacing
them.
"""
if 'artist' in self.handles:
self.handles['artist'].remove()
class ColorbarPlot(ElementPlot):
clabel = param.String(default=None, doc="""
An explicit override of the color bar label, if set takes precedence
over the title key in colorbar_opts.""")
clim = param.Tuple(default=(np.nan, np.nan), length=2, doc="""
User-specified colorbar axis range limits for the plot, as a
tuple (low,high). If specified, takes precedence over data
and dimension ranges.""")
clim_percentile = param.ClassSelector(default=False, class_=(int, float, bool), doc="""
Percentile value to compute colorscale robust to outliers. If
True, uses 2nd and 98th percentile; otherwise uses the specified
numerical percentile value.""")
cformatter = param.ClassSelector(
default=None, class_=(str, ticker.Formatter, FunctionType), doc="""
Formatter for ticks along the colorbar axis.""")
colorbar = param.Boolean(default=False, doc="""
Whether to draw a colorbar.""")
colorbar_opts = param.Dict(default={}, doc="""
Allows setting specific styling options for the colorbar.""")
color_levels = param.ClassSelector(default=None, class_=(int, list), doc="""
Number of discrete colors to use when colormapping or a set of color
intervals defining the range of values to map each color to.""")
cnorm = param.ObjectSelector(default='linear', objects=['linear', 'log', 'eq_hist'], doc="""
Color normalization to be applied during colormapping.""")
clipping_colors = param.Dict(default={}, doc="""
Dictionary to specify colors for clipped values, allows
setting color for NaN values and for values above and below
the min and max value. The min, max or NaN color may specify
an RGB(A) color as a color hex string of the form #FFFFFF or
#FFFFFFFF or a length 3 or length 4 tuple specifying values in
the range 0-1 or a named HTML color.""")
cbar_padding = param.Number(default=0.01, doc="""
Padding between colorbar and other plots.""")
cbar_ticks = param.Parameter(default=None, doc="""
Ticks along colorbar-axis specified as an integer, explicit
list of tick locations, list of tuples containing the
locations and labels or a matplotlib tick locator object. If
set to None default matplotlib ticking behavior is
applied.""")
cbar_width = param.Number(default=0.05, doc="""
Width of the colorbar as a fraction of the main plot""")
cbar_extend = param.ObjectSelector(
objects=['neither', 'both', 'min', 'max'], default=None, doc="""
If not 'neither', make pointed end(s) for out-of- range values."""
)
rescale_discrete_levels = param.Boolean(default=True, doc="""
If ``cnorm='eq_hist`` and there are only a few discrete values,
then ``rescale_discrete_levels=True`` decreases the lower
limit of the autoranged span so that the values are rendering
towards the (more visible) top of the palette, thus
avoiding washout of the lower values. Has no effect if
``cnorm!=`eq_hist``. Set this value to False if you need to
match historical unscaled behavior, prior to HoloViews 1.14.4.""")
symmetric = param.Boolean(default=False, doc="""
Whether to make the colormap symmetric around zero.""")
_colorbars = {}
_default_nan = '#8b8b8b'
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def _adjust_cbar(self, cbar, label, dim):
noalpha = math.floor(self.style[self.cyclic_index].get('alpha', 1)) == 1
for lb in ['clabel', 'labels']:
labelsize = self._fontsize(lb, common=False).get('fontsize')
if labelsize is not None:
break
if (cbar.solids and noalpha):
cbar.solids.set_edgecolor("face")
cbar.set_label(label, fontsize=labelsize)
if isinstance(self.cbar_ticks, ticker.Locator):
cbar.ax.yaxis.set_major_locator(self.cbar_ticks)
elif self.cbar_ticks == 0:
cbar.set_ticks([])
elif isinstance(self.cbar_ticks, int):
locator = ticker.MaxNLocator(self.cbar_ticks)
cbar.ax.yaxis.set_major_locator(locator)
elif isinstance(self.cbar_ticks, list):
if all(isinstance(t, tuple) for t in self.cbar_ticks):
ticks, labels = zip(*self.cbar_ticks)
else:
ticks, labels = zip(*[(t, dim.pprint_value(t))
for t in self.cbar_ticks])
cbar.set_ticks(ticks)
cbar.set_ticklabels(labels)
for tk in ['cticks', 'ticks']:
ticksize = self._fontsize(tk, common=False).get('fontsize')
if ticksize is not None:
cbar.ax.tick_params(labelsize=ticksize)
break
def _finalize_artist(self, element):
if self.colorbar:
dims = [h for k, h in self.handles.items() if k.endswith('color_dim')]
for d in dims:
self._draw_colorbar(element, d)
def _draw_colorbar(self, element=None, dimension=None, redraw=True):
if element is None:
element = self.hmap.last
artist = self.handles.get('artist', None)
fig = self.handles['fig']
axis = self.handles['axis']
ax_colorbars, position = ColorbarPlot._colorbars.get(id(axis), ([], None))
specs = [spec[:2] for _, _, spec, _ in ax_colorbars]
spec = util.get_spec(element)
if position is None or not redraw:
if redraw:
fig.canvas.draw()
bbox = axis.get_position()
l, b, w, h = bbox.x0, bbox.y0, bbox.width, bbox.height
else:
l, b, w, h = position
# Get colorbar label
if isinstance(dimension, dim):
dimension = dimension.dimension
dimension = element.get_dimension(dimension)
if self.clabel is not None:
label = self.clabel
elif dimension:
label = dimension.pprint_label
elif element.vdims:
label = element.vdims[0].pprint_label
elif dimension is None:
label = ''
padding = self.cbar_padding
width = self.cbar_width
if spec[:2] not in specs:
offset = len(ax_colorbars)
scaled_w = w*width
cax = fig.add_axes([l+w+padding+(scaled_w+padding+w*0.15)*offset,
b, scaled_w, h])
cbar = fig.colorbar(artist, cax=cax, ax=axis,
extend=self.cbar_extend, **self.colorbar_opts)
self._set_axis_formatter(cbar.ax.yaxis, dimension, self.cformatter)
self._adjust_cbar(cbar, label, dimension)
self.handles['cax'] = cax
self.handles['cbar'] = cbar
ylabel = cax.yaxis.get_label()
self.handles['bbox_extra_artists'] += [cax, ylabel]
ax_colorbars.append((artist, cax, spec, label))
for i, (_artist, cax, _spec, _label) in enumerate(ax_colorbars):
scaled_w = w*width
cax.set_position([l+w+padding+(scaled_w+padding+w*0.15)*i,
b, scaled_w, h])
ColorbarPlot._colorbars[id(axis)] = (ax_colorbars, (l, b, w, h))
def _norm_kwargs(self, element, ranges, opts, vdim, values=None, prefix=''):
"""
Returns valid color normalization kwargs
to be passed to matplotlib plot function.
"""
dim_name = dim_range_key(vdim)
if values is None:
if isinstance(vdim, dim):
values = vdim.apply(element, flat=True)
else:
expanded = not (
isinstance(element, Dataset) and
element.interface.multi and
(getattr(element, 'level', None) is not None or
element.interface.isunique(element, vdim.name, True))
)
values = np.asarray(element.dimension_values(vdim, expanded=expanded))
# Store dimension being colormapped for colorbars
if prefix+'color_dim' not in self.handles:
self.handles[prefix+'color_dim'] = vdim
clim = opts.pop(prefix+'clims', None)
# check if there's an actual value (not np.nan)
if clim is None and self.clim is not None and any(util.isfinite(cl) for cl in self.clim):
clim = self.clim
if clim is None:
if not len(values):
clim = (0, 0)
categorical = False
elif values.dtype.kind in 'uif':
if dim_name in ranges:
if self.clim_percentile and 'robust' in ranges[dim_name]:
clim = ranges[dim_name]['robust']
else:
clim = ranges[dim_name]['combined']
elif isinstance(vdim, dim):
if values.dtype.kind == 'M':
clim = values.min(), values.max()
elif len(values) == 0:
clim = np.nan, np.nan
else:
try:
with warnings.catch_warnings():
warnings.filterwarnings('ignore', r'All-NaN (slice|axis) encountered')
clim = (np.nanmin(values), np.nanmax(values))
except Exception:
clim = np.nan, np.nan
else:
clim = element.range(vdim)
if self.logz:
# Lower clim must be >0 when logz=True
# Choose the maximum between the lowest non-zero value
# and the overall range
if clim[0] == 0:
clim = (values[values!=0].min(), clim[1])
if self.symmetric:
clim = -np.abs(clim).max(), np.abs(clim).max()
categorical = False
else:
range_key = dim_range_key(vdim)
if range_key in ranges and 'factors' in ranges[range_key]:
factors = ranges[range_key]['factors']
else:
factors = util.unique_array(values)
clim = (0, len(factors)-1)
categorical = True
else:
categorical = values.dtype.kind not in 'uif'
if self.cnorm == 'eq_hist':
opts[prefix+'norm'] = EqHistNormalize(
vmin=clim[0], vmax=clim[1],
rescale_discrete_levels=self.rescale_discrete_levels
)
if self.cnorm == 'log' or self.logz:
if self.symmetric:
norm = mpl_colors.SymLogNorm(vmin=clim[0], vmax=clim[1],
linthresh=clim[1]/np.e)
else:
norm = mpl_colors.LogNorm(vmin=clim[0], vmax=clim[1])
opts[prefix+'norm'] = norm
opts[prefix+'vmin'] = clim[0]
opts[prefix+'vmax'] = clim[1]
cmap = opts.get(prefix+'cmap', opts.get('cmap', 'viridis'))
if values.dtype.kind not in 'OSUM':
ncolors = None
if isinstance(self.color_levels, int):
ncolors = self.color_levels
elif isinstance(self.color_levels, list):
ncolors = len(self.color_levels) - 1
if isinstance(cmap, list) and len(cmap) != ncolors:
raise ValueError('The number of colors in the colormap '
'must match the intervals defined in the '