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.
- hvplot/__init__.py +322 -0
- hvplot/_version.py +16 -0
- hvplot/backend_transforms.py +329 -0
- hvplot/converter.py +2855 -0
- hvplot/cudf.py +26 -0
- hvplot/dask.py +42 -0
- hvplot/data/crime.csv +56 -0
- hvplot/datasets.yaml +48 -0
- hvplot/fugue.py +64 -0
- hvplot/ibis.py +21 -0
- hvplot/intake.py +32 -0
- hvplot/interactive.py +968 -0
- hvplot/networkx.py +625 -0
- hvplot/pandas.py +30 -0
- hvplot/plotting/__init__.py +63 -0
- hvplot/plotting/andrews_curves.py +99 -0
- hvplot/plotting/core.py +2288 -0
- hvplot/plotting/lag_plot.py +34 -0
- hvplot/plotting/parallel_coordinates.py +85 -0
- hvplot/plotting/scatter_matrix.py +220 -0
- hvplot/polars.py +21 -0
- hvplot/sample_data.py +30 -0
- hvplot/streamz.py +21 -0
- hvplot/tests/__init__.py +0 -0
- hvplot/tests/conftest.py +44 -0
- hvplot/tests/data/README.md +5 -0
- hvplot/tests/data/RGB-red.byte.tif +0 -0
- hvplot/tests/plotting/__init__.py +0 -0
- hvplot/tests/plotting/testcore.py +108 -0
- hvplot/tests/plotting/testohlc.py +34 -0
- hvplot/tests/plotting/testscattermatrix.py +138 -0
- hvplot/tests/test_links.py +99 -0
- hvplot/tests/testbackend_transforms.py +89 -0
- hvplot/tests/testcharts.py +452 -0
- hvplot/tests/testfugue.py +46 -0
- hvplot/tests/testgeo.py +468 -0
- hvplot/tests/testgeowithoutgv.py +60 -0
- hvplot/tests/testgridplots.py +259 -0
- hvplot/tests/testhelp.py +75 -0
- hvplot/tests/testibis.py +17 -0
- hvplot/tests/testinteractive.py +1442 -0
- hvplot/tests/testnetworkx.py +26 -0
- hvplot/tests/testoperations.py +385 -0
- hvplot/tests/testoptions.py +596 -0
- hvplot/tests/testoverrides.py +74 -0
- hvplot/tests/testpanel.py +70 -0
- hvplot/tests/testpatch.py +135 -0
- hvplot/tests/testplotting.py +69 -0
- hvplot/tests/teststreaming.py +28 -0
- hvplot/tests/testtransforms.py +39 -0
- hvplot/tests/testui.py +383 -0
- hvplot/tests/testutil.py +378 -0
- hvplot/tests/util.py +82 -0
- hvplot/ui.py +1032 -0
- hvplot/util.py +677 -0
- hvplot/utilities.py +129 -0
- hvplot/xarray.py +62 -0
- hvplot-0.9.3a1.dist-info/LICENSE +29 -0
- hvplot-0.9.3a1.dist-info/METADATA +243 -0
- hvplot-0.9.3a1.dist-info/RECORD +63 -0
- hvplot-0.9.3a1.dist-info/WHEEL +5 -0
- hvplot-0.9.3a1.dist-info/entry_points.txt +2 -0
- 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')
|