hvplot 0.9.3a1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. hvplot/__init__.py +322 -0
  2. hvplot/_version.py +16 -0
  3. hvplot/backend_transforms.py +329 -0
  4. hvplot/converter.py +2855 -0
  5. hvplot/cudf.py +26 -0
  6. hvplot/dask.py +42 -0
  7. hvplot/data/crime.csv +56 -0
  8. hvplot/datasets.yaml +48 -0
  9. hvplot/fugue.py +64 -0
  10. hvplot/ibis.py +21 -0
  11. hvplot/intake.py +32 -0
  12. hvplot/interactive.py +968 -0
  13. hvplot/networkx.py +625 -0
  14. hvplot/pandas.py +30 -0
  15. hvplot/plotting/__init__.py +63 -0
  16. hvplot/plotting/andrews_curves.py +99 -0
  17. hvplot/plotting/core.py +2288 -0
  18. hvplot/plotting/lag_plot.py +34 -0
  19. hvplot/plotting/parallel_coordinates.py +85 -0
  20. hvplot/plotting/scatter_matrix.py +220 -0
  21. hvplot/polars.py +21 -0
  22. hvplot/sample_data.py +30 -0
  23. hvplot/streamz.py +21 -0
  24. hvplot/tests/__init__.py +0 -0
  25. hvplot/tests/conftest.py +44 -0
  26. hvplot/tests/data/README.md +5 -0
  27. hvplot/tests/data/RGB-red.byte.tif +0 -0
  28. hvplot/tests/plotting/__init__.py +0 -0
  29. hvplot/tests/plotting/testcore.py +108 -0
  30. hvplot/tests/plotting/testohlc.py +34 -0
  31. hvplot/tests/plotting/testscattermatrix.py +138 -0
  32. hvplot/tests/test_links.py +99 -0
  33. hvplot/tests/testbackend_transforms.py +89 -0
  34. hvplot/tests/testcharts.py +452 -0
  35. hvplot/tests/testfugue.py +46 -0
  36. hvplot/tests/testgeo.py +468 -0
  37. hvplot/tests/testgeowithoutgv.py +60 -0
  38. hvplot/tests/testgridplots.py +259 -0
  39. hvplot/tests/testhelp.py +75 -0
  40. hvplot/tests/testibis.py +17 -0
  41. hvplot/tests/testinteractive.py +1442 -0
  42. hvplot/tests/testnetworkx.py +26 -0
  43. hvplot/tests/testoperations.py +385 -0
  44. hvplot/tests/testoptions.py +596 -0
  45. hvplot/tests/testoverrides.py +74 -0
  46. hvplot/tests/testpanel.py +70 -0
  47. hvplot/tests/testpatch.py +135 -0
  48. hvplot/tests/testplotting.py +69 -0
  49. hvplot/tests/teststreaming.py +28 -0
  50. hvplot/tests/testtransforms.py +39 -0
  51. hvplot/tests/testui.py +383 -0
  52. hvplot/tests/testutil.py +378 -0
  53. hvplot/tests/util.py +82 -0
  54. hvplot/ui.py +1032 -0
  55. hvplot/util.py +677 -0
  56. hvplot/utilities.py +129 -0
  57. hvplot/xarray.py +62 -0
  58. hvplot-0.9.3a1.dist-info/LICENSE +29 -0
  59. hvplot-0.9.3a1.dist-info/METADATA +243 -0
  60. hvplot-0.9.3a1.dist-info/RECORD +63 -0
  61. hvplot-0.9.3a1.dist-info/WHEEL +5 -0
  62. hvplot-0.9.3a1.dist-info/entry_points.txt +2 -0
  63. hvplot-0.9.3a1.dist-info/top_level.txt +1 -0
hvplot/converter.py ADDED
@@ -0,0 +1,2855 @@
1
+ import difflib
2
+ import sys
3
+ from functools import partial
4
+
5
+ import param
6
+ import holoviews as hv
7
+ import pandas as pd
8
+ import numpy as np
9
+ import colorcet as cc
10
+
11
+ from bokeh.models import HoverTool
12
+ from holoviews.core.dimension import Dimension
13
+ from holoviews.core.spaces import DynamicMap, HoloMap, Callable
14
+ from holoviews.core.overlay import NdOverlay
15
+ from holoviews.core.options import Store, Cycle, Palette
16
+ from holoviews.core.layout import NdLayout
17
+ from holoviews.core.util import max_range
18
+ from holoviews.element import (
19
+ Curve,
20
+ Scatter,
21
+ Area,
22
+ Bars,
23
+ BoxWhisker,
24
+ Dataset,
25
+ Distribution,
26
+ Table,
27
+ HeatMap,
28
+ Image,
29
+ HexTiles,
30
+ QuadMesh,
31
+ Bivariate,
32
+ Histogram,
33
+ Violin,
34
+ Contours,
35
+ Polygons,
36
+ Points,
37
+ Path,
38
+ Labels,
39
+ RGB,
40
+ ErrorBars,
41
+ VectorField,
42
+ Rectangles,
43
+ Segments,
44
+ )
45
+ from holoviews.plotting.bokeh import OverlayPlot, colormap_generator
46
+ from holoviews.plotting.util import process_cmap
47
+ from holoviews.operation import histogram, apply_when
48
+ from holoviews.streams import Buffer, Pipe
49
+ from holoviews.util.transform import dim
50
+ from packaging.version import Version
51
+ from pandas import DatetimeIndex, MultiIndex
52
+
53
+ from .backend_transforms import _transfer_opts_cur_backend
54
+ from .util import (
55
+ filter_opts,
56
+ hv_version,
57
+ is_tabular,
58
+ is_series,
59
+ is_dask,
60
+ is_intake,
61
+ is_cudf,
62
+ is_streamz,
63
+ is_ibis,
64
+ is_xarray,
65
+ is_xarray_dataarray,
66
+ process_crs,
67
+ process_intake,
68
+ process_xarray,
69
+ check_library,
70
+ is_geodataframe,
71
+ process_derived_datetime_xarray,
72
+ process_derived_datetime_pandas,
73
+ _convert_col_names_to_str,
74
+ import_datashader,
75
+ )
76
+ from .utilities import hvplot_extension
77
+
78
+ renderer = hv.renderer('bokeh')
79
+
80
+
81
+ class StreamingCallable(Callable):
82
+ """
83
+ StreamingCallable is a DynamicMap callback wrapper which keeps
84
+ a handle to start and stop a dynamic stream.
85
+ """
86
+
87
+ periodic = param.Parameter()
88
+
89
+ def clone(self, callable=None, **overrides):
90
+ """
91
+ Allows making a copy of the Callable optionally overriding
92
+ the callable and other parameters.
93
+ """
94
+ old = {k: v for k, v in self.param.get_param_values() if k not in ['callable', 'name']}
95
+ params = dict(old, **overrides)
96
+ callable = self.callable if callable is None else callable
97
+ return self.__class__(callable, **params)
98
+
99
+ def start(self):
100
+ """
101
+ Start the periodic callback
102
+ """
103
+ if not self.periodic._running:
104
+ self.periodic.start()
105
+ else:
106
+ raise Exception('PeriodicCallback already running.')
107
+
108
+ def stop(self):
109
+ """
110
+ Stop the periodic callback
111
+ """
112
+ if self.periodic._running:
113
+ self.periodic.stop()
114
+ else:
115
+ raise Exception('PeriodicCallback not running.')
116
+
117
+
118
+ class HoloViewsConverter:
119
+ """
120
+ Generic options
121
+ ---------------
122
+ autorange (default=None): Literal['x', 'y'] | None
123
+ Whether to enable auto-ranging along the x- or y-axis when
124
+ zooming. Requires HoloViews >= 1.16.
125
+ bgcolor (default=None): str
126
+ Background color of the data area of the plot
127
+ clim: tuple
128
+ Lower and upper bound of the color scale
129
+ cnorm (default='linear'): str
130
+ Color scaling which must be one of 'linear', 'log' or 'eq_hist'
131
+ colorbar (default=False): boolean
132
+ Enables a colorbar
133
+ fontscale: number
134
+ Scales the size of all fonts by the same amount, e.g. fontscale=1.5
135
+ enlarges all fonts (title, xticks, labels etc.) by 50%
136
+ fontsize: number or dict
137
+ Set title, label and legend text to the same fontsize. Finer control
138
+ by using a dict: {'title': '15pt', 'ylabel': '5px', 'ticks': 20}
139
+ flip_xaxis/flip_yaxis: boolean
140
+ Whether to flip the axis left to right or up and down respectively
141
+ grid (default=False): boolean
142
+ Whether to show a grid
143
+ hover : boolean
144
+ Whether to show hover tooltips, default is True unless datashade is
145
+ True in which case hover is False by default
146
+ hover_cols (default=[]): list or str
147
+ Additional columns to add to the hover tool or 'all' which will
148
+ includes all columns (including indexes if use_index is True).
149
+ invert (default=False): boolean
150
+ Swaps x- and y-axis
151
+ frame_width/frame_height: int
152
+ The width and height of the data area of the plot
153
+ legend (default=True): boolean or str
154
+ Whether to show a legend, or a legend position
155
+ ('top', 'bottom', 'left', 'right')
156
+ logx/logy (default=False): boolean
157
+ Enables logarithmic x- and y-axis respectively
158
+ logz (default=False): boolean
159
+ Enables logarithmic colormapping
160
+ loglog (default=False): boolean
161
+ Enables logarithmic x- and y-axis
162
+ max_width/max_height: int
163
+ The maximum width and height of the plot for responsive modes
164
+ min_width/min_height: int
165
+ The minimum width and height of the plot for responsive modes
166
+ padding: number or tuple
167
+ Fraction by which to increase auto-ranged extents to make
168
+ datapoints more visible around borders. Supports tuples to
169
+ specify different amount of padding for x- and y-axis and
170
+ tuples of tuples to specify different amounts of padding for
171
+ upper and lower bounds.
172
+ rescale_discrete_levels (default=True): boolean
173
+ If `cnorm='eq_hist'` and there are only a few discrete values,
174
+ then `rescale_discrete_levels=True` (the default) decreases
175
+ the lower limit of the autoranged span so that the values are
176
+ rendering towards the (more visible) top of the `cmap` range,
177
+ thus avoiding washout of the lower values. Has no effect if
178
+ `cnorm!=`eq_hist`.
179
+ responsive: boolean
180
+ Whether the plot should responsively resize depending on the
181
+ size of the browser. Responsive mode will only work if at
182
+ least one dimension of the plot is left undefined, e.g. when
183
+ width and height or width and aspect are set the plot is set
184
+ to a fixed size, ignoring any responsive option.
185
+ robust: bool
186
+ If True and clim are absent, the colormap range is computed
187
+ with 2nd and 98th percentiles instead of the extreme values
188
+ for image elements. For RGB elements, clips the "RGB", or
189
+ raw reflectance values between 2nd and 98th percentiles.
190
+ Follows the same logic as xarray's robust option.
191
+ rot: number
192
+ Rotates the axis ticks along the x-axis by the specified
193
+ number of degrees.
194
+ shared_axes (default=True): boolean
195
+ Whether to link axes between plots
196
+ transforms (default={}): dict
197
+ A dictionary of HoloViews dim transforms to apply before plotting
198
+ title (default=''): str
199
+ Title for the plot
200
+ tools (default=[]): list
201
+ List of tool instances or strings (e.g. ['tap', 'box_select'])
202
+ xaxis/yaxis: str or None
203
+ Whether to show the x/y-axis and whether to place it at the
204
+ 'top'/'bottom' and 'left'/'right' respectively.
205
+ xformatter/yformatter (default=None): str or TickFormatter
206
+ Formatter for the x-axis and y-axis (accepts printf formatter,
207
+ e.g. '%.3f', and bokeh TickFormatter)
208
+ xlabel/ylabel/clabel (default=None): str
209
+ Axis labels for the x-axis, y-axis, and colorbar
210
+ xlim/ylim (default=None): tuple or list
211
+ Plot limits of the x- and y-axis
212
+ xticks/yticks (default=None): int or list
213
+ Ticks along x- and y-axis specified as an integer, list of
214
+ ticks positions, or list of tuples of the tick positions and labels
215
+ width (default=700)/height (default=300): int
216
+ The width and height of the plot in pixels
217
+ attr_labels (default=None): bool
218
+ Whether to use an xarray object's attributes as labels, defaults to
219
+ None to allow best effort without throwing a warning. Set to True
220
+ to see warning if the attrs can't be found, set to False to disable
221
+ the behavior.
222
+ sort_date (default=True): bool
223
+ Whether to sort the x-axis by date before plotting
224
+ symmetric (default=None): bool
225
+ Whether the data are symmetric around zero. If left unset, the data
226
+ will be checked for symmetry as long as the size is less than
227
+ ``check_symmetric_max``.
228
+ check_symmetric_max (default=1000000):
229
+ Size above which to stop checking for symmetry by default on the data.
230
+
231
+ Resampling options
232
+ ------------------
233
+ aggregator (default=None):
234
+ Aggregator to use when applying rasterize or datashade operation
235
+ (valid options include 'mean', 'count', 'min', 'max' and more, and
236
+ datashader reduction objects)
237
+ dynamic (default=True):
238
+ Whether to return a dynamic plot which sends updates on widget and
239
+ zoom/pan events or whether all the data should be embedded
240
+ (warning: for large groupby operations embedded data can become
241
+ very large if dynamic=False)
242
+ datashade (default=False):
243
+ Whether to apply rasterization and shading (colormapping) using
244
+ the Datashader library, returning an RGB object instead of
245
+ individual points
246
+ downsample (default=False):
247
+ Controls the application of downsampling to the plotted data,
248
+ which is particularly useful for large timeseries datasets to
249
+ reduce the amount of data sent to browser and improve
250
+ visualization performance. Requires HoloViews >= 1.16. Additional
251
+ dependencies: Installing the `tsdownsample` library is required
252
+ for using any downsampling methods other than the default 'lttb'.
253
+ Acceptable values:
254
+ - False: No downsampling is applied.
255
+ - True: Applies downsampling using HoloViews' default algorithm
256
+ (LTTB - Largest Triangle Three Buckets).
257
+ - 'lttb': Explicitly applies the Largest Triangle Three Buckets
258
+ algorithm.
259
+ - 'minmax': Applies the MinMax algorithm, selecting the minimum
260
+ and maximum values in each bin. Requires `tsdownsample`.
261
+ - 'm4': Applies the M4 algorithm, selecting the minimum, maximum,
262
+ first, and last values in each bin. Requires `tsdownsample`.
263
+ - 'minmax-lttb': Combines MinMax and LTTB algorithms for
264
+ downsampling, first applying MinMax to reduce to a preliminary
265
+ set of points, then LTTB for further reduction. Requires
266
+ `tsdownsample`.
267
+ Other string values corresponding to supported algorithms in
268
+ HoloViews may also be used.
269
+ dynspread (default=False):
270
+ For plots generated with datashade=True or rasterize=True,
271
+ automatically increase the point size when the data is sparse
272
+ so that individual points become more visible
273
+ rasterize (default=False):
274
+ Whether to apply rasterization using the Datashader library,
275
+ returning an aggregated Image (to be colormapped by the
276
+ plotting backend) instead of individual points
277
+ resample_when (default=None):
278
+ Applies a resampling operation (datashade, rasterize or downsample) if
279
+ the number of individual data points present in the current zoom range
280
+ is above this threshold. The raw plot is displayed otherwise.
281
+ x_sampling/y_sampling (default=None):
282
+ Specifies the smallest allowed sampling interval along the x/y axis.
283
+
284
+ Geographic options
285
+ ------------------
286
+ coastline (default=False):
287
+ Whether to display a coastline on top of the plot, setting
288
+ coastline='10m'/'50m'/'110m' specifies a specific scale.
289
+ crs (default=None):
290
+ Coordinate reference system of the data specified as a string
291
+ or integer EPSG code, a CRS or Proj pyproj object, a Cartopy
292
+ CRS object, a WKT string, or a proj.4 string. Defaults to
293
+ PlateCarree.
294
+ features (default=None): dict or list
295
+ A list of features or a dictionary of features and the scale
296
+ at which to render it. Available features include 'borders',
297
+ 'coastline', 'lakes', 'land', 'ocean', 'rivers' and 'states'.
298
+ Available scales include '10m'/'50m'/'110m'.
299
+ geo (default=False):
300
+ Whether the plot should be treated as geographic (and assume
301
+ PlateCarree, i.e. lat/lon coordinates).
302
+ global_extent (default=False):
303
+ Whether to expand the plot extent to span the whole globe.
304
+ project (default=False):
305
+ Whether to project the data before plotting (adds initial
306
+ overhead but avoids projecting data when plot is dynamically
307
+ updated).
308
+ projection (default=None): str or Cartopy CRS
309
+ Coordinate reference system of the plot specified as Cartopy
310
+ CRS object or class name.
311
+ tiles (default=False):
312
+ Whether to overlay the plot on a tile source:
313
+ - `True`: OpenStreetMap layer
314
+ - `xyzservices.TileProvider` instance (requires xyzservices to
315
+ be installed)
316
+ - a map string name based on one of the default layers made
317
+ available by HoloViews or GeoViews.
318
+ - a `holoviews.Tiles` or `geoviews.WMTS` instance or class
319
+ tiles_opts (default=None): dict
320
+ Options to customize the tiles layer created when `tiles` is set,
321
+ e.g. `dict(alpha=0.5)`.
322
+ """
323
+
324
+ _gridded_types = ['image', 'contour', 'contourf', 'quadmesh', 'rgb', 'points', 'dataset']
325
+
326
+ _geom_types = ['paths', 'polygons']
327
+
328
+ _geo_types = sorted(
329
+ _gridded_types + _geom_types + ['points', 'vectorfield', 'labels', 'hexbin', 'bivariate']
330
+ )
331
+
332
+ _stats_types = ['hist', 'kde', 'violin', 'box', 'density']
333
+
334
+ _data_options = [
335
+ 'x',
336
+ 'y',
337
+ 'kind',
338
+ 'by',
339
+ 'use_index',
340
+ 'use_dask',
341
+ 'dynamic',
342
+ 'crs',
343
+ 'value_label',
344
+ 'group_label',
345
+ 'backlog',
346
+ 'persist',
347
+ 'sort_date',
348
+ ]
349
+
350
+ _geo_options = [
351
+ 'geo',
352
+ 'crs',
353
+ 'features',
354
+ 'project',
355
+ 'coastline',
356
+ 'tiles',
357
+ 'projection',
358
+ 'global_extents',
359
+ ]
360
+
361
+ _axis_options = [
362
+ 'width',
363
+ 'height',
364
+ 'shared_axes',
365
+ 'grid',
366
+ 'legend',
367
+ 'rot',
368
+ 'xlim',
369
+ 'ylim',
370
+ 'xticks',
371
+ 'yticks',
372
+ 'colorbar',
373
+ 'invert',
374
+ 'title',
375
+ 'logx',
376
+ 'logy',
377
+ 'loglog',
378
+ 'xaxis',
379
+ 'yaxis',
380
+ 'xformatter',
381
+ 'yformatter',
382
+ 'xlabel',
383
+ 'ylabel',
384
+ 'clabel',
385
+ 'padding',
386
+ 'responsive',
387
+ 'max_height',
388
+ 'max_width',
389
+ 'min_height',
390
+ 'min_width',
391
+ 'frame_height',
392
+ 'frame_width',
393
+ 'aspect',
394
+ 'data_aspect',
395
+ 'fontscale',
396
+ 'bgcolor',
397
+ ]
398
+
399
+ _style_options = [
400
+ 'color',
401
+ 'alpha',
402
+ 'colormap',
403
+ 'fontsize',
404
+ 'c',
405
+ 'cmap',
406
+ 'color_key',
407
+ 'cnorm',
408
+ 'rescale_discrete_levels',
409
+ ]
410
+
411
+ _op_options = [
412
+ 'datashade',
413
+ 'rasterize',
414
+ 'x_sampling',
415
+ 'y_sampling',
416
+ 'downsample',
417
+ 'aggregator',
418
+ 'resample_when',
419
+ ]
420
+
421
+ # Options specific to a particular plot type
422
+ _kind_options = {
423
+ 'area': ['y2'],
424
+ 'errorbars': ['yerr1', 'yerr2'],
425
+ 'bivariate': ['bandwidth', 'cut', 'filled', 'levels'],
426
+ 'contour': ['z', 'levels', 'logz'],
427
+ 'contourf': ['z', 'levels', 'logz'],
428
+ 'dataset': ['columns'],
429
+ 'heatmap': ['C', 'reduce_function', 'logz'],
430
+ 'hexbin': ['C', 'reduce_function', 'gridsize', 'logz', 'min_count'],
431
+ 'hist': ['bins', 'bin_range', 'normed', 'cumulative'],
432
+ 'image': ['z', 'logz'],
433
+ 'kde': ['bw_method', 'ind', 'bandwidth', 'cut', 'filled'],
434
+ 'labels': ['text', 'c', 'xoffset', 'yoffset', 'text_font', 'text_font_size'],
435
+ 'ohlc': ['bar_width', 'pos_color', 'neg_color', 'line_color'],
436
+ 'points': ['s', 'marker', 'c', 'scale', 'logz'],
437
+ 'polygons': ['logz', 'c'],
438
+ 'rgb': ['z', 'bands'],
439
+ 'scatter': ['s', 'c', 'scale', 'logz', 'marker'],
440
+ 'step': ['where'],
441
+ 'table': ['columns'],
442
+ 'quadmesh': ['z', 'logz'],
443
+ 'vectorfield': ['angle', 'mag'],
444
+ }
445
+
446
+ # Mapping from kind to HoloViews element type
447
+ _kind_mapping = {
448
+ 'area': Area,
449
+ 'bar': Bars,
450
+ 'barh': Bars,
451
+ 'bivariate': Bivariate,
452
+ 'box': BoxWhisker,
453
+ 'contour': Contours,
454
+ 'contourf': Polygons,
455
+ 'dataset': Dataset,
456
+ 'density': Distribution,
457
+ 'errorbars': ErrorBars,
458
+ 'hist': Histogram,
459
+ 'image': Image,
460
+ 'kde': Distribution,
461
+ 'labels': Labels,
462
+ 'line': Curve,
463
+ 'scatter': Scatter,
464
+ 'heatmap': HeatMap,
465
+ 'hexbin': HexTiles,
466
+ 'ohlc': Rectangles,
467
+ 'paths': Path,
468
+ 'points': Points,
469
+ 'polygons': Polygons,
470
+ 'quadmesh': QuadMesh,
471
+ 'rgb': RGB,
472
+ 'step': Curve,
473
+ 'table': Table,
474
+ 'vectorfield': VectorField,
475
+ 'violin': Violin,
476
+ }
477
+
478
+ # Types which have a colorbar by default
479
+ _colorbar_types = [
480
+ 'bivariate',
481
+ 'contour',
482
+ 'contourf',
483
+ 'heatmap',
484
+ 'image',
485
+ 'hexbin',
486
+ 'quadmesh',
487
+ 'polygons',
488
+ ]
489
+
490
+ _legend_positions = (
491
+ 'top_right',
492
+ 'top_left',
493
+ 'bottom_left',
494
+ 'bottom_right',
495
+ 'right',
496
+ 'left',
497
+ 'top',
498
+ 'bottom',
499
+ )
500
+
501
+ _default_plot_opts = {
502
+ 'logx': False,
503
+ 'logy': False,
504
+ 'show_legend': True,
505
+ 'legend_position': 'right',
506
+ 'show_grid': False,
507
+ 'responsive': False,
508
+ 'shared_axes': True,
509
+ }
510
+
511
+ _default_cmaps = {
512
+ 'linear': 'kbc_r',
513
+ 'categorical': cc.palette['glasbey_category10'],
514
+ 'cyclic': 'colorwheel',
515
+ 'diverging': 'coolwarm',
516
+ }
517
+
518
+ def __init__(
519
+ self,
520
+ data,
521
+ x,
522
+ y,
523
+ kind=None,
524
+ by=None,
525
+ use_index=True,
526
+ group_label=None,
527
+ value_label='value',
528
+ backlog=1000,
529
+ persist=False,
530
+ use_dask=False,
531
+ crs=None,
532
+ fields={},
533
+ groupby=None,
534
+ dynamic=True,
535
+ grid=None,
536
+ legend=None,
537
+ rot=None,
538
+ title=None,
539
+ xlim=None,
540
+ ylim=None,
541
+ clim=None,
542
+ robust=None,
543
+ symmetric=None,
544
+ logx=None,
545
+ logy=None,
546
+ loglog=None,
547
+ hover=None,
548
+ subplots=False,
549
+ label=None,
550
+ invert=False,
551
+ stacked=False,
552
+ colorbar=None,
553
+ datashade=False,
554
+ rasterize=False,
555
+ downsample=None,
556
+ resample_when=None,
557
+ row=None,
558
+ col=None,
559
+ debug=False,
560
+ framewise=True,
561
+ aggregator=None,
562
+ projection=None,
563
+ global_extent=None,
564
+ geo=False,
565
+ precompute=False,
566
+ flip_xaxis=None,
567
+ flip_yaxis=None,
568
+ dynspread=False,
569
+ hover_cols=[],
570
+ x_sampling=None,
571
+ y_sampling=None,
572
+ project=False,
573
+ tools=[],
574
+ attr_labels=None,
575
+ coastline=False,
576
+ tiles=False,
577
+ tiles_opts=None,
578
+ sort_date=True,
579
+ check_symmetric_max=1000000,
580
+ transforms={},
581
+ stream=None,
582
+ cnorm=None,
583
+ features=None,
584
+ rescale_discrete_levels=None,
585
+ autorange=None,
586
+ **kwds,
587
+ ):
588
+ # Process data and related options
589
+ self._redim = fields
590
+ self.use_index = use_index
591
+ self.value_label = value_label
592
+ self.label = label
593
+ self._process_data(
594
+ kind,
595
+ data,
596
+ x,
597
+ y,
598
+ by,
599
+ groupby,
600
+ row,
601
+ col,
602
+ use_dask,
603
+ persist,
604
+ backlog,
605
+ label,
606
+ group_label,
607
+ value_label,
608
+ hover_cols,
609
+ attr_labels,
610
+ transforms,
611
+ stream,
612
+ robust,
613
+ kwds,
614
+ )
615
+
616
+ self.dynamic = dynamic
617
+ self.geo = any([geo, crs, global_extent, projection, project, coastline, features])
618
+ self.crs = self._process_crs(data, crs) if self.geo else None
619
+ self.output_projection = self.crs
620
+ self.project = project
621
+ self.coastline = coastline
622
+ self.features = features
623
+ self.tiles = tiles
624
+ self.tiles_opts = tiles_opts or {}
625
+ self.sort_date = sort_date
626
+
627
+ # Import geoviews if geo-features requested
628
+ if self.geo or self.datatype == 'geopandas':
629
+ try:
630
+ import geoviews # noqa
631
+ except ImportError:
632
+ raise ImportError(
633
+ 'In order to use geo-related features '
634
+ 'the geoviews library must be available. '
635
+ 'It can be installed with:\n conda '
636
+ 'install geoviews'
637
+ )
638
+ if self.geo:
639
+ if self.kind not in self._geo_types:
640
+ param.main.param.warning(
641
+ f'geo option cannot be used with kind={self.kind!r} plot '
642
+ 'type. Geographic plots are only supported for '
643
+ f'following plot types: {self._geo_types!r}'
644
+ )
645
+ from cartopy import crs as ccrs
646
+ from geoviews.util import project_extents
647
+
648
+ if isinstance(projection, str):
649
+ all_crs = [
650
+ proj
651
+ for proj in dir(ccrs)
652
+ if callable(getattr(ccrs, proj))
653
+ and proj not in ['ABCMeta', 'CRS']
654
+ and proj[0].isupper()
655
+ or proj == 'GOOGLE_MERCATOR'
656
+ ]
657
+ if projection in all_crs and projection != 'GOOGLE_MERCATOR':
658
+ projection = getattr(ccrs, projection)()
659
+ elif projection == 'GOOGLE_MERCATOR':
660
+ projection = getattr(ccrs, projection)
661
+ else:
662
+ raise ValueError(
663
+ 'Projection must be defined as cartopy CRS or '
664
+ f'one of the following CRS string:\n {all_crs}'
665
+ )
666
+
667
+ self.output_projection = projection or (ccrs.GOOGLE_MERCATOR if tiles else self.crs)
668
+ if tiles and self.output_projection != ccrs.GOOGLE_MERCATOR:
669
+ raise ValueError(
670
+ 'Tiles can only be used with output projection of '
671
+ '`cartopy.crs.GOOGLE_MERCATOR`. To get rid of this error '
672
+ 'remove `projection=` or `tiles=`'
673
+ )
674
+ if self.crs != projection and (xlim or ylim):
675
+ px0, py0, px1, py1 = ccrs.GOOGLE_MERCATOR.boundary.bounds
676
+ x0, x1 = xlim or (px0, px1)
677
+ y0, y1 = ylim or (py0, py1)
678
+ extents = (x0, y0, x1, y1)
679
+ x0, y0, x1, y1 = project_extents(extents, self.crs, self.output_projection)
680
+ if xlim:
681
+ xlim = (x0, x1)
682
+ if ylim:
683
+ ylim = (y0, y1)
684
+
685
+ # Operations
686
+ if resample_when is not None and not any([rasterize, datashade, downsample]):
687
+ raise ValueError(
688
+ 'At least one resampling operation (rasterize, datashader, '
689
+ 'downsample) must be enabled when resample_when is set.'
690
+ )
691
+ self.resample_when = resample_when
692
+ self.datashade = datashade
693
+ self.rasterize = rasterize
694
+ self.downsample = downsample
695
+ self.dynspread = dynspread
696
+ self.aggregator = aggregator
697
+ self.precompute = precompute
698
+ self.x_sampling = x_sampling
699
+ self.y_sampling = y_sampling
700
+
701
+ # By type
702
+ self.subplots = subplots
703
+ self._by_type = NdLayout if subplots else NdOverlay
704
+
705
+ self._backend = Store.current_backend
706
+ if hvplot_extension.compatibility is None:
707
+ self._backend_compat = self._backend
708
+ else:
709
+ self._backend_compat = hvplot_extension.compatibility
710
+
711
+ self.stacked = stacked
712
+
713
+ plot_opts = dict(self._default_plot_opts, **self._process_plot())
714
+ if xlim is not None:
715
+ plot_opts['xlim'] = tuple(xlim)
716
+ if ylim is not None:
717
+ plot_opts['ylim'] = tuple(ylim)
718
+
719
+ if autorange is not None:
720
+ if hv_version < Version('1.16.0'):
721
+ param.main.param.warning('autorange option requires HoloViews >= 1.16')
722
+ else:
723
+ plot_opts['autorange'] = autorange
724
+
725
+ self.invert = invert
726
+ if loglog is not None:
727
+ logx = logx or loglog
728
+ logy = logy or loglog
729
+ if logx is not None:
730
+ plot_opts['logx'] = logx
731
+ if logy is not None:
732
+ plot_opts['logy'] = logy
733
+
734
+ if grid is not None:
735
+ plot_opts['show_grid'] = grid
736
+
737
+ if legend is not None:
738
+ plot_opts['show_legend'] = bool(legend)
739
+
740
+ if legend in self._legend_positions:
741
+ plot_opts['legend_position'] = legend
742
+ elif legend not in (True, False, None):
743
+ raise ValueError(
744
+ 'The legend option should be a boolean or '
745
+ 'a valid legend position (i.e. one of %s).' % list(self._legend_positions)
746
+ )
747
+ plotwds = [
748
+ 'xticks',
749
+ 'yticks',
750
+ 'xlabel',
751
+ 'ylabel',
752
+ 'clabel',
753
+ 'padding',
754
+ 'xformatter',
755
+ 'yformatter',
756
+ 'height',
757
+ 'width',
758
+ 'frame_height',
759
+ 'frame_width',
760
+ 'min_width',
761
+ 'min_height',
762
+ 'max_width',
763
+ 'max_height',
764
+ 'fontsize',
765
+ 'fontscale',
766
+ 'responsive',
767
+ 'shared_axes',
768
+ 'aspect',
769
+ 'data_aspect',
770
+ 'bgcolor',
771
+ ]
772
+ for plotwd in plotwds:
773
+ if plotwd in kwds:
774
+ plot_opts[plotwd] = kwds.pop(plotwd)
775
+
776
+ self._style_opts, plot_opts, kwds = self._process_style(kwds, plot_opts)
777
+
778
+ for axis_name in ['xaxis', 'yaxis']:
779
+ if axis_name in kwds:
780
+ axis = kwds.pop(axis_name)
781
+ if not axis:
782
+ plot_opts[axis_name] = None
783
+ elif not axis:
784
+ plot_opts[axis_name] = axis
785
+ elif axis_name in plot_opts:
786
+ plot_opts.pop(axis_name, None)
787
+
788
+ if flip_xaxis:
789
+ plot_opts['invert_xaxis'] = True
790
+ if flip_yaxis:
791
+ plot_opts['invert_yaxis'] = True
792
+
793
+ if self.geo and not plot_opts.get('data_aspect'):
794
+ plot_opts['data_aspect'] = 1
795
+
796
+ ignore_opts = ['responsive', 'aspect', 'data_aspect', 'frame_height', 'frame_width']
797
+ if not any(plot_opts.get(opt) for opt in ignore_opts):
798
+ plot_opts['width'] = plot_opts.get('width', 700)
799
+ plot_opts['height'] = plot_opts.get('height', 300)
800
+
801
+ if isinstance(colorbar, bool):
802
+ plot_opts['colorbar'] = colorbar
803
+ elif self.kind in self._colorbar_types:
804
+ plot_opts['colorbar'] = True
805
+ elif self.rasterize:
806
+ plot_opts['colorbar'] = plot_opts.get('colorbar', True)
807
+ if 'logz' in kwds and 'logz' in self._kind_options.get(self.kind, {}):
808
+ plot_opts['logz'] = kwds.pop('logz')
809
+ if invert:
810
+ plot_opts['invert_axes'] = self.kind != 'barh'
811
+ if rot:
812
+ axis = 'yrotation' if invert else 'xrotation'
813
+ plot_opts[axis] = rot
814
+
815
+ tools = list(tools) or list(plot_opts.get('tools', []))
816
+ # Disable hover for errorbars plot as Bokeh annotations can't be hovered.
817
+ if kind == 'errorbars':
818
+ hover = False
819
+ elif hover is None:
820
+ hover = not self.datashade
821
+ if hover and not any(
822
+ t for t in tools if isinstance(t, HoverTool) or t in ['hover', 'vline', 'hline']
823
+ ):
824
+ if hover in ['vline', 'hline']:
825
+ tools.append(hover)
826
+ else:
827
+ tools.append('hover')
828
+ plot_opts['tools'] = tools
829
+
830
+ if self.crs and global_extent:
831
+ plot_opts['global_extent'] = global_extent
832
+ if projection:
833
+ plot_opts['projection'] = self.output_projection
834
+ title = title if title is not None else getattr(self, '_title', None)
835
+ if title is not None:
836
+ plot_opts['title'] = title
837
+ if (
838
+ self.kind in self._colorbar_types
839
+ or self.rasterize
840
+ or self.datashade
841
+ or self._color_dim
842
+ ):
843
+ try:
844
+ if not use_dask:
845
+ symmetric = self._process_symmetric(symmetric, clim, check_symmetric_max)
846
+ if self._style_opts.get('cmap') is None:
847
+ if self._process_categorical_datashader()[0]:
848
+ self._style_opts['cmap'] = self._default_cmaps['categorical']
849
+ elif symmetric:
850
+ self._style_opts['cmap'] = self._default_cmaps['diverging']
851
+ else:
852
+ self._style_opts['cmap'] = self._default_cmaps['linear']
853
+ if robust:
854
+ plot_opts['clim_percentile'] = True
855
+ if symmetric is not None:
856
+ plot_opts['symmetric'] = symmetric
857
+ except TypeError:
858
+ pass
859
+ if cnorm is not None:
860
+ plot_opts['cnorm'] = cnorm
861
+ if rescale_discrete_levels is not None and hv_version >= Version('1.15.0'):
862
+ plot_opts['rescale_discrete_levels'] = rescale_discrete_levels
863
+
864
+ self._plot_opts = plot_opts
865
+ self._overlay_opts = {
866
+ k: v for k, v in self._plot_opts.items() if k in OverlayPlot.param.objects()
867
+ }
868
+
869
+ self._norm_opts = {'framewise': framewise, 'axiswise': not plot_opts.get('shared_axes')}
870
+ self.kwds = kwds
871
+
872
+ # Process dimensions and labels
873
+ self.label = label
874
+ self._relabel = {'label': label} if label else {}
875
+ self._dim_ranges = {'c': clim or (None, None)}
876
+
877
+ # High-level options
878
+ self._validate_kwds(kwds)
879
+ if debug:
880
+ kwds = dict(
881
+ x=self.x,
882
+ y=self.y,
883
+ by=self.by,
884
+ kind=self.kind,
885
+ groupby=self.groupby,
886
+ grid=self.grid,
887
+ )
888
+ param.main.param.warning(
889
+ 'Plotting {kind} plot with parameters x: {x}, '
890
+ 'y: {y}, by: {by}, groupby: {groupby}, row/col: {grid}'.format(**kwds)
891
+ )
892
+
893
+ def _process_categorical_datashader(self):
894
+ categorical, agg = False, None
895
+ if not (self.datashade or self.rasterize):
896
+ return categorical, agg
897
+
898
+ ds = import_datashader()
899
+
900
+ categorical = False
901
+ if self.aggregator:
902
+ agg = self.aggregator
903
+ if isinstance(agg, str) and self._color_dim:
904
+ categorical = agg == 'count_cat'
905
+ agg = getattr(ds.reductions, agg)(self._color_dim)
906
+ else:
907
+ categorical = isinstance(agg, (ds.count_cat, ds.by))
908
+ elif self._color_dim:
909
+ agg = ds.reductions.mean(self._color_dim)
910
+ elif self.by and not self.subplots:
911
+ agg = ds.reductions.count_cat(self.by[0])
912
+ categorical = True
913
+ elif (
914
+ (isinstance(self.y, list) and len(self.y) > 1)
915
+ or (self.y is None and len(set(self.variables) - set(self.indexes)) > 1)
916
+ ) and self.kind in ('scatter', 'line', 'area'):
917
+ agg = ds.reductions.count_cat(self.group_label)
918
+ categorical = True
919
+
920
+ (
921
+ (isinstance(self.y, list) and len(self.y) > 1)
922
+ or (self.y is None and len(set(self.variables) - set(self.indexes)) > 1)
923
+ )
924
+
925
+ return categorical, agg
926
+
927
+ def _process_symmetric(self, symmetric, clim, check_symmetric_max):
928
+ if symmetric is not None or clim is not None:
929
+ return symmetric
930
+
931
+ if is_xarray(self.data):
932
+ # chunks mean it's lazily loaded; nanquantile will eagerly load
933
+ data = self.data[self.z]
934
+ if not getattr(data, '_in_memory', True) or data.chunks:
935
+ return False
936
+ if is_xarray_dataarray(data):
937
+ if data.size > check_symmetric_max:
938
+ return False
939
+ else:
940
+ return False
941
+
942
+ elif self._color_dim:
943
+ data = self.data[self._color_dim]
944
+ else:
945
+ return
946
+
947
+ if data.size == 0:
948
+ return False
949
+
950
+ cmin = np.nanquantile(data, 0.05)
951
+ cmax = np.nanquantile(data, 0.95)
952
+
953
+ return bool(cmin < 0 and cmax > 0)
954
+
955
+ def _process_crs(self, data, crs):
956
+ """Given crs as proj4 string, data.attr, or cartopy.crs return cartopy.crs"""
957
+ if hasattr(data, 'rio') and data.rio.crs is not None:
958
+ # if data is a rioxarray
959
+ _crs = data.rio.crs.to_wkt()
960
+ # get the proj string: either the value of data.attrs[crs] or crs itself
961
+ elif isinstance(crs, str):
962
+ _crs = getattr(data, 'attrs', {}).get(crs or 'crs', crs)
963
+ else:
964
+ _crs = crs
965
+
966
+ try:
967
+ return process_crs(_crs)
968
+ except ValueError as e:
969
+ # only raise error if crs was specified in kwargs
970
+ if crs:
971
+ raise ValueError(
972
+ f"'{crs}' must be either a valid crs or an reference to "
973
+ f'a `data.attr` containing a valid crs: {e}'
974
+ )
975
+
976
+ def _process_data(
977
+ self,
978
+ kind,
979
+ data,
980
+ x,
981
+ y,
982
+ by,
983
+ groupby,
984
+ row,
985
+ col,
986
+ use_dask,
987
+ persist,
988
+ backlog,
989
+ label,
990
+ group_label,
991
+ value_label,
992
+ hover_cols,
993
+ attr_labels,
994
+ transforms,
995
+ stream,
996
+ robust,
997
+ kwds,
998
+ ):
999
+ gridded = kind in self._gridded_types
1000
+ gridded_data = False
1001
+ da = None
1002
+
1003
+ # Validate DataSource
1004
+ self.data_source = data
1005
+ self.is_series = is_series(data)
1006
+ if self.is_series:
1007
+ data = data.to_frame()
1008
+ if is_intake(data):
1009
+ data = process_intake(data, use_dask or persist)
1010
+ # Pandas interface in HoloViews doesn't accept non-string columns.
1011
+ # The converter stores a reference to the source data to
1012
+ # update the `_dataset` property (of the hv object its __call__ method
1013
+ # returns) with a hv Dataset created from the source data, which
1014
+ # is done for optimizating some operations in HoloViews.
1015
+ data = _convert_col_names_to_str(data)
1016
+
1017
+ self.source_data = data
1018
+
1019
+ if groupby is not None and not isinstance(groupby, list):
1020
+ groupby = [groupby]
1021
+ if by is not None and not isinstance(by, list):
1022
+ by = [by]
1023
+ grid = []
1024
+ if row is not None:
1025
+ grid.append(row)
1026
+ if col is not None:
1027
+ grid.append(col)
1028
+ streaming = False
1029
+ if is_geodataframe(data):
1030
+ datatype = 'geopandas' if hasattr(data, 'geom_type') else 'spatialpandas'
1031
+ self.data = data
1032
+ if kind is None:
1033
+ if datatype == 'geopandas':
1034
+ geom_types = {gt[5:] if 'Multi' in gt else gt for gt in data.geom_type}
1035
+ else:
1036
+ geom_types = [
1037
+ type(data.geometry.dtype)
1038
+ .__name__.replace('Multi', '')
1039
+ .replace('Dtype', '')
1040
+ ]
1041
+ if len(geom_types) > 1:
1042
+ raise ValueError(
1043
+ 'The GeopandasInterface can only read dataframes which '
1044
+ 'share a common geometry type'
1045
+ )
1046
+ geom_type = list(geom_types)[0]
1047
+ if geom_type == 'Point':
1048
+ kind = 'points'
1049
+ elif geom_type == 'Polygon':
1050
+ kind = 'polygons'
1051
+ elif geom_type in ('LineString', 'LineRing', 'Line'):
1052
+ kind = 'paths'
1053
+ # if only one arg is provided, treat it like color
1054
+ if x is not None and y is None:
1055
+ kwds['color'] = kwds.pop('color', kwds.pop('c', x))
1056
+ x = None
1057
+ elif isinstance(data, pd.DataFrame):
1058
+ datatype = 'pandas'
1059
+ self.data = data
1060
+ elif is_dask(data):
1061
+ datatype = 'dask'
1062
+ self.data = data.persist() if persist else data
1063
+ elif is_cudf(data):
1064
+ datatype = 'cudf'
1065
+ self.data = data
1066
+ elif is_ibis(data):
1067
+ datatype = 'ibis'
1068
+ self.data = data
1069
+ elif is_streamz(data):
1070
+ datatype = 'streamz'
1071
+ self.data = data.example
1072
+ if isinstance(self.data, pd.DataFrame):
1073
+ self.data = self.data.iloc[:0]
1074
+ self.stream_type = data._stream_type
1075
+ streaming = True
1076
+ self.cb = data
1077
+ if data._stream_type == 'updating':
1078
+ self.stream = Pipe(data=self.data)
1079
+ else:
1080
+ self.stream = Buffer(data=self.data, length=backlog, index=False)
1081
+ data.stream.gather().sink(self.stream.send)
1082
+ elif is_xarray(data):
1083
+ import xarray as xr
1084
+
1085
+ z = kwds.get('z')
1086
+ if isinstance(data, xr.Dataset):
1087
+ if len(data.data_vars) == 0:
1088
+ raise ValueError('Cannot plot an empty xarray.Dataset object.')
1089
+ if z is None:
1090
+ if isinstance(data, xr.Dataset):
1091
+ z = list(data.data_vars)[0]
1092
+ else:
1093
+ z = data.name or label or value_label
1094
+ if gridded and isinstance(data, xr.Dataset) and not isinstance(z, list):
1095
+ data = data[z]
1096
+ self.z = z
1097
+
1098
+ ignore = (groupby or []) + (by or []) + grid
1099
+ coords = [c for c in data.coords if data[c].shape != () and c not in ignore]
1100
+ dims = [c for c in data.dims if data[c].shape != () and c not in ignore]
1101
+
1102
+ if kind is None and (not (x or y) or all(c in data.coords for c in (x, y))):
1103
+ if list(data.coords) == ['band', 'y', 'x']:
1104
+ kind = 'rgb'
1105
+ gridded = True
1106
+ elif len(coords) == 1:
1107
+ kind = 'line'
1108
+ elif len(coords) == 2 or (x and y) or len([c for c in coords if c in dims]) == 2:
1109
+ kind = 'image'
1110
+ gridded = True
1111
+ else:
1112
+ kind = 'hist'
1113
+
1114
+ datatype = 'dask' if use_dask else 'pandas'
1115
+ if gridded:
1116
+ datatype = 'xarray'
1117
+ gridded_data = True
1118
+ if kind == 'rgb':
1119
+ if 'bands' in kwds:
1120
+ other_dims = [kwds['bands']]
1121
+ else:
1122
+ other_dims = [d for d in data.coords if d not in (groupby or [])][0]
1123
+ else:
1124
+ other_dims = []
1125
+ da = data
1126
+ data, x, y, by_new, groupby_new = process_xarray(
1127
+ data,
1128
+ x,
1129
+ y,
1130
+ by,
1131
+ groupby,
1132
+ use_dask,
1133
+ persist,
1134
+ gridded,
1135
+ label,
1136
+ value_label,
1137
+ other_dims,
1138
+ kind=kind,
1139
+ )
1140
+ if kind == 'rgb' and robust:
1141
+ # adapted from xarray
1142
+ # https://github.com/pydata/xarray/blob/6af547cdd9beac3b18420ccb204f801603e11519/xarray/plot/utils.py#L729
1143
+ vmax = np.nanpercentile(data[z], 100 - 2)
1144
+ vmin = np.nanpercentile(data[z], 2)
1145
+ # Scale interval [vmin .. vmax] to [0 .. 1], with darray as 64-bit float
1146
+ # to avoid precision loss, integer over/underflow, etc with extreme inputs.
1147
+ # After scaling, downcast to 32-bit float. This substantially reduces
1148
+ # memory usage
1149
+ data[z] = ((data[z].astype('f8') - vmin) / (vmax - vmin)).astype('f4')
1150
+ data[z] = np.minimum(np.maximum(data[z], 0), 1)
1151
+
1152
+ if kind not in self._stats_types:
1153
+ if by is None:
1154
+ by = by_new
1155
+ if groupby is None:
1156
+ groupby = groupby_new
1157
+
1158
+ if groupby:
1159
+ groupby = [g for g in groupby if g not in grid]
1160
+
1161
+ # Add a title to hvplot.xarray plots that displays scalar coords values,
1162
+ # as done by xarray.plot()
1163
+ if not groupby and not grid:
1164
+ if isinstance(da, xr.DataArray):
1165
+ self._title = da._title_for_slice()
1166
+ elif isinstance(da, xr.Dataset):
1167
+ self._title = partial(xr.DataArray._title_for_slice, da)()
1168
+
1169
+ self.data = data
1170
+ else:
1171
+ raise ValueError('Supplied data type %s not understood' % type(data).__name__)
1172
+
1173
+ if stream is not None:
1174
+ if streaming:
1175
+ raise ValueError('Cannot supply streamz.DataFrame and stream argument.')
1176
+ self.stream = stream
1177
+ self.cb = None
1178
+ if isinstance(stream, Pipe):
1179
+ self.stream_type = 'updating'
1180
+ elif isinstance(stream, Buffer):
1181
+ self.stream_type = 'streaming'
1182
+ else:
1183
+ raise ValueError('Stream of type %s not recognized.' % type(stream))
1184
+ streaming = True
1185
+
1186
+ # Validate data and arguments
1187
+ if by is None:
1188
+ by = []
1189
+ if groupby is None:
1190
+ groupby = []
1191
+
1192
+ if gridded_data:
1193
+ not_found = [g for g in groupby if g not in data.coords]
1194
+ not_found, _, _ = process_derived_datetime_xarray(data, not_found)
1195
+ data_vars = list(data.data_vars) if isinstance(data, xr.Dataset) else [data.name]
1196
+ indexes = list(data.coords.indexes)
1197
+ # Handle undeclared indexes
1198
+ if x is not None and x not in indexes:
1199
+ indexes.append(x)
1200
+ if y is not None and y not in indexes:
1201
+ indexes.append(y)
1202
+ for data_dim in data.dims:
1203
+ if not any(data_dim in data[c].dims for c in indexes):
1204
+ for coord in data.coords:
1205
+ if coord not in indexes and {data_dim} == set(data[coord].dims):
1206
+ indexes.append(data_dim)
1207
+ self.data = self.data.set_index({data_dim: coord})
1208
+ if coord not in groupby + by:
1209
+ groupby.append(data_dim)
1210
+ self.variables = list(data.coords) + data_vars
1211
+ if groupby and not_found:
1212
+ raise ValueError(
1213
+ f'The supplied groupby dimension(s) {not_found} '
1214
+ 'could not be found, expected one or '
1215
+ f'more of: {list(data.coords)}'
1216
+ )
1217
+ else:
1218
+ if gridded and kind not in ('points', 'dataset'):
1219
+ raise ValueError(
1220
+ f'{kind} plot type requires gridded data, '
1221
+ 'e.g. a NumPy array or xarray Dataset, '
1222
+ f'found {type(self.data).__name__} type'
1223
+ )
1224
+
1225
+ if (
1226
+ hasattr(data, 'columns')
1227
+ and hasattr(data.columns, 'name')
1228
+ and data.columns.name
1229
+ and not group_label
1230
+ ):
1231
+ group_label = data.columns.name
1232
+ elif not group_label:
1233
+ group_label = 'Variable'
1234
+
1235
+ if (
1236
+ isinstance(data.columns, pd.MultiIndex)
1237
+ and x in (None, 'index')
1238
+ and y is None
1239
+ and not by
1240
+ ):
1241
+ self.data = data.stack().reset_index(1).rename(columns={'level_1': group_label})
1242
+ by = group_label
1243
+ x = 'index'
1244
+
1245
+ # Determine valid indexes
1246
+ if isinstance(self.data, pd.DataFrame):
1247
+ if self.data.index.names == [None]:
1248
+ indexes = [self.data.index.name or 'index']
1249
+ else:
1250
+ indexes = list(self.data.index.names)
1251
+ elif hasattr(self.data, 'reset_index'):
1252
+ indexes = [
1253
+ c for c in self.data.reset_index().columns if c not in self.data.columns
1254
+ ]
1255
+ else:
1256
+ indexes = [c for c in self.data.columns if c not in self.data.columns]
1257
+
1258
+ if len(indexes) == 2 and not (x or y or by):
1259
+ if kind == 'heatmap':
1260
+ x, y = indexes
1261
+ elif kind in ('bar', 'barh'):
1262
+ x, by = indexes
1263
+
1264
+ self.variables = indexes + list(self.data.columns)
1265
+
1266
+ # Reset groupby dimensions
1267
+ groupby_index = [g for g in groupby if g in indexes]
1268
+ if groupby_index:
1269
+ self.data = self.data.reset_index(groupby_index)
1270
+
1271
+ if isinstance(by, (np.ndarray, pd.Series)):
1272
+ by_cols = []
1273
+ else:
1274
+ by_cols = by if isinstance(by, list) else [by]
1275
+ not_found = [
1276
+ g for g in groupby + by_cols if g not in list(self.data.columns) + indexes
1277
+ ]
1278
+ not_found, self.data = process_derived_datetime_pandas(self.data, not_found, indexes)
1279
+ if groupby and not_found:
1280
+ raise ValueError(
1281
+ f'The supplied groupby dimension(s) {not_found} '
1282
+ 'could not be found, expected one or '
1283
+ f'more of: {list(self.data.columns)}'
1284
+ )
1285
+
1286
+ if transforms:
1287
+ self.data = Dataset(self.data, indexes).transform(**transforms).data
1288
+
1289
+ # Set data-level options
1290
+ self.x = x
1291
+ self.y = y
1292
+ self.kind = kind or 'line'
1293
+ self.datatype = datatype
1294
+ self.gridded = gridded
1295
+ self.gridded_data = gridded_data
1296
+ self.use_dask = use_dask
1297
+ self.indexes = indexes
1298
+ self.group_label = group_label or 'Variable'
1299
+ if isinstance(by, (np.ndarray, pd.Series)):
1300
+ self.data = self.data.assign(_by=by)
1301
+ self.by = ['_by']
1302
+ self.variables.append('_by')
1303
+ elif not by:
1304
+ self.by = []
1305
+ else:
1306
+ self.by = by if isinstance(by, list) else [by]
1307
+ self.groupby = groupby
1308
+ self.grid = grid
1309
+ self.streaming = streaming
1310
+
1311
+ if not hover_cols:
1312
+ self.hover_cols = []
1313
+ elif isinstance(hover_cols, list):
1314
+ self.hover_cols = hover_cols
1315
+ elif hover_cols == 'all' and self.use_index:
1316
+ self.hover_cols = self.variables
1317
+ elif hover_cols == 'all' and not self.use_index:
1318
+ self.hover_cols = [v for v in self.variables if v not in self.indexes]
1319
+ elif hover_cols != 'all' and isinstance(hover_cols, str):
1320
+ self.hover_cols = [hover_cols]
1321
+
1322
+ if self.datatype in ('geopandas', 'spatialpandas'):
1323
+ self.hover_cols = [c for c in self.hover_cols if c != 'geometry']
1324
+
1325
+ if da is not None and attr_labels is True or attr_labels is None:
1326
+ try:
1327
+ var_tuples = [(var, da[var].attrs) for var in da.coords]
1328
+ if isinstance(da, xr.Dataset):
1329
+ var_tuples.extend([(var, da[var].attrs) for var in da.data_vars])
1330
+ else:
1331
+ var_tuples.append((da.name, da.attrs))
1332
+ labels = {}
1333
+ units = {}
1334
+ for var_name, var_attrs in var_tuples:
1335
+ if var_name is None:
1336
+ var_name = 'value'
1337
+ if isinstance(var_attrs.get('long_name'), str):
1338
+ labels[var_name] = var_attrs['long_name']
1339
+ if 'units' in var_attrs:
1340
+ units[var_name] = var_attrs['units']
1341
+ self._redim = self._merge_redim(labels, 'label')
1342
+ self._redim = self._merge_redim(units, 'unit')
1343
+ except Exception as e:
1344
+ if attr_labels is True:
1345
+ param.main.param.warning(
1346
+ 'Unable to auto label using xarray attrs ' f'because {e}'
1347
+ )
1348
+
1349
+ def _process_plot(self):
1350
+ kind = self.kind
1351
+ options = Store.options(backend='bokeh')
1352
+ elname = self._kind_mapping[kind].__name__
1353
+ plot_opts = options[elname].groups['plot'].options if elname in options else {}
1354
+
1355
+ if kind.startswith('bar'):
1356
+ plot_opts['stacked'] = self.stacked
1357
+
1358
+ if kind == 'hist':
1359
+ if self.stacked:
1360
+ param.main.param.warning(
1361
+ 'Stacking for histograms is not yet implemented in '
1362
+ 'holoviews. Use bar plots if stacking is required.'
1363
+ )
1364
+
1365
+ return plot_opts
1366
+
1367
+ def _process_style(self, kwds, plot_opts):
1368
+ kind = self.kind
1369
+ eltype = self._kind_mapping[kind]
1370
+ registry = Store.registry[self._backend_compat]
1371
+
1372
+ if eltype in registry:
1373
+ valid_opts = registry[eltype].style_opts
1374
+ else:
1375
+ valid_opts = []
1376
+
1377
+ cmap_opts = ('cmap', 'colormap', 'color_key')
1378
+ categories = [
1379
+ 'accent',
1380
+ 'category',
1381
+ 'dark',
1382
+ 'colorblind',
1383
+ 'pastel',
1384
+ 'set1',
1385
+ 'set2',
1386
+ 'set3',
1387
+ 'paired',
1388
+ 'glasbey',
1389
+ ]
1390
+ for opt in valid_opts:
1391
+ if opt not in kwds or not isinstance(kwds[opt], list) or opt in cmap_opts:
1392
+ continue
1393
+ kwds[opt] = Cycle(kwds[opt])
1394
+
1395
+ # Process style options
1396
+ options = Store.options(backend=self._backend_compat)
1397
+ elname = eltype.__name__
1398
+ style = options[elname].groups['style'].kwargs if elname in options else {}
1399
+ style_opts = {
1400
+ k: v for k, v in style.items() if not isinstance(v, Cycle) and k not in cmap_opts
1401
+ }
1402
+ style_opts.update(**{k: v for k, v in kwds.items() if k in valid_opts})
1403
+
1404
+ # Color
1405
+ cmap_kwds = set(cmap_opts).intersection(kwds)
1406
+ if len(cmap_kwds) > 1:
1407
+ raise TypeError('Specify at most one of `cmap`, `colormap`, or `color_key`.')
1408
+
1409
+ cmap = kwds.pop(cmap_kwds.pop()) if cmap_kwds else None
1410
+ color = kwds.pop('color', kwds.pop('c', None))
1411
+
1412
+ if color is not None:
1413
+ if (self.datashade or self.rasterize) and color in [self.x, self.y]:
1414
+ self.data = self.data.assign(_color=self.data[color])
1415
+ style_opts['color'] = color = '_color'
1416
+ self.variables.append('_color')
1417
+ elif isinstance(color, (np.ndarray, pd.Series)):
1418
+ self.data = self.data.assign(_color=color)
1419
+ style_opts['color'] = color = '_color'
1420
+ self.variables.append('_color')
1421
+ else:
1422
+ style_opts['color'] = color
1423
+
1424
+ if (
1425
+ not isinstance(color, list)
1426
+ and color in self.variables
1427
+ and 'c' in self._kind_options.get(kind, [])
1428
+ ):
1429
+ if self.data[color].dtype.kind in 'OSU':
1430
+ cmap = cmap or self._default_cmaps['categorical']
1431
+ else:
1432
+ plot_opts['colorbar'] = plot_opts.get('colorbar', True)
1433
+
1434
+ if isinstance(cmap, str) and cmap in self._default_cmaps:
1435
+ cmap = self._default_cmaps[cmap]
1436
+
1437
+ if cmap is not None:
1438
+ style_opts['cmap'] = cmap
1439
+
1440
+ if 'color' in style_opts:
1441
+ color = style_opts['color']
1442
+ elif not isinstance(cmap, dict):
1443
+ if cmap and any(c in cmap for c in categories):
1444
+ color = process_cmap(cmap or self._default_cmaps['categorical'], categorical=True)
1445
+ else:
1446
+ color = cmap
1447
+ else:
1448
+ color = style_opts.get('color')
1449
+
1450
+ for k, v in style.items():
1451
+ if isinstance(v, Cycle) and isinstance(v, str):
1452
+ if color == cmap:
1453
+ if color not in Palette.colormaps and color.title() in Palette.colormaps:
1454
+ color = color.title()
1455
+ else:
1456
+ Palette.colormaps[color] = colormap_generator(process_cmap(color))
1457
+ style_opts[k] = Palette(color)
1458
+ elif isinstance(color, list):
1459
+ style_opts[k] = Cycle(values=color)
1460
+ else:
1461
+ style_opts[k] = color
1462
+
1463
+ # Size
1464
+ size = kwds.pop('size', kwds.pop('s', None))
1465
+ if size is not None:
1466
+ scale = kwds.get('scale', 1)
1467
+ if self.datashade or self.rasterize:
1468
+ param.main.param.warning(
1469
+ 'There is no reasonable way to use size (or s) with '
1470
+ 'rasterize or datashade. To aggregate along a third '
1471
+ 'dimension, set color (or c) to the desired dimension.'
1472
+ )
1473
+ if isinstance(size, (np.ndarray, pd.Series)):
1474
+ size = np.sqrt(size) * scale
1475
+ self.data = self.data.assign(_size=size)
1476
+ style_opts['size'] = '_size'
1477
+ self.variables.append('_size')
1478
+ elif isinstance(size, str):
1479
+ style_opts['size'] = np.sqrt(dim(size)) * scale
1480
+ elif not isinstance(size, dim):
1481
+ style_opts['size'] = np.sqrt(size) * scale
1482
+ elif 'size' in valid_opts:
1483
+ style_opts['size'] = np.sqrt(30)
1484
+
1485
+ # Marker
1486
+ if 'marker' in kwds and 'marker' in self._kind_options[self.kind]:
1487
+ style_opts['marker'] = kwds.pop('marker')
1488
+
1489
+ return style_opts, plot_opts, kwds
1490
+
1491
+ def _validate_kwds(self, kwds):
1492
+ kind_opts = self._kind_options.get(self.kind, [])
1493
+ kind = self.kind
1494
+ eltype = self._kind_mapping[kind]
1495
+ if eltype in Store.registry[self._backend_compat]:
1496
+ valid_opts = Store.registry[self._backend_compat][eltype].style_opts
1497
+ else:
1498
+ valid_opts = []
1499
+ ds_opts = ['max_px', 'threshold']
1500
+ mismatches = sorted(k for k in kwds if k not in kind_opts + ds_opts + valid_opts)
1501
+ if not mismatches:
1502
+ return
1503
+
1504
+ if 'ax' in mismatches:
1505
+ mismatches.pop(mismatches.index('ax'))
1506
+ param.main.param.warning(
1507
+ 'hvPlot does not have the concept of axes, '
1508
+ 'and the ax keyword will be ignored. Compose '
1509
+ 'plots with the * operator to overlay plots or the '
1510
+ '+ operator to lay out plots beside each other '
1511
+ 'instead.'
1512
+ )
1513
+ if 'figsize' in mismatches:
1514
+ mismatches.pop(mismatches.index('figsize'))
1515
+ param.main.param.warning(
1516
+ 'hvPlot does not have the concept of a figure, '
1517
+ 'and the figsize keyword will be ignored. The '
1518
+ 'size of each subplot in a layout is set '
1519
+ 'individually using the width and height options.'
1520
+ )
1521
+
1522
+ combined_opts = (
1523
+ self._data_options
1524
+ + self._axis_options
1525
+ + self._op_options
1526
+ + self._geo_options
1527
+ + kind_opts
1528
+ + valid_opts
1529
+ )
1530
+ # Only add the global styling options in the suggestions for bokeh
1531
+ # since they may not be supported by all the backends.
1532
+ # See e.g. alpha for Area plots with plotly:
1533
+ # https://github.com/holoviz/holoviews/issues/5226
1534
+ if self._backend_compat == 'bokeh':
1535
+ combined_opts = combined_opts + self._style_options
1536
+ for mismatch in mismatches:
1537
+ suggestions = difflib.get_close_matches(mismatch, combined_opts)
1538
+ param.main.param.warning(
1539
+ f'{mismatch} option not found for {self.kind} plot with '
1540
+ f'{self._backend_compat}; similar options include: {suggestions}'
1541
+ )
1542
+
1543
+ def __call__(self, kind, x, y):
1544
+ kind = self.kind or kind
1545
+ method = getattr(self, kind)
1546
+
1547
+ groups = self.groupby
1548
+ zs = self.kwds.get('z', [])
1549
+ if not isinstance(zs, list):
1550
+ zs = [zs]
1551
+ groups += self.grid
1552
+ if self.gridded and not kind == 'points':
1553
+ groups += self.by
1554
+ if groups or len(zs) > 1:
1555
+ if self.streaming:
1556
+ raise NotImplementedError('Streaming and groupby not yet implemented')
1557
+ data = self.data
1558
+ if not self.gridded and any(g in self.indexes for g in groups):
1559
+ data = data.reset_index()
1560
+
1561
+ if self.datatype in ('geopandas', 'spatialpandas'):
1562
+ columns = [c for c in data.columns if c != 'geometry']
1563
+ shape_dims = ['Longitude', 'Latitude'] if self.geo else ['x', 'y']
1564
+ dataset = Dataset(data, kdims=shape_dims + columns)
1565
+ elif self.datatype == 'xarray':
1566
+ import xarray as xr
1567
+
1568
+ if isinstance(data, xr.Dataset):
1569
+ if kind == 'image':
1570
+ dims_with_coords = set(data.coords.keys())
1571
+ missing_coords = set(self.indexes) - dims_with_coords
1572
+ new_coords = {
1573
+ coord: np.arange(data[coord].shape[0]) for coord in missing_coords
1574
+ }
1575
+ data = data.assign_coords(**new_coords)
1576
+ dataset = Dataset(data, self.indexes)
1577
+ else:
1578
+ name = data.name or self.label or self.value_label
1579
+ dataset = Dataset(data, self.indexes, name)
1580
+ else:
1581
+ dataset = Dataset(data)
1582
+ dataset = dataset.redim(**self._redim)
1583
+
1584
+ if groups:
1585
+ datasets = dataset.groupby(groups, dynamic=self.dynamic)
1586
+ if len(zs) > 1:
1587
+ dimensions = [Dimension(self.group_label, values=zs)] + datasets.kdims
1588
+ if self.dynamic:
1589
+
1590
+ def z_wrapper(**kwargs):
1591
+ z = kwargs.pop(self.group_label)
1592
+ ds = datasets.select(**kwargs)
1593
+ return ds.reindex(vdims=[ds.get_dimension(z)])
1594
+
1595
+ obj = DynamicMap(z_wrapper, kdims=dimensions)
1596
+
1597
+ def method_wrapper(ds, x, y):
1598
+ z = ds.vdims[0].name
1599
+ el = method(x, y, z, data=ds.data)
1600
+ el._transforms = dataset._transforms
1601
+ el._dataset = ds
1602
+ return el
1603
+
1604
+ obj = obj.apply(
1605
+ method_wrapper, x=x, y=y, per_element=True, link_inputs=False
1606
+ )
1607
+ else:
1608
+ obj = HoloMap(
1609
+ {
1610
+ (z,) + k: method(x, y, z, dataset[k])
1611
+ for k, v in datasets.data.items()
1612
+ for z in zs
1613
+ },
1614
+ kdims=dimensions,
1615
+ )
1616
+ else:
1617
+
1618
+ def method_wrapper(ds, x, y):
1619
+ el = method(x, y, data=ds.data)
1620
+ el._transforms = dataset._transforms
1621
+ el._dataset = ds
1622
+ return el
1623
+
1624
+ obj = datasets.apply(
1625
+ method_wrapper, x=x, y=y, per_element=True, link_inputs=False
1626
+ )
1627
+ elif len(zs) > 1:
1628
+ dimensions = [Dimension(self.group_label, values=zs)]
1629
+ if self.dynamic:
1630
+ obj = DynamicMap(
1631
+ lambda z: method(x, y, z, data=dataset.data), kdims=dimensions
1632
+ )
1633
+ else:
1634
+ obj = HoloMap(
1635
+ {z: method(x, y, z, data=dataset.data) for z in zs}, kdims=dimensions
1636
+ )
1637
+ else:
1638
+ obj = method(x, y, data=dataset.data)
1639
+
1640
+ if self.gridded and self.by and not kind == 'points':
1641
+ obj = obj.layout(self.by) if self.subplots else obj.overlay(self.by)
1642
+ if self.grid:
1643
+ obj = obj.grid(self.grid).opts(
1644
+ shared_xaxis=True, shared_yaxis=True, backend='bokeh'
1645
+ )
1646
+ else:
1647
+ if self.streaming:
1648
+ cb = partial(method, x, y)
1649
+ if self.cb is None:
1650
+ cbcallable = cb
1651
+ else:
1652
+ cbcallable = StreamingCallable(cb, periodic=self.cb)
1653
+ obj = DynamicMap(cbcallable, streams=[self.stream])
1654
+ else:
1655
+ data = self.source_data
1656
+ if self.datatype in ('geopandas', 'spatialpandas'):
1657
+ columns = [c for c in data.columns if c != 'geometry']
1658
+ shape_dims = ['Longitude', 'Latitude'] if self.geo else ['x', 'y']
1659
+ dataset = Dataset(data, kdims=shape_dims + columns)
1660
+ elif self.datatype == 'xarray':
1661
+ import xarray as xr
1662
+
1663
+ if isinstance(data, xr.Dataset):
1664
+ dataset = Dataset(data, self.indexes)
1665
+ else:
1666
+ name = data.name or self.label or self.value_label
1667
+ dataset = Dataset(data, self.indexes, name)
1668
+ else:
1669
+ try:
1670
+ dataset = Dataset(data, self.indexes)
1671
+ except Exception:
1672
+ dataset = Dataset(data)
1673
+ dataset = dataset.redim(**self._redim)
1674
+
1675
+ obj = method(x, y)
1676
+ obj._dataset = dataset
1677
+
1678
+ if self.crs and self.project:
1679
+ # Apply projection before rasterizing
1680
+ import geoviews as gv
1681
+
1682
+ obj = gv.project(obj, projection=self.output_projection)
1683
+
1684
+ if not (self.datashade or self.rasterize or self.downsample):
1685
+ layers = self._apply_layers(obj)
1686
+ layers = _transfer_opts_cur_backend(layers)
1687
+ return layers
1688
+
1689
+ opts = dict(dynamic=self.dynamic)
1690
+ if self._plot_opts.get('width') is not None:
1691
+ opts['width'] = self._plot_opts['width']
1692
+ if self._plot_opts.get('height') is not None:
1693
+ opts['height'] = self._plot_opts['height']
1694
+
1695
+ if self.downsample:
1696
+ try:
1697
+ from holoviews.operation.downsample import downsample1d
1698
+ except ImportError:
1699
+ raise ImportError('Downsampling requires HoloViews >=1.16')
1700
+
1701
+ # Let HoloViews choose the default algo if 'downsample' is True.
1702
+ # Otherwise, user-specified algorithm
1703
+ if isinstance(self.downsample, str):
1704
+ opts['algorithm'] = self.downsample
1705
+
1706
+ if self.x_sampling:
1707
+ opts['x_sampling'] = self.x_sampling
1708
+ if self._plot_opts.get('xlim') is not None:
1709
+ opts['x_range'] = self._plot_opts['xlim']
1710
+ layers = self._resample_obj(downsample1d, obj, opts)
1711
+ layers = _transfer_opts_cur_backend(layers)
1712
+ return layers
1713
+
1714
+ import_datashader()
1715
+ from holoviews.operation.datashader import datashade, rasterize, dynspread
1716
+
1717
+ categorical, agg = self._process_categorical_datashader()
1718
+ if agg:
1719
+ opts['aggregator'] = agg
1720
+
1721
+ if self.precompute:
1722
+ opts['precompute'] = self.precompute
1723
+ if self.x_sampling:
1724
+ opts['x_sampling'] = self.x_sampling
1725
+ if self.y_sampling:
1726
+ opts['y_sampling'] = self.y_sampling
1727
+ if self._plot_opts.get('xlim') is not None:
1728
+ opts['x_range'] = self._plot_opts['xlim']
1729
+ if self._plot_opts.get('ylim') is not None:
1730
+ opts['y_range'] = self._plot_opts['ylim']
1731
+
1732
+ if 'cmap' in self._style_opts and self.datashade:
1733
+ levels = self._plot_opts.get('color_levels')
1734
+ cmap = self._style_opts['cmap']
1735
+ if isinstance(cmap, dict):
1736
+ opts['color_key'] = cmap
1737
+ else:
1738
+ opts['cmap'] = process_cmap(cmap, levels, categorical=categorical)
1739
+
1740
+ if 'line_width' in self._style_opts:
1741
+ opts['line_width'] = self._style_opts['line_width']
1742
+
1743
+ style = {}
1744
+ if self.datashade:
1745
+ operation = datashade
1746
+ if 'cmap' in opts and 'color_key' not in opts:
1747
+ opts['color_key'] = opts['cmap']
1748
+ eltype = 'RGB'
1749
+ if 'cnorm' in self._plot_opts:
1750
+ opts['cnorm'] = self._plot_opts['cnorm']
1751
+ if 'rescale_discrete_levels' in self._plot_opts:
1752
+ opts['rescale_discrete_levels'] = self._plot_opts['rescale_discrete_levels']
1753
+ elif self.rasterize:
1754
+ operation = rasterize
1755
+ if Version(hv.__version__) < Version('1.18.0a1'):
1756
+ eltype = 'Image'
1757
+ else:
1758
+ eltype = 'ImageStack' if categorical else 'Image'
1759
+ if 'cmap' in self._style_opts:
1760
+ style['cmap'] = self._style_opts['cmap']
1761
+ if self._dim_ranges.get('c', (None, None)) != (None, None):
1762
+ style['clim'] = self._dim_ranges['c']
1763
+
1764
+ processed = self._resample_obj(operation, obj, opts)
1765
+ if self.dynspread:
1766
+ processed = dynspread(
1767
+ processed,
1768
+ max_px=self.kwds.get('max_px', 3),
1769
+ threshold=self.kwds.get('threshold', 0.5),
1770
+ )
1771
+
1772
+ opts = filter_opts(eltype, dict(self._plot_opts, **style), backend='bokeh')
1773
+ layers = self._apply_layers(processed).opts(eltype, **opts, backend='bokeh')
1774
+ layers = _transfer_opts_cur_backend(layers)
1775
+ return layers
1776
+
1777
+ def _resample_obj(self, operation, obj, opts):
1778
+ def exceeds_resample_when(plot):
1779
+ return len(plot) > self.resample_when
1780
+
1781
+ if self.resample_when is not None:
1782
+ processed = apply_when(
1783
+ obj, operation=partial(operation, **opts), predicate=exceeds_resample_when
1784
+ )
1785
+ else:
1786
+ processed = operation(obj, **opts)
1787
+ return processed
1788
+
1789
+ def _get_opts(self, eltype, backend='bokeh', **custom):
1790
+ opts = dict(self._plot_opts, **dict(self._style_opts, **self._norm_opts))
1791
+ opts.update(custom)
1792
+ return filter_opts(eltype, opts, backend=backend)
1793
+
1794
+ def _apply_layers(self, obj):
1795
+ if self.coastline:
1796
+ import geoviews as gv
1797
+
1798
+ coastline = gv.feature.coastline.clone()
1799
+ if self.coastline in ['10m', '50m', '110m']:
1800
+ coastline = coastline.opts(scale=self.coastline)
1801
+ elif self.coastline is not True:
1802
+ param.main.param.warning(
1803
+ 'coastline scale of %s not recognized, must be one '
1804
+ "'10m', '50m' or '110m'." % self.coastline
1805
+ )
1806
+ obj = obj * coastline.opts(projection=self.output_projection)
1807
+
1808
+ if self.features:
1809
+ import geoviews as gv
1810
+
1811
+ for feature in reversed(self.features):
1812
+ feature_obj = getattr(gv.feature, feature)
1813
+ if feature_obj is None:
1814
+ raise ValueError(
1815
+ 'Feature %r was not recognized, must be one of '
1816
+ "'borders', 'coastline', 'lakes', 'land', 'ocean', "
1817
+ "'rivers' and 'states'." % feature
1818
+ )
1819
+ feature_obj = feature_obj.clone()
1820
+ if isinstance(self.features, dict):
1821
+ scale = self.features[feature]
1822
+ if scale not in ['10m', '50m', '110m']:
1823
+ param.main.param.warning(
1824
+ 'Feature scale of %r not recognized, '
1825
+ "must be one of '10m', '50m' or '110m'." % scale
1826
+ )
1827
+ else:
1828
+ feature_obj = feature_obj.opts(scale=scale)
1829
+ if feature_obj.group in ['Land', 'Ocean']:
1830
+ # Underlay land/ocean
1831
+ obj = feature_obj.opts(projection=self.output_projection) * obj
1832
+ else:
1833
+ # overlay everything else
1834
+ obj = obj * feature_obj.opts(projection=self.output_projection)
1835
+
1836
+ if self.tiles:
1837
+ if not self.geo:
1838
+ tiles = self._get_tiles(self.tiles)
1839
+ else:
1840
+ tiles = self._get_tiles(self.tiles, lib='geoviews')
1841
+ tiles = tiles.opts(clone=True, **self.tiles_opts)
1842
+ obj = tiles * obj
1843
+ return obj
1844
+
1845
+ def _get_tiles(self, source, lib='holoviews'):
1846
+ if lib == 'geoviews':
1847
+ import geoviews as gv
1848
+
1849
+ sources = gv.tile_sources.tile_sources
1850
+ kls = gv.element.WMTS
1851
+ types = (kls, hv.element.tiles.Tiles)
1852
+ else:
1853
+ sources = hv.element.tile_sources
1854
+ kls = hv.element.tiles.Tiles
1855
+ types = kls
1856
+
1857
+ tile_source = 'EsriImagery' if source == 'ESRI' else source
1858
+ tiles = None
1859
+ if tile_source is True:
1860
+ tiles = sources['OSM']()
1861
+ elif isinstance(tile_source, str) and tile_source in sources:
1862
+ tiles = sources[tile_source]()
1863
+ elif tile_source in sources.values():
1864
+ tiles = tile_source()
1865
+ elif isinstance(tile_source, types):
1866
+ tiles = tile_source
1867
+ elif 'xyzservices' in sys.modules:
1868
+ import xyzservices
1869
+
1870
+ if isinstance(tile_source, xyzservices.TileProvider):
1871
+ tiles = kls(tile_source)
1872
+ if tiles is None:
1873
+ msg = (
1874
+ f'{tile_source} tiles not recognized. tiles must be either True, a '
1875
+ 'xyzservices.TileProvider instance, a HoloViews'
1876
+ + (' or Geoviews' if lib == 'geoviews' else '')
1877
+ + " basemap string "
1878
+ f"(one of {', '.join(sorted(sources))}), a HoloViews Tiles instance"
1879
+ + (', a Geoviews WMTS instance' if lib == 'geoviews' else '')
1880
+ + '.'
1881
+ )
1882
+ raise ValueError(msg)
1883
+ return tiles
1884
+
1885
+ def _merge_redim(self, ranges, attr='range'):
1886
+ redim = dict(self._redim)
1887
+ for k, r in ranges.items():
1888
+ replace = {attr: r}
1889
+ if k in redim:
1890
+ dim = redim[k]
1891
+ if isinstance(dim, Dimension):
1892
+ dim = dim.clone(**replace)
1893
+ elif isinstance(dim, dict) and 'range' not in dim:
1894
+ dim = dict(dim, **replace)
1895
+ elif isinstance(dim, (tuple, str)):
1896
+ dim = Dimension(dim, **replace)
1897
+ else:
1898
+ dim = replace
1899
+ redim[k] = dim
1900
+ return redim
1901
+
1902
+ def _validate_dim(self, dimension):
1903
+ if isinstance(dimension, dim):
1904
+ dimension = dimension.dimension.name
1905
+ if isinstance(dimension, str) and dimension in self.variables:
1906
+ return dimension
1907
+
1908
+ @property
1909
+ def _color_dim(self):
1910
+ return self._validate_dim(self._style_opts.get('color'))
1911
+
1912
+ def _get_dimensions(self, kdims, vdims):
1913
+ for style in ('color', 'size', 'marker', 'alpha'):
1914
+ dimension = self._style_opts.get(style)
1915
+ dimensions = (kdims if kdims else []) + vdims
1916
+ dimension = self._validate_dim(dimension)
1917
+ if dimension is None:
1918
+ continue
1919
+ elif dimension not in dimensions:
1920
+ vdims.append(dimension)
1921
+ dimensions.append(dimension)
1922
+ agg_col = getattr(self.aggregator, 'column', None)
1923
+ if agg_col is not None:
1924
+ if agg_col not in dimensions:
1925
+ vdims.append(agg_col)
1926
+ dimensions.append(agg_col)
1927
+ for dimension in self.hover_cols:
1928
+ if (
1929
+ isinstance(dimension, str)
1930
+ and dimension in self.variables
1931
+ and dimension not in dimensions
1932
+ ):
1933
+ vdims.append(dimension)
1934
+ return kdims, vdims
1935
+
1936
+ def _set_backends_opts(self, element, cur_opts, compat_opts):
1937
+ # Bokeh is still the main backend
1938
+ element = element.opts(**cur_opts, backend='bokeh')
1939
+ if compat_opts:
1940
+ element = element.opts(**compat_opts, backend=self._backend_compat)
1941
+ return element
1942
+
1943
+ def _get_compat_opts(self, el_name, **custom):
1944
+ # Bokeh is still the main backend
1945
+ cur_opts = self._get_opts(el_name, backend='bokeh', **custom)
1946
+ if self._backend_compat != 'bokeh':
1947
+ # Don't pass custom opts to the compatibility layer
1948
+ compat_opts = self._get_opts(el_name, backend=self._backend_compat)
1949
+ compat_opts = {k: v for k, v in compat_opts.items() if k not in cur_opts}
1950
+ else:
1951
+ compat_opts = {}
1952
+ return cur_opts, compat_opts
1953
+
1954
+ def _error_if_unavailable(self, kind, element=None):
1955
+ """
1956
+ Raise an error if the element is not available with the current backend.
1957
+ """
1958
+ if not element:
1959
+ element = self._kind_mapping[kind]
1960
+ if element not in Store.registry[self._backend]:
1961
+ raise NotImplementedError(
1962
+ f'{kind!r} plot not supported by the {self._backend!r} backend.'
1963
+ )
1964
+
1965
+ ##########################
1966
+ # Simple charts #
1967
+ ##########################
1968
+
1969
+ def single_chart(self, element, x, y, data=None):
1970
+ labelled = ['y' if self.invert else 'x'] if x != 'index' else []
1971
+ if not self.is_series:
1972
+ labelled.append('x' if self.invert else 'y')
1973
+ elif not self.label:
1974
+ self._relabel['label'] = y
1975
+
1976
+ if 'xlabel' in self._plot_opts and 'x' not in labelled:
1977
+ labelled.append('x')
1978
+ if 'ylabel' in self._plot_opts and 'y' not in labelled:
1979
+ labelled.append('y')
1980
+
1981
+ cur_el_opts = self._get_opts(element.name, labelled=labelled, backend='bokeh')
1982
+ compat_el_opts = self._get_opts(
1983
+ element.name, labelled=labelled, backend=self._backend_compat
1984
+ )
1985
+ for opts_ in [cur_el_opts, compat_el_opts]:
1986
+ if 'color' in opts_ and opts_['color'] in data.columns:
1987
+ opts_['color'] = hv.dim(opts_['color'])
1988
+ cur_opts = {
1989
+ element.name: cur_el_opts,
1990
+ 'NdOverlay': filter_opts(
1991
+ 'NdOverlay', dict(self._overlay_opts, batched=False), backend='bokeh'
1992
+ ),
1993
+ }
1994
+ compat_opts = {
1995
+ element.name: compat_el_opts,
1996
+ 'NdOverlay': filter_opts(
1997
+ 'NdOverlay', dict(self._overlay_opts), backend=self._backend_compat
1998
+ ),
1999
+ }
2000
+
2001
+ ys = [y]
2002
+ if element is Area and self.kwds.get('y2'):
2003
+ ys += [self.kwds['y2']]
2004
+ if element is ErrorBars and self.kwds.get('yerr1') and self.kwds.get('yerr2'):
2005
+ ys += [self.kwds['yerr1'], self.kwds['yerr2']]
2006
+ elif element is ErrorBars and self.kwds.get('yerr1'):
2007
+ ys += [self.kwds['yerr1']]
2008
+ kdims, vdims = self._get_dimensions([x], ys)
2009
+
2010
+ if self.by:
2011
+ if element is Bars and not self.subplots:
2012
+ if any(y in self.indexes for y in ys):
2013
+ data = data.reset_index()
2014
+ return (
2015
+ element(data, ([x] if x else []) + self.by, ys)
2016
+ .relabel(**self._relabel)
2017
+ .redim(**self._redim)
2018
+ .opts(cur_opts, backend='bokeh')
2019
+ .opts(compat_opts, backend=self._backend_compat)
2020
+ )
2021
+ chart = (
2022
+ Dataset(data, self.by + kdims, vdims)
2023
+ .to(element, kdims, vdims, self.by)
2024
+ .relabel(**self._relabel)
2025
+ )
2026
+ chart = chart.layout() if self.subplots else chart.overlay(sort=False)
2027
+ else:
2028
+ chart = element(data, kdims, vdims).relabel(**self._relabel)
2029
+ return (
2030
+ chart.redim(**self._redim)
2031
+ .opts(cur_opts, backend='bokeh')
2032
+ .opts(compat_opts, backend=self._backend_compat)
2033
+ )
2034
+
2035
+ def _process_chart_x(self, data, x, y, single_y, categories=None):
2036
+ """This should happen before _process_chart_y"""
2037
+ if x is False:
2038
+ return None
2039
+
2040
+ x = x or (self.x if self.x != y else None)
2041
+ if x is None:
2042
+ if self.use_index:
2043
+ xs = self.indexes
2044
+ else:
2045
+ xs = list(data.columns)
2046
+ xs = [c for c in xs if c not in self.by + self.groupby + self.grid + [y]]
2047
+ x = xs[0] if len(xs) else None
2048
+
2049
+ if not x and not categories:
2050
+ raise ValueError('Could not determine what to plot. Set x explicitly')
2051
+ return x
2052
+
2053
+ def _process_chart_y(self, data, x, y, single_y):
2054
+ """This should happen after _process_chart_x"""
2055
+ y = y or self.y
2056
+ if y is None:
2057
+ ys = [c for c in data.columns if c not in [x] + self.by + self.groupby + self.grid]
2058
+ if len(ys) > 1:
2059
+ # if columns have different dtypes, only include numeric columns
2060
+ from pandas.api.types import is_numeric_dtype as isnum
2061
+
2062
+ num_ys = [dim for dim in ys if isnum(data[dim])]
2063
+ if len(num_ys) >= 1:
2064
+ ys = num_ys
2065
+ y = ys[0] if len(ys) == 1 or single_y else ys
2066
+ return y
2067
+
2068
+ def _process_chart_args(self, data, x, y, single_y=False, categories=None):
2069
+ if data is None:
2070
+ data = self.data
2071
+ elif not self.gridded_data:
2072
+ data = _convert_col_names_to_str(data)
2073
+
2074
+ x = self._process_chart_x(data, x, y, single_y, categories=categories)
2075
+ y = self._process_chart_y(data, x, y, single_y)
2076
+
2077
+ # sort by date if enabled and x is a date
2078
+ if x is not None and self.sort_date and self.datatype == 'pandas':
2079
+ from pandas.api.types import is_datetime64_any_dtype as is_datetime
2080
+
2081
+ if x in self.indexes:
2082
+ index = self.indexes.index(x)
2083
+ if is_datetime(data.axes[index]):
2084
+ data = data.sort_index(axis=self.indexes.index(x))
2085
+ elif x in data.columns:
2086
+ if is_datetime(data[x]):
2087
+ data = data.sort_values(x)
2088
+
2089
+ # set index to column if needed in hover_cols
2090
+ if self.use_index and any(
2091
+ c for c in self.hover_cols if c in self.indexes and c not in data.columns
2092
+ ):
2093
+ data = data.reset_index()
2094
+
2095
+ # calculate any derived time
2096
+ dimensions = []
2097
+ for col in [x, y, self.by, self.hover_cols]:
2098
+ if col is not None:
2099
+ dimensions.extend(col if isinstance(col, list) else [col])
2100
+
2101
+ not_found = [dim for dim in dimensions if dim not in self.variables]
2102
+ _, data = process_derived_datetime_pandas(data, not_found, self.indexes)
2103
+
2104
+ return data, x, y
2105
+
2106
+ def chart(self, element, x, y, data=None):
2107
+ "Helper method for simple x vs. y charts"
2108
+ data, x, y = self._process_chart_args(data, x, y)
2109
+ if x and y and not isinstance(y, (list, tuple)):
2110
+ return self.single_chart(element, x, y, data)
2111
+ elif x and y and len(y) == 1:
2112
+ return self.single_chart(element, x, y[0], data)
2113
+
2114
+ labelled = ['y' if self.invert else 'x'] if x != 'index' else []
2115
+ if self.value_label != 'value':
2116
+ labelled.append('x' if self.invert else 'y')
2117
+
2118
+ if 'xlabel' in self._plot_opts and 'x' not in labelled:
2119
+ labelled.append('x')
2120
+ if 'ylabel' in self._plot_opts and 'y' not in labelled:
2121
+ labelled.append('y')
2122
+
2123
+ cur_opts = {
2124
+ element.name: self._get_opts(element.name, labelled=labelled, backend='bokeh'),
2125
+ 'NdOverlay': filter_opts(
2126
+ 'NdOverlay', dict(self._overlay_opts, batched=False), backend='bokeh'
2127
+ ),
2128
+ }
2129
+ compat_opts = {
2130
+ element.name: self._get_opts(
2131
+ element.name, labelled=labelled, backend=self._backend_compat
2132
+ ),
2133
+ 'NdOverlay': filter_opts(
2134
+ 'NdOverlay', dict(self._overlay_opts), backend=self._backend_compat
2135
+ ),
2136
+ }
2137
+
2138
+ charts = []
2139
+ for c in y:
2140
+ kdims, vdims = self._get_dimensions([x], [c])
2141
+ chart = element(data, kdims, vdims).redim(**{c: self.value_label})
2142
+ charts.append((c, chart.relabel(**self._relabel).redim(**self._redim)))
2143
+ return (
2144
+ self._by_type(charts, self.group_label, sort=False)
2145
+ .opts(cur_opts, backend='bokeh')
2146
+ .opts(compat_opts, backend=self._backend_compat)
2147
+ )
2148
+
2149
+ def line(self, x=None, y=None, data=None):
2150
+ self._error_if_unavailable('line')
2151
+ return self.chart(Curve, x, y, data)
2152
+
2153
+ def step(self, x=None, y=None, data=None):
2154
+ where = self.kwds.get('where', 'mid')
2155
+ return self.line(x, y, data).options('Curve', interpolation='steps-' + where)
2156
+
2157
+ def scatter(self, x=None, y=None, data=None):
2158
+ self._error_if_unavailable('scatter')
2159
+ return self.chart(Scatter, x, y, data)
2160
+
2161
+ def area(self, x=None, y=None, data=None):
2162
+ self._error_if_unavailable('area')
2163
+ areas = self.chart(Area, x, y, data)
2164
+ if self.stacked:
2165
+ areas = areas.map(Area.stack, NdOverlay)
2166
+ return areas
2167
+
2168
+ def errorbars(self, x=None, y=None, data=None):
2169
+ self._error_if_unavailable('errorbars')
2170
+ return self.chart(ErrorBars, x, y, data)
2171
+
2172
+ ##########################
2173
+ # Categorical charts #
2174
+ ##########################
2175
+
2176
+ def _category_plot(self, element, x, y, data):
2177
+ """
2178
+ Helper method to generate element from indexed dataframe.
2179
+ """
2180
+ labelled = ['y' if self.invert else 'x'] if x != 'index' else []
2181
+ if self.value_label != 'value':
2182
+ labelled.append('x' if self.invert else 'y')
2183
+
2184
+ if 'xlabel' in self._plot_opts and 'x' not in labelled:
2185
+ labelled.append('x')
2186
+ if 'ylabel' in self._plot_opts and 'y' not in labelled:
2187
+ labelled.append('y')
2188
+
2189
+ cur_opts, compat_opts = self._get_compat_opts(element.name, labelled=labelled)
2190
+
2191
+ id_vars = [x]
2192
+ if any(v in self.indexes for v in id_vars):
2193
+ data = data.reset_index()
2194
+ data = data[y + [x]]
2195
+
2196
+ if check_library(data, 'dask'):
2197
+ from dask.dataframe import melt
2198
+ else:
2199
+ melt = pd.melt
2200
+
2201
+ df = melt(data, id_vars=[x], var_name=self.group_label, value_name=self.value_label)
2202
+ kdims = [x, self.group_label]
2203
+ vdims = [self.value_label] + self.hover_cols
2204
+ if self.subplots:
2205
+ obj = Dataset(df, kdims, vdims).to(element, x).layout()
2206
+ else:
2207
+ obj = element(df, kdims, vdims)
2208
+ return (
2209
+ obj.redim(**self._redim)
2210
+ .relabel(**self._relabel)
2211
+ .apply(self._set_backends_opts, cur_opts=cur_opts, compat_opts=compat_opts)
2212
+ )
2213
+
2214
+ def bar(self, x=None, y=None, data=None):
2215
+ self._error_if_unavailable('bar')
2216
+ data, x, y = self._process_chart_args(data, x, y, categories=self.by)
2217
+ if (x or self.by) and y and (self.by or not isinstance(y, (list, tuple) or len(y) == 1)):
2218
+ y = y[0] if isinstance(y, (list, tuple)) else y
2219
+ return self.single_chart(Bars, x, y, data)
2220
+ return self._category_plot(Bars, x, list(y), data)
2221
+
2222
+ def barh(self, x=None, y=None, data=None):
2223
+ return self.bar(x, y, data).opts('Bars', invert_axes=True)
2224
+
2225
+ ##########################
2226
+ # Statistical charts #
2227
+ ##########################
2228
+
2229
+ def _stats_plot(self, element, y, data=None):
2230
+ """
2231
+ Helper method to generate element from indexed dataframe.
2232
+ """
2233
+ data, x, y = self._process_chart_args(data, False, y)
2234
+
2235
+ custom = {}
2236
+ if 'color' in self._style_opts:
2237
+ prefix = 'violin' if issubclass(element, Violin) else 'box'
2238
+ custom[prefix + '_fill_color'] = self._style_opts['color']
2239
+ cur_opts, compat_opts = self._get_compat_opts(element.name, **custom)
2240
+ ylim = self._plot_opts.get('ylim', (None, None))
2241
+ if not isinstance(y, (list, tuple)):
2242
+ ranges = {y: ylim}
2243
+ return (
2244
+ element(data, self.by, y)
2245
+ .redim.range(**ranges)
2246
+ .relabel(**self._relabel)
2247
+ .apply(self._set_backends_opts, cur_opts=cur_opts, compat_opts=compat_opts)
2248
+ )
2249
+
2250
+ labelled = ['y' if self.invert else 'x'] if self.group_label != 'Group' else []
2251
+ if self.value_label != 'value':
2252
+ labelled.append('x' if self.invert else 'y')
2253
+
2254
+ if 'xlabel' in self._plot_opts and 'x' not in labelled:
2255
+ labelled.append('x')
2256
+ if 'ylabel' in self._plot_opts and 'y' not in labelled:
2257
+ labelled.append('y')
2258
+
2259
+ cur_opts['labelled'] = labelled
2260
+
2261
+ kdims = [self.group_label]
2262
+ data = data[list(y)]
2263
+ if check_library(data, 'dask'):
2264
+ from dask.dataframe import melt
2265
+ else:
2266
+ melt = pd.melt
2267
+ df = melt(data, var_name=self.group_label, value_name=self.value_label)
2268
+ if list(y) and df[self.value_label].dtype is not data[y[0]].dtype:
2269
+ df[self.value_label] = df[self.value_label].astype(data[y[0]].dtype)
2270
+ redim = self._merge_redim({self.value_label: ylim})
2271
+ return (
2272
+ element(df, kdims, self.value_label)
2273
+ .redim(**redim)
2274
+ .relabel(**self._relabel)
2275
+ .apply(self._set_backends_opts, cur_opts=cur_opts, compat_opts=compat_opts)
2276
+ )
2277
+
2278
+ def box(self, x=None, y=None, data=None):
2279
+ self._error_if_unavailable('box')
2280
+ return self._stats_plot(BoxWhisker, y, data).redim(**self._redim)
2281
+
2282
+ def violin(self, x=None, y=None, data=None):
2283
+ self._error_if_unavailable('violin')
2284
+ try:
2285
+ from holoviews.element import Violin
2286
+ except ImportError:
2287
+ raise ImportError('Violin plot requires HoloViews version >=1.10')
2288
+ return self._stats_plot(Violin, y, data).redim(**self._redim)
2289
+
2290
+ def hist(self, x=None, y=None, data=None):
2291
+ self._error_if_unavailable('hist')
2292
+ data, x, y = self._process_chart_args(data, False, y)
2293
+
2294
+ labelled = ['y'] if self.invert else ['x']
2295
+
2296
+ if 'xlabel' in self._plot_opts and 'x' not in labelled:
2297
+ labelled.append('x')
2298
+ if 'ylabel' in self._plot_opts and 'y' not in labelled:
2299
+ labelled.append('y')
2300
+
2301
+ cur_opts = {
2302
+ 'Histogram': self._get_opts('Histogram', labelled=labelled, backend='bokeh'),
2303
+ 'NdOverlay': filter_opts('NdOverlay', self._overlay_opts, backend='bokeh'),
2304
+ }
2305
+ compat_opts = {
2306
+ 'Histogram': self._get_opts('Histogram', backend=self._backend_compat),
2307
+ 'NdOverlay': filter_opts(
2308
+ 'NdOverlay', self._overlay_opts, backend=self._backend_compat
2309
+ ),
2310
+ }
2311
+ hist_opts = {
2312
+ 'bin_range': self.kwds.get('bin_range', None),
2313
+ 'normed': self.kwds.get('normed', False),
2314
+ 'cumulative': self.kwds.get('cumulative', False),
2315
+ }
2316
+ if 'bins' in self.kwds:
2317
+ bins = self.kwds['bins']
2318
+ if isinstance(bins, int):
2319
+ hist_opts['num_bins'] = bins
2320
+ else:
2321
+ hist_opts['bins'] = bins
2322
+
2323
+ if not isinstance(y, (list, tuple)):
2324
+ if 'bin_range' not in self.kwds:
2325
+ ys = data[y]
2326
+ ymin, ymax = (ys.min(), ys.max())
2327
+ if is_dask(ys):
2328
+ ymin, ymax = ymin.compute(), ymax.compute()
2329
+ elif is_ibis(ys):
2330
+ ymin, ymax = ymin.execute(), ymax.execute()
2331
+ hist_opts['bin_range'] = ymin, ymax
2332
+
2333
+ ds = Dataset(data, self.by)
2334
+ if self.by:
2335
+ hist = hists = histogram(ds.groupby(self.by), dimension=y, **hist_opts)
2336
+ hist = hists.last
2337
+ hists = hists.layout() if self.subplots else hists.overlay(sort=False)
2338
+ else:
2339
+ hists = histogram(ds, dimension=y, **hist_opts)
2340
+
2341
+ return (
2342
+ hists.redim(**self._redim)
2343
+ .opts(cur_opts, backend='bokeh')
2344
+ .opts(compat_opts, backend=self._backend_compat)
2345
+ )
2346
+
2347
+ ranges = []
2348
+ for col in y:
2349
+ if 'bin_range' not in self.kwds:
2350
+ ys = data[col]
2351
+ ymin, ymax = (ys.min(), ys.max())
2352
+ if is_dask(ys):
2353
+ ymin, ymax = ymin.compute(), ymax.compute()
2354
+ elif is_ibis(ys):
2355
+ ymin, ymax = ymin.execute(), ymax.execute()
2356
+ ranges.append((ymin, ymax))
2357
+ if ranges:
2358
+ hist_opts['bin_range'] = max_range(ranges)
2359
+
2360
+ ds = Dataset(data)
2361
+ hists = []
2362
+ for col in y:
2363
+ hist = histogram(ds, dimension=col, **hist_opts)
2364
+ hists.append((col, hist.relabel(**self._relabel)))
2365
+ return (
2366
+ self._by_type(hists, self.group_label, sort=False)
2367
+ .redim(**self._redim)
2368
+ .opts(cur_opts, backend='bokeh')
2369
+ .opts(compat_opts, backend=self._backend_compat)
2370
+ )
2371
+
2372
+ def kde(self, x=None, y=None, data=None):
2373
+ self._error_if_unavailable('kde')
2374
+ bw_method = self.kwds.pop('bw_method', None)
2375
+ ind = self.kwds.pop('ind', None)
2376
+ if bw_method is not None or ind is not None:
2377
+ raise ValueError('hvplot does not support bw_method and ind')
2378
+
2379
+ dist_opts = dict(self.kwds)
2380
+ data, x, y = self._process_chart_args(data, x, y)
2381
+ cur_opts = {
2382
+ 'Distribution': self._get_opts('Distribution', backend='bokeh', **dist_opts),
2383
+ 'Area': self._get_opts('Distribution', backend='bokeh'),
2384
+ 'NdOverlay': filter_opts(
2385
+ 'NdOverlay', dict(self._overlay_opts, legend_limit=0), backend='bokeh'
2386
+ ),
2387
+ }
2388
+ compat_opts = {
2389
+ 'Distribution': self._get_opts(
2390
+ 'Distribution', backend=self._backend_compat, **dist_opts
2391
+ ),
2392
+ 'Area': self._get_opts('Distribution', backend=self._backend_compat),
2393
+ 'NdOverlay': filter_opts(
2394
+ 'NdOverlay', self._overlay_opts, backend=self._backend_compat
2395
+ ),
2396
+ }
2397
+
2398
+ xlim = self._plot_opts.get('xlim', (None, None))
2399
+ if not isinstance(y, (list, tuple)):
2400
+ ranges = {y: xlim}
2401
+ if self.by:
2402
+ dists = Dataset(data).to(Distribution, y, [], self.by)
2403
+ dists = dists.layout() if self.subplots else dists.overlay(sort=False)
2404
+ else:
2405
+ dists = Distribution(data, y, [])
2406
+ else:
2407
+ ranges = {self.value_label: xlim}
2408
+ data = data[y]
2409
+ df = data.melt(var_name=self.group_label, value_name=self.value_label)
2410
+ ds = Dataset(df)
2411
+ if len(df):
2412
+ dists = ds.to(Distribution, self.value_label)
2413
+ dists = dists.layout() if self.subplots else dists.overlay(sort=False)
2414
+ else:
2415
+ vdim = self.value_label + ' Density'
2416
+ dists = NdOverlay({0: Area([], self.value_label, vdim)}, [self.group_label])
2417
+ redim = self._merge_redim(ranges)
2418
+ return (
2419
+ dists.redim(**redim)
2420
+ .relabel(**self._relabel)
2421
+ .opts(cur_opts, backend='bokeh')
2422
+ .opts(compat_opts, backend=self._backend_compat)
2423
+ )
2424
+
2425
+ def density(self, x=None, y=None, data=None):
2426
+ return self.kde(x, y, data)
2427
+
2428
+ ##########################
2429
+ # Other charts #
2430
+ ##########################
2431
+
2432
+ def dataset(self, x=None, y=None, data=None):
2433
+ data = self.data if data is None else data
2434
+ if self.gridded:
2435
+ kdims = [self.x, self.y] if len(self.indexes) == 2 else None
2436
+ return Dataset(data, kdims=kdims).redim(**self._redim)
2437
+ else:
2438
+ return Dataset(data, self.kwds.get('columns')).redim(**self._redim)
2439
+
2440
+ def heatmap(self, x=None, y=None, data=None):
2441
+ self._error_if_unavailable('heatmap')
2442
+ data = self.data if data is None else data
2443
+ cur_opts, compat_opts = self._get_compat_opts('HeatMap')
2444
+
2445
+ if not (x or y) or (x == 'columns' and y in ('index', data.index.name)):
2446
+ cur_opts['labelled'] = []
2447
+ x, y = 'columns', 'index'
2448
+ data = (data.columns, data.index, data.values)
2449
+ z = ['value']
2450
+ else:
2451
+ z = self.kwds.get('C', [c for c in data.columns if c not in (x, y)][0])
2452
+ z = [z] + self.hover_cols
2453
+ self.use_index = False
2454
+ data, x, y = self._process_chart_args(data, x, y, single_y=True)
2455
+
2456
+ redim = self._merge_redim({z[0]: self._dim_ranges['c']})
2457
+ hmap = HeatMap(data, [x, y], z, **self._relabel)
2458
+ if 'reduce_function' in self.kwds:
2459
+ hmap = hmap.aggregate(function=self.kwds['reduce_function'])
2460
+ return hmap.redim(**redim).apply(
2461
+ self._set_backends_opts, cur_opts=cur_opts, compat_opts=compat_opts
2462
+ )
2463
+
2464
+ def hexbin(self, x=None, y=None, data=None):
2465
+ self._error_if_unavailable('hexbin')
2466
+ self.use_index = False
2467
+ data, x, y = self._process_chart_args(data, x, y, single_y=True)
2468
+
2469
+ z = [self.kwds['C']] if self.kwds.get('C') else []
2470
+ z += self.hover_cols
2471
+
2472
+ cur_opts, compat_opts = self._get_compat_opts('HexTiles')
2473
+ if 'reduce_function' in self.kwds:
2474
+ cur_opts['aggregator'] = self.kwds['reduce_function']
2475
+ if 'gridsize' in self.kwds:
2476
+ cur_opts['gridsize'] = self.kwds['gridsize']
2477
+ if 'min_count' in self.kwds:
2478
+ cur_opts['min_count'] = self.kwds['min_count']
2479
+ redim = self._merge_redim({(z[0] if z else 'Count'): self._dim_ranges['c']})
2480
+ element = self._get_element('hexbin')
2481
+ params = dict(self._relabel)
2482
+ if self.geo:
2483
+ params['crs'] = self.crs
2484
+ return (
2485
+ element(data, [x, y], z or [], **params)
2486
+ .redim(**redim)
2487
+ .apply(self._set_backends_opts, cur_opts=cur_opts, compat_opts=compat_opts)
2488
+ )
2489
+
2490
+ def bivariate(self, x=None, y=None, data=None):
2491
+ self._error_if_unavailable('bivariate')
2492
+ self.use_index = False
2493
+ data, x, y = self._process_chart_args(data, x, y, single_y=True)
2494
+
2495
+ cur_opts, compat_opts = self._get_compat_opts('Bivariate', **self.kwds)
2496
+ element = self._get_element('bivariate')
2497
+ return (
2498
+ element(data, [x, y])
2499
+ .redim(**self._redim)
2500
+ .apply(self._set_backends_opts, cur_opts=cur_opts, compat_opts=compat_opts)
2501
+ )
2502
+
2503
+ def ohlc(self, x=None, y=None, data=None):
2504
+ self._error_if_unavailable('ohlc', Rectangles)
2505
+ self._error_if_unavailable('ohlc', Segments)
2506
+ data = self.data if data is None else data
2507
+ if x is None:
2508
+ variables = [var for var in self.variables if var not in self.indexes]
2509
+ if data[variables[0]].dtype.kind == 'M':
2510
+ x = variables[0]
2511
+ else:
2512
+ x = self.indexes[0]
2513
+ width = self.kwds.get('bar_width', 0.5)
2514
+ if y is None:
2515
+ o, h, l, c = [col for col in data.columns if col != x][:4] # noqa: E741
2516
+ else:
2517
+ o, h, l, c = y # noqa: E741
2518
+ neg, pos = self.kwds.get('neg_color', 'red'), self.kwds.get('pos_color', 'green')
2519
+ color_exp = (dim(o) > dim(c)).categorize({True: neg, False: pos})
2520
+ ohlc_cols = [o, h, l, c]
2521
+ if x in self.hover_cols:
2522
+ self.hover_cols.remove(x)
2523
+ vdims = list(dict.fromkeys(ohlc_cols + self.hover_cols))
2524
+ ds = Dataset(data, [x], vdims)
2525
+ if ds.dimension_values(x).dtype.kind in 'SUO':
2526
+ rects = Rectangles(ds, [x, o, x, c])
2527
+ else:
2528
+ if len(ds):
2529
+ sampling = np.min(np.diff(ds[x])) * width / 2.0
2530
+ ds = ds.transform(lbound=dim(x) - sampling, ubound=dim(x) + sampling)
2531
+ else:
2532
+ ds = ds.transform(lbound=dim(x), ubound=dim(x))
2533
+ rects = Rectangles(ds, ['lbound', o, 'ubound', c])
2534
+ segments = Segments(ds, [x, l, x, h])
2535
+ rect_cur_opts, rect_compat_opts = self._get_compat_opts('Rectangles')
2536
+ rect_cur_opts.pop('tools')
2537
+ rect_cur_opts['color'] = color_exp
2538
+ seg_cur_opts, seg_compat_opts = self._get_compat_opts('Segments')
2539
+ tools = seg_cur_opts.pop('tools', [])
2540
+ if 'hover' in tools:
2541
+ tools[tools.index('hover')] = HoverTool(
2542
+ tooltips=[
2543
+ (x, f'@{x}'),
2544
+ ('Open', f'@{o}'),
2545
+ ('High', f'@{h}'),
2546
+ ('Low', f'@{l}'),
2547
+ ('Close', f'@{c}'),
2548
+ ]
2549
+ + [(hc, f'@{hc}') for hc in vdims[4:]]
2550
+ )
2551
+ seg_cur_opts['tools'] = tools
2552
+ seg_cur_opts['color'] = self.kwds.get('line_color', 'black')
2553
+ if 'xlabel' not in seg_cur_opts:
2554
+ seg_cur_opts['xlabel'] = '' if x == 'index' else x
2555
+ if 'ylabel' not in seg_cur_opts:
2556
+ seg_cur_opts['ylabel'] = ''
2557
+ segments = segments.redim(**self._redim).apply(
2558
+ self._set_backends_opts, cur_opts=seg_cur_opts, compat_opts=seg_compat_opts
2559
+ )
2560
+ rects = rects.redim(**self._redim).apply(
2561
+ self._set_backends_opts, cur_opts=rect_cur_opts, compat_opts=rect_compat_opts
2562
+ )
2563
+ return segments * rects
2564
+
2565
+ def table(self, x=None, y=None, data=None):
2566
+ self._error_if_unavailable('table')
2567
+ data = self.data if data is None else data
2568
+ if isinstance(data.index, (DatetimeIndex, MultiIndex)):
2569
+ data = data.reset_index()
2570
+
2571
+ cur_opts, compat_opts = self._get_compat_opts('Table')
2572
+ element = self._get_element('table')
2573
+ return (
2574
+ element(data, self.kwds.get('columns'), [])
2575
+ .redim(**self._redim)
2576
+ .apply(self._set_backends_opts, cur_opts=cur_opts, compat_opts=compat_opts)
2577
+ )
2578
+
2579
+ def labels(self, x=None, y=None, data=None):
2580
+ self._error_if_unavailable('labels')
2581
+ self.use_index = False
2582
+ data, x, y = self._process_chart_args(data, x, y, single_y=True)
2583
+
2584
+ text = self.kwds.get('text')
2585
+ if not text:
2586
+ text = [c for c in data.columns if c not in (x, y)][0]
2587
+ elif text not in data.columns:
2588
+ template_str = text # needed for dask lazy compute
2589
+ data['label'] = data.apply(lambda row: template_str.format(**row), axis=1)
2590
+ text = 'label'
2591
+
2592
+ kdims, vdims = self._get_dimensions([x, y], [text])
2593
+ cur_opts, compat_opts = self._get_compat_opts('Labels')
2594
+ element = self._get_element('labels')
2595
+ return (
2596
+ element(data, kdims, vdims)
2597
+ .redim(**self._redim)
2598
+ .apply(self._set_backends_opts, cur_opts=cur_opts, compat_opts=compat_opts)
2599
+ )
2600
+
2601
+ ##########################
2602
+ # Gridded plots #
2603
+ ##########################
2604
+
2605
+ def _process_gridded_args(self, data, x, y, z):
2606
+ data = self.data if data is None else data
2607
+ x = x or self.x
2608
+ y = y or self.y
2609
+ z = z or self.kwds.get('z')
2610
+
2611
+ if is_xarray(data):
2612
+ import xarray as xr
2613
+
2614
+ if isinstance(data, xr.DataArray):
2615
+ data = data.to_dataset(name=data.name or 'value')
2616
+ if is_tabular(data):
2617
+ if self.use_index and any(
2618
+ c for c in self.hover_cols if c in self.indexes and c not in data.columns
2619
+ ):
2620
+ data = data.reset_index()
2621
+ # calculate any derived time
2622
+ dimensions = []
2623
+ for dimension in [x, y, self.by, self.hover_cols]:
2624
+ if dimension is not None:
2625
+ dimensions.extend(dimension if isinstance(dimension, list) else [dimension])
2626
+
2627
+ not_found = [dim for dim in dimensions if dim not in self.variables]
2628
+ _, data = process_derived_datetime_pandas(data, not_found, self.indexes)
2629
+
2630
+ return data, x, y, z
2631
+
2632
+ def _get_element(self, kind):
2633
+ element = self._kind_mapping[kind]
2634
+ if self.geo:
2635
+ import geoviews
2636
+
2637
+ element = getattr(geoviews, element.__name__)
2638
+ return element
2639
+
2640
+ def image(self, x=None, y=None, z=None, data=None):
2641
+ self._error_if_unavailable('image')
2642
+ data, x, y, z = self._process_gridded_args(data, x, y, z)
2643
+ if not (x and y):
2644
+ x, y = list(data.dims)[::-1]
2645
+ if not z:
2646
+ z = list(data.data_vars)[0]
2647
+ z = [z] + self.hover_cols
2648
+
2649
+ params = dict(self._relabel)
2650
+ cur_opts, compat_opts = self._get_compat_opts('Image')
2651
+ redim = self._merge_redim({z[0]: self._dim_ranges['c']})
2652
+
2653
+ element = self._get_element('image')
2654
+ if self.geo:
2655
+ params['crs'] = self.crs
2656
+ return (
2657
+ element(data, [x, y], z, **params)
2658
+ .redim(**redim)
2659
+ .apply(self._set_backends_opts, cur_opts=cur_opts, compat_opts=compat_opts)
2660
+ )
2661
+
2662
+ def rgb(self, x=None, y=None, z=None, data=None):
2663
+ self._error_if_unavailable('rgb')
2664
+ data, x, y, z = self._process_gridded_args(data, x, y, z)
2665
+
2666
+ coords = [c for c in data.coords if c not in self.by + self.groupby + self.grid]
2667
+ if len(coords) < 3:
2668
+ raise ValueError('Data must be 3D array to be converted to RGB.')
2669
+ x = x or coords[2]
2670
+ y = y or coords[1]
2671
+ bands = self.kwds.get('bands', coords[0])
2672
+ if z is None:
2673
+ z = list(data.data_vars)[0]
2674
+ data = data[z]
2675
+ nbands = len(data.coords[bands])
2676
+ if nbands < 3:
2677
+ raise ValueError(
2678
+ 'Selected bands coordinate (%s) has only %d channels,'
2679
+ 'expected at least three channels to convert to RGB.' % (bands, nbands)
2680
+ )
2681
+
2682
+ params = dict(self._relabel)
2683
+ xres, yres = data.attrs['res'] if 'res' in data.attrs else (1, 1)
2684
+ xs = data.coords[x][::-1] if xres < 0 else data.coords[x]
2685
+ ys = data.coords[y][::-1] if yres < 0 else data.coords[y]
2686
+ eldata = (xs, ys)
2687
+ for b in range(nbands):
2688
+ eldata += (data.isel(**{bands: b}).values,)
2689
+
2690
+ element = self._get_element('rgb')
2691
+ cur_opts, compat_opts = self._get_compat_opts('RGB')
2692
+ if self.geo:
2693
+ params['crs'] = self.crs
2694
+ rgb = element(eldata, [x, y], element.vdims[:nbands], **params)
2695
+ return rgb.redim(**self._redim).apply(
2696
+ self._set_backends_opts, cur_opts=cur_opts, compat_opts=compat_opts
2697
+ )
2698
+
2699
+ def quadmesh(self, x=None, y=None, z=None, data=None):
2700
+ self._error_if_unavailable('quadmesh')
2701
+ data, x, y, z = self._process_gridded_args(data, x, y, z)
2702
+
2703
+ if not (x and y):
2704
+ x, y = list(k for k, v in data.coords.items() if v.size > 1)
2705
+ if not z:
2706
+ z = list(data.data_vars)[0]
2707
+ z = [z] + self.hover_cols
2708
+
2709
+ params = dict(self._relabel)
2710
+ redim = self._merge_redim({z[0]: self._dim_ranges['c']})
2711
+
2712
+ element = self._get_element('quadmesh')
2713
+ cur_opts, compat_opts = self._get_compat_opts('QuadMesh')
2714
+ if self.geo:
2715
+ params['crs'] = self.crs
2716
+ return (
2717
+ element(data, [x, y], z, **params)
2718
+ .redim(**redim)
2719
+ .apply(self._set_backends_opts, cur_opts=cur_opts, compat_opts=compat_opts)
2720
+ )
2721
+
2722
+ def contour(self, x=None, y=None, z=None, data=None, filled=False):
2723
+ self._error_if_unavailable('contour')
2724
+ from holoviews.operation import contours
2725
+
2726
+ if 'projection' in self._plot_opts:
2727
+ import cartopy.crs as ccrs
2728
+
2729
+ t = self._plot_opts['projection']
2730
+ if isinstance(t, ccrs.CRS) and not isinstance(t, ccrs.Projection):
2731
+ raise ValueError(
2732
+ 'invalid transform:'
2733
+ ' Spherical contouring is not supported - '
2734
+ ' consider using PlateCarree/RotatedPole.'
2735
+ )
2736
+
2737
+ cur_opts, compat_opts = self._get_compat_opts('Contours')
2738
+ qmesh = self.quadmesh(x, y, z, data)
2739
+
2740
+ if self.geo:
2741
+ # Apply projection before contouring
2742
+ import cartopy.crs as ccrs
2743
+ from geoviews import project
2744
+
2745
+ projection = self._plot_opts.get('projection', ccrs.GOOGLE_MERCATOR)
2746
+ qmesh = project(qmesh, projection=projection)
2747
+
2748
+ if filled:
2749
+ cur_opts['line_alpha'] = 0
2750
+ if cur_opts['colorbar']:
2751
+ cur_opts['show_legend'] = False
2752
+ levels = self.kwds.get('levels', 5)
2753
+ if isinstance(levels, int):
2754
+ cur_opts['color_levels'] = levels
2755
+ return contours(qmesh, filled=filled, levels=levels).apply(
2756
+ self._set_backends_opts, cur_opts=cur_opts, compat_opts=compat_opts
2757
+ )
2758
+
2759
+ def contourf(self, x=None, y=None, z=None, data=None):
2760
+ self._error_if_unavailable('contourf')
2761
+ contourf = self.contour(x, y, z, data, filled=True)
2762
+ # The holoviews contours operation used in self.contour adapts
2763
+ # the value dim range, so we need to redimension it if the user
2764
+ # has asked for it by setting `clim`. Internally an unset `clim`
2765
+ # is identified by `(None, None)`.
2766
+ if self._dim_ranges['c'] != (None, None):
2767
+ z_name = contourf.vdims[0].name
2768
+ redim = {z_name: self._dim_ranges['c']}
2769
+ else:
2770
+ redim = {}
2771
+ return contourf.redim.range(**redim)
2772
+
2773
+ def vectorfield(self, x=None, y=None, angle=None, mag=None, data=None):
2774
+ self._error_if_unavailable('vectorfield')
2775
+ data, x, y, _ = self._process_gridded_args(data, x, y, z=None)
2776
+
2777
+ if not (x and y):
2778
+ x, y = list(k for k, v in data.coords.items() if v.size > 1)
2779
+
2780
+ angle = self.kwds.get('angle')
2781
+ mag = self.kwds.get('mag')
2782
+ z = [angle, mag] + self.hover_cols
2783
+ redim = self._merge_redim({z[1]: self._dim_ranges['c']})
2784
+ params = dict(self._relabel)
2785
+
2786
+ element = self._get_element('vectorfield')
2787
+ cur_opts, compat_opts = self._get_compat_opts('VectorField')
2788
+ if self.geo:
2789
+ params['crs'] = self.crs
2790
+ return (
2791
+ element(data, [x, y], z, **params)
2792
+ .redim(**redim)
2793
+ .apply(self._set_backends_opts, cur_opts=cur_opts, compat_opts=compat_opts)
2794
+ )
2795
+
2796
+ ##########################
2797
+ # Geometry plots #
2798
+ ##########################
2799
+
2800
+ def _geom_plot(self, x=None, y=None, data=None, kind='polygons'):
2801
+ data, x, y, _ = self._process_gridded_args(data, x, y, z=None)
2802
+ params = dict(self._relabel)
2803
+
2804
+ if not (x and y):
2805
+ if is_geodataframe(data):
2806
+ x, y = ('Longitude', 'Latitude') if self.geo else ('x', 'y')
2807
+ elif self.gridded_data:
2808
+ x, y = self.variables[:2:-1]
2809
+ else:
2810
+ x, y = data.columns[:2]
2811
+
2812
+ redim = self._merge_redim(
2813
+ {self._color_dim: self._dim_ranges['c']} if self._color_dim else {}
2814
+ )
2815
+ kdims, vdims = self._get_dimensions([x, y], [])
2816
+ if self.gridded_data:
2817
+ vdims = Dataset(data).vdims
2818
+ element = self._get_element(kind)
2819
+ cur_opts, compat_opts = self._get_compat_opts(element.name)
2820
+ for opts_ in [cur_opts, compat_opts]:
2821
+ if 'color' in opts_ and opts_['color'] in vdims:
2822
+ opts_['color'] = hv.dim(opts_['color'])
2823
+ # if there is nothing to put in hover, turn it off
2824
+ if 'tools' in opts_ and kind in ['polygons', 'paths'] and not vdims:
2825
+ opts_['tools'] = [t for t in opts_['tools'] if t != 'hover']
2826
+ if self.geo:
2827
+ params['crs'] = self.crs
2828
+ if self.by:
2829
+ obj = Dataset(data, self.by + kdims, vdims).to(
2830
+ element, kdims, vdims, self.by, **params
2831
+ )
2832
+ if self.subplots:
2833
+ obj = obj.layout(sort=False)
2834
+ else:
2835
+ obj = obj.overlay(sort=False)
2836
+ else:
2837
+ obj = element(data, kdims, vdims, **params)
2838
+
2839
+ return (
2840
+ obj.redim(**redim)
2841
+ .opts({element.name: cur_opts}, backend='bokeh')
2842
+ .opts({element.name: compat_opts}, backend=self._backend_compat)
2843
+ )
2844
+
2845
+ def polygons(self, x=None, y=None, data=None):
2846
+ self._error_if_unavailable('polygons')
2847
+ return self._geom_plot(x, y, data, kind='polygons')
2848
+
2849
+ def paths(self, x=None, y=None, data=None):
2850
+ self._error_if_unavailable('paths')
2851
+ return self._geom_plot(x, y, data, kind='paths')
2852
+
2853
+ def points(self, x=None, y=None, data=None):
2854
+ self._error_if_unavailable('points')
2855
+ return self._geom_plot(x, y, data, kind='points')