hvplot 0.9.3a1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. hvplot/__init__.py +322 -0
  2. hvplot/_version.py +16 -0
  3. hvplot/backend_transforms.py +329 -0
  4. hvplot/converter.py +2855 -0
  5. hvplot/cudf.py +26 -0
  6. hvplot/dask.py +42 -0
  7. hvplot/data/crime.csv +56 -0
  8. hvplot/datasets.yaml +48 -0
  9. hvplot/fugue.py +64 -0
  10. hvplot/ibis.py +21 -0
  11. hvplot/intake.py +32 -0
  12. hvplot/interactive.py +968 -0
  13. hvplot/networkx.py +625 -0
  14. hvplot/pandas.py +30 -0
  15. hvplot/plotting/__init__.py +63 -0
  16. hvplot/plotting/andrews_curves.py +99 -0
  17. hvplot/plotting/core.py +2288 -0
  18. hvplot/plotting/lag_plot.py +34 -0
  19. hvplot/plotting/parallel_coordinates.py +85 -0
  20. hvplot/plotting/scatter_matrix.py +220 -0
  21. hvplot/polars.py +21 -0
  22. hvplot/sample_data.py +30 -0
  23. hvplot/streamz.py +21 -0
  24. hvplot/tests/__init__.py +0 -0
  25. hvplot/tests/conftest.py +44 -0
  26. hvplot/tests/data/README.md +5 -0
  27. hvplot/tests/data/RGB-red.byte.tif +0 -0
  28. hvplot/tests/plotting/__init__.py +0 -0
  29. hvplot/tests/plotting/testcore.py +108 -0
  30. hvplot/tests/plotting/testohlc.py +34 -0
  31. hvplot/tests/plotting/testscattermatrix.py +138 -0
  32. hvplot/tests/test_links.py +99 -0
  33. hvplot/tests/testbackend_transforms.py +89 -0
  34. hvplot/tests/testcharts.py +452 -0
  35. hvplot/tests/testfugue.py +46 -0
  36. hvplot/tests/testgeo.py +468 -0
  37. hvplot/tests/testgeowithoutgv.py +60 -0
  38. hvplot/tests/testgridplots.py +259 -0
  39. hvplot/tests/testhelp.py +75 -0
  40. hvplot/tests/testibis.py +17 -0
  41. hvplot/tests/testinteractive.py +1442 -0
  42. hvplot/tests/testnetworkx.py +26 -0
  43. hvplot/tests/testoperations.py +385 -0
  44. hvplot/tests/testoptions.py +596 -0
  45. hvplot/tests/testoverrides.py +74 -0
  46. hvplot/tests/testpanel.py +70 -0
  47. hvplot/tests/testpatch.py +135 -0
  48. hvplot/tests/testplotting.py +69 -0
  49. hvplot/tests/teststreaming.py +28 -0
  50. hvplot/tests/testtransforms.py +39 -0
  51. hvplot/tests/testui.py +383 -0
  52. hvplot/tests/testutil.py +378 -0
  53. hvplot/tests/util.py +82 -0
  54. hvplot/ui.py +1032 -0
  55. hvplot/util.py +677 -0
  56. hvplot/utilities.py +129 -0
  57. hvplot/xarray.py +62 -0
  58. hvplot-0.9.3a1.dist-info/LICENSE +29 -0
  59. hvplot-0.9.3a1.dist-info/METADATA +243 -0
  60. hvplot-0.9.3a1.dist-info/RECORD +63 -0
  61. hvplot-0.9.3a1.dist-info/WHEEL +5 -0
  62. hvplot-0.9.3a1.dist-info/entry_points.txt +2 -0
  63. hvplot-0.9.3a1.dist-info/top_level.txt +1 -0
hvplot/ui.py ADDED
@@ -0,0 +1,1032 @@
1
+ import holoviews as _hv
2
+ import numpy as np
3
+ import panel as pn
4
+ import param
5
+
6
+ from holoviews.core.util import datetime_types, dt_to_int, is_number, max_range
7
+ from holoviews.element import tile_sources
8
+ from holoviews.plotting.util import list_cmaps
9
+ from panel.viewable import Viewer
10
+
11
+ from .converter import HoloViewsConverter as _hvConverter
12
+ from .plotting import hvPlot as _hvPlot
13
+ from .util import is_geodataframe, is_xarray, instantiate_crs_str
14
+
15
+ # Defaults
16
+ KINDS = {
17
+ # these are for the kind selector
18
+ 'dataframe': sorted(
19
+ set(_hvConverter._kind_mapping)
20
+ - set(_hvConverter._gridded_types)
21
+ - set(_hvConverter._geom_types)
22
+ | set(['points'])
23
+ ),
24
+ 'gridded': sorted(set(_hvConverter._gridded_types) - set(['dataset'])),
25
+ 'geom': _hvConverter._geom_types,
26
+ }
27
+
28
+ KINDS['2d'] = (
29
+ ['bivariate', 'heatmap', 'hexbin', 'labels', 'vectorfield', 'points']
30
+ + KINDS['gridded']
31
+ + KINDS['geom']
32
+ )
33
+ KINDS['stats'] = ['hist', 'kde', 'boxwhisker', 'violin', 'heatmap', 'bar', 'barh']
34
+ KINDS['all'] = sorted(set(KINDS['dataframe'] + KINDS['gridded'] + KINDS['geom']))
35
+
36
+ CMAPS = [cm for cm in list_cmaps() if not cm.endswith('_r_r')]
37
+ DEFAULT_CMAPS = _hvConverter._default_cmaps
38
+ GEO_FEATURES = ['borders', 'coastline', 'land', 'lakes', 'ocean', 'rivers', 'states', 'grid']
39
+ GEO_TILES = [None] + sorted(tile_sources)
40
+ GEO_KEYS = [
41
+ 'crs',
42
+ 'crs_kwargs',
43
+ 'projection',
44
+ 'projection_kwargs',
45
+ 'global_extent',
46
+ 'project',
47
+ 'features',
48
+ 'feature_scale',
49
+ ]
50
+ AGGREGATORS = [None, 'count', 'min', 'max', 'mean', 'sum', 'any']
51
+ MAX_ROWS = 10000
52
+ CONTROLS_WIDTH = 200
53
+
54
+
55
+ def explorer(data, **kwargs):
56
+ """Explore your data and design your plot via an interactive user interface.
57
+
58
+ This function returns an interactive Panel component that enable you to quickly change the
59
+ settings of your plot via widgets.
60
+
61
+ Reference: https://hvplot.holoviz.org/getting_started/explorer.html
62
+
63
+ Parameters
64
+ ----------
65
+ data : pandas.DataFrame | xarray.DataArray | xarray.Dataset
66
+ Data structure to explore.
67
+ kwargs : optional
68
+ Arguments that `data.hvplot()` would also accept like `kind='bar'`.
69
+
70
+ Returns
71
+ -------
72
+ hvplotExporer
73
+ Panel component to explore the data and design your plot.
74
+
75
+ Example
76
+ -------
77
+
78
+ >>> import hvplot.pandas
79
+ >>> import pandas as pd
80
+ >>> df = pd.DataFrame({"x": [1, 2, 3], "y": [1, 4, 9]})
81
+ >>> hvplot.explorer(df)
82
+
83
+ You can also specify initial values
84
+
85
+ >>> hvplot.explorer(df, kind='bar', x='x')
86
+ """
87
+ return hvPlotExplorer.from_data(data, **kwargs)
88
+
89
+
90
+ def _create_param_pane(inst, widgets_kwargs=None, parameters=None):
91
+ widgets_kwargs = widgets_kwargs or {}
92
+ # Augment widgets kwargs by default, throttling all Number/Range parameters
93
+ # and setting their sizing mode.
94
+ for pname in inst.param:
95
+ if pname == 'name':
96
+ continue
97
+ if pname not in widgets_kwargs:
98
+ widgets_kwargs[pname] = {}
99
+ if isinstance(inst.param[pname], (param.Number, param.Range)):
100
+ widgets_kwargs[pname]['throttled'] = True
101
+ widgets_kwargs[pname]['sizing_mode'] = 'stretch_width'
102
+ kwargs = {
103
+ 'show_name': False,
104
+ 'widgets': widgets_kwargs,
105
+ 'width': CONTROLS_WIDTH,
106
+ }
107
+ if parameters:
108
+ kwargs['parameters'] = parameters
109
+ pane = pn.Param(inst.param, **kwargs)
110
+ return pane
111
+
112
+
113
+ class Controls(Viewer):
114
+ explorer = param.ClassSelector(class_=Viewer, precedence=-1)
115
+
116
+ _widgets_kwargs = {}
117
+
118
+ __abstract = True
119
+
120
+ def __init__(self, df, **params):
121
+ self._data = df
122
+ super().__init__(**params)
123
+
124
+ def __panel__(self):
125
+ return _create_param_pane(self, widgets_kwargs=self._widgets_kwargs)
126
+
127
+ @property
128
+ def kwargs(self):
129
+ return {
130
+ k: v
131
+ for k, v in self.param.values().items()
132
+ if k not in ('name', 'explorer') and v is not None and v != ''
133
+ }
134
+
135
+
136
+ class Colormapping(Controls):
137
+ clim = param.Range(
138
+ label='Colorbar Limits (clim)',
139
+ doc="""
140
+ Upper and lower limits of the colorbar.""",
141
+ )
142
+
143
+ cnorm = param.Selector(
144
+ label='Colorbar Normalization (cnorm)',
145
+ default='linear',
146
+ objects=['linear', 'log', 'eq_hist'],
147
+ )
148
+
149
+ color = param.String(default=None)
150
+
151
+ colorbar = param.Boolean(default=None)
152
+
153
+ cmap = param.Selector(default=DEFAULT_CMAPS['linear'], label='Colormap', objects=CMAPS)
154
+
155
+ rescale_discrete_levels = param.Boolean(default=True)
156
+
157
+ symmetric = param.Boolean(
158
+ default=False,
159
+ doc="""
160
+ Whether the data are symmetric around zero.""",
161
+ )
162
+
163
+ _widgets_kwargs = {'clim': {'placeholder': '(min, max)'}}
164
+
165
+ def __init__(self, data, **params):
166
+ super().__init__(data, **params)
167
+ if 'symmetric' not in params:
168
+ self.symmetric = self.explorer._converter._plot_opts.get('symmetric', False)
169
+
170
+ @property
171
+ def colormapped(self):
172
+ if self.explorer.kind in _hvConverter._colorbar_types:
173
+ return True
174
+ return self.color is not None and self.color in self._data
175
+
176
+ @param.depends('color', 'explorer.kind', 'symmetric', watch=True)
177
+ def _update_coloropts(self):
178
+ if not self.colormapped or self.cmap not in list(DEFAULT_CMAPS.values()):
179
+ return
180
+ if self.explorer.kind in _hvConverter._colorbar_types:
181
+ key = 'diverging' if self.symmetric else 'linear'
182
+ self.colorbar = True
183
+ elif self.color in self._data:
184
+ kind = self._data[self.color].dtype.kind
185
+ if kind in 'OSU':
186
+ key = 'categorical'
187
+ elif self.symmetric:
188
+ key = 'diverging'
189
+ else:
190
+ key = 'linear'
191
+ else:
192
+ return
193
+ self.cmap = DEFAULT_CMAPS[key]
194
+
195
+
196
+ class Style(Controls):
197
+ alpha = param.Magnitude(default=1)
198
+
199
+
200
+ class Axes(Controls):
201
+ legend = param.Selector(default='bottom_right', objects=_hvConverter._legend_positions)
202
+
203
+ logx = param.Boolean(default=False)
204
+
205
+ logy = param.Boolean(default=False)
206
+
207
+ height = param.Integer(default=None, bounds=(0, None))
208
+
209
+ width = param.Integer(default=None, bounds=(0, None))
210
+
211
+ responsive = param.Boolean(default=True)
212
+
213
+ shared_axes = param.Boolean(default=True)
214
+
215
+ xlim = param.Range()
216
+
217
+ ylim = param.Range()
218
+
219
+ logx = param.Boolean(default=False)
220
+
221
+ logy = param.Boolean(default=False)
222
+
223
+ def __init__(self, data, **params):
224
+ super().__init__(data, **params)
225
+ self._update_ranges()
226
+
227
+ @param.depends('explorer.xlim', 'explorer.ylim', watch=True)
228
+ def _update_ranges(self):
229
+ xlim = self.explorer.xlim()
230
+ if xlim is not None and is_number(xlim[0]) and is_number(xlim[1]) and xlim[0] != xlim[1]:
231
+ xlim = self._convert_to_int(xlim)
232
+ self.param.xlim.precedence = 0
233
+ self.param.xlim.bounds = xlim
234
+ else:
235
+ self.param.xlim.precedence = -1
236
+ ylim = self.explorer.ylim()
237
+ if ylim is not None and is_number(ylim[0]) and is_number(ylim[1]) and ylim[0] != ylim[1]:
238
+ ylim = self._convert_to_int(ylim)
239
+ self.param.ylim.precedence = 0
240
+ self.param.ylim.bounds = ylim
241
+ else:
242
+ self.param.ylim.precedence = -1
243
+
244
+ @staticmethod
245
+ def _convert_to_int(val):
246
+ """
247
+ Converts datetime to int to avoid the error in https://github.com/holoviz/hvplot/issues/964.
248
+
249
+ This function is a workaround and should be removed when a better solution is found.
250
+
251
+ """
252
+
253
+ if isinstance(val[0], datetime_types) and isinstance(val[1], datetime_types):
254
+ val = (dt_to_int(val[0], 'ms'), dt_to_int(val[1], 'ms'))
255
+
256
+ return val
257
+
258
+
259
+ class Labels(Controls):
260
+ title = param.String(doc='Title for the plot')
261
+
262
+ xlabel = param.String(doc='Axis labels for the x-axis.')
263
+
264
+ ylabel = param.String(doc='Axis labels for the y-axis.')
265
+
266
+ clabel = param.String(
267
+ label='Colorbar Label (clabel)',
268
+ doc="""
269
+ Axis labels for the colorbar.""",
270
+ )
271
+
272
+ fontscale = param.Number(
273
+ default=1,
274
+ doc="""
275
+ Scales the size of all fonts by the same amount, e.g. fontscale=1.5
276
+ enlarges all fonts (title, xticks, labels etc.) by 50%.""",
277
+ )
278
+
279
+ rot = param.Integer(
280
+ default=0,
281
+ bounds=(0, 360),
282
+ label='X Tick Labels Rotation (rot)',
283
+ doc="""
284
+ Rotates the axis ticks along the x-axis by the specified
285
+ number of degrees.""",
286
+ )
287
+
288
+
289
+ class Geographic(Controls):
290
+ tiles = param.ObjectSelector(
291
+ default=None,
292
+ objects=GEO_TILES,
293
+ doc="""
294
+ Whether to overlay the plot on a tile source. Tiles sources
295
+ can be selected by name or a tiles object or class can be passed,
296
+ the default is 'Wikipedia'.""",
297
+ )
298
+
299
+ geo = param.Boolean(
300
+ default=False,
301
+ doc="""
302
+ Whether the plot should be treated as geographic (and assume
303
+ PlateCarree, i.e. lat/lon coordinates). Require GeoViews.""",
304
+ )
305
+
306
+ crs = param.Selector(
307
+ default=None,
308
+ doc="""
309
+ Coordinate reference system of the data specified as Cartopy
310
+ CRS object, proj.4 string or EPSG code.""",
311
+ )
312
+
313
+ crs_kwargs = param.Dict(
314
+ default={},
315
+ doc="""
316
+ Keyword arguments to pass to selected CRS.""",
317
+ )
318
+
319
+ projection = param.ObjectSelector(
320
+ default=None,
321
+ doc="""
322
+ Projection to use for cartographic plots.""",
323
+ )
324
+
325
+ projection_kwargs = param.Dict(
326
+ default={},
327
+ doc="""
328
+ Keyword arguments to pass to selected projection.""",
329
+ )
330
+
331
+ global_extent = param.Boolean(
332
+ default=None,
333
+ doc="""
334
+ Whether to expand the plot extent to span the whole globe.""",
335
+ )
336
+
337
+ project = param.Boolean(
338
+ default=False,
339
+ doc="""
340
+ Whether to project the data before plotting (adds initial
341
+ overhead but avoids projecting data when plot is dynamically
342
+ updated).""",
343
+ )
344
+
345
+ features = param.ListSelector(
346
+ default=None,
347
+ objects=GEO_FEATURES,
348
+ doc="""
349
+ A list of features or a dictionary of features and the scale
350
+ at which to render it. Available features include 'borders',
351
+ 'coastline', 'lakes', 'land', 'ocean', 'rivers' and 'states'.""",
352
+ )
353
+
354
+ feature_scale = param.ObjectSelector(
355
+ default='110m',
356
+ objects=['110m', '50m', '10m'],
357
+ doc="""
358
+ The scale at which to render the features.""",
359
+ )
360
+
361
+ _widgets_kwargs = {'geo': {'type': pn.widgets.Toggle}}
362
+
363
+ def __init__(self, data, **params):
364
+ gv_available = False
365
+ try:
366
+ import geoviews # noqa
367
+
368
+ gv_available = True
369
+ except ImportError:
370
+ pass
371
+
372
+ geo_params = GEO_KEYS + ['geo']
373
+ if not gv_available and any(p in params for p in geo_params):
374
+ raise ImportError('GeoViews must be installed to enable the geographic options.')
375
+ super().__init__(data, **params)
376
+ if not gv_available:
377
+ for p in geo_params:
378
+ self.param[p].constant = True
379
+ # Workaround: Checkbox widgets don't yet have a tooltip
380
+ self.param['geo'].label = 'geo (require GeoViews)'
381
+ else:
382
+ self._update_crs_projection()
383
+
384
+ @param.depends('geo', watch=True)
385
+ def _update_crs_projection(self):
386
+ enabled = bool(self.geo or self.project)
387
+ for key in GEO_KEYS:
388
+ self.param[key].constant = not enabled
389
+ self.geo = enabled
390
+ if not enabled:
391
+ return
392
+
393
+ from cartopy.crs import CRS
394
+
395
+ crs_list = sorted(
396
+ k
397
+ for k in param.concrete_descendents(CRS).keys()
398
+ if not k.startswith('_') and k != 'CRS'
399
+ )
400
+ crs_list.insert(0, 'GOOGLE_MERCATOR')
401
+ crs_list.insert(0, 'PlateCarree')
402
+ crs_list.remove('PlateCarree')
403
+
404
+ self.param.crs.objects = crs_list
405
+ self.param.projection.objects = crs_list
406
+ updates = {}
407
+ if self.projection is None:
408
+ updates['projection'] = crs_list[0]
409
+
410
+ if self.global_extent is None:
411
+ updates['global_extent'] = True
412
+
413
+ if self.features is None:
414
+ updates['features'] = ['coastline']
415
+
416
+ self.param.update(**updates)
417
+
418
+
419
+ class Operations(Controls):
420
+ datashade = param.Boolean(
421
+ default=False,
422
+ doc="""
423
+ Whether to apply rasterization and shading using datashader
424
+ library returning an RGB object.""",
425
+ )
426
+
427
+ rasterize = param.Boolean(
428
+ default=False,
429
+ doc="""
430
+ Whether to apply rasterization using the datashader library
431
+ returning an aggregated Image.""",
432
+ )
433
+
434
+ aggregator = param.Selector(
435
+ default=None,
436
+ objects=AGGREGATORS,
437
+ doc="""
438
+ Aggregator to use when applying rasterize or datashade operation.""",
439
+ )
440
+
441
+ dynspread = param.Boolean(
442
+ default=False,
443
+ doc="""
444
+ Allows plots generated with datashade=True or rasterize=True
445
+ to increase the point size to make sparse regions more visible.""",
446
+ )
447
+
448
+ x_sampling = param.Number(
449
+ default=None,
450
+ doc="""
451
+ Specifies the smallest allowed sampling interval along the x-axis.""",
452
+ )
453
+
454
+ y_sampling = param.Number(
455
+ default=None,
456
+ doc="""
457
+ Specifies the smallest allowed sampling interval along the y-axis.""",
458
+ )
459
+
460
+ @param.depends('datashade', watch=True)
461
+ def _toggle_rasterize(self):
462
+ if self.datashade:
463
+ self.rasterize = False
464
+
465
+ @param.depends('rasterize', watch=True)
466
+ def _toggle_datashade(self):
467
+ if self.rasterize:
468
+ self.datashade = False
469
+
470
+ @param.depends('rasterize', 'datashade', watch=True, on_init=True)
471
+ def _update_options(self):
472
+ enabled = self.rasterize or self.datashade
473
+ self.param.dynspread.constant = not enabled
474
+ self.param.x_sampling.constant = not enabled
475
+ self.param.y_sampling.constant = not enabled
476
+ self.param.aggregator.constant = not enabled
477
+
478
+
479
+ class Advanced(Controls):
480
+ opts = param.Dict(
481
+ label='HoloViews .opts()',
482
+ doc="""
483
+ Options applied via HoloViews .opts().
484
+ Examples:
485
+ - image: {"color_levels": 11}
486
+ - line: {"line_dash": "dashed"}
487
+ - scatter: {'size': 5, 'marker': '^'}""",
488
+ )
489
+
490
+ _widgets_kwargs = {'opts': {'placeholder': "{'size': 5, 'marker': '^'}"}}
491
+
492
+
493
+ class StatusBar(param.Parameterized):
494
+ live_update = param.Boolean(
495
+ default=True,
496
+ doc="""
497
+ Whether to automatically update the plot when a param is changed""",
498
+ )
499
+
500
+
501
+ class hvPlotExplorer(Viewer):
502
+ kind = param.Selector()
503
+
504
+ x = param.Selector()
505
+
506
+ y = param.Selector()
507
+
508
+ y_multi = param.ListSelector(default=[], label='Y')
509
+
510
+ by = param.ListSelector(default=[])
511
+
512
+ groupby = param.ListSelector(default=[])
513
+
514
+ # Controls that will show up as new tabs, must be ClassSelector
515
+
516
+ axes = param.ClassSelector(class_=Axes)
517
+
518
+ colormapping = param.ClassSelector(class_=Colormapping)
519
+
520
+ labels = param.ClassSelector(class_=Labels)
521
+
522
+ geographic = param.ClassSelector(class_=Geographic)
523
+
524
+ operations = param.ClassSelector(class_=Operations)
525
+
526
+ statusbar = param.ClassSelector(class_=StatusBar)
527
+
528
+ style = param.ClassSelector(class_=Style)
529
+
530
+ advanced = param.ClassSelector(class_=Advanced)
531
+
532
+ code = param.String(
533
+ precedence=-1,
534
+ doc="""
535
+ Code to generate the plot.""",
536
+ )
537
+
538
+ @classmethod
539
+ def from_data(cls, data, **params):
540
+ if is_geodataframe(data):
541
+ # cls = hvGeomExplorer
542
+ raise TypeError('GeoDataFrame objects not yet supported.')
543
+ elif is_xarray(data):
544
+ cls = hvGridExplorer
545
+ else:
546
+ cls = hvDataFrameExplorer
547
+ return cls(data, **params)
548
+
549
+ def __panel__(self):
550
+ return self._layout
551
+
552
+ def __init__(self, df, **params):
553
+ x, y = params.get('x'), params.get('y')
554
+ if 'y' in params:
555
+ params['y_multi'] = params.pop('y') if isinstance(params['y'], list) else [params['y']]
556
+ statusbar_params = {k: params.pop(k) for k in params.copy() if k in StatusBar.param}
557
+ converter = _hvConverter(
558
+ df, x, y, **{k: v for k, v in params.items() if k not in ('x', 'y', 'y_multi')}
559
+ )
560
+ # Collect kwargs passed to the constructor but meant for the controls
561
+ extras = {k: params.pop(k) for k in params.copy() if k not in self.param}
562
+ super().__init__(**params)
563
+ self._data = df
564
+ self._converter = converter
565
+ groups = {group: KINDS[group] for group in self._groups}
566
+ # Create pane for the main controls as done by the Controls instances.
567
+ self._controls = _create_param_pane(
568
+ self,
569
+ parameters=['kind', 'x', 'y', 'groupby', 'by'],
570
+ widgets_kwargs={'kind': {'options': [], 'groups': groups}},
571
+ )
572
+ self.param.watch(self._toggle_controls, 'kind')
573
+ self.param.watch(self._check_y, 'y_multi')
574
+ self.param.watch(self._check_by, 'by')
575
+ self._populate()
576
+ self._control_tabs = pn.Tabs(tabs_location='left')
577
+ self.statusbar = StatusBar(**statusbar_params)
578
+ self._statusbar = pn.Param(
579
+ self.statusbar, show_name=False, default_layout=pn.Row, margin=(5, 56, 0, 56)
580
+ )
581
+ controls = [
582
+ p.class_
583
+ for p in self.param.objects().values()
584
+ if isinstance(p, param.ClassSelector) and issubclass(p.class_, Controls)
585
+ ]
586
+ controller_params = {}
587
+ for cls in controls:
588
+ controller_params[cls] = {k: extras.pop(k) for k in extras.copy() if k in cls.param}
589
+ if extras:
590
+ raise TypeError(f'__init__() got keyword(s) not supported by any control: {extras}')
591
+ self._controllers = {
592
+ cls.name.lower(): cls(df, explorer=self, **cparams)
593
+ for cls, cparams in controller_params.items()
594
+ }
595
+ self.param.update(**self._controllers)
596
+ self.param.watch(self._refresh, list(self.param))
597
+ for controller in self._controllers.values():
598
+ controller.param.watch(self._refresh, list(controller.param))
599
+ self.statusbar.param.watch(self._refresh, list(self.statusbar.param))
600
+ self._alert = pn.pane.Alert(
601
+ alert_type='danger', visible=False, sizing_mode='stretch_width'
602
+ )
603
+ self._hv_pane = pn.pane.HoloViews(
604
+ sizing_mode='stretch_both',
605
+ min_height=250,
606
+ margin=(5, 5, 5, 20),
607
+ widget_location='bottom',
608
+ widget_layout=pn.Row,
609
+ )
610
+ self._code_pane = pn.pane.Markdown(
611
+ sizing_mode='stretch_both', min_height=250, margin=(5, 5, 0, 20)
612
+ )
613
+ self._layout = pn.Column(
614
+ self._alert,
615
+ self._statusbar,
616
+ pn.layout.Divider(),
617
+ pn.Row(
618
+ self._control_tabs,
619
+ # Using .layout on the HoloViews pane to display the widgets
620
+ # https://github.com/holoviz/panel/issues/5628#issuecomment-1763443895
621
+ pn.Tabs(('Plot', self._hv_pane.layout), ('Code', self._code_pane)),
622
+ sizing_mode='stretch_both',
623
+ ),
624
+ sizing_mode='stretch_width',
625
+ height=600,
626
+ )
627
+
628
+ # initialize
629
+ self.param.trigger('kind')
630
+
631
+ def _populate(self):
632
+ """
633
+ Populates the options of the controls based on the data type.
634
+ """
635
+ variables = self._converter.variables
636
+ indexes = getattr(self._converter, 'indexes', [])
637
+ variables_no_index = [v for v in variables if v not in indexes]
638
+ for pname in self.param:
639
+ if pname == 'kind':
640
+ continue
641
+ p = self.param[pname]
642
+ if isinstance(p, param.Selector):
643
+ if pname == 'x':
644
+ p.objects = variables
645
+ else:
646
+ p.objects = variables_no_index
647
+
648
+ # Setting the default value if not set
649
+ if (pname == 'x' or pname == 'y') and getattr(self, pname, None) is None:
650
+ setattr(self, pname, p.objects[0])
651
+
652
+ def _plot(self):
653
+ y = self.y_multi if 'y_multi' in self._controls.parameters else self.y
654
+ if isinstance(y, list) and len(y) == 1:
655
+ y = y[0]
656
+ kwargs = {}
657
+ for v in self.param.values().values():
658
+ # Geo is not enabled so not adding it to kwargs
659
+ if isinstance(v, Geographic) and not v.geo:
660
+ continue
661
+
662
+ if isinstance(v, Advanced):
663
+ opts_kwargs = v.kwargs.get('opts', {})
664
+ elif isinstance(v, Controls):
665
+ kwargs.update(v.kwargs)
666
+
667
+ if kwargs.get('geo'):
668
+ if 'crs' not in kwargs:
669
+ xmax = np.max(np.abs(self.xlim()))
670
+ self.geographic.crs = 'PlateCarree' if xmax <= 360 else 'GOOGLE_MERCATOR'
671
+ kwargs['crs'] = self.geographic.crs
672
+ for key in ['crs', 'projection']:
673
+ crs_kwargs = kwargs.pop(f'{key}_kwargs', {})
674
+ kwargs[key] = instantiate_crs_str(kwargs.pop(key), **crs_kwargs)
675
+
676
+ feature_scale = kwargs.pop('feature_scale', None)
677
+ kwargs['features'] = {feature: feature_scale for feature in kwargs.pop('features', [])}
678
+
679
+ kwargs['min_height'] = 400
680
+ df = self._data
681
+ if len(df) > MAX_ROWS and not (
682
+ self.kind in KINDS['stats'] or kwargs.get('rasterize') or kwargs.get('datashade')
683
+ ):
684
+ df = df.sample(n=MAX_ROWS)
685
+ self._layout.loading = True
686
+ try:
687
+ self._hvplot = _hvPlot(df)(
688
+ kind=self.kind, x=self.x, y=y, by=self.by, groupby=self.groupby, **kwargs
689
+ )
690
+ if opts_kwargs:
691
+ self._hvplot.opts(**opts_kwargs)
692
+ self._hv_pane.object = self._hvplot
693
+ # Working around a Panel issue that cause the widgets not to
694
+ # be well aligned when in a Row layout.
695
+ # See https://github.com/holoviz/panel/issues/6110
696
+ if len(self._hv_pane.widget_box) > 1:
697
+ for w in self._hv_pane.widget_box:
698
+ w.margin = (20, 5, 5, 5)
699
+ self._alert.visible = False
700
+ except Exception as e:
701
+ self._alert.param.update(
702
+ object=f'**Rendering failed with following error**: {e}', visible=True
703
+ )
704
+ finally:
705
+ self._layout.loading = False
706
+
707
+ def _refresh(self, *events):
708
+ if not self.statusbar.live_update:
709
+ return
710
+ self._plot()
711
+ with param.parameterized.discard_events(self):
712
+ self.code = self.plot_code()
713
+ self._code_pane.object = f"""```python\n{self.code}\n```"""
714
+
715
+ @property
716
+ def _var_name(self):
717
+ return 'data'
718
+
719
+ @property
720
+ def _single_y(self):
721
+ if self.kind in KINDS['2d']:
722
+ return True
723
+ return False
724
+
725
+ @property
726
+ def _groups(self):
727
+ raise NotImplementedError('Must be implemented by subclasses.')
728
+
729
+ def _toggle_controls(self, event=None):
730
+ # Control high-level parameters
731
+ visible = True
732
+ if event and event.new in ('table', 'dataset'):
733
+ parameters = ['kind', 'columns']
734
+ visible = False
735
+ elif event and event.new in KINDS['2d']:
736
+ parameters = ['kind', 'x', 'y', 'by', 'groupby']
737
+ elif event and event.new in ('hist', 'kde', 'density'):
738
+ self.x = None
739
+ parameters = ['kind', 'y_multi', 'by', 'groupby']
740
+ else:
741
+ parameters = ['kind', 'x', 'y_multi', 'by', 'groupby']
742
+ self._controls.parameters = parameters
743
+ # Control other tabs
744
+ tabs = [('Fields', self._controls)]
745
+ if visible:
746
+ tabs += [
747
+ ('Axes', self.axes),
748
+ ('Labels', self.labels),
749
+ ('Style', self.style),
750
+ ('Operations', self.operations),
751
+ ('Geographic', self.geographic),
752
+ ('Advanced', self.advanced),
753
+ ]
754
+ if event and event.new not in ('area', 'kde', 'line', 'ohlc', 'rgb', 'step'):
755
+ tabs.insert(5, ('Colormapping', self.colormapping))
756
+ self._control_tabs[:] = tabs
757
+
758
+ def _check_y(self, event):
759
+ if len(event.new) > 1 and self.by:
760
+ self.y = event.old
761
+
762
+ def _check_by(self, event):
763
+ if (
764
+ event.new
765
+ and 'y_multi' in self._controls.parameters
766
+ and self.y_multi
767
+ and len(self.y_multi) > 1
768
+ ):
769
+ self.by = []
770
+
771
+ # ----------------------------------------------------------------
772
+ # Public API
773
+ # ----------------------------------------------------------------
774
+
775
+ def hvplot(self):
776
+ """Return the plot as a HoloViews object."""
777
+ return self._hvplot.clone()
778
+
779
+ def plot_code(self, var_name=None):
780
+ """Return a string representation that can be easily copy-pasted
781
+ in a notebook cell to create a plot from a call to the `.hvplot`
782
+ accessor, and that includes all the customized settings of the explorer.
783
+
784
+ >>> hvexplorer.plot_code(var_name='data')
785
+ "data.hvplot(x='time', y='value')"
786
+
787
+ Parameters
788
+ ----------
789
+ var_name: string
790
+ Data variable name by which the returned string will start.
791
+ """
792
+ settings = self.settings()
793
+ if 'legend' not in settings:
794
+ settings['legend'] = 'bottom_right'
795
+ settings['widget_location'] = 'bottom'
796
+ settings_args = ''
797
+ if settings:
798
+ settings_args = self._build_kwargs_string(settings)
799
+ snippet = f'{var_name or self._var_name}.hvplot(\n{settings_args}\n)'
800
+ opts = self.advanced.opts
801
+ if opts:
802
+ opts_args = self._build_kwargs_string(opts)
803
+ snippet += f'.opts(\n{opts_args}\n)'
804
+ return snippet
805
+
806
+ def _build_kwargs_string(self, kwargs):
807
+ args = ''
808
+ if kwargs:
809
+ for k, v in kwargs.items():
810
+ args += f' {k}={v!r},\n'
811
+ args = args[:-1]
812
+ return args
813
+
814
+ def save(self, filename, **kwargs):
815
+ """Save the plot to file.
816
+
817
+ Calls the `holoviews.save` utility, refer to its documentation
818
+ for a full description of the available kwargs.
819
+
820
+ Parameters
821
+ ----------
822
+ filename: string, pathlib.Path or IO object
823
+ The path or BytesIO/StringIO object to save to
824
+ """
825
+ _hv.save(self._hvplot, filename, **kwargs)
826
+
827
+ def settings(self):
828
+ """Return a dictionary of the customized settings.
829
+
830
+ This dictionary can be reused as an unpacked input to the explorer or
831
+ a call to the `.hvplot` accessor.
832
+
833
+ >>> hvplot.explorer(df, **settings)
834
+ >>> df.hvplot(**settings)
835
+ """
836
+ settings = {}
837
+ for controller in self._controllers.values():
838
+ params = set(controller.param) - {'name', 'explorer'}
839
+ for p in params:
840
+ value = getattr(controller, p)
841
+ if value != controller.param[p].default:
842
+ settings[p] = value
843
+ for p in self._controls.parameters:
844
+ value = getattr(self, p)
845
+ if value != self.param[p].default or p == 'kind':
846
+ settings[p] = value
847
+ if 'y_multi' in settings:
848
+ settings['y'] = settings.pop('y_multi')
849
+ settings.pop('opts', None)
850
+ settings = {k: v for k, v in sorted(list(settings.items()))}
851
+ return settings
852
+
853
+
854
+ class hvGeomExplorer(hvPlotExplorer):
855
+ kind = param.Selector(default=None, objects=KINDS['all'])
856
+
857
+ @property
858
+ def _var_name(self):
859
+ return 'gdf'
860
+
861
+ @property
862
+ def _single_y(self):
863
+ return True
864
+
865
+ @property
866
+ def _x(self):
867
+ return None
868
+
869
+ @property
870
+ def _y(self):
871
+ return None
872
+
873
+ @param.depends('x')
874
+ def xlim(self):
875
+ pass
876
+
877
+ @param.depends('y')
878
+ def ylim(self):
879
+ pass
880
+
881
+ @property
882
+ def _groups(self):
883
+ return ['gridded', 'dataframe']
884
+
885
+
886
+ class hvGridExplorer(hvPlotExplorer):
887
+ kind = param.Selector(default='image', objects=KINDS['all'])
888
+
889
+ def __init__(self, ds, **params):
890
+ import xarray as xr
891
+
892
+ var_name_suffix = ''
893
+ if isinstance(ds, xr.Dataset):
894
+ data_vars = list(ds.data_vars)
895
+ if len(data_vars) == 1:
896
+ ds = ds[data_vars[0]]
897
+ var_name_suffix = f"['{data_vars[0]}']"
898
+ else:
899
+ ds = ds.to_array('variable').transpose(..., 'variable')
900
+ var_name_suffix = ".to_array('variable').transpose(..., 'variable')"
901
+ if 'kind' not in params:
902
+ params['kind'] = 'image'
903
+ self._var_name_suffix = var_name_suffix
904
+ super().__init__(ds, **params)
905
+
906
+ @property
907
+ def _var_name(self):
908
+ if self._var_name_suffix:
909
+ return f'ds{self._var_name_suffix}'
910
+ else:
911
+ return 'da'
912
+
913
+ @property
914
+ def _x(self):
915
+ return (self._converter.x or self._converter.indexes[0]) if self.x is None else self.x
916
+
917
+ @property
918
+ def _y(self):
919
+ return (self._converter.y or self._converter.indexes[1]) if self.y is None else self.y
920
+
921
+ @param.depends('x')
922
+ def xlim(self):
923
+ try:
924
+ values = self._data[self._x]
925
+ except Exception:
926
+ return 0, 1
927
+ if values.dtype.kind in 'OSU':
928
+ return None
929
+ return (np.nanmin(values), np.nanmax(values))
930
+
931
+ @param.depends('y', 'y_multi')
932
+ def ylim(self):
933
+ y = self._y
934
+ if not isinstance(y, list):
935
+ y = [y]
936
+ values = (self._data[y] for y in y)
937
+ return max_range([(np.nanmin(vs), np.nanmax(vs)) for vs in values])
938
+
939
+ @property
940
+ def _groups(self):
941
+ return ['gridded', 'dataframe', 'geom']
942
+
943
+ def _populate(self):
944
+ variables = self._converter.variables
945
+ indexes = getattr(self._converter, 'indexes', [])
946
+ variables_no_index = [v for v in variables if v not in indexes]
947
+ for pname in self.param:
948
+ if pname == 'kind':
949
+ continue
950
+ p = self.param[pname]
951
+ if isinstance(p, param.Selector):
952
+ if pname in ['x', 'y', 'groupby', 'by']:
953
+ p.objects = indexes
954
+ else:
955
+ p.objects = variables_no_index
956
+
957
+ # Setting the default value if not set
958
+ if pname == 'x' and getattr(self, pname, None) is None:
959
+ setattr(self, pname, p.objects[0])
960
+ elif pname == 'y' and getattr(self, pname, None) is None:
961
+ setattr(self, pname, p.objects[1])
962
+ elif (
963
+ pname == 'groupby'
964
+ and len(getattr(self, pname, [])) == 0
965
+ and len(p.objects) > 2
966
+ ):
967
+ setattr(self, pname, p.objects[2:])
968
+
969
+
970
+ class hvDataFrameExplorer(hvPlotExplorer):
971
+ z = param.Selector()
972
+
973
+ kind = param.Selector(default='scatter', objects=KINDS['all'])
974
+
975
+ @property
976
+ def _var_name(self):
977
+ return 'df'
978
+
979
+ @property
980
+ def xcat(self):
981
+ if self.kind in ('bar', 'box', 'violin'):
982
+ return False
983
+ values = self._data[self.x]
984
+ return values.dtype.kind in 'OSU'
985
+
986
+ @property
987
+ def _x(self):
988
+ return (self._converter.x or self._converter.variables[0]) if self.x is None else self.x
989
+
990
+ @property
991
+ def _y(self):
992
+ if 'y_multi' in self._controls.parameters and self.y_multi:
993
+ y = self.y_multi
994
+ elif 'y_multi' not in self._controls.parameters and self.y:
995
+ y = self.y
996
+ else:
997
+ y = self._converter._process_chart_y(self._data, self._x, None, self._single_y)
998
+ if isinstance(y, list) and len(y) == 1:
999
+ y = y[0]
1000
+ return y
1001
+
1002
+ @property
1003
+ def _groups(self):
1004
+ return ['dataframe']
1005
+
1006
+ @param.depends('x')
1007
+ def xlim(self):
1008
+ if self._x == 'index':
1009
+ values = self._data.index.values
1010
+ else:
1011
+ try:
1012
+ values = self._data[self._x]
1013
+ except Exception:
1014
+ return 0, 1
1015
+ # for dask series; else it cannot get length
1016
+ if hasattr(values, 'compute_chunk_sizes'):
1017
+ values = values.compute_chunk_sizes()
1018
+ if values.dtype.kind in 'OSU':
1019
+ return None
1020
+ elif not len(values):
1021
+ return (np.nan, np.nan)
1022
+ return (np.nanmin(values), np.nanmax(values))
1023
+
1024
+ @param.depends('y', 'y_multi')
1025
+ def ylim(self):
1026
+ y = self._y
1027
+ if not isinstance(y, list):
1028
+ y = [y]
1029
+ values = [ys for ys in (self._data[y] for y in y) if len(ys)]
1030
+ if not len(values):
1031
+ return (np.nan, np.nan)
1032
+ return max_range([(np.nanmin(vs), np.nanmax(vs)) for vs in values])