From 3af41741f3335fbe3158a733d4cbbd87fbbf91bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro?= Date: Sat, 30 Jul 2022 17:39:19 +0200 Subject: [PATCH 01/65] Enhancement to Geo tab in explorer --- hvplot/ui.py | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/hvplot/ui.py b/hvplot/ui.py index 1dd750db9..dc6d6e86f 100644 --- a/hvplot/ui.py +++ b/hvplot/ui.py @@ -22,7 +22,7 @@ 'borders', 'coastline', 'land', 'lakes', 'ocean', 'rivers', 'states', 'grid' ] -GEO_TILES = list(tile_sources) +GEO_TILES = [None] + list(tile_sources) AGGREGATORS = [None, 'count', 'min', 'max', 'mean', 'sum', 'any'] MAX_ROWS = 10000 @@ -204,14 +204,18 @@ class Geo(Controls): can be selected by name or a tiles object or class can be passed, the default is 'Wikipedia'.""") - @param.depends('geo', 'project', 'features', watch=True, on_init=True) - def _update_crs(self): - enabled = bool(self.geo or self.project or self.features) - self.param.crs.constant = not enabled - self.param.crs_kwargs.constant = not enabled - self.geo = enabled - if not enabled: - return + @param.depends('geo', watch=True, on_init=True) + def _update_params(self): + enabled = bool(self.geo) + geo_controls = set(self.param) - set(Controls.param) - {"geo"} + for p in geo_controls: + self.param[p].constant = not enabled + + if self.crs is None and enabled: + self._populate_crs() + + def _populate_crs(self): + # Method exists because cartopy is not a dependency of hvplot from cartopy.crs import CRS, GOOGLE_MERCATOR crs = { k: v for k, v in param.concrete_descendents(CRS).items() @@ -219,7 +223,7 @@ def _update_crs(self): } crs['WebMercator'] = GOOGLE_MERCATOR self.param.crs.objects = crs - + self.crs = next(iter(crs)) class Operations(Controls): @@ -366,7 +370,11 @@ def _plot(self, *events): if isinstance(y, list) and len(y) == 1: y = y[0] kwargs = {} - for p, v in self.param.values().items(): + for v in self.param.values().values(): + # Geo is not enabled so not adding it to kwargs + if isinstance(v, Geo) and not v.geo: + continue + if isinstance(v, Controls): kwargs.update(v.kwargs) From 5242f88b511cbf4ba1783b6bc9daac3dae64a4f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro?= Date: Mon, 1 Aug 2022 07:27:52 +0200 Subject: [PATCH 02/65] Naming the geo_controls --- hvplot/ui.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/hvplot/ui.py b/hvplot/ui.py index dc6d6e86f..95ac288c1 100644 --- a/hvplot/ui.py +++ b/hvplot/ui.py @@ -207,7 +207,9 @@ class Geo(Controls): @param.depends('geo', watch=True, on_init=True) def _update_params(self): enabled = bool(self.geo) - geo_controls = set(self.param) - set(Controls.param) - {"geo"} + geo_controls = [ + "crs", "crs_kwargs", "global_extent", "project", "features", "tiles" + ] for p in geo_controls: self.param[p].constant = not enabled From 433100feab355b345a38adba86d1ec01d4b5eea9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro?= Date: Thu, 4 Aug 2022 16:51:16 +0200 Subject: [PATCH 03/65] Only setting default value if not set --- hvplot/ui.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hvplot/ui.py b/hvplot/ui.py index 6c7d43a60..e529560cc 100644 --- a/hvplot/ui.py +++ b/hvplot/ui.py @@ -370,8 +370,8 @@ def _populate(self): else: p.objects = variables_no_index - # Setting the default value - if pname == "x" or pname == "y": + # Setting the default value if not set + if (pname == "x" or pname == "y") and getattr(self, pname, None) is None: setattr(self, pname, p.objects[0]) def _plot(self, *events): From 997375ad50bfb92d17d1b9a6c2dfc80f206fe6fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Wed, 13 Sep 2023 09:17:31 +0200 Subject: [PATCH 04/65] DEBUG: Try to fix failing tests --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 0bc3a79cc..1901e8293 100644 --- a/setup.py +++ b/setup.py @@ -97,6 +97,7 @@ def get_setup_version(reponame): extras_require['examples_conda'] = [ 'hdf5 !=1.14.1', # Gives coredump in test suite on Linux and Mac + 'gdal !=3.7.1', ] # Run the example tests by installing examples_tests together with tests From 7b90c56a05b9c658deb7359ad70fbc7a6027f591 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Wed, 13 Sep 2023 09:45:16 +0200 Subject: [PATCH 05/65] Try without pin --- setup.py | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.py b/setup.py index 1901e8293..0bc3a79cc 100644 --- a/setup.py +++ b/setup.py @@ -97,7 +97,6 @@ def get_setup_version(reponame): extras_require['examples_conda'] = [ 'hdf5 !=1.14.1', # Gives coredump in test suite on Linux and Mac - 'gdal !=3.7.1', ] # Run the example tests by installing examples_tests together with tests From ba21dc9bee1d673c908fc98e8a5e74f29c6c72b8 Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Fri, 15 Sep 2023 17:33:23 -0700 Subject: [PATCH 06/65] Step towards implementing xarray explorer --- hvplot/ui.py | 41 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 37 insertions(+), 4 deletions(-) diff --git a/hvplot/ui.py b/hvplot/ui.py index 2c677bb37..ff178ecb4 100644 --- a/hvplot/ui.py +++ b/hvplot/ui.py @@ -325,7 +325,7 @@ class hvPlotExplorer(Viewer): y = param.Selector() - y_multi = param.ListSelector(default=[], label='y') + y_multi = param.ListSelector(default=[], label='Y Multi') by = param.ListSelector(default=[]) @@ -352,8 +352,7 @@ def from_data(cls, data, **params): # cls = hvGeomExplorer raise TypeError('GeoDataFrame objects not yet supported.') elif is_xarray(data): - # cls = hvGridExplorer - raise TypeError('Xarray objects not yet supported.') + cls = hvGridExplorer else: cls = hvDataFrameExplorer return cls(data, **params) @@ -410,7 +409,7 @@ def __init__(self, df, **params): cls.name.lower(): cls(df, explorer=self, **cparams) for cls, cparams in controller_params.items() } - self.param.set_param(**self._controllers) + self.param.update(**self._controllers) self.param.watch(self._plot, list(self.param)) for controller in self._controllers.values(): controller.param.watch(self._plot, list(controller.param)) @@ -660,6 +659,40 @@ def ylim(self): values = (self._data[y] for y in y) return max_range([(np.nanmin(vs), np.nanmax(vs)) for vs in values]) + def _populate(self): + variables = self._converter.variables + indexes = getattr(self._converter, "indexes", []) + variables_no_index = [v for v in variables if v not in indexes] + is_gridded_kind = self.kind in GRIDDED_KINDS + print(self.kind) + for pname in self.param: + if pname == 'kind': + continue + p = self.param[pname] + if isinstance(p, param.Selector) and is_gridded_kind: + if pname in ["x", "y", "groupby"]: + p.objects = indexes + elif pname == "by": + p.objects = [] + else: + p.objects = variables_no_index + + # Setting the default value if not set + if (pname == "x" or pname == "y") and getattr(self, pname, None) is None: + setattr(self, pname, p.objects[0]) + elif pname == "groupby" and len(getattr(self, pname, [])) == 0: + setattr(self, pname, p.objects[-1:]) + elif isinstance(p, param.Selector): + # TODO: update this when self.kind is updated + if pname == "x": + p.objects = variables + else: + p.objects = variables_no_index + + # Setting the default value if not set + if (pname == "x" or pname == "y") and getattr(self, pname, None) is None: + setattr(self, pname, p.objects[0]) + class hvDataFrameExplorer(hvPlotExplorer): From 69854e0b48926802a02a37075abf3f0ce4b383f7 Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Mon, 18 Sep 2023 15:42:38 -0700 Subject: [PATCH 07/65] Group by kinds and add auto refresh --- hvplot/ui.py | 75 ++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 55 insertions(+), 20 deletions(-) diff --git a/hvplot/ui.py b/hvplot/ui.py index ff178ecb4..e356dea13 100644 --- a/hvplot/ui.py +++ b/hvplot/ui.py @@ -13,11 +13,21 @@ from .util import is_geodataframe, is_xarray # Defaults -DATAFRAME_KINDS = sorted(set(_hvConverter._kind_mapping) - set(_hvConverter._gridded_types)) -GRIDDED_KINDS = sorted(_hvConverter._kind_mapping) -GEOM_KINDS = ['paths', 'polygons', 'points'] -STATS_KINDS = ['hist', 'kde', 'boxwhisker', 'violin', 'heatmap', 'bar', 'barh'] -TWOD_KINDS = ['bivariate', 'heatmap', 'hexbin', 'labels', 'vectorfield'] + GEOM_KINDS +KINDS = { + # these are for the kind selector + "dataframe": sorted( + set(_hvConverter._kind_mapping) - + set(_hvConverter._gridded_types) - + set(_hvConverter._geom_types) + ), + "gridded": sorted(set(_hvConverter._gridded_types) - set(["dataset"])), + "geom": _hvConverter._geom_types, +} + +KINDS["2d"] = ['bivariate', 'heatmap', 'hexbin', 'labels', 'vectorfield', 'points'] + KINDS["gridded"] + KINDS["geom"] +KINDS["stats"] = ['hist', 'kde', 'boxwhisker', 'violin', 'heatmap', 'bar', 'barh'] +KINDS["all"] = sorted(set(KINDS["dataframe"] + KINDS["gridded"] + KINDS["geom"])) + CMAPS = [cm for cm in list_cmaps() if not cm.endswith('_r_r')] DEFAULT_CMAPS = _hvConverter._default_cmaps GEO_FEATURES = [ @@ -377,16 +387,18 @@ def __init__(self, df, **params): super().__init__(**params) self._data = df self._converter = converter + groups = {group: KINDS[group] for group in self._groups} self._controls = pn.Param( self.param, parameters=['kind', 'x', 'y', 'by', 'groupby'], sizing_mode='stretch_width', max_width=300, show_name=False, + widgets={"kind": {"options": [], "groups": groups}} ) self.param.watch(self._toggle_controls, 'kind') self.param.watch(self._check_y, 'y_multi') self.param.watch(self._check_by, 'by') self._populate() self._tabs = pn.Tabs( - tabs_location='left', width=400 + tabs_location='left', width=450 ) controls = [ p.class_ @@ -416,18 +428,24 @@ def __init__(self, df, **params): self._alert = pn.pane.Alert( alert_type='danger', visible=False, sizing_mode='stretch_width' ) + self._refresh_control = pn.widgets.Toggle(value=True, name="Auto-refresh", sizing_mode="stretch_width") + self._refresh_control.param.watch(self._refresh, 'value') + self._hv_pane = pn.pane.HoloViews(sizing_mode='stretch_both', margin=(0, 20, 0, 20)) self._layout = pn.Column( self._alert, + self._refresh_control, pn.Row( self._tabs, pn.layout.HSpacer(), + self._hv_pane, sizing_mode='stretch_width' ), pn.layout.HSpacer(), sizing_mode='stretch_both' ) - self._toggle_controls() - self._plot() + + # initialize + self.param.trigger("kind") def _populate(self): variables = self._converter.variables @@ -448,6 +466,8 @@ def _populate(self): setattr(self, pname, p.objects[0]) def _plot(self, *events): + if not self._refresh_control.value: + return y = self.y_multi if 'y_multi' in self._controls.parameters else self.y if isinstance(y, list) and len(y) == 1: y = y[0] @@ -464,17 +484,14 @@ def _plot(self, *events): kwargs['min_height'] = 300 df = self._data - if len(df) > MAX_ROWS and not (self.kind in STATS_KINDS or kwargs.get('rasterize') or kwargs.get('datashade')): + if len(df) > MAX_ROWS and not (self.kind in KINDS["stats"] or kwargs.get('rasterize') or kwargs.get('datashade')): df = df.sample(n=MAX_ROWS) self._layout.loading = True try: self._hvplot = _hvPlot(df)( kind=self.kind, x=self.x, y=y, by=self.by, groupby=self.groupby, **kwargs ) - self._hvpane = pn.pane.HoloViews( - self._hvplot, sizing_mode='stretch_width', margin=(0, 20, 0, 20) - ).layout - self._layout[1][1] = self._hvpane + self._hv_pane.object = self._hvplot self._alert.visible = False except Exception as e: self._alert.param.set_param( @@ -484,19 +501,27 @@ def _plot(self, *events): finally: self._layout.loading = False + def _refresh(self, event): + if event.new: + self._plot() + @property def _single_y(self): - if self.kind in ['labels', 'hexbin', 'heatmap', 'bivariate'] + GRIDDED_KINDS: + if self.kind in KINDS["2d"]: return True return False + @property + def _groups(self): + raise NotImplementedError('Must be implemented by subclasses.') + def _toggle_controls(self, event=None): # Control high-level parameters visible = True if event and event.new in ('table', 'dataset'): parameters = ['kind', 'columns'] visible = False - elif event and event.new in TWOD_KINDS: + elif event and event.new in KINDS['2d']: parameters = ['kind', 'x', 'y', 'by', 'groupby'] elif event and event.new in ('hist', 'kde', 'density'): self.x = None @@ -603,7 +628,7 @@ def settings(self): class hvGeomExplorer(hvPlotExplorer): - kind = param.Selector(default=None, objects=sorted(GEOM_KINDS)) + kind = param.Selector(default=None, objects=KINDS["all"]) @property def _single_y(self): @@ -625,10 +650,13 @@ def xlim(self): def ylim(self): pass + @property + def _groups(self): + return ["gridded", "dataframe"] class hvGridExplorer(hvPlotExplorer): - kind = param.Selector(default=None, objects=sorted(GRIDDED_KINDS)) + kind = param.Selector(default="image", objects=KINDS['all']) @property def _x(self): @@ -659,12 +687,15 @@ def ylim(self): values = (self._data[y] for y in y) return max_range([(np.nanmin(vs), np.nanmax(vs)) for vs in values]) + @property + def _groups(self): + return ["gridded", "dataframe", "geom"] + def _populate(self): variables = self._converter.variables indexes = getattr(self._converter, "indexes", []) variables_no_index = [v for v in variables if v not in indexes] - is_gridded_kind = self.kind in GRIDDED_KINDS - print(self.kind) + is_gridded_kind = self.kind in KINDS['gridded'] for pname in self.param: if pname == 'kind': continue @@ -698,7 +729,7 @@ class hvDataFrameExplorer(hvPlotExplorer): z = param.Selector() - kind = param.Selector(default='line', objects=sorted(DATAFRAME_KINDS)) + kind = param.Selector(default='line', objects=KINDS["all"]) @property def xcat(self): @@ -723,6 +754,10 @@ def _y(self): y = y[0] return y + @property + def _groups(self): + return ["dataframe"] + @param.depends('x') def xlim(self): if self._x == 'index': From 7ef37a048a0bba701a180d9c35cd70eb3e28ca1a Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Mon, 18 Sep 2023 16:15:13 -0700 Subject: [PATCH 08/65] Fix layout and by --- hvplot/ui.py | 36 ++++++++++++------------------------ 1 file changed, 12 insertions(+), 24 deletions(-) diff --git a/hvplot/ui.py b/hvplot/ui.py index e356dea13..351580d13 100644 --- a/hvplot/ui.py +++ b/hvplot/ui.py @@ -89,7 +89,6 @@ def __init__(self, df, **params): widget_kwargs[p] = {'throttled': True} self._controls = pn.Param( self.param, - max_width=300, show_name=False, sizing_mode='stretch_width', widgets=widget_kwargs, @@ -169,7 +168,7 @@ class Axes(Controls): width = param.Integer(default=None, bounds=(0, None)) - responsive = param.Boolean(default=False) + responsive = param.Boolean(default=True) shared_axes = param.Boolean(default=True) @@ -390,7 +389,7 @@ def __init__(self, df, **params): groups = {group: KINDS[group] for group in self._groups} self._controls = pn.Param( self.param, parameters=['kind', 'x', 'y', 'by', 'groupby'], - sizing_mode='stretch_width', max_width=300, show_name=False, + sizing_mode='stretch_width', show_name=False, widgets={"kind": {"options": [], "groups": groups}} ) self.param.watch(self._toggle_controls, 'kind') @@ -398,7 +397,7 @@ def __init__(self, df, **params): self.param.watch(self._check_by, 'by') self._populate() self._tabs = pn.Tabs( - tabs_location='left', width=450 + tabs_location='left', width=425 ) controls = [ p.class_ @@ -428,17 +427,16 @@ def __init__(self, df, **params): self._alert = pn.pane.Alert( alert_type='danger', visible=False, sizing_mode='stretch_width' ) - self._refresh_control = pn.widgets.Toggle(value=True, name="Auto-refresh", sizing_mode="stretch_width") + self._refresh_control = pn.widgets.Toggle(value=True, name="Auto-refresh plot", sizing_mode="stretch_width") self._refresh_control.param.watch(self._refresh, 'value') - self._hv_pane = pn.pane.HoloViews(sizing_mode='stretch_both', margin=(0, 20, 0, 20)) + self._hv_pane = pn.pane.HoloViews(sizing_mode='stretch_width', margin=(5, 20, 5, 20)) self._layout = pn.Column( self._alert, self._refresh_control, pn.Row( self._tabs, - pn.layout.HSpacer(), self._hv_pane, - sizing_mode='stretch_width' + sizing_mode="stretch_width", ), pn.layout.HSpacer(), sizing_mode='stretch_both' @@ -448,6 +446,9 @@ def __init__(self, df, **params): self.param.trigger("kind") def _populate(self): + """ + Populates the options of the controls based on the data type. + """ variables = self._converter.variables indexes = getattr(self._converter, "indexes", []) variables_no_index = [v for v in variables if v not in indexes] @@ -494,7 +495,7 @@ def _plot(self, *events): self._hv_pane.object = self._hvplot self._alert.visible = False except Exception as e: - self._alert.param.set_param( + self._alert.param.update( object=f'**Rendering failed with following error**: {e}', visible=True ) @@ -695,16 +696,13 @@ def _populate(self): variables = self._converter.variables indexes = getattr(self._converter, "indexes", []) variables_no_index = [v for v in variables if v not in indexes] - is_gridded_kind = self.kind in KINDS['gridded'] for pname in self.param: if pname == 'kind': continue p = self.param[pname] - if isinstance(p, param.Selector) and is_gridded_kind: - if pname in ["x", "y", "groupby"]: + if isinstance(p, param.Selector): + if pname in ["x", "y", "groupby", "by"]: p.objects = indexes - elif pname == "by": - p.objects = [] else: p.objects = variables_no_index @@ -713,16 +711,6 @@ def _populate(self): setattr(self, pname, p.objects[0]) elif pname == "groupby" and len(getattr(self, pname, [])) == 0: setattr(self, pname, p.objects[-1:]) - elif isinstance(p, param.Selector): - # TODO: update this when self.kind is updated - if pname == "x": - p.objects = variables - else: - p.objects = variables_no_index - - # Setting the default value if not set - if (pname == "x" or pname == "y") and getattr(self, pname, None) is None: - setattr(self, pname, p.objects[0]) class hvDataFrameExplorer(hvPlotExplorer): From a8c6a51c51a3c44df184291777b8ef04658e2a0d Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Mon, 18 Sep 2023 16:52:11 -0700 Subject: [PATCH 09/65] Add back geo --- hvplot/ui.py | 38 +++++++++++++++++++++++++------------- hvplot/util.py | 10 ++++++++++ 2 files changed, 35 insertions(+), 13 deletions(-) diff --git a/hvplot/ui.py b/hvplot/ui.py index 351580d13..607fdcc31 100644 --- a/hvplot/ui.py +++ b/hvplot/ui.py @@ -10,7 +10,7 @@ from .converter import HoloViewsConverter as _hvConverter from .plotting import hvPlot as _hvPlot -from .util import is_geodataframe, is_xarray +from .util import is_geodataframe, is_xarray, instantiate_crs_str # Defaults KINDS = { @@ -248,6 +248,12 @@ class Geo(Controls): crs_kwargs = param.Dict(default={}, doc=""" Keyword arguments to pass to selected CRS.""") + projection = param.ObjectSelector(default=None, doc=""" + Projection to use for cartographic plots.""") + + projection_kwargs = param.Dict(default={}, doc=""" + Keyword arguments to pass to selected projection.""") + global_extent = param.Boolean(default=False, doc=""" Whether to expand the plot extent to span the whole globe.""") @@ -267,10 +273,12 @@ class Geo(Controls): the default is 'Wikipedia'.""") @param.depends('geo', 'project', 'features', watch=True, on_init=True) - def _update_crs(self): + def _update_crs_projection(self): enabled = bool(self.geo or self.project or self.features) self.param.crs.constant = not enabled self.param.crs_kwargs.constant = not enabled + self.param.projection.constant = not enabled + self.param.projection_kwargs.constant = not enabled self.geo = enabled if not enabled: return @@ -279,9 +287,10 @@ def _update_crs(self): k: v for k, v in param.concrete_descendents(CRS).items() if not k.startswith('_') and k != 'CRS' } - crs['WebMercator'] = GOOGLE_MERCATOR + crs["-"] = "" + crs['GOOGLE_MERCATOR'] = GOOGLE_MERCATOR self.param.crs.objects = crs - + self.param.projection.objects = crs class Operations(Controls): @@ -348,8 +357,7 @@ class hvPlotExplorer(Viewer): labels = param.ClassSelector(class_=Labels) - # Hide the geo tab until it's better supported - # geo = param.ClassSelector(class_=Geo) + geo = param.ClassSelector(class_=Geo) operations = param.ClassSelector(class_=Operations) @@ -388,7 +396,7 @@ def __init__(self, df, **params): self._converter = converter groups = {group: KINDS[group] for group in self._groups} self._controls = pn.Param( - self.param, parameters=['kind', 'x', 'y', 'by', 'groupby'], + self.param, parameters=['kind', 'x', 'y', 'groupby', 'by'], sizing_mode='stretch_width', show_name=False, widgets={"kind": {"options": [], "groups": groups}} ) @@ -477,11 +485,15 @@ def _plot(self, *events): if isinstance(v, Controls): kwargs.update(v.kwargs) - # Initialize CRS - crs_kwargs = kwargs.pop('crs_kwargs', {}) - if 'crs' in kwargs: - if isinstance(kwargs['crs'], type): - kwargs['crs'] = kwargs['crs'](**crs_kwargs) + if kwargs.get("geo"): + for key in ["crs", "projection"]: + if key in kwargs: + crs_kwargs = kwargs.pop(f"{key}_kwargs", {}) + kwargs[key] = instantiate_crs_str(kwargs.pop(key), **crs_kwargs) + else: + # Always remove these intermediate keys from kwargs + kwargs.pop('crs_kwargs', {}) + kwargs.pop('projection_kwargs', {}) kwargs['min_height'] = 300 df = self._data @@ -544,7 +556,7 @@ def _toggle_controls(self, event=None): }, show_name=False)), ('Style', self.style), ('Operations', self.operations), - # ('Geo', self.geo) + ('Geo', self.geo) ] if event and event.new not in ('area', 'kde', 'line', 'ohlc', 'rgb', 'step'): tabs.insert(5, ('Colormapping', self.colormapping)) diff --git a/hvplot/util.py b/hvplot/util.py index 394b102d5..3eaca1d91 100644 --- a/hvplot/util.py +++ b/hvplot/util.py @@ -580,3 +580,13 @@ def _convert_col_names_to_str(data): if renamed: data = data.rename(columns=renamed) return data + + +def instantiate_crs_str(crs_str: str, **kwargs): + """ + Instantiate a cartopy.crs.Projection from a string. + """ + import cartopy.crs as ccrs + if crs_str.upper() == "GOOGLE_MERCATOR": + return ccrs.GOOGLE_MERCATOR + return getattr(ccrs, crs_str)(**kwargs) From d7323ca465cd84f8f8c0d1dca8c014128f5d7b9e Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Mon, 18 Sep 2023 17:13:57 -0700 Subject: [PATCH 10/65] Tweak y_multi --- hvplot/ui.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/hvplot/ui.py b/hvplot/ui.py index 607fdcc31..051998a4d 100644 --- a/hvplot/ui.py +++ b/hvplot/ui.py @@ -267,6 +267,9 @@ class Geo(Controls): at which to render it. Available features include 'borders', 'coastline', 'lakes', 'land', 'ocean', 'rivers' and 'states'.""") + feature_scale = param.ObjectSelector(default="110m", objects=["110m", "50m", "10m"], doc=""" + The scale at which to render the features.""") + tiles = param.ObjectSelector(default=None, objects=GEO_TILES, doc=""" Whether to overlay the plot on a tile source. Tiles sources can be selected by name or a tiles object or class can be passed, @@ -486,16 +489,22 @@ def _plot(self, *events): kwargs.update(v.kwargs) if kwargs.get("geo"): + print("geo") for key in ["crs", "projection"]: if key in kwargs: crs_kwargs = kwargs.pop(f"{key}_kwargs", {}) kwargs[key] = instantiate_crs_str(kwargs.pop(key), **crs_kwargs) + + feature_scale = kwargs.pop("feature_scale", None) + kwargs['features'] = {feature: feature_scale for feature in kwargs.pop("features", [])} else: # Always remove these intermediate keys from kwargs + kwargs.pop('geo') kwargs.pop('crs_kwargs', {}) kwargs.pop('projection_kwargs', {}) + kwargs.pop('feature_scale', None) - kwargs['min_height'] = 300 + kwargs['min_height'] = 600 df = self._data if len(df) > MAX_ROWS and not (self.kind in KINDS["stats"] or kwargs.get('rasterize') or kwargs.get('datashade')): df = df.sample(n=MAX_ROWS) @@ -541,7 +550,10 @@ def _toggle_controls(self, event=None): parameters = ['kind', 'y_multi', 'by', 'groupby'] else: parameters = ['kind', 'x', 'y_multi', 'by', 'groupby'] - self._controls.parameters = parameters + with param.batch_watch(self): + self._controls.parameters = parameters + if 'y_multi' in self._controls.parameters: + self.y_multi = self.param["y_multi"].objects[:1] # Control other tabs tabs = [('Fields', self._controls)] From cc1d8d2bb8cb987a54b108703d6af825acfbc845 Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Mon, 18 Sep 2023 17:17:48 -0700 Subject: [PATCH 11/65] Fix missing kwarg --- hvplot/ui.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/hvplot/ui.py b/hvplot/ui.py index 051998a4d..f80e7bea2 100644 --- a/hvplot/ui.py +++ b/hvplot/ui.py @@ -489,10 +489,9 @@ def _plot(self, *events): kwargs.update(v.kwargs) if kwargs.get("geo"): - print("geo") for key in ["crs", "projection"]: + crs_kwargs = kwargs.pop(f"{key}_kwargs", {}) if key in kwargs: - crs_kwargs = kwargs.pop(f"{key}_kwargs", {}) kwargs[key] = instantiate_crs_str(kwargs.pop(key), **crs_kwargs) feature_scale = kwargs.pop("feature_scale", None) From ac94126fc5529e6cd889fbc253047d4a6c743f81 Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Thu, 21 Sep 2023 08:56:00 -0700 Subject: [PATCH 12/65] Add tests --- hvplot/tests/testui.py | 129 +++++++++++++++++++++++++++++++---------- hvplot/ui.py | 38 +++++++----- 2 files changed, 120 insertions(+), 47 deletions(-) diff --git a/hvplot/tests/testui.py b/hvplot/tests/testui.py index 2d9062ecb..c60819b34 100644 --- a/hvplot/tests/testui.py +++ b/hvplot/tests/testui.py @@ -2,11 +2,13 @@ import holoviews as hv import hvplot.pandas +import hvplot.xarray +import xarray as xr import pytest from bokeh.sampledata import penguins -from hvplot.ui import hvDataFrameExplorer +from hvplot.ui import hvDataFrameExplorer, hvGridExplorer df = penguins.data @@ -15,28 +17,28 @@ def test_explorer_basic(): explorer = hvplot.explorer(df) assert isinstance(explorer, hvDataFrameExplorer) - assert explorer.kind == 'line' - assert explorer.x == 'index' - assert explorer.y == 'species' + assert explorer.kind == "line" + assert explorer.x == "index" + assert explorer.y == "species" def test_explorer_settings(): explorer = hvplot.explorer(df) explorer.param.set_param( - kind='scatter', - x='bill_length_mm', - y_multi=['bill_depth_mm'], - by=['species'], + kind="scatter", + x="bill_length_mm", + y_multi=["bill_depth_mm"], + by=["species"], ) settings = explorer.settings() assert settings == dict( - by=['species'], - kind='scatter', - x='bill_length_mm', - y=['bill_depth_mm'], + by=["species"], + kind="scatter", + x="bill_length_mm", + y=["bill_depth_mm"], ) @@ -44,47 +46,53 @@ def test_explorer_plot_code(): explorer = hvplot.explorer(df) explorer.param.set_param( - kind='scatter', - x='bill_length_mm', - y_multi=['bill_depth_mm'], - by=['species'], + kind="scatter", + x="bill_length_mm", + y_multi=["bill_depth_mm"], + by=["species"], ) hvplot_code = explorer.plot_code() - assert hvplot_code == "df.hvplot(by=['species'], kind='scatter', x='bill_length_mm', y=['bill_depth_mm'])" + assert ( + hvplot_code + == "df.hvplot(by=['species'], kind='scatter', x='bill_length_mm', y=['bill_depth_mm'])" + ) - hvplot_code = explorer.plot_code(var_name='othername') + hvplot_code = explorer.plot_code(var_name="othername") - assert hvplot_code == "othername.hvplot(by=['species'], kind='scatter', x='bill_length_mm', y=['bill_depth_mm'])" + assert ( + hvplot_code + == "othername.hvplot(by=['species'], kind='scatter', x='bill_length_mm', y=['bill_depth_mm'])" + ) def test_explorer_hvplot(): explorer = hvplot.explorer(df) explorer.param.set_param( - kind='scatter', - x='bill_length_mm', - y_multi=['bill_depth_mm'], + kind="scatter", + x="bill_length_mm", + y_multi=["bill_depth_mm"], ) plot = explorer.hvplot() assert isinstance(plot, hv.Scatter) - assert plot.kdims[0].name == 'bill_length_mm' - assert plot.vdims[0].name == 'bill_depth_mm' + assert plot.kdims[0].name == "bill_length_mm" + assert plot.vdims[0].name == "bill_depth_mm" def test_explorer_save(tmp_path): explorer = hvplot.explorer(df) explorer.param.set_param( - kind='scatter', - x='bill_length_mm', - y_multi=['bill_depth_mm'], + kind="scatter", + x="bill_length_mm", + y_multi=["bill_depth_mm"], ) - outfile = tmp_path / 'plot.html' + outfile = tmp_path / "plot.html" explorer.save(outfile) @@ -92,14 +100,71 @@ def test_explorer_save(tmp_path): def test_explorer_kwargs_controls(): - explorer = hvplot.explorer(df, title='Dummy title', width=200) + explorer = hvplot.explorer(df, title="Dummy title", width=200) - assert explorer.labels.title == 'Dummy title' + assert explorer.labels.title == "Dummy title" assert explorer.axes.width == 200 def test_explorer_kwargs_controls_error_not_supported(): with pytest.raises( - TypeError, match=re.escape("__init__() got keyword(s) not supported by any control: {'not_a_control_kwarg': None}") + TypeError, + match=re.escape( + "__init__() got keyword(s) not supported by any control: {'not_a_control_kwarg': None}" + ), ): - hvplot.explorer(df, title='Dummy title', not_a_control_kwarg=None) + hvplot.explorer(df, title="Dummy title", not_a_control_kwarg=None) + + +def test_explorer_hvplot_gridded_basic(): + ds = xr.tutorial.open_dataset("air_temperature") + explorer = hvplot.explorer(ds) + + assert isinstance(explorer, hvGridExplorer) + assert isinstance(explorer._data, xr.DataArray) + assert explorer.kind == "image" + assert explorer.x == "lat" + assert explorer.y == "lon" + assert explorer.by == [] + assert explorer.groupby == ["time"] + + +def test_explorer_hvplot_gridded_2d(): + ds = xr.tutorial.open_dataset("air_temperature").isel(time=0) + explorer = hvplot.explorer(ds) + + assert isinstance(explorer, hvGridExplorer) + assert isinstance(explorer._data, xr.DataArray) + assert explorer.kind == "image" + assert explorer.x == "lat" + assert explorer.y == "lon" + assert explorer.by == [] + assert explorer.groupby == [] + + +def test_explorer_hvplot_gridded_two_variables(): + ds = xr.tutorial.open_dataset("air_temperature") + ds["airx2"] = ds["air"] * 2 + explorer = hvplot.explorer(ds) + + assert isinstance(explorer, hvGridExplorer) + assert isinstance(explorer._data, xr.DataArray) + assert list(explorer._data["variable"]) == ["air", "airx2"] + assert explorer.kind == "image" + assert explorer.x == "lat" + assert explorer.y == "lon" + assert explorer.by == [] + assert explorer.groupby == ["time", "variable"] + + +def test_explorer_hvplot_gridded_dataarray(): + da = xr.tutorial.open_dataset("air_temperature")["air"] + explorer = hvplot.explorer(da) + + assert isinstance(explorer, hvGridExplorer) + assert isinstance(explorer._data, xr.DataArray) + assert explorer.kind == "image" + assert explorer.x == "lat" + assert explorer.y == "lon" + assert explorer.by == [] + assert explorer.groupby == ["time"] diff --git a/hvplot/ui.py b/hvplot/ui.py index f80e7bea2..a195c0a30 100644 --- a/hvplot/ui.py +++ b/hvplot/ui.py @@ -49,7 +49,7 @@ def explorer(data, **kwargs): Parameters ---------- - data : pandas.DataFrame + data : pandas.DataFrame | xarray.Dataset Data structure to explore. kwargs : optional Arguments that `data.hvplot()` would also accept like `kind='bar'`. @@ -549,10 +549,7 @@ def _toggle_controls(self, event=None): parameters = ['kind', 'y_multi', 'by', 'groupby'] else: parameters = ['kind', 'x', 'y_multi', 'by', 'groupby'] - with param.batch_watch(self): - self._controls.parameters = parameters - if 'y_multi' in self._controls.parameters: - self.y_multi = self.param["y_multi"].objects[:1] + self._controls.parameters = parameters # Control other tabs tabs = [('Fields', self._controls)] @@ -682,6 +679,18 @@ class hvGridExplorer(hvPlotExplorer): kind = param.Selector(default="image", objects=KINDS['all']) + def __init__(self, ds, **params): + import xarray as xr + if isinstance(ds, xr.Dataset): + data_vars = list(ds.data_vars) + if len(data_vars) == 1: + ds = ds[data_vars[0]] + else: + ds = ds.to_array('variable').transpose(..., "variable") + if "kind" not in params: + params["kind"] = "image" + super().__init__(ds, **params) + @property def _x(self): return (self._converter.x or self._converter.indexes[0]) if self.x is None else self.x @@ -692,13 +701,10 @@ def _y(self): @param.depends('x') def xlim(self): - if self._x == 'index': - values = self._data.index.values - else: - try: - values = self._data[self._x] - except: - return 0, 1 + try: + values = self._data[self._x] + except: + return 0, 1 if values.dtype.kind in 'OSU': return None return (np.nanmin(values), np.nanmax(values)) @@ -730,10 +736,12 @@ def _populate(self): p.objects = variables_no_index # Setting the default value if not set - if (pname == "x" or pname == "y") and getattr(self, pname, None) is None: + if pname == "x" and getattr(self, pname, None) is None: setattr(self, pname, p.objects[0]) - elif pname == "groupby" and len(getattr(self, pname, [])) == 0: - setattr(self, pname, p.objects[-1:]) + elif pname == "y" and getattr(self, pname, None) is None: + setattr(self, pname, p.objects[1]) + elif pname == "groupby" and len(getattr(self, pname, [])) == 0 and len(p.objects) > 2: + setattr(self, pname, p.objects[2:]) class hvDataFrameExplorer(hvPlotExplorer): From 4279754a5a4195e7fa99c7a1d45d7dd087f61ac3 Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Thu, 21 Sep 2023 08:56:43 -0700 Subject: [PATCH 13/65] Add more tests --- hvplot/tests/testui.py | 8 +++++++- hvplot/tests/testutil.py | 19 ++++++++++++++++++- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/hvplot/tests/testui.py b/hvplot/tests/testui.py index c60819b34..e61b4c7b1 100644 --- a/hvplot/tests/testui.py +++ b/hvplot/tests/testui.py @@ -167,4 +167,10 @@ def test_explorer_hvplot_gridded_dataarray(): assert explorer.x == "lat" assert explorer.y == "lon" assert explorer.by == [] - assert explorer.groupby == ["time"] + assert explorer.groupby == ['time'] + + +def test_explorer_hvplot_gridded_options(): + ds = xr.tutorial.open_dataset("air_temperature") + explorer = hvplot.explorer(ds) + assert explorer._controls[0].groups.keys() == {"dataframe", "gridded", "geom"} diff --git a/hvplot/tests/testutil.py b/hvplot/tests/testutil.py index 6c72bb672..1e72db07a 100644 --- a/hvplot/tests/testutil.py +++ b/hvplot/tests/testutil.py @@ -5,13 +5,14 @@ import numpy as np import pandas as pd +import cartopy.crs as ccrs import pytest from unittest import TestCase, SkipTest from hvplot.util import ( check_crs, is_list_like, process_crs, process_xarray, - _convert_col_names_to_str + _convert_col_names_to_str, instantiate_crs_str ) @@ -330,3 +331,19 @@ def test_convert_col_names_to_str(): assert all(not isinstance(col, str) for col in df.columns) df = _convert_col_names_to_str(df) assert all(isinstance(col, str) for col in df.columns) + + +def test_instantiate_crs_str(): + assert isinstance(instantiate_crs_str("PlateCarree"), ccrs.PlateCarree) + + +def test_instantiate_crs_google_mercator(): + assert instantiate_crs_str("GOOGLE_MERCATOR") == ccrs.GOOGLE_MERCATOR + assert instantiate_crs_str("google_mercator") == ccrs.GOOGLE_MERCATOR + + +def test_instantiate_crs_str_kwargs(): + crs = instantiate_crs_str("PlateCarree", globe=ccrs.Globe(datum="WGS84")) + assert isinstance(crs, ccrs.PlateCarree) + assert isinstance(crs.globe, ccrs.Globe) + assert crs.globe.datum == "WGS84" From c04f38e614b2e5b7f0da2029592a3c331fe060f7 Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Thu, 21 Sep 2023 09:35:11 -0700 Subject: [PATCH 14/65] Improve defaults --- hvplot/ui.py | 33 ++++++++++++++++++++------------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/hvplot/ui.py b/hvplot/ui.py index a195c0a30..61faae4a4 100644 --- a/hvplot/ui.py +++ b/hvplot/ui.py @@ -18,7 +18,8 @@ "dataframe": sorted( set(_hvConverter._kind_mapping) - set(_hvConverter._gridded_types) - - set(_hvConverter._geom_types) + set(_hvConverter._geom_types) | + set(["points"]) ), "gridded": sorted(set(_hvConverter._gridded_types) - set(["dataset"])), "geom": _hvConverter._geom_types, @@ -34,7 +35,7 @@ 'borders', 'coastline', 'land', 'lakes', 'ocean', 'rivers', 'states', 'grid' ] -GEO_TILES = list(tile_sources) +GEO_TILES = [None] + sorted(tile_sources) AGGREGATORS = [None, 'count', 'min', 'max', 'mean', 'sum', 'any'] MAX_ROWS = 10000 @@ -254,7 +255,7 @@ class Geo(Controls): projection_kwargs = param.Dict(default={}, doc=""" Keyword arguments to pass to selected projection.""") - global_extent = param.Boolean(default=False, doc=""" + global_extent = param.Boolean(default=None, doc=""" Whether to expand the plot extent to span the whole globe.""") project = param.Boolean(default=False, doc=""" @@ -262,7 +263,7 @@ class Geo(Controls): overhead but avoids projecting data when plot is dynamically updated).""") - features = param.ListSelector(default=[], objects=GEO_FEATURES, doc=""" + features = param.ListSelector(default=None, objects=GEO_FEATURES, doc=""" A list of features or a dictionary of features and the scale at which to render it. Available features include 'borders', 'coastline', 'lakes', 'land', 'ocean', 'rivers' and 'states'.""") @@ -275,9 +276,9 @@ class Geo(Controls): can be selected by name or a tiles object or class can be passed, the default is 'Wikipedia'.""") - @param.depends('geo', 'project', 'features', watch=True, on_init=True) + @param.depends('geo', 'project', watch=True, on_init=True) def _update_crs_projection(self): - enabled = bool(self.geo or self.project or self.features) + enabled = bool(self.geo or self.project) self.param.crs.constant = not enabled self.param.crs_kwargs.constant = not enabled self.param.projection.constant = not enabled @@ -285,15 +286,20 @@ def _update_crs_projection(self): self.geo = enabled if not enabled: return - from cartopy.crs import CRS, GOOGLE_MERCATOR - crs = { - k: v for k, v in param.concrete_descendents(CRS).items() + from cartopy.crs import CRS + crs = sorted( + k for k in param.concrete_descendents(CRS).keys() if not k.startswith('_') and k != 'CRS' - } - crs["-"] = "" - crs['GOOGLE_MERCATOR'] = GOOGLE_MERCATOR + ) + crs.insert(0, "GOOGLE_MERCATOR") + crs.insert(0, "PlateCarree") + crs.remove("PlateCarree") self.param.crs.objects = crs self.param.projection.objects = crs + if self.global_extent is None: + self.global_extent = True + if self.features is None: + self.features = ["coastline"] class Operations(Controls): @@ -346,7 +352,7 @@ class hvPlotExplorer(Viewer): y = param.Selector() - y_multi = param.ListSelector(default=[], label='Y Multi') + y_multi = param.ListSelector(default=[], label='Y') by = param.ListSelector(default=[]) @@ -509,6 +515,7 @@ def _plot(self, *events): df = df.sample(n=MAX_ROWS) self._layout.loading = True try: + print(kwargs, self.kind, self.x, y, self.by, self.groupby) self._hvplot = _hvPlot(df)( kind=self.kind, x=self.x, y=y, by=self.by, groupby=self.groupby, **kwargs ) From d4144e267fbe5bbfece3a0b747534ecc5ca07f6b Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Thu, 21 Sep 2023 11:22:30 -0700 Subject: [PATCH 15/65] Fix geo --- hvplot/converter.py | 4 ++-- hvplot/ui.py | 26 +++++++++++++++++--------- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/hvplot/converter.py b/hvplot/converter.py index 16e80a956..9e3d0a08f 100644 --- a/hvplot/converter.py +++ b/hvplot/converter.py @@ -671,12 +671,12 @@ def _process_crs(self, data, crs): try: return process_crs(_crs) - except ValueError: + except ValueError as e: # only raise error if crs was specified in kwargs if crs: raise ValueError( f"'{crs}' must be either a valid crs or an reference to " - "a `data.attr` containing a valid crs.") + f"a `data.attr` containing a valid crs: {e}") def _process_data(self, kind, data, x, y, by, groupby, row, col, use_dask, persist, backlog, label, group_label, diff --git a/hvplot/ui.py b/hvplot/ui.py index 61faae4a4..0a019fd3d 100644 --- a/hvplot/ui.py +++ b/hvplot/ui.py @@ -286,18 +286,27 @@ def _update_crs_projection(self): self.geo = enabled if not enabled: return + from cartopy.crs import CRS - crs = sorted( + crs_list = sorted( k for k in param.concrete_descendents(CRS).keys() if not k.startswith('_') and k != 'CRS' ) - crs.insert(0, "GOOGLE_MERCATOR") - crs.insert(0, "PlateCarree") - crs.remove("PlateCarree") - self.param.crs.objects = crs - self.param.projection.objects = crs + crs_list.insert(0, "GOOGLE_MERCATOR") + crs_list.insert(0, "PlateCarree") + crs_list.remove("PlateCarree") + + self.param.crs.objects = crs_list + if self.crs is None: + self.crs = crs_list[0] + + self.param.projection.objects = crs_list + if self.projection is None: + self.projection = crs_list[0] + if self.global_extent is None: self.global_extent = True + if self.features is None: self.features = ["coastline"] @@ -348,9 +357,9 @@ class hvPlotExplorer(Viewer): kind = param.Selector() - x = param.Selector() + x = param.Selector(default="x") - y = param.Selector() + y = param.Selector(default="y") y_multi = param.ListSelector(default=[], label='Y') @@ -515,7 +524,6 @@ def _plot(self, *events): df = df.sample(n=MAX_ROWS) self._layout.loading = True try: - print(kwargs, self.kind, self.x, y, self.by, self.groupby) self._hvplot = _hvPlot(df)( kind=self.kind, x=self.x, y=y, by=self.by, groupby=self.groupby, **kwargs ) From a7e6da70b439faeaed4811e2966a8555f9835be0 Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Thu, 21 Sep 2023 12:10:17 -0700 Subject: [PATCH 16/65] Prevent class name conflict with kwarg geo and fix tests --- hvplot/tests/testui.py | 11 +++++++++++ hvplot/ui.py | 10 +++++----- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/hvplot/tests/testui.py b/hvplot/tests/testui.py index e61b4c7b1..7ba81a21d 100644 --- a/hvplot/tests/testui.py +++ b/hvplot/tests/testui.py @@ -1,6 +1,7 @@ import re import holoviews as hv +import pandas as pd import hvplot.pandas import hvplot.xarray import xarray as xr @@ -174,3 +175,13 @@ def test_explorer_hvplot_gridded_options(): ds = xr.tutorial.open_dataset("air_temperature") explorer = hvplot.explorer(ds) assert explorer._controls[0].groups.keys() == {"dataframe", "gridded", "geom"} + + +def test_explorer_hvplot_geo(): + df = pd.DataFrame({"x": [-9796115.18980811], "y": [4838471.398061159]}) + explorer = hvplot.explorer(df, geo=True) + assert explorer.geographic.geo + assert explorer.geographic.global_extent + assert explorer.geographic.features == ["coastline"] + assert explorer.geographic.crs == "GOOGLE_MERCATOR" + assert explorer.geographic.projection == "GOOGLE_MERCATOR" diff --git a/hvplot/ui.py b/hvplot/ui.py index 0a019fd3d..fe72c033f 100644 --- a/hvplot/ui.py +++ b/hvplot/ui.py @@ -236,7 +236,7 @@ class Labels(Controls): number of degrees.""") -class Geo(Controls): +class Geographic(Controls): geo = param.Boolean(default=False, doc=""" Whether the plot should be treated as geographic (and assume @@ -357,9 +357,9 @@ class hvPlotExplorer(Viewer): kind = param.Selector() - x = param.Selector(default="x") + x = param.Selector() - y = param.Selector(default="y") + y = param.Selector() y_multi = param.ListSelector(default=[], label='Y') @@ -375,7 +375,7 @@ class hvPlotExplorer(Viewer): labels = param.ClassSelector(class_=Labels) - geo = param.ClassSelector(class_=Geo) + geographic = param.ClassSelector(class_=Geographic) operations = param.ClassSelector(class_=Operations) @@ -579,7 +579,7 @@ def _toggle_controls(self, event=None): }, show_name=False)), ('Style', self.style), ('Operations', self.operations), - ('Geo', self.geo) + ('Geographic', self.geographic) ] if event and event.new not in ('area', 'kde', 'line', 'ohlc', 'rgb', 'step'): tabs.insert(5, ('Colormapping', self.colormapping)) From 4ffcfa054b2d58a451beb1b9824603fc93c2ca4c Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Tue, 26 Sep 2023 15:30:01 -0400 Subject: [PATCH 17/65] Add docs --- examples/user_guide/Explorer.ipynb | 84 +++++++++++++++++++++++++++++- 1 file changed, 82 insertions(+), 2 deletions(-) diff --git a/examples/user_guide/Explorer.ipynb b/examples/user_guide/Explorer.ipynb index bd5dae2d6..21cd421f4 100644 --- a/examples/user_guide/Explorer.ipynb +++ b/examples/user_guide/Explorer.ipynb @@ -8,6 +8,7 @@ "source": [ "import hvplot.pandas\n", "\n", + "import xarray as xr\n", "from bokeh.sampledata.penguins import data as df" ] }, @@ -16,12 +17,13 @@ "metadata": {}, "source": [ "
\n", - " The Explorer has been added to hvPlot in version 0.8.0, and improved and documented in version 0.8.1. It does not yet support all the data structures supported by hvPlot, for now it works best with Pandas DataFrame objects. Please report any issue or feature request on GitHub.\n", + " The Explorer has been added to hvPlot in version 0.8.0, and improved and documented in version 0.8.1 and 0.8.5. It does not yet support all the data structures supported by hvPlot. Please report any issue or feature request on GitHub.\n", "
" ] }, { "cell_type": "markdown", + "id": "bd5ab787", "metadata": {}, "source": [ "hvPlot API provides a simple and intuitive way to create plots. However when you are exploring data you don't always know in advance the best way to display it, or even what kind of plot would be best to visualize the data. You will very likely embark in an iterative process that implies choosing a kind of plot, setting various options, running some code, and repeat until you're satisfied with the output and the insights you get. The *Explorer* is a *Graphical User Interface* that allows you to easily generate customized plots, which in practice gives you the possibility to **explore** both your data and hvPlot's extensive API.\n", @@ -34,6 +36,7 @@ { "cell_type": "code", "execution_count": null, + "id": "6d36b75a", "metadata": {}, "outputs": [], "source": [ @@ -68,7 +71,7 @@ "metadata": {}, "outputs": [], "source": [ - "hvexplorer.param.set_param(kind='scatter', x='bill_length_mm', y_multi=['bill_depth_mm'], by=['species'])\n", + "hvexplorer.param.update(kind='scatter', x='bill_length_mm', y_multi=['bill_depth_mm'], by=['species'])\n", "hvexplorer.labels.title = 'Penguins Scatter'" ] }, @@ -153,6 +156,83 @@ "df.hvplot(by=['species'], kind='scatter', title='Penguins Scatter', x='bill_length_mm', y=['bill_depth_mm'])" ] }, + { + "cell_type": "markdown", + "id": "b986b42d", + "metadata": {}, + "source": [ + "Or you can simply call `hvplot` on the explorer instance." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "db9ada50", + "metadata": {}, + "outputs": [], + "source": [ + "hvexplorer.hvplot()" + ] + }, + { + "cell_type": "markdown", + "id": "8c183122", + "metadata": {}, + "source": [ + "It also works for xarray objects." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f15dde1f", + "metadata": {}, + "outputs": [], + "source": [ + "ds = xr.tutorial.open_dataset(\"air_temperature\")\n", + "\n", + "hvplot.explorer(ds, x=\"lon\", y=\"lat\")" + ] + }, + { + "cell_type": "markdown", + "id": "d6d90743", + "metadata": {}, + "source": [ + "It's also possible to geographically reference the data, with cartopy and geoviews installed." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "87d4ab4f", + "metadata": {}, + "outputs": [], + "source": [ + "hvexplorer = hvplot.explorer(ds, x=\"lon\", y=\"lat\", geo=True)\n", + "hvexplorer.geographic.param.update(crs=\"PlateCarree\", tiles=\"CartoDark\")\n", + "hvexplorer" + ] + }, + { + "cell_type": "markdown", + "id": "c4736831", + "metadata": {}, + "source": [ + "Large datasets can be plotted efficiently using Datashader's `rasterize`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d01077b1", + "metadata": {}, + "outputs": [], + "source": [ + "hvexplorer = hvplot.explorer(ds, x=\"lon\", y=\"lat\", rasterize=True)\n", + "hvexplorer" + ] + }, { "cell_type": "markdown", "metadata": {}, From 92605c74cddef1edffdf24df4a4b085c1d08d818 Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Tue, 26 Sep 2023 16:02:26 -0400 Subject: [PATCH 18/65] Improve default --- hvplot/ui.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/hvplot/ui.py b/hvplot/ui.py index fe72c033f..bd5ded480 100644 --- a/hvplot/ui.py +++ b/hvplot/ui.py @@ -297,9 +297,6 @@ def _update_crs_projection(self): crs_list.remove("PlateCarree") self.param.crs.objects = crs_list - if self.crs is None: - self.crs = crs_list[0] - self.param.projection.objects = crs_list if self.projection is None: self.projection = crs_list[0] @@ -504,10 +501,13 @@ def _plot(self, *events): kwargs.update(v.kwargs) if kwargs.get("geo"): + if "crs" not in kwargs: + xmax = np.max(np.abs(self.xlim())) + self.crs = "PlateCarree" if xmax <= 360 else "GOOGLE_MERCATOR" + kwargs["crs"] = self.crs for key in ["crs", "projection"]: crs_kwargs = kwargs.pop(f"{key}_kwargs", {}) - if key in kwargs: - kwargs[key] = instantiate_crs_str(kwargs.pop(key), **crs_kwargs) + kwargs[key] = instantiate_crs_str(kwargs.pop(key), **crs_kwargs) feature_scale = kwargs.pop("feature_scale", None) kwargs['features'] = {feature: feature_scale for feature in kwargs.pop("features", [])} From 67f3cbff17797314d87d8d16f55ea26851ccb225 Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Tue, 26 Sep 2023 17:33:30 -0400 Subject: [PATCH 19/65] Add code tab --- hvplot/ui.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/hvplot/ui.py b/hvplot/ui.py index bd5ded480..b0b7ceca9 100644 --- a/hvplot/ui.py +++ b/hvplot/ui.py @@ -453,12 +453,13 @@ def __init__(self, df, **params): self._refresh_control = pn.widgets.Toggle(value=True, name="Auto-refresh plot", sizing_mode="stretch_width") self._refresh_control.param.watch(self._refresh, 'value') self._hv_pane = pn.pane.HoloViews(sizing_mode='stretch_width', margin=(5, 20, 5, 20)) + self._code_pane = pn.pane.Markdown(sizing_mode='stretch_width', margin=(5, 20, 0, 20)) self._layout = pn.Column( self._alert, self._refresh_control, pn.Row( self._tabs, - self._hv_pane, + pn.Tabs(("Plot", self._hv_pane), ("Code", self._code_pane)), sizing_mode="stretch_width", ), pn.layout.HSpacer(), @@ -503,8 +504,8 @@ def _plot(self, *events): if kwargs.get("geo"): if "crs" not in kwargs: xmax = np.max(np.abs(self.xlim())) - self.crs = "PlateCarree" if xmax <= 360 else "GOOGLE_MERCATOR" - kwargs["crs"] = self.crs + self.geographic.crs = "PlateCarree" if xmax <= 360 else "GOOGLE_MERCATOR" + kwargs["crs"] = self.geographic.crs for key in ["crs", "projection"]: crs_kwargs = kwargs.pop(f"{key}_kwargs", {}) kwargs[key] = instantiate_crs_str(kwargs.pop(key), **crs_kwargs) @@ -528,6 +529,7 @@ def _plot(self, *events): kind=self.kind, x=self.x, y=y, by=self.by, groupby=self.groupby, **kwargs ) self._hv_pane.object = self._hvplot + self._code_pane.object = f"```python\n{self.plot_code()}\n```" self._alert.visible = False except Exception as e: self._alert.param.update( @@ -619,9 +621,9 @@ def plot_code(self, var_name='df'): args = '' if settings: for k, v in settings.items(): - args += f'{k}={v!r}, ' + args += f' {k}={v!r},\n' args = args[:-2] - return f'{var_name}.hvplot({args})' + return f'{var_name}.hvplot(\n{args}\n)' def save(self, filename, **kwargs): """Save the plot to file. From 006f9972805238f4ed585736b11926fe349d3346 Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Wed, 27 Sep 2023 10:45:20 -0400 Subject: [PATCH 20/65] Fix tests --- hvplot/tests/testui.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hvplot/tests/testui.py b/hvplot/tests/testui.py index 7ba81a21d..f0195e618 100644 --- a/hvplot/tests/testui.py +++ b/hvplot/tests/testui.py @@ -179,7 +179,7 @@ def test_explorer_hvplot_gridded_options(): def test_explorer_hvplot_geo(): df = pd.DataFrame({"x": [-9796115.18980811], "y": [4838471.398061159]}) - explorer = hvplot.explorer(df, geo=True) + explorer = hvplot.explorer(df, x="x", geo=True, kind="points") assert explorer.geographic.geo assert explorer.geographic.global_extent assert explorer.geographic.features == ["coastline"] From 8f7be4da9fe7dd72b5cdfd9c6e919fa228131000 Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Wed, 27 Sep 2023 14:19:56 -0400 Subject: [PATCH 21/65] Refactor into param, add tests, docs --- examples/user_guide/Explorer.ipynb | 11 ++++++- hvplot/tests/testui.py | 29 +++++++++++++++++ hvplot/ui.py | 50 ++++++++++++++++++++++++++---- 3 files changed, 83 insertions(+), 7 deletions(-) diff --git a/examples/user_guide/Explorer.ipynb b/examples/user_guide/Explorer.ipynb index 21cd421f4..f8340d45f 100644 --- a/examples/user_guide/Explorer.ipynb +++ b/examples/user_guide/Explorer.ipynb @@ -124,6 +124,14 @@ "df.hvplot(**settings)" ] }, + { + "cell_type": "markdown", + "id": "be5f3cc6", + "metadata": {}, + "source": [ + "Or you can simply copy/paste the code displayed under the `Code` tab." + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -142,6 +150,7 @@ }, { "cell_type": "markdown", + "id": "76c92eaf", "metadata": {}, "source": [ "Copy/pasting this string without the outer quotes into the next cell is all you need to recreate the exact same plot." @@ -161,7 +170,7 @@ "id": "b986b42d", "metadata": {}, "source": [ - "Or you can simply call `hvplot` on the explorer instance." + "You may also call `hvplot` on the explorer instance." ] }, { diff --git a/hvplot/tests/testui.py b/hvplot/tests/testui.py index f0195e618..14bc2f6c1 100644 --- a/hvplot/tests/testui.py +++ b/hvplot/tests/testui.py @@ -1,4 +1,5 @@ import re +from textwrap import dedent import holoviews as hv import pandas as pd @@ -185,3 +186,31 @@ def test_explorer_hvplot_geo(): assert explorer.geographic.features == ["coastline"] assert explorer.geographic.crs == "GOOGLE_MERCATOR" assert explorer.geographic.projection == "GOOGLE_MERCATOR" + + +def test_explorer_code_dataframe(): + explorer = hvplot.explorer(df, x="bill_length_mm", kind="points") + explorer._code() + code = explorer.code + assert code == dedent("""\ + df.hvplot( + kind='points', + x='bill_length_mm', + y='species' + )""" + ) + + +def test_explorer_code_gridded(): + ds = xr.tutorial.open_dataset("air_temperature") + explorer = hvplot.explorer(ds, x="lon", y="lat", kind="image") + explorer._code() + code = explorer.code + assert code == dedent("""\ + ds.hvplot( + colorbar=True, + groupby=['time'], + kind='image', + x='lon', + y='lat' + )""") diff --git a/hvplot/ui.py b/hvplot/ui.py index b0b7ceca9..7db23ec23 100644 --- a/hvplot/ui.py +++ b/hvplot/ui.py @@ -378,6 +378,9 @@ class hvPlotExplorer(Viewer): style = param.ClassSelector(class_=Style) + code = param.String(precedence=-1, doc=""" + Code to generate the plot.""") + @classmethod def from_data(cls, data, **params): if is_geodataframe(data): @@ -444,7 +447,9 @@ def __init__(self, df, **params): for cls, cparams in controller_params.items() } self.param.update(**self._controllers) - self.param.watch(self._plot, list(self.param)) + params_to_watch = list(self.param) + params_to_watch.remove("code") + self.param.watch(self._plot, params_to_watch) for controller in self._controllers.values(): controller.param.watch(self._plot, list(controller.param)) self._alert = pn.pane.Alert( @@ -529,7 +534,6 @@ def _plot(self, *events): kind=self.kind, x=self.x, y=y, by=self.by, groupby=self.groupby, **kwargs ) self._hv_pane.object = self._hvplot - self._code_pane.object = f"```python\n{self.plot_code()}\n```" self._alert.visible = False except Exception as e: self._alert.param.update( @@ -539,9 +543,22 @@ def _plot(self, *events): finally: self._layout.loading = False + def _code(self): + self.code = self._build_code_snippet() + self._code_pane.object = f"""```python\nimport hvplot.{self._backend}\n\n{self.code}\n```""" + def _refresh(self, event): if event.new: self._plot() + self._code() + + @property + def _var_name(self): + return "data" + + @property + def _backend(self): + return "pandas" @property def _single_y(self): @@ -581,7 +598,7 @@ def _toggle_controls(self, event=None): }, show_name=False)), ('Style', self.style), ('Operations', self.operations), - ('Geographic', self.geographic) + ('Geographic', self.geographic), ] if event and event.new not in ('area', 'kde', 'line', 'ohlc', 'rgb', 'step'): tabs.insert(5, ('Colormapping', self.colormapping)) @@ -595,6 +612,15 @@ def _check_by(self, event): if event.new and 'y_multi' in self._controls.parameters and self.y_multi and len(self.y_multi) > 1: self.by = [] + def _build_code_snippet(self): + settings = self.settings() + args = '' + if settings: + for k, v in settings.items(): + args += f' {k}={v!r},\n' + args = args[:-2] + return f'{self._var_name}.hvplot(\n{args}\n)' + #---------------------------------------------------------------- # Public API #---------------------------------------------------------------- @@ -621,9 +647,9 @@ def plot_code(self, var_name='df'): args = '' if settings: for k, v in settings.items(): - args += f' {k}={v!r},\n' + args += f'{k}={v!r}, ' args = args[:-2] - return f'{var_name}.hvplot(\n{args}\n)' + return f'{var_name}.hvplot({args})' def save(self, filename, **kwargs): """Save the plot to file. @@ -656,7 +682,7 @@ def settings(self): settings[p] = value for p in self._controls.parameters: value = getattr(self, p) - if value != self.param[p].default: + if value != self.param[p].default or p == "kind": settings[p] = value if 'y_multi' in settings: settings['y'] = settings.pop('y_multi') @@ -668,6 +694,10 @@ class hvGeomExplorer(hvPlotExplorer): kind = param.Selector(default=None, objects=KINDS["all"]) + @property + def _var_name(self): + return "gdf" + @property def _single_y(self): return True @@ -708,6 +738,10 @@ def __init__(self, ds, **params): params["kind"] = "image" super().__init__(ds, **params) + @property + def _var_name(self): + return "ds" + @property def _x(self): return (self._converter.x or self._converter.indexes[0]) if self.x is None else self.x @@ -767,6 +801,10 @@ class hvDataFrameExplorer(hvPlotExplorer): kind = param.Selector(default='line', objects=KINDS["all"]) + @property + def _var_name(self): + return "df" + @property def xcat(self): if self.kind in ('bar', 'box', 'violin'): From 59d9b39410cacffb13c219c19a59e5bd98e628ff Mon Sep 17 00:00:00 2001 From: Andrew <15331990+ahuang11@users.noreply.github.com> Date: Wed, 27 Sep 2023 14:23:15 -0400 Subject: [PATCH 22/65] Update hvplot/ui.py --- hvplot/ui.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hvplot/ui.py b/hvplot/ui.py index 7db23ec23..929f24af3 100644 --- a/hvplot/ui.py +++ b/hvplot/ui.py @@ -509,8 +509,8 @@ def _plot(self, *events): if kwargs.get("geo"): if "crs" not in kwargs: xmax = np.max(np.abs(self.xlim())) - self.geographic.crs = "PlateCarree" if xmax <= 360 else "GOOGLE_MERCATOR" - kwargs["crs"] = self.geographic.crs + self.crs = "PlateCarree" if xmax <= 360 else "GOOGLE_MERCATOR" + kwargs["crs"] = self.crs for key in ["crs", "projection"]: crs_kwargs = kwargs.pop(f"{key}_kwargs", {}) kwargs[key] = instantiate_crs_str(kwargs.pop(key), **crs_kwargs) From a0eb5f5ab814143ea300b8888f6a29ec9b749167 Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Wed, 27 Sep 2023 14:26:30 -0400 Subject: [PATCH 23/65] Add another test and fix --- hvplot/tests/testui.py | 25 +++++++++++++++++++++++++ hvplot/ui.py | 4 ++++ 2 files changed, 29 insertions(+) diff --git a/hvplot/tests/testui.py b/hvplot/tests/testui.py index 14bc2f6c1..6b67a164d 100644 --- a/hvplot/tests/testui.py +++ b/hvplot/tests/testui.py @@ -199,6 +199,17 @@ def test_explorer_code_dataframe(): y='species' )""" ) + assert explorer._code_pane.object == dedent("""\ + ```python + import hvplot.pandas + + df.hvplot( + kind='points', + x='bill_length_mm', + y='species' + ) + ```""" + ) def test_explorer_code_gridded(): @@ -214,3 +225,17 @@ def test_explorer_code_gridded(): x='lon', y='lat' )""") + + assert explorer._code_pane.object == dedent("""\ + ```python + import hvplot.xarray + + ds.hvplot( + colorbar=True, + groupby=['time'], + kind='image', + x='lon', + y='lat' + ) + ```""" + ) diff --git a/hvplot/ui.py b/hvplot/ui.py index 929f24af3..5f67223ec 100644 --- a/hvplot/ui.py +++ b/hvplot/ui.py @@ -742,6 +742,10 @@ def __init__(self, ds, **params): def _var_name(self): return "ds" + @property + def _backend(self): + return "xarray" + @property def _x(self): return (self._converter.x or self._converter.indexes[0]) if self.x is None else self.x From 2cd9a105fe0d8975338a361f720eb9c9dc630050 Mon Sep 17 00:00:00 2001 From: Andrew <15331990+ahuang11@users.noreply.github.com> Date: Tue, 19 Sep 2023 08:31:06 -0700 Subject: [PATCH 24/65] Support plots that use `by` with `rasterize` with hv.ImageStack (#1132) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * WIP overlaying rasterized * Clean up * Add rasterize to check for categorical * Add test * Add version check * Update hvplot/converter.py Co-authored-by: Simon Høxbro Hansen --------- Co-authored-by: Simon Høxbro Hansen --- hvplot/converter.py | 7 +++++-- hvplot/tests/testoperations.py | 8 +++++++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/hvplot/converter.py b/hvplot/converter.py index 6eb4a7249..f17e81f56 100644 --- a/hvplot/converter.py +++ b/hvplot/converter.py @@ -582,7 +582,7 @@ def __init__( symmetric = self._process_symmetric(symmetric, clim, check_symmetric_max) if self._style_opts.get('cmap') is None: # Default to categorical camp if we detect categorical shading - if (self.datashade and (self.aggregator is None or 'count_cat' in str(self.aggregator)) and + if ((self.datashade or self.rasterize) and (self.aggregator is None or 'count_cat' in str(self.aggregator)) and ((self.by and not self.subplots) or (isinstance(self.y, list) or (self.y is None and len(set(self.variables) - set(self.indexes)) > 1)))): self._style_opts['cmap'] = self._default_cmaps['categorical'] @@ -1330,7 +1330,10 @@ def method_wrapper(ds, x, y): opts['rescale_discrete_levels'] = self._plot_opts['rescale_discrete_levels'] else: operation = rasterize - eltype = 'Image' + if Version(hv.__version__) < Version('1.18.0a1'): + eltype = 'Image' + else: + eltype = 'ImageStack' if self.by else 'Image' if 'cmap' in self._style_opts: style['cmap'] = self._style_opts['cmap'] if self._dim_ranges.get('c', (None, None)) != (None, None): diff --git a/hvplot/tests/testoperations.py b/hvplot/tests/testoperations.py index c2abd4cde..f3f303e04 100644 --- a/hvplot/tests/testoperations.py +++ b/hvplot/tests/testoperations.py @@ -3,12 +3,13 @@ from unittest import SkipTest from parameterized import parameterized +import colorcet as cc import hvplot.pandas # noqa import numpy as np import pandas as pd from holoviews import Store -from holoviews.element import Image, QuadMesh +from holoviews.element import Image, QuadMesh, ImageStack from holoviews.element.comparison import ComparisonTestCase from hvplot.converter import HoloViewsConverter @@ -194,6 +195,11 @@ def test_datashade_rescale_discrete_levels_default_True(self): actual = plot.callback.inputs[0].callback.operation.p['rescale_discrete_levels'] assert actual is expected + def test_rasterize_by(self): + expected = 'category' + plot = self.df.hvplot(x='x', y='y', by=expected, rasterize=True, dynamic=False) + assert isinstance(plot, ImageStack) + assert plot.opts["cmap"] == cc.palette['glasbey_category10'] class TestChart2D(ComparisonTestCase): From ba9eb929ae5858b94ffa525646534a7c7580388b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Tue, 19 Sep 2023 18:41:02 +0200 Subject: [PATCH 25/65] Handle wkt format (#1092) --- hvplot/tests/testutil.py | 14 ++++++++++++-- hvplot/util.py | 3 +-- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/hvplot/tests/testutil.py b/hvplot/tests/testutil.py index 704b6e7f3..6c72bb672 100644 --- a/hvplot/tests/testutil.py +++ b/hvplot/tests/testutil.py @@ -11,7 +11,7 @@ from hvplot.util import ( check_crs, is_list_like, process_crs, process_xarray, - _convert_col_names_to_str, + _convert_col_names_to_str ) @@ -287,7 +287,10 @@ def test_check_crs(): 4326, "epsg:4326", "EPSG: 4326", -]) + 'PROJCS["WGS 84 / Pseudo-Mercator",GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563,AUTHORITY["EPSG","7030"]],AUTHORITY["EPSG","6326"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.0174532925199433,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4326"]],PROJECTION["Mercator_1SP"],PARAMETER["central_meridian",0],PARAMETER["scale_factor",1],PARAMETER["false_easting",0],PARAMETER["false_northing",0],UNIT["metre",1,AUTHORITY["EPSG","9001"]],AXIS["Easting",EAST],AXIS["Northing",NORTH],EXTENSION["PROJ4","+proj=merc +a=6378137 +b=6378137 +lat_ts=0 +lon_0=0 +x_0=0 +y_0=0 +k=1 +units=m +nadgrids=@null +wktext +no_defs"],AUTHORITY["EPSG","3857"]]', + # Created with pyproj.CRS("EPSG:3857").to_wkt() + 'PROJCRS["WGS 84 / Pseudo-Mercator",BASEGEOGCRS["WGS 84",ENSEMBLE["World Geodetic System 1984 ensemble",MEMBER["World Geodetic System 1984 (Transit)"],MEMBER["World Geodetic System 1984 (G730)"],MEMBER["World Geodetic System 1984 (G873)"],MEMBER["World Geodetic System 1984 (G1150)"],MEMBER["World Geodetic System 1984 (G1674)"],MEMBER["World Geodetic System 1984 (G1762)"],MEMBER["World Geodetic System 1984 (G2139)"],ELLIPSOID["WGS 84",6378137,298.257223563,LENGTHUNIT["metre",1]],ENSEMBLEACCURACY[2.0]],PRIMEM["Greenwich",0,ANGLEUNIT["degree",0.0174532925199433]],ID["EPSG",4326]],CONVERSION["Popular Visualisation Pseudo-Mercator",METHOD["Popular Visualisation Pseudo Mercator",ID["EPSG",1024]],PARAMETER["Latitude of natural origin",0,ANGLEUNIT["degree",0.0174532925199433],ID["EPSG",8801]],PARAMETER["Longitude of natural origin",0,ANGLEUNIT["degree",0.0174532925199433],ID["EPSG",8802]],PARAMETER["False easting",0,LENGTHUNIT["metre",1],ID["EPSG",8806]],PARAMETER["False northing",0,LENGTHUNIT["metre",1],ID["EPSG",8807]]],CS[Cartesian,2],AXIS["easting (X)",east,ORDER[1],LENGTHUNIT["metre",1]],AXIS["northing (Y)",north,ORDER[2],LENGTHUNIT["metre",1]],USAGE[SCOPE["Web mapping and visualisation."],AREA["World between 85.06°S and 85.06°N."],BBOX[-85.06,-180,85.06,180]],ID["EPSG",3857]]' +], ids=lambda x: str(x)[:20]) def test_process_crs(input): pytest.importorskip("pyproj") ccrs = pytest.importorskip("cartopy.crs") @@ -295,6 +298,13 @@ def test_process_crs(input): assert isinstance(crs, ccrs.CRS) +def test_process_crs_rasterio(): + pytest.importorskip("pyproj") + rcrs = pytest.importorskip("rasterio.crs") + ccrs = pytest.importorskip("cartopy.crs") + input = rcrs.CRS.from_epsg(4326).to_wkt() + crs = process_crs(input) + assert isinstance(crs, ccrs.CRS) def test_process_crs_raises_error(): pytest.importorskip("pyproj") diff --git a/hvplot/util.py b/hvplot/util.py index 83043c8f2..394b102d5 100644 --- a/hvplot/util.py +++ b/hvplot/util.py @@ -117,7 +117,6 @@ def proj_to_cartopy(proj): a cartopy.crs.Projection object """ - import cartopy import cartopy.crs as ccrs try: from osgeo import osr @@ -200,7 +199,7 @@ def proj_to_cartopy(proj): if cl.__name__ == 'Mercator': kw_proj.pop('false_easting', None) kw_proj.pop('false_northing', None) - if Version(cartopy.__version__) < Version('0.15'): + if "scale_factor" in kw_proj: kw_proj.pop('latitude_true_scale', None) elif cl.__name__ == 'Stereographic': kw_proj.pop('scale_factor', None) From 98c2612440a77a604ab5d83d40199e8abcda6739 Mon Sep 17 00:00:00 2001 From: Andrew <15331990+ahuang11@users.noreply.github.com> Date: Tue, 19 Sep 2023 11:50:01 -0700 Subject: [PATCH 26/65] Add version check for test (#1144) * Add version check for test * commit --- hvplot/tests/testoperations.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/hvplot/tests/testoperations.py b/hvplot/tests/testoperations.py index f3f303e04..e19e5d89c 100644 --- a/hvplot/tests/testoperations.py +++ b/hvplot/tests/testoperations.py @@ -4,6 +4,7 @@ from parameterized import parameterized import colorcet as cc +import holoviews as hv import hvplot.pandas # noqa import numpy as np import pandas as pd @@ -12,6 +13,7 @@ from holoviews.element import Image, QuadMesh, ImageStack from holoviews.element.comparison import ComparisonTestCase from hvplot.converter import HoloViewsConverter +from packaging.version import Version class TestDatashader(ComparisonTestCase): @@ -196,6 +198,8 @@ def test_datashade_rescale_discrete_levels_default_True(self): assert actual is expected def test_rasterize_by(self): + if Version(hv.__version__) < Version('1.18.0a1'): + raise SkipTest('hv.ImageStack introduced after 1.18.0a1') expected = 'category' plot = self.df.hvplot(x='x', y='y', by=expected, rasterize=True, dynamic=False) assert isinstance(plot, ImageStack) From 8923efe0162882e00332801b22580e4d45bc11ac Mon Sep 17 00:00:00 2001 From: Maxime Liquet <35924738+maximlt@users.noreply.github.com> Date: Wed, 20 Sep 2023 14:51:44 +0200 Subject: [PATCH 27/65] Param 2: don't call param.objects() before `super().__init__` in the explorer (#1146) * call param.objects() after super().__init__ * add tests * update the docs * update error message --- examples/user_guide/Explorer.ipynb | 2 +- hvplot/tests/testui.py | 18 ++++++++++++++ hvplot/ui.py | 40 +++++++++++++++++++----------- 3 files changed, 44 insertions(+), 16 deletions(-) diff --git a/examples/user_guide/Explorer.ipynb b/examples/user_guide/Explorer.ipynb index e86b21490..bd5dae2d6 100644 --- a/examples/user_guide/Explorer.ipynb +++ b/examples/user_guide/Explorer.ipynb @@ -26,7 +26,7 @@ "source": [ "hvPlot API provides a simple and intuitive way to create plots. However when you are exploring data you don't always know in advance the best way to display it, or even what kind of plot would be best to visualize the data. You will very likely embark in an iterative process that implies choosing a kind of plot, setting various options, running some code, and repeat until you're satisfied with the output and the insights you get. The *Explorer* is a *Graphical User Interface* that allows you to easily generate customized plots, which in practice gives you the possibility to **explore** both your data and hvPlot's extensive API.\n", "\n", - "To create an *Explorer* you pass your data to the high-level `hvplot.explorer` function which returns a [Panel](https://panel.holoviz.org/) layout that can be displayed in a notebook or served in a web application. This object displays on the right-hand side a preview of the plot you are building, and on the left-hand side the various options that you can set to customize the plot.\n", + "To create an *Explorer* you pass your data to the high-level `hvplot.explorer` function which returns a [Panel](https://panel.holoviz.org/) layout that can be displayed in a notebook or served in a web application. This object displays on the right-hand side a preview of the plot you are building, and on the left-hand side the various options that you can set to customize the plot. These options can passed to the constructor if you already have pre-defined some, for example `hvplot.explorer(data, title='Penguins', width=200)`.\n", "\n", "Note that for the explorer to be displayed in a notebook you need to load the hvPlot extension, which happens automatically when you execute `import hvplot.pandas`. If instead of building Bokeh plots you would rather build Matplotlib or Plotly plot, simply execute once `hvplot.extension('matplotlib')` or `hvplot.extension('matplotlib')` before displaying the explorer." ] diff --git a/hvplot/tests/testui.py b/hvplot/tests/testui.py index 12f60172f..2d9062ecb 100644 --- a/hvplot/tests/testui.py +++ b/hvplot/tests/testui.py @@ -1,6 +1,10 @@ +import re + import holoviews as hv import hvplot.pandas +import pytest + from bokeh.sampledata import penguins from hvplot.ui import hvDataFrameExplorer @@ -85,3 +89,17 @@ def test_explorer_save(tmp_path): explorer.save(outfile) assert outfile.exists() + + +def test_explorer_kwargs_controls(): + explorer = hvplot.explorer(df, title='Dummy title', width=200) + + assert explorer.labels.title == 'Dummy title' + assert explorer.axes.width == 200 + + +def test_explorer_kwargs_controls_error_not_supported(): + with pytest.raises( + TypeError, match=re.escape("__init__() got keyword(s) not supported by any control: {'not_a_control_kwarg': None}") + ): + hvplot.explorer(df, title='Dummy title', not_a_control_kwarg=None) diff --git a/hvplot/ui.py b/hvplot/ui.py index 102e7b42d..cf14557aa 100644 --- a/hvplot/ui.py +++ b/hvplot/ui.py @@ -375,19 +375,12 @@ def __init__(self, df, **params): df, x, y, **{k: v for k, v in params.items() if k not in ('x', 'y', 'y_multi')} ) - controller_params = {} - # Assumes the controls aren't passed on instantiation. - controls = [ - p.class_ - for p in self.param.objects().values() - if isinstance(p, param.ClassSelector) - and issubclass(p.class_, Controls) - ] - for cls in controls: - controller_params[cls] = { - k: params.pop(k) for k, v in dict(params).items() - if k in cls.param - } + # Collect kwargs passed to the constructor but meant for the controls + extras = { + k: params.pop(k) + for k in params.copy() + if k not in self.param + } super().__init__(**params) self._data = df self._converter = converter @@ -402,9 +395,26 @@ def __init__(self, df, **params): self._tabs = pn.Tabs( tabs_location='left', width=400 ) + controls = [ + p.class_ + for p in self.param.objects().values() + if isinstance(p, param.ClassSelector) + and issubclass(p.class_, Controls) + ] + controller_params = {} + for cls in controls: + controller_params[cls] = { + k: extras.pop(k) + for k in extras.copy() + if k in cls.param + } + if extras: + raise TypeError( + f'__init__() got keyword(s) not supported by any control: {extras}' + ) self._controllers = { - cls.name.lower(): cls(df, explorer=self, **params) - for cls, params in controller_params.items() + cls.name.lower(): cls(df, explorer=self, **cparams) + for cls, cparams in controller_params.items() } self.param.set_param(**self._controllers) self.param.watch(self._plot, list(self.param)) From 4a9b8d242ee120334ccb47b92b2070a494362df5 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Wed, 20 Sep 2023 15:08:36 +0200 Subject: [PATCH 28/65] Add autorange option (#1128) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add autorange option * Add small documentation about autorange --------- Co-authored-by: Simon Høxbro Hansen --- examples/user_guide/Timeseries_Data.ipynb | 17 +++++++++++++++++ hvplot/converter.py | 8 +++++++- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/examples/user_guide/Timeseries_Data.ipynb b/examples/user_guide/Timeseries_Data.ipynb index e4ebe709f..18fc67563 100644 --- a/examples/user_guide/Timeseries_Data.ipynb +++ b/examples/user_guide/Timeseries_Data.ipynb @@ -56,6 +56,23 @@ "sst.hvplot(xformatter=formatter)" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Auto range\n", + "Automatic auto-ranging on the data in x or y is supported, making it easy to scale the given axes and fit the entire visible curve after a zoom or pan." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "sst.hvplot(autorange=\"y\")" + ] + }, { "cell_type": "markdown", "metadata": {}, diff --git a/hvplot/converter.py b/hvplot/converter.py index f17e81f56..b7d119038 100644 --- a/hvplot/converter.py +++ b/hvplot/converter.py @@ -84,6 +84,9 @@ class HoloViewsConverter: """ Generic options --------------- + autorange (default=None): Literal['x', 'y'] | None + Whether to enable auto-ranging along the x- or y-axis when + zooming. clim: tuple Lower and upper bound of the color scale cnorm (default='linear'): str @@ -383,7 +386,8 @@ def __init__( y_sampling=None, project=False, tools=[], attr_labels=None, coastline=False, tiles=False, sort_date=True, check_symmetric_max=1000000, transforms={}, stream=None, - cnorm=None, features=None, rescale_discrete_levels=None, **kwds + cnorm=None, features=None, rescale_discrete_levels=None, + autorange=None, **kwds ): # Process data and related options self._redim = fields @@ -485,6 +489,8 @@ def __init__( if ylim is not None: plot_opts['ylim'] = tuple(ylim) + plot_opts['autorange'] = autorange + self.invert = invert if loglog is not None: logx = logx or loglog From dcbf12a35759e11224533eb6d06954dbd65854c0 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Wed, 20 Sep 2023 15:08:51 +0200 Subject: [PATCH 29/65] Expose timeseries downsampling as an option (#1127) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Expose timeseries downsampling as an option * Add small documentation about downsample --------- Co-authored-by: Simon Høxbro Hansen --- examples/user_guide/Timeseries_Data.ipynb | 17 +++++++++ hvplot/converter.py | 43 +++++++++++++++-------- 2 files changed, 46 insertions(+), 14 deletions(-) diff --git a/examples/user_guide/Timeseries_Data.ipynb b/examples/user_guide/Timeseries_Data.ipynb index 18fc67563..48852d137 100644 --- a/examples/user_guide/Timeseries_Data.ipynb +++ b/examples/user_guide/Timeseries_Data.ipynb @@ -183,6 +183,23 @@ "source": [ "Note that xarray supports grouping and aggregation using a similar syntax. To learn more about timeseries in xarray, see the [xarray timeseries docs](https://xarray.pydata.org/en/stable/time-series.html)." ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Downsample time series\n", + "An option when working with large time series is to downsample the data before plotting it. This can be done with `downsample=True`, which applies the `lttb` (Largest Triangle Three Buckets) algorithm to the data." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "sst.hvplot(label=\"original\") * sst.hvplot(downsample=True, label=\"downsampled\")" + ] } ], "metadata": { diff --git a/hvplot/converter.py b/hvplot/converter.py index b7d119038..16e80a956 100644 --- a/hvplot/converter.py +++ b/hvplot/converter.py @@ -185,7 +185,7 @@ class HoloViewsConverter: check_symmetric_max (default=1000000): Size above which to stop checking for symmetry by default on the data. - Datashader options + Downsampling options ------------------ aggregator (default=None): Aggregator to use when applying rasterize or datashade operation @@ -200,6 +200,10 @@ class HoloViewsConverter: Whether to apply rasterization and shading (colormapping) using the Datashader library, returning an RGB object instead of individual points + downsample (default=False): + Whether to apply LTTB (Largest Triangle Three Buckets) + downsampling to the element (note this is only well behaved for + timeseries data). dynspread (default=False): For plots generated with datashade=True or rasterize=True, automatically increase the point size when the data is sparse @@ -378,10 +382,10 @@ def __init__( title=None, xlim=None, ylim=None, clim=None, symmetric=None, logx=None, logy=None, loglog=None, hover=None, subplots=False, label=None, invert=False, stacked=False, colorbar=None, - datashade=False, rasterize=False, row=None, col=None, - debug=False, framewise=True, aggregator=None, - projection=None, global_extent=None, geo=False, - precompute=False, flip_xaxis=None, flip_yaxis=None, + datashade=False, rasterize=False, downsample=None, + row=None, col=None, debug=False, framewise=True, + aggregator=None, projection=None, global_extent=None, + geo=False, precompute=False, flip_xaxis=None, flip_yaxis=None, dynspread=False, hover_cols=[], x_sampling=None, y_sampling=None, project=False, tools=[], attr_labels=None, coastline=False, tiles=False, sort_date=True, @@ -464,6 +468,7 @@ def __init__( # Operations self.datashade = datashade self.rasterize = rasterize + self.downsample = downsample self.dynspread = dynspread self.aggregator = aggregator self.precompute = precompute @@ -1262,10 +1267,28 @@ def method_wrapper(ds, x, y): projection = self._plot_opts.get('projection', ccrs.GOOGLE_MERCATOR) obj = project(obj, projection=projection) - if not (self.datashade or self.rasterize): + if not (self.datashade or self.rasterize or self.downsample): layers = self._apply_layers(obj) layers = _transfer_opts_cur_backend(layers) return layers + + opts = dict(dynamic=self.dynamic) + if self._plot_opts.get('width') is not None: + opts['width'] = self._plot_opts['width'] + if self._plot_opts.get('height') is not None: + opts['height'] = self._plot_opts['height'] + + if self.downsample: + from holoviews.operation.downsample import downsample1d + + if self.x_sampling: + opts['x_sampling'] = self.x_sampling + if self._plot_opts.get('xlim') is not None: + opts['x_range'] = self._plot_opts['xlim'] + layers = downsample1d(obj, **opts) + layers = _transfer_opts_cur_backend(layers) + return layers + try: from holoviews.operation.datashader import datashade, rasterize, dynspread from datashader import reductions @@ -1275,12 +1298,6 @@ def method_wrapper(ds, x, y): 'It can be installed with:\n conda ' 'install datashader') - opts = dict(dynamic=self.dynamic) - if self._plot_opts.get('width') is not None: - opts['width'] = self._plot_opts['width'] - if self._plot_opts.get('height') is not None: - opts['height'] = self._plot_opts['height'] - categorical = False if self.by and not self.subplots: opts['aggregator'] = reductions.count_cat(self.by[0]) @@ -1310,8 +1327,6 @@ def method_wrapper(ds, x, y): opts['x_range'] = self._plot_opts['xlim'] if self._plot_opts.get('ylim') is not None: opts['y_range'] = self._plot_opts['ylim'] - if not self.dynamic: - opts['dynamic'] = self.dynamic if 'cmap' in self._style_opts and self.datashade: levels = self._plot_opts.get('color_levels') From 13ef34c55294ef23b146ab4452d4ae3256cedfaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Wed, 20 Sep 2023 18:26:51 +0200 Subject: [PATCH 30/65] Fixing conda package build (#1148) --- .github/workflows/build.yaml | 2 +- conda.recipe/meta.yaml | 8 -------- setup.py | 8 ++------ 3 files changed, 3 insertions(+), 15 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index d85e16f71..c72800eaa 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -35,7 +35,7 @@ jobs: - uses: conda-incubator/setup-miniconda@v2 with: miniconda-version: "latest" - python-version: 3.8 + python-version: ${{ matrix.python-version }} - name: Fetch unshallow run: git fetch --prune --tags --unshallow -f - name: Set output diff --git a/conda.recipe/meta.yaml b/conda.recipe/meta.yaml index 4797bc90b..ad84047d9 100644 --- a/conda.recipe/meta.yaml +++ b/conda.recipe/meta.yaml @@ -28,14 +28,6 @@ requirements: - {{ dep }} {% endfor %} -test: - imports: - - hvplot - requires: - {% for dep in sdata['extras_require']['tests'] %} - - {{ dep }} - {% endfor %} - about: home: {{ sdata['url'] }} summary: {{ sdata['description'] }} diff --git a/setup.py b/setup.py index 0bc3a79cc..61b3c7195 100644 --- a/setup.py +++ b/setup.py @@ -32,7 +32,7 @@ def get_setup_version(reponame): 'colorcet >=2', 'holoviews >=1.11.0', 'pandas', - 'numpy>=1.15', + 'numpy >=1.15', 'packaging', 'panel >=0.11.0', 'param >=1.9.0', @@ -95,16 +95,12 @@ def get_setup_version(reponame): 'spatialpandas >=0.4.3', ] -extras_require['examples_conda'] = [ - 'hdf5 !=1.14.1', # Gives coredump in test suite on Linux and Mac -] - # Run the example tests by installing examples_tests together with tests extras_require["examples_tests"] = extras_require["examples"] + extras_require['tests_nb'] # Additional packages required to build the docs extras_require['doc'] = extras_require['examples'] + [ - 'nbsite >=0.8.0rc33', + 'nbsite >=0.8.2', ] # until pyproject.toml/equivalent is widely supported (setup_requires From 99ec889c6b8b90915e31dc191d0ed81e5bf454cb Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Fri, 15 Sep 2023 17:33:23 -0700 Subject: [PATCH 31/65] Step towards implementing xarray explorer --- hvplot/ui.py | 41 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 37 insertions(+), 4 deletions(-) diff --git a/hvplot/ui.py b/hvplot/ui.py index cf14557aa..b5e417b11 100644 --- a/hvplot/ui.py +++ b/hvplot/ui.py @@ -331,7 +331,7 @@ class hvPlotExplorer(Viewer): y = param.Selector() - y_multi = param.ListSelector(default=[], label='y') + y_multi = param.ListSelector(default=[], label='Y Multi') by = param.ListSelector(default=[]) @@ -358,8 +358,7 @@ def from_data(cls, data, **params): # cls = hvGeomExplorer raise TypeError('GeoDataFrame objects not yet supported.') elif is_xarray(data): - # cls = hvGridExplorer - raise TypeError('Xarray objects not yet supported.') + cls = hvGridExplorer else: cls = hvDataFrameExplorer return cls(data, **params) @@ -416,7 +415,7 @@ def __init__(self, df, **params): cls.name.lower(): cls(df, explorer=self, **cparams) for cls, cparams in controller_params.items() } - self.param.set_param(**self._controllers) + self.param.update(**self._controllers) self.param.watch(self._plot, list(self.param)) for controller in self._controllers.values(): controller.param.watch(self._plot, list(controller.param)) @@ -670,6 +669,40 @@ def ylim(self): values = (self._data[y] for y in y) return max_range([(np.nanmin(vs), np.nanmax(vs)) for vs in values]) + def _populate(self): + variables = self._converter.variables + indexes = getattr(self._converter, "indexes", []) + variables_no_index = [v for v in variables if v not in indexes] + is_gridded_kind = self.kind in GRIDDED_KINDS + print(self.kind) + for pname in self.param: + if pname == 'kind': + continue + p = self.param[pname] + if isinstance(p, param.Selector) and is_gridded_kind: + if pname in ["x", "y", "groupby"]: + p.objects = indexes + elif pname == "by": + p.objects = [] + else: + p.objects = variables_no_index + + # Setting the default value if not set + if (pname == "x" or pname == "y") and getattr(self, pname, None) is None: + setattr(self, pname, p.objects[0]) + elif pname == "groupby" and len(getattr(self, pname, [])) == 0: + setattr(self, pname, p.objects[-1:]) + elif isinstance(p, param.Selector): + # TODO: update this when self.kind is updated + if pname == "x": + p.objects = variables + else: + p.objects = variables_no_index + + # Setting the default value if not set + if (pname == "x" or pname == "y") and getattr(self, pname, None) is None: + setattr(self, pname, p.objects[0]) + class hvDataFrameExplorer(hvPlotExplorer): From 9180fececc421cac4ad55095e8b25c0c7e40ca70 Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Mon, 18 Sep 2023 15:42:38 -0700 Subject: [PATCH 32/65] Group by kinds and add auto refresh --- hvplot/ui.py | 75 ++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 55 insertions(+), 20 deletions(-) diff --git a/hvplot/ui.py b/hvplot/ui.py index b5e417b11..4b446137f 100644 --- a/hvplot/ui.py +++ b/hvplot/ui.py @@ -13,11 +13,21 @@ from .util import is_geodataframe, is_xarray # Defaults -DATAFRAME_KINDS = sorted(set(_hvConverter._kind_mapping) - set(_hvConverter._gridded_types)) -GRIDDED_KINDS = sorted(_hvConverter._kind_mapping) -GEOM_KINDS = ['paths', 'polygons', 'points'] -STATS_KINDS = ['hist', 'kde', 'boxwhisker', 'violin', 'heatmap', 'bar', 'barh'] -TWOD_KINDS = ['bivariate', 'heatmap', 'hexbin', 'labels', 'vectorfield'] + GEOM_KINDS +KINDS = { + # these are for the kind selector + "dataframe": sorted( + set(_hvConverter._kind_mapping) - + set(_hvConverter._gridded_types) - + set(_hvConverter._geom_types) + ), + "gridded": sorted(set(_hvConverter._gridded_types) - set(["dataset"])), + "geom": _hvConverter._geom_types, +} + +KINDS["2d"] = ['bivariate', 'heatmap', 'hexbin', 'labels', 'vectorfield', 'points'] + KINDS["gridded"] + KINDS["geom"] +KINDS["stats"] = ['hist', 'kde', 'boxwhisker', 'violin', 'heatmap', 'bar', 'barh'] +KINDS["all"] = sorted(set(KINDS["dataframe"] + KINDS["gridded"] + KINDS["geom"])) + CMAPS = [cm for cm in list_cmaps() if not cm.endswith('_r_r')] DEFAULT_CMAPS = _hvConverter._default_cmaps GEO_FEATURES = [ @@ -383,16 +393,18 @@ def __init__(self, df, **params): super().__init__(**params) self._data = df self._converter = converter + groups = {group: KINDS[group] for group in self._groups} self._controls = pn.Param( self.param, parameters=['kind', 'x', 'y', 'by', 'groupby'], sizing_mode='stretch_width', max_width=300, show_name=False, + widgets={"kind": {"options": [], "groups": groups}} ) self.param.watch(self._toggle_controls, 'kind') self.param.watch(self._check_y, 'y_multi') self.param.watch(self._check_by, 'by') self._populate() self._tabs = pn.Tabs( - tabs_location='left', width=400 + tabs_location='left', width=450 ) controls = [ p.class_ @@ -422,18 +434,24 @@ def __init__(self, df, **params): self._alert = pn.pane.Alert( alert_type='danger', visible=False, sizing_mode='stretch_width' ) + self._refresh_control = pn.widgets.Toggle(value=True, name="Auto-refresh", sizing_mode="stretch_width") + self._refresh_control.param.watch(self._refresh, 'value') + self._hv_pane = pn.pane.HoloViews(sizing_mode='stretch_both', margin=(0, 20, 0, 20)) self._layout = pn.Column( self._alert, + self._refresh_control, pn.Row( self._tabs, pn.layout.HSpacer(), + self._hv_pane, sizing_mode='stretch_width' ), pn.layout.HSpacer(), sizing_mode='stretch_both' ) - self._toggle_controls() - self._plot() + + # initialize + self.param.trigger("kind") def _populate(self): variables = self._converter.variables @@ -454,6 +472,8 @@ def _populate(self): setattr(self, pname, p.objects[0]) def _plot(self, *events): + if not self._refresh_control.value: + return y = self.y_multi if 'y_multi' in self._controls.parameters else self.y if isinstance(y, list) and len(y) == 1: y = y[0] @@ -474,17 +494,14 @@ def _plot(self, *events): kwargs['min_height'] = 300 df = self._data - if len(df) > MAX_ROWS and not (self.kind in STATS_KINDS or kwargs.get('rasterize') or kwargs.get('datashade')): + if len(df) > MAX_ROWS and not (self.kind in KINDS["stats"] or kwargs.get('rasterize') or kwargs.get('datashade')): df = df.sample(n=MAX_ROWS) self._layout.loading = True try: self._hvplot = _hvPlot(df)( kind=self.kind, x=self.x, y=y, by=self.by, groupby=self.groupby, **kwargs ) - self._hvpane = pn.pane.HoloViews( - self._hvplot, sizing_mode='stretch_width', margin=(0, 20, 0, 20) - ).layout - self._layout[1][1] = self._hvpane + self._hv_pane.object = self._hvplot self._alert.visible = False except Exception as e: self._alert.param.set_param( @@ -494,19 +511,27 @@ def _plot(self, *events): finally: self._layout.loading = False + def _refresh(self, event): + if event.new: + self._plot() + @property def _single_y(self): - if self.kind in ['labels', 'hexbin', 'heatmap', 'bivariate'] + GRIDDED_KINDS: + if self.kind in KINDS["2d"]: return True return False + @property + def _groups(self): + raise NotImplementedError('Must be implemented by subclasses.') + def _toggle_controls(self, event=None): # Control high-level parameters visible = True if event and event.new in ('table', 'dataset'): parameters = ['kind', 'columns'] visible = False - elif event and event.new in TWOD_KINDS: + elif event and event.new in KINDS['2d']: parameters = ['kind', 'x', 'y', 'by', 'groupby'] elif event and event.new in ('hist', 'kde', 'density'): self.x = None @@ -613,7 +638,7 @@ def settings(self): class hvGeomExplorer(hvPlotExplorer): - kind = param.Selector(default=None, objects=sorted(GEOM_KINDS)) + kind = param.Selector(default=None, objects=KINDS["all"]) @property def _single_y(self): @@ -635,10 +660,13 @@ def xlim(self): def ylim(self): pass + @property + def _groups(self): + return ["gridded", "dataframe"] class hvGridExplorer(hvPlotExplorer): - kind = param.Selector(default=None, objects=sorted(GRIDDED_KINDS)) + kind = param.Selector(default="image", objects=KINDS['all']) @property def _x(self): @@ -669,12 +697,15 @@ def ylim(self): values = (self._data[y] for y in y) return max_range([(np.nanmin(vs), np.nanmax(vs)) for vs in values]) + @property + def _groups(self): + return ["gridded", "dataframe", "geom"] + def _populate(self): variables = self._converter.variables indexes = getattr(self._converter, "indexes", []) variables_no_index = [v for v in variables if v not in indexes] - is_gridded_kind = self.kind in GRIDDED_KINDS - print(self.kind) + is_gridded_kind = self.kind in KINDS['gridded'] for pname in self.param: if pname == 'kind': continue @@ -708,7 +739,7 @@ class hvDataFrameExplorer(hvPlotExplorer): z = param.Selector() - kind = param.Selector(default='line', objects=sorted(DATAFRAME_KINDS)) + kind = param.Selector(default='line', objects=KINDS["all"]) @property def xcat(self): @@ -733,6 +764,10 @@ def _y(self): y = y[0] return y + @property + def _groups(self): + return ["dataframe"] + @param.depends('x') def xlim(self): if self._x == 'index': From a15b3eec94caa6e5f541c4e892f1cd0cac36529d Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Mon, 18 Sep 2023 16:15:13 -0700 Subject: [PATCH 33/65] Fix layout and by --- hvplot/ui.py | 36 ++++++++++++------------------------ 1 file changed, 12 insertions(+), 24 deletions(-) diff --git a/hvplot/ui.py b/hvplot/ui.py index 4b446137f..fa6d720e6 100644 --- a/hvplot/ui.py +++ b/hvplot/ui.py @@ -89,7 +89,6 @@ def __init__(self, df, **params): widget_kwargs[p] = {'throttled': True} self._controls = pn.Param( self.param, - max_width=300, show_name=False, sizing_mode='stretch_width', widgets=widget_kwargs, @@ -169,7 +168,7 @@ class Axes(Controls): width = param.Integer(default=None, bounds=(0, None)) - responsive = param.Boolean(default=False) + responsive = param.Boolean(default=True) shared_axes = param.Boolean(default=True) @@ -396,7 +395,7 @@ def __init__(self, df, **params): groups = {group: KINDS[group] for group in self._groups} self._controls = pn.Param( self.param, parameters=['kind', 'x', 'y', 'by', 'groupby'], - sizing_mode='stretch_width', max_width=300, show_name=False, + sizing_mode='stretch_width', show_name=False, widgets={"kind": {"options": [], "groups": groups}} ) self.param.watch(self._toggle_controls, 'kind') @@ -404,7 +403,7 @@ def __init__(self, df, **params): self.param.watch(self._check_by, 'by') self._populate() self._tabs = pn.Tabs( - tabs_location='left', width=450 + tabs_location='left', width=425 ) controls = [ p.class_ @@ -434,17 +433,16 @@ def __init__(self, df, **params): self._alert = pn.pane.Alert( alert_type='danger', visible=False, sizing_mode='stretch_width' ) - self._refresh_control = pn.widgets.Toggle(value=True, name="Auto-refresh", sizing_mode="stretch_width") + self._refresh_control = pn.widgets.Toggle(value=True, name="Auto-refresh plot", sizing_mode="stretch_width") self._refresh_control.param.watch(self._refresh, 'value') - self._hv_pane = pn.pane.HoloViews(sizing_mode='stretch_both', margin=(0, 20, 0, 20)) + self._hv_pane = pn.pane.HoloViews(sizing_mode='stretch_width', margin=(5, 20, 5, 20)) self._layout = pn.Column( self._alert, self._refresh_control, pn.Row( self._tabs, - pn.layout.HSpacer(), self._hv_pane, - sizing_mode='stretch_width' + sizing_mode="stretch_width", ), pn.layout.HSpacer(), sizing_mode='stretch_both' @@ -454,6 +452,9 @@ def __init__(self, df, **params): self.param.trigger("kind") def _populate(self): + """ + Populates the options of the controls based on the data type. + """ variables = self._converter.variables indexes = getattr(self._converter, "indexes", []) variables_no_index = [v for v in variables if v not in indexes] @@ -504,7 +505,7 @@ def _plot(self, *events): self._hv_pane.object = self._hvplot self._alert.visible = False except Exception as e: - self._alert.param.set_param( + self._alert.param.update( object=f'**Rendering failed with following error**: {e}', visible=True ) @@ -705,16 +706,13 @@ def _populate(self): variables = self._converter.variables indexes = getattr(self._converter, "indexes", []) variables_no_index = [v for v in variables if v not in indexes] - is_gridded_kind = self.kind in KINDS['gridded'] for pname in self.param: if pname == 'kind': continue p = self.param[pname] - if isinstance(p, param.Selector) and is_gridded_kind: - if pname in ["x", "y", "groupby"]: + if isinstance(p, param.Selector): + if pname in ["x", "y", "groupby", "by"]: p.objects = indexes - elif pname == "by": - p.objects = [] else: p.objects = variables_no_index @@ -723,16 +721,6 @@ def _populate(self): setattr(self, pname, p.objects[0]) elif pname == "groupby" and len(getattr(self, pname, [])) == 0: setattr(self, pname, p.objects[-1:]) - elif isinstance(p, param.Selector): - # TODO: update this when self.kind is updated - if pname == "x": - p.objects = variables - else: - p.objects = variables_no_index - - # Setting the default value if not set - if (pname == "x" or pname == "y") and getattr(self, pname, None) is None: - setattr(self, pname, p.objects[0]) class hvDataFrameExplorer(hvPlotExplorer): From 6f9dd5754083e00cbdedbc7261f3c4d20cdb11da Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Wed, 27 Sep 2023 14:31:39 -0400 Subject: [PATCH 34/65] Add back geo --- hvplot/ui.py | 44 +++++++++++++++++++++++++------------------- hvplot/util.py | 10 ++++++++++ 2 files changed, 35 insertions(+), 19 deletions(-) diff --git a/hvplot/ui.py b/hvplot/ui.py index fa6d720e6..b87df1521 100644 --- a/hvplot/ui.py +++ b/hvplot/ui.py @@ -10,7 +10,7 @@ from .converter import HoloViewsConverter as _hvConverter from .plotting import hvPlot as _hvPlot -from .util import is_geodataframe, is_xarray +from .util import is_geodataframe, is_xarray, instantiate_crs_str # Defaults KINDS = { @@ -248,6 +248,12 @@ class Geo(Controls): crs_kwargs = param.Dict(default={}, doc=""" Keyword arguments to pass to selected CRS.""") + projection = param.ObjectSelector(default=None, doc=""" + Projection to use for cartographic plots.""") + + projection_kwargs = param.Dict(default={}, doc=""" + Keyword arguments to pass to selected projection.""") + global_extent = param.Boolean(default=False, doc=""" Whether to expand the plot extent to span the whole globe.""") @@ -267,27 +273,24 @@ class Geo(Controls): the default is 'Wikipedia'.""") @param.depends('geo', watch=True, on_init=True) - def _update_params(self): + def _update_crs_projection(self): enabled = bool(self.geo) geo_controls = [ "crs", "crs_kwargs", "global_extent", "project", "features", "tiles" ] for p in geo_controls: self.param[p].constant = not enabled - - if self.crs is None and enabled: - self._populate_crs() - - def _populate_crs(self): - # Method exists because cartopy is not a dependency of hvplot + if not enabled: + return from cartopy.crs import CRS, GOOGLE_MERCATOR crs = { k: v for k, v in param.concrete_descendents(CRS).items() if not k.startswith('_') and k != 'CRS' } - crs['WebMercator'] = GOOGLE_MERCATOR + crs["-"] = "" + crs['GOOGLE_MERCATOR'] = GOOGLE_MERCATOR self.param.crs.objects = crs - self.crs = next(iter(crs)) + self.param.projection.objects = crs class Operations(Controls): @@ -354,8 +357,7 @@ class hvPlotExplorer(Viewer): labels = param.ClassSelector(class_=Labels) - # Hide the geo tab until it's better supported - # geo = param.ClassSelector(class_=Geo) + geo = param.ClassSelector(class_=Geo) operations = param.ClassSelector(class_=Operations) @@ -394,7 +396,7 @@ def __init__(self, df, **params): self._converter = converter groups = {group: KINDS[group] for group in self._groups} self._controls = pn.Param( - self.param, parameters=['kind', 'x', 'y', 'by', 'groupby'], + self.param, parameters=['kind', 'x', 'y', 'groupby', 'by'], sizing_mode='stretch_width', show_name=False, widgets={"kind": {"options": [], "groups": groups}} ) @@ -487,11 +489,15 @@ def _plot(self, *events): if isinstance(v, Controls): kwargs.update(v.kwargs) - # Initialize CRS - crs_kwargs = kwargs.pop('crs_kwargs', {}) - if 'crs' in kwargs: - if isinstance(kwargs['crs'], type): - kwargs['crs'] = kwargs['crs'](**crs_kwargs) + if kwargs.get("geo"): + for key in ["crs", "projection"]: + if key in kwargs: + crs_kwargs = kwargs.pop(f"{key}_kwargs", {}) + kwargs[key] = instantiate_crs_str(kwargs.pop(key), **crs_kwargs) + else: + # Always remove these intermediate keys from kwargs + kwargs.pop('crs_kwargs', {}) + kwargs.pop('projection_kwargs', {}) kwargs['min_height'] = 300 df = self._data @@ -554,7 +560,7 @@ def _toggle_controls(self, event=None): }, show_name=False)), ('Style', self.style), ('Operations', self.operations), - # ('Geo', self.geo) + ('Geo', self.geo) ] if event and event.new not in ('area', 'kde', 'line', 'ohlc', 'rgb', 'step'): tabs.insert(5, ('Colormapping', self.colormapping)) diff --git a/hvplot/util.py b/hvplot/util.py index 394b102d5..3eaca1d91 100644 --- a/hvplot/util.py +++ b/hvplot/util.py @@ -580,3 +580,13 @@ def _convert_col_names_to_str(data): if renamed: data = data.rename(columns=renamed) return data + + +def instantiate_crs_str(crs_str: str, **kwargs): + """ + Instantiate a cartopy.crs.Projection from a string. + """ + import cartopy.crs as ccrs + if crs_str.upper() == "GOOGLE_MERCATOR": + return ccrs.GOOGLE_MERCATOR + return getattr(ccrs, crs_str)(**kwargs) From 79e65d82c1d58eb9d3d21033770bee04a6e14db3 Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Mon, 18 Sep 2023 17:13:57 -0700 Subject: [PATCH 35/65] Tweak y_multi --- hvplot/ui.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/hvplot/ui.py b/hvplot/ui.py index b87df1521..9901bacf6 100644 --- a/hvplot/ui.py +++ b/hvplot/ui.py @@ -267,6 +267,9 @@ class Geo(Controls): at which to render it. Available features include 'borders', 'coastline', 'lakes', 'land', 'ocean', 'rivers' and 'states'.""") + feature_scale = param.ObjectSelector(default="110m", objects=["110m", "50m", "10m"], doc=""" + The scale at which to render the features.""") + tiles = param.ObjectSelector(default=None, objects=GEO_TILES, doc=""" Whether to overlay the plot on a tile source. Tiles sources can be selected by name or a tiles object or class can be passed, @@ -490,16 +493,22 @@ def _plot(self, *events): kwargs.update(v.kwargs) if kwargs.get("geo"): + print("geo") for key in ["crs", "projection"]: if key in kwargs: crs_kwargs = kwargs.pop(f"{key}_kwargs", {}) kwargs[key] = instantiate_crs_str(kwargs.pop(key), **crs_kwargs) + + feature_scale = kwargs.pop("feature_scale", None) + kwargs['features'] = {feature: feature_scale for feature in kwargs.pop("features", [])} else: # Always remove these intermediate keys from kwargs + kwargs.pop('geo') kwargs.pop('crs_kwargs', {}) kwargs.pop('projection_kwargs', {}) + kwargs.pop('feature_scale', None) - kwargs['min_height'] = 300 + kwargs['min_height'] = 600 df = self._data if len(df) > MAX_ROWS and not (self.kind in KINDS["stats"] or kwargs.get('rasterize') or kwargs.get('datashade')): df = df.sample(n=MAX_ROWS) @@ -545,7 +554,10 @@ def _toggle_controls(self, event=None): parameters = ['kind', 'y_multi', 'by', 'groupby'] else: parameters = ['kind', 'x', 'y_multi', 'by', 'groupby'] - self._controls.parameters = parameters + with param.batch_watch(self): + self._controls.parameters = parameters + if 'y_multi' in self._controls.parameters: + self.y_multi = self.param["y_multi"].objects[:1] # Control other tabs tabs = [('Fields', self._controls)] From c497f768cf6d8ebb382716b2dec5f8fb3ae326fb Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Mon, 18 Sep 2023 17:17:48 -0700 Subject: [PATCH 36/65] Fix missing kwarg --- hvplot/ui.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/hvplot/ui.py b/hvplot/ui.py index 9901bacf6..b4f66e764 100644 --- a/hvplot/ui.py +++ b/hvplot/ui.py @@ -493,10 +493,9 @@ def _plot(self, *events): kwargs.update(v.kwargs) if kwargs.get("geo"): - print("geo") for key in ["crs", "projection"]: + crs_kwargs = kwargs.pop(f"{key}_kwargs", {}) if key in kwargs: - crs_kwargs = kwargs.pop(f"{key}_kwargs", {}) kwargs[key] = instantiate_crs_str(kwargs.pop(key), **crs_kwargs) feature_scale = kwargs.pop("feature_scale", None) From c6bcfb900eddbd8dd96fa96d550f8de29a12f46e Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Thu, 21 Sep 2023 08:56:00 -0700 Subject: [PATCH 37/65] Add tests --- hvplot/tests/testui.py | 129 +++++++++++++++++++++++++++++++---------- hvplot/ui.py | 38 +++++++----- 2 files changed, 120 insertions(+), 47 deletions(-) diff --git a/hvplot/tests/testui.py b/hvplot/tests/testui.py index 2d9062ecb..c60819b34 100644 --- a/hvplot/tests/testui.py +++ b/hvplot/tests/testui.py @@ -2,11 +2,13 @@ import holoviews as hv import hvplot.pandas +import hvplot.xarray +import xarray as xr import pytest from bokeh.sampledata import penguins -from hvplot.ui import hvDataFrameExplorer +from hvplot.ui import hvDataFrameExplorer, hvGridExplorer df = penguins.data @@ -15,28 +17,28 @@ def test_explorer_basic(): explorer = hvplot.explorer(df) assert isinstance(explorer, hvDataFrameExplorer) - assert explorer.kind == 'line' - assert explorer.x == 'index' - assert explorer.y == 'species' + assert explorer.kind == "line" + assert explorer.x == "index" + assert explorer.y == "species" def test_explorer_settings(): explorer = hvplot.explorer(df) explorer.param.set_param( - kind='scatter', - x='bill_length_mm', - y_multi=['bill_depth_mm'], - by=['species'], + kind="scatter", + x="bill_length_mm", + y_multi=["bill_depth_mm"], + by=["species"], ) settings = explorer.settings() assert settings == dict( - by=['species'], - kind='scatter', - x='bill_length_mm', - y=['bill_depth_mm'], + by=["species"], + kind="scatter", + x="bill_length_mm", + y=["bill_depth_mm"], ) @@ -44,47 +46,53 @@ def test_explorer_plot_code(): explorer = hvplot.explorer(df) explorer.param.set_param( - kind='scatter', - x='bill_length_mm', - y_multi=['bill_depth_mm'], - by=['species'], + kind="scatter", + x="bill_length_mm", + y_multi=["bill_depth_mm"], + by=["species"], ) hvplot_code = explorer.plot_code() - assert hvplot_code == "df.hvplot(by=['species'], kind='scatter', x='bill_length_mm', y=['bill_depth_mm'])" + assert ( + hvplot_code + == "df.hvplot(by=['species'], kind='scatter', x='bill_length_mm', y=['bill_depth_mm'])" + ) - hvplot_code = explorer.plot_code(var_name='othername') + hvplot_code = explorer.plot_code(var_name="othername") - assert hvplot_code == "othername.hvplot(by=['species'], kind='scatter', x='bill_length_mm', y=['bill_depth_mm'])" + assert ( + hvplot_code + == "othername.hvplot(by=['species'], kind='scatter', x='bill_length_mm', y=['bill_depth_mm'])" + ) def test_explorer_hvplot(): explorer = hvplot.explorer(df) explorer.param.set_param( - kind='scatter', - x='bill_length_mm', - y_multi=['bill_depth_mm'], + kind="scatter", + x="bill_length_mm", + y_multi=["bill_depth_mm"], ) plot = explorer.hvplot() assert isinstance(plot, hv.Scatter) - assert plot.kdims[0].name == 'bill_length_mm' - assert plot.vdims[0].name == 'bill_depth_mm' + assert plot.kdims[0].name == "bill_length_mm" + assert plot.vdims[0].name == "bill_depth_mm" def test_explorer_save(tmp_path): explorer = hvplot.explorer(df) explorer.param.set_param( - kind='scatter', - x='bill_length_mm', - y_multi=['bill_depth_mm'], + kind="scatter", + x="bill_length_mm", + y_multi=["bill_depth_mm"], ) - outfile = tmp_path / 'plot.html' + outfile = tmp_path / "plot.html" explorer.save(outfile) @@ -92,14 +100,71 @@ def test_explorer_save(tmp_path): def test_explorer_kwargs_controls(): - explorer = hvplot.explorer(df, title='Dummy title', width=200) + explorer = hvplot.explorer(df, title="Dummy title", width=200) - assert explorer.labels.title == 'Dummy title' + assert explorer.labels.title == "Dummy title" assert explorer.axes.width == 200 def test_explorer_kwargs_controls_error_not_supported(): with pytest.raises( - TypeError, match=re.escape("__init__() got keyword(s) not supported by any control: {'not_a_control_kwarg': None}") + TypeError, + match=re.escape( + "__init__() got keyword(s) not supported by any control: {'not_a_control_kwarg': None}" + ), ): - hvplot.explorer(df, title='Dummy title', not_a_control_kwarg=None) + hvplot.explorer(df, title="Dummy title", not_a_control_kwarg=None) + + +def test_explorer_hvplot_gridded_basic(): + ds = xr.tutorial.open_dataset("air_temperature") + explorer = hvplot.explorer(ds) + + assert isinstance(explorer, hvGridExplorer) + assert isinstance(explorer._data, xr.DataArray) + assert explorer.kind == "image" + assert explorer.x == "lat" + assert explorer.y == "lon" + assert explorer.by == [] + assert explorer.groupby == ["time"] + + +def test_explorer_hvplot_gridded_2d(): + ds = xr.tutorial.open_dataset("air_temperature").isel(time=0) + explorer = hvplot.explorer(ds) + + assert isinstance(explorer, hvGridExplorer) + assert isinstance(explorer._data, xr.DataArray) + assert explorer.kind == "image" + assert explorer.x == "lat" + assert explorer.y == "lon" + assert explorer.by == [] + assert explorer.groupby == [] + + +def test_explorer_hvplot_gridded_two_variables(): + ds = xr.tutorial.open_dataset("air_temperature") + ds["airx2"] = ds["air"] * 2 + explorer = hvplot.explorer(ds) + + assert isinstance(explorer, hvGridExplorer) + assert isinstance(explorer._data, xr.DataArray) + assert list(explorer._data["variable"]) == ["air", "airx2"] + assert explorer.kind == "image" + assert explorer.x == "lat" + assert explorer.y == "lon" + assert explorer.by == [] + assert explorer.groupby == ["time", "variable"] + + +def test_explorer_hvplot_gridded_dataarray(): + da = xr.tutorial.open_dataset("air_temperature")["air"] + explorer = hvplot.explorer(da) + + assert isinstance(explorer, hvGridExplorer) + assert isinstance(explorer._data, xr.DataArray) + assert explorer.kind == "image" + assert explorer.x == "lat" + assert explorer.y == "lon" + assert explorer.by == [] + assert explorer.groupby == ["time"] diff --git a/hvplot/ui.py b/hvplot/ui.py index b4f66e764..e015e7341 100644 --- a/hvplot/ui.py +++ b/hvplot/ui.py @@ -49,7 +49,7 @@ def explorer(data, **kwargs): Parameters ---------- - data : pandas.DataFrame + data : pandas.DataFrame | xarray.Dataset Data structure to explore. kwargs : optional Arguments that `data.hvplot()` would also accept like `kind='bar'`. @@ -553,10 +553,7 @@ def _toggle_controls(self, event=None): parameters = ['kind', 'y_multi', 'by', 'groupby'] else: parameters = ['kind', 'x', 'y_multi', 'by', 'groupby'] - with param.batch_watch(self): - self._controls.parameters = parameters - if 'y_multi' in self._controls.parameters: - self.y_multi = self.param["y_multi"].objects[:1] + self._controls.parameters = parameters # Control other tabs tabs = [('Fields', self._controls)] @@ -686,6 +683,18 @@ class hvGridExplorer(hvPlotExplorer): kind = param.Selector(default="image", objects=KINDS['all']) + def __init__(self, ds, **params): + import xarray as xr + if isinstance(ds, xr.Dataset): + data_vars = list(ds.data_vars) + if len(data_vars) == 1: + ds = ds[data_vars[0]] + else: + ds = ds.to_array('variable').transpose(..., "variable") + if "kind" not in params: + params["kind"] = "image" + super().__init__(ds, **params) + @property def _x(self): return (self._converter.x or self._converter.indexes[0]) if self.x is None else self.x @@ -696,13 +705,10 @@ def _y(self): @param.depends('x') def xlim(self): - if self._x == 'index': - values = self._data.index.values - else: - try: - values = self._data[self._x] - except: - return 0, 1 + try: + values = self._data[self._x] + except: + return 0, 1 if values.dtype.kind in 'OSU': return None return (np.nanmin(values), np.nanmax(values)) @@ -734,10 +740,12 @@ def _populate(self): p.objects = variables_no_index # Setting the default value if not set - if (pname == "x" or pname == "y") and getattr(self, pname, None) is None: + if pname == "x" and getattr(self, pname, None) is None: setattr(self, pname, p.objects[0]) - elif pname == "groupby" and len(getattr(self, pname, [])) == 0: - setattr(self, pname, p.objects[-1:]) + elif pname == "y" and getattr(self, pname, None) is None: + setattr(self, pname, p.objects[1]) + elif pname == "groupby" and len(getattr(self, pname, [])) == 0 and len(p.objects) > 2: + setattr(self, pname, p.objects[2:]) class hvDataFrameExplorer(hvPlotExplorer): From 3afd3d63fbada0d3f4d6d2e653a648452596e3d2 Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Thu, 21 Sep 2023 08:56:43 -0700 Subject: [PATCH 38/65] Add more tests --- hvplot/tests/testui.py | 8 +++++++- hvplot/tests/testutil.py | 19 ++++++++++++++++++- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/hvplot/tests/testui.py b/hvplot/tests/testui.py index c60819b34..e61b4c7b1 100644 --- a/hvplot/tests/testui.py +++ b/hvplot/tests/testui.py @@ -167,4 +167,10 @@ def test_explorer_hvplot_gridded_dataarray(): assert explorer.x == "lat" assert explorer.y == "lon" assert explorer.by == [] - assert explorer.groupby == ["time"] + assert explorer.groupby == ['time'] + + +def test_explorer_hvplot_gridded_options(): + ds = xr.tutorial.open_dataset("air_temperature") + explorer = hvplot.explorer(ds) + assert explorer._controls[0].groups.keys() == {"dataframe", "gridded", "geom"} diff --git a/hvplot/tests/testutil.py b/hvplot/tests/testutil.py index 6c72bb672..1e72db07a 100644 --- a/hvplot/tests/testutil.py +++ b/hvplot/tests/testutil.py @@ -5,13 +5,14 @@ import numpy as np import pandas as pd +import cartopy.crs as ccrs import pytest from unittest import TestCase, SkipTest from hvplot.util import ( check_crs, is_list_like, process_crs, process_xarray, - _convert_col_names_to_str + _convert_col_names_to_str, instantiate_crs_str ) @@ -330,3 +331,19 @@ def test_convert_col_names_to_str(): assert all(not isinstance(col, str) for col in df.columns) df = _convert_col_names_to_str(df) assert all(isinstance(col, str) for col in df.columns) + + +def test_instantiate_crs_str(): + assert isinstance(instantiate_crs_str("PlateCarree"), ccrs.PlateCarree) + + +def test_instantiate_crs_google_mercator(): + assert instantiate_crs_str("GOOGLE_MERCATOR") == ccrs.GOOGLE_MERCATOR + assert instantiate_crs_str("google_mercator") == ccrs.GOOGLE_MERCATOR + + +def test_instantiate_crs_str_kwargs(): + crs = instantiate_crs_str("PlateCarree", globe=ccrs.Globe(datum="WGS84")) + assert isinstance(crs, ccrs.PlateCarree) + assert isinstance(crs.globe, ccrs.Globe) + assert crs.globe.datum == "WGS84" From 1c05554e2bf80eb3b2fed666f7f427d9804100dd Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Wed, 27 Sep 2023 14:32:16 -0400 Subject: [PATCH 39/65] Improve defaults --- hvplot/ui.py | 43 +++++++++++++++++++++++++------------------ 1 file changed, 25 insertions(+), 18 deletions(-) diff --git a/hvplot/ui.py b/hvplot/ui.py index e015e7341..c42b48e57 100644 --- a/hvplot/ui.py +++ b/hvplot/ui.py @@ -18,7 +18,8 @@ "dataframe": sorted( set(_hvConverter._kind_mapping) - set(_hvConverter._gridded_types) - - set(_hvConverter._geom_types) + set(_hvConverter._geom_types) | + set(["points"]) ), "gridded": sorted(set(_hvConverter._gridded_types) - set(["dataset"])), "geom": _hvConverter._geom_types, @@ -34,7 +35,7 @@ 'borders', 'coastline', 'land', 'lakes', 'ocean', 'rivers', 'states', 'grid' ] -GEO_TILES = [None] + list(tile_sources) +GEO_TILES = [None] + sorted(tile_sources) AGGREGATORS = [None, 'count', 'min', 'max', 'mean', 'sum', 'any'] MAX_ROWS = 10000 @@ -254,7 +255,7 @@ class Geo(Controls): projection_kwargs = param.Dict(default={}, doc=""" Keyword arguments to pass to selected projection.""") - global_extent = param.Boolean(default=False, doc=""" + global_extent = param.Boolean(default=None, doc=""" Whether to expand the plot extent to span the whole globe.""") project = param.Boolean(default=False, doc=""" @@ -262,7 +263,7 @@ class Geo(Controls): overhead but avoids projecting data when plot is dynamically updated).""") - features = param.ListSelector(default=[], objects=GEO_FEATURES, doc=""" + features = param.ListSelector(default=None, objects=GEO_FEATURES, doc=""" A list of features or a dictionary of features and the scale at which to render it. Available features include 'borders', 'coastline', 'lakes', 'land', 'ocean', 'rivers' and 'states'.""") @@ -275,25 +276,30 @@ class Geo(Controls): can be selected by name or a tiles object or class can be passed, the default is 'Wikipedia'.""") - @param.depends('geo', watch=True, on_init=True) + @param.depends('geo', watch=True, on_init=True) def _update_crs_projection(self): - enabled = bool(self.geo) - geo_controls = [ - "crs", "crs_kwargs", "global_extent", "project", "features", "tiles" - ] - for p in geo_controls: - self.param[p].constant = not enabled + enabled = bool(self.geo or self.project) + self.param.crs.constant = not enabled + self.param.crs_kwargs.constant = not enabled + self.param.projection.constant = not enabled + self.param.projection_kwargs.constant = not enabled + self.geo = enabled if not enabled: return - from cartopy.crs import CRS, GOOGLE_MERCATOR - crs = { - k: v for k, v in param.concrete_descendents(CRS).items() + from cartopy.crs import CRS + crs = sorted( + k for k in param.concrete_descendents(CRS).keys() if not k.startswith('_') and k != 'CRS' - } - crs["-"] = "" - crs['GOOGLE_MERCATOR'] = GOOGLE_MERCATOR + ) + crs.insert(0, "GOOGLE_MERCATOR") + crs.insert(0, "PlateCarree") + crs.remove("PlateCarree") self.param.crs.objects = crs self.param.projection.objects = crs + if self.global_extent is None: + self.global_extent = True + if self.features is None: + self.features = ["coastline"] class Operations(Controls): @@ -346,7 +352,7 @@ class hvPlotExplorer(Viewer): y = param.Selector() - y_multi = param.ListSelector(default=[], label='Y Multi') + y_multi = param.ListSelector(default=[], label='Y') by = param.ListSelector(default=[]) @@ -513,6 +519,7 @@ def _plot(self, *events): df = df.sample(n=MAX_ROWS) self._layout.loading = True try: + print(kwargs, self.kind, self.x, y, self.by, self.groupby) self._hvplot = _hvPlot(df)( kind=self.kind, x=self.x, y=y, by=self.by, groupby=self.groupby, **kwargs ) From e7073c5512e5af3970a2b1ba02e392dbd25df9dc Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Thu, 21 Sep 2023 11:22:30 -0700 Subject: [PATCH 40/65] Fix geo --- hvplot/converter.py | 4 ++-- hvplot/ui.py | 26 +++++++++++++++++--------- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/hvplot/converter.py b/hvplot/converter.py index 16e80a956..9e3d0a08f 100644 --- a/hvplot/converter.py +++ b/hvplot/converter.py @@ -671,12 +671,12 @@ def _process_crs(self, data, crs): try: return process_crs(_crs) - except ValueError: + except ValueError as e: # only raise error if crs was specified in kwargs if crs: raise ValueError( f"'{crs}' must be either a valid crs or an reference to " - "a `data.attr` containing a valid crs.") + f"a `data.attr` containing a valid crs: {e}") def _process_data(self, kind, data, x, y, by, groupby, row, col, use_dask, persist, backlog, label, group_label, diff --git a/hvplot/ui.py b/hvplot/ui.py index c42b48e57..f3b287bf5 100644 --- a/hvplot/ui.py +++ b/hvplot/ui.py @@ -286,18 +286,27 @@ def _update_crs_projection(self): self.geo = enabled if not enabled: return + from cartopy.crs import CRS - crs = sorted( + crs_list = sorted( k for k in param.concrete_descendents(CRS).keys() if not k.startswith('_') and k != 'CRS' ) - crs.insert(0, "GOOGLE_MERCATOR") - crs.insert(0, "PlateCarree") - crs.remove("PlateCarree") - self.param.crs.objects = crs - self.param.projection.objects = crs + crs_list.insert(0, "GOOGLE_MERCATOR") + crs_list.insert(0, "PlateCarree") + crs_list.remove("PlateCarree") + + self.param.crs.objects = crs_list + if self.crs is None: + self.crs = crs_list[0] + + self.param.projection.objects = crs_list + if self.projection is None: + self.projection = crs_list[0] + if self.global_extent is None: self.global_extent = True + if self.features is None: self.features = ["coastline"] @@ -348,9 +357,9 @@ class hvPlotExplorer(Viewer): kind = param.Selector() - x = param.Selector() + x = param.Selector(default="x") - y = param.Selector() + y = param.Selector(default="y") y_multi = param.ListSelector(default=[], label='Y') @@ -519,7 +528,6 @@ def _plot(self, *events): df = df.sample(n=MAX_ROWS) self._layout.loading = True try: - print(kwargs, self.kind, self.x, y, self.by, self.groupby) self._hvplot = _hvPlot(df)( kind=self.kind, x=self.x, y=y, by=self.by, groupby=self.groupby, **kwargs ) From 40395de7f01742f4b609be725f9f1ae57f9cc5a6 Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Thu, 21 Sep 2023 12:10:17 -0700 Subject: [PATCH 41/65] Prevent class name conflict with kwarg geo and fix tests --- hvplot/tests/testui.py | 11 +++++++++++ hvplot/ui.py | 10 +++++----- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/hvplot/tests/testui.py b/hvplot/tests/testui.py index e61b4c7b1..7ba81a21d 100644 --- a/hvplot/tests/testui.py +++ b/hvplot/tests/testui.py @@ -1,6 +1,7 @@ import re import holoviews as hv +import pandas as pd import hvplot.pandas import hvplot.xarray import xarray as xr @@ -174,3 +175,13 @@ def test_explorer_hvplot_gridded_options(): ds = xr.tutorial.open_dataset("air_temperature") explorer = hvplot.explorer(ds) assert explorer._controls[0].groups.keys() == {"dataframe", "gridded", "geom"} + + +def test_explorer_hvplot_geo(): + df = pd.DataFrame({"x": [-9796115.18980811], "y": [4838471.398061159]}) + explorer = hvplot.explorer(df, geo=True) + assert explorer.geographic.geo + assert explorer.geographic.global_extent + assert explorer.geographic.features == ["coastline"] + assert explorer.geographic.crs == "GOOGLE_MERCATOR" + assert explorer.geographic.projection == "GOOGLE_MERCATOR" diff --git a/hvplot/ui.py b/hvplot/ui.py index f3b287bf5..36317570a 100644 --- a/hvplot/ui.py +++ b/hvplot/ui.py @@ -236,7 +236,7 @@ class Labels(Controls): number of degrees.""") -class Geo(Controls): +class Geographic(Controls): geo = param.Boolean(default=False, doc=""" Whether the plot should be treated as geographic (and assume @@ -357,9 +357,9 @@ class hvPlotExplorer(Viewer): kind = param.Selector() - x = param.Selector(default="x") + x = param.Selector() - y = param.Selector(default="y") + y = param.Selector() y_multi = param.ListSelector(default=[], label='Y') @@ -375,7 +375,7 @@ class hvPlotExplorer(Viewer): labels = param.ClassSelector(class_=Labels) - geo = param.ClassSelector(class_=Geo) + geographic = param.ClassSelector(class_=Geographic) operations = param.ClassSelector(class_=Operations) @@ -583,7 +583,7 @@ def _toggle_controls(self, event=None): }, show_name=False)), ('Style', self.style), ('Operations', self.operations), - ('Geo', self.geo) + ('Geographic', self.geographic) ] if event and event.new not in ('area', 'kde', 'line', 'ohlc', 'rgb', 'step'): tabs.insert(5, ('Colormapping', self.colormapping)) From 92efd95509d29590365eb3518f706ab5261ca515 Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Tue, 26 Sep 2023 15:30:01 -0400 Subject: [PATCH 42/65] Add docs --- examples/user_guide/Explorer.ipynb | 84 +++++++++++++++++++++++++++++- 1 file changed, 82 insertions(+), 2 deletions(-) diff --git a/examples/user_guide/Explorer.ipynb b/examples/user_guide/Explorer.ipynb index bd5dae2d6..21cd421f4 100644 --- a/examples/user_guide/Explorer.ipynb +++ b/examples/user_guide/Explorer.ipynb @@ -8,6 +8,7 @@ "source": [ "import hvplot.pandas\n", "\n", + "import xarray as xr\n", "from bokeh.sampledata.penguins import data as df" ] }, @@ -16,12 +17,13 @@ "metadata": {}, "source": [ "
\n", - " The Explorer has been added to hvPlot in version 0.8.0, and improved and documented in version 0.8.1. It does not yet support all the data structures supported by hvPlot, for now it works best with Pandas DataFrame objects. Please report any issue or feature request on GitHub.\n", + " The Explorer has been added to hvPlot in version 0.8.0, and improved and documented in version 0.8.1 and 0.8.5. It does not yet support all the data structures supported by hvPlot. Please report any issue or feature request on GitHub.\n", "
" ] }, { "cell_type": "markdown", + "id": "bd5ab787", "metadata": {}, "source": [ "hvPlot API provides a simple and intuitive way to create plots. However when you are exploring data you don't always know in advance the best way to display it, or even what kind of plot would be best to visualize the data. You will very likely embark in an iterative process that implies choosing a kind of plot, setting various options, running some code, and repeat until you're satisfied with the output and the insights you get. The *Explorer* is a *Graphical User Interface* that allows you to easily generate customized plots, which in practice gives you the possibility to **explore** both your data and hvPlot's extensive API.\n", @@ -34,6 +36,7 @@ { "cell_type": "code", "execution_count": null, + "id": "6d36b75a", "metadata": {}, "outputs": [], "source": [ @@ -68,7 +71,7 @@ "metadata": {}, "outputs": [], "source": [ - "hvexplorer.param.set_param(kind='scatter', x='bill_length_mm', y_multi=['bill_depth_mm'], by=['species'])\n", + "hvexplorer.param.update(kind='scatter', x='bill_length_mm', y_multi=['bill_depth_mm'], by=['species'])\n", "hvexplorer.labels.title = 'Penguins Scatter'" ] }, @@ -153,6 +156,83 @@ "df.hvplot(by=['species'], kind='scatter', title='Penguins Scatter', x='bill_length_mm', y=['bill_depth_mm'])" ] }, + { + "cell_type": "markdown", + "id": "b986b42d", + "metadata": {}, + "source": [ + "Or you can simply call `hvplot` on the explorer instance." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "db9ada50", + "metadata": {}, + "outputs": [], + "source": [ + "hvexplorer.hvplot()" + ] + }, + { + "cell_type": "markdown", + "id": "8c183122", + "metadata": {}, + "source": [ + "It also works for xarray objects." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f15dde1f", + "metadata": {}, + "outputs": [], + "source": [ + "ds = xr.tutorial.open_dataset(\"air_temperature\")\n", + "\n", + "hvplot.explorer(ds, x=\"lon\", y=\"lat\")" + ] + }, + { + "cell_type": "markdown", + "id": "d6d90743", + "metadata": {}, + "source": [ + "It's also possible to geographically reference the data, with cartopy and geoviews installed." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "87d4ab4f", + "metadata": {}, + "outputs": [], + "source": [ + "hvexplorer = hvplot.explorer(ds, x=\"lon\", y=\"lat\", geo=True)\n", + "hvexplorer.geographic.param.update(crs=\"PlateCarree\", tiles=\"CartoDark\")\n", + "hvexplorer" + ] + }, + { + "cell_type": "markdown", + "id": "c4736831", + "metadata": {}, + "source": [ + "Large datasets can be plotted efficiently using Datashader's `rasterize`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d01077b1", + "metadata": {}, + "outputs": [], + "source": [ + "hvexplorer = hvplot.explorer(ds, x=\"lon\", y=\"lat\", rasterize=True)\n", + "hvexplorer" + ] + }, { "cell_type": "markdown", "metadata": {}, From ab4fa59b66ebecba41fbd9e210c214a2b872bf37 Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Tue, 26 Sep 2023 16:02:26 -0400 Subject: [PATCH 43/65] Improve default --- hvplot/ui.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/hvplot/ui.py b/hvplot/ui.py index 36317570a..49f9143be 100644 --- a/hvplot/ui.py +++ b/hvplot/ui.py @@ -297,9 +297,6 @@ def _update_crs_projection(self): crs_list.remove("PlateCarree") self.param.crs.objects = crs_list - if self.crs is None: - self.crs = crs_list[0] - self.param.projection.objects = crs_list if self.projection is None: self.projection = crs_list[0] @@ -508,10 +505,13 @@ def _plot(self, *events): kwargs.update(v.kwargs) if kwargs.get("geo"): + if "crs" not in kwargs: + xmax = np.max(np.abs(self.xlim())) + self.crs = "PlateCarree" if xmax <= 360 else "GOOGLE_MERCATOR" + kwargs["crs"] = self.crs for key in ["crs", "projection"]: crs_kwargs = kwargs.pop(f"{key}_kwargs", {}) - if key in kwargs: - kwargs[key] = instantiate_crs_str(kwargs.pop(key), **crs_kwargs) + kwargs[key] = instantiate_crs_str(kwargs.pop(key), **crs_kwargs) feature_scale = kwargs.pop("feature_scale", None) kwargs['features'] = {feature: feature_scale for feature in kwargs.pop("features", [])} From 1ae0a1612b03d334dd3f473201dea616568c7f3f Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Wed, 27 Sep 2023 10:45:20 -0400 Subject: [PATCH 44/65] Fix tests --- hvplot/tests/testui.py | 2 +- hvplot/ui.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/hvplot/tests/testui.py b/hvplot/tests/testui.py index 7ba81a21d..f0195e618 100644 --- a/hvplot/tests/testui.py +++ b/hvplot/tests/testui.py @@ -179,7 +179,7 @@ def test_explorer_hvplot_gridded_options(): def test_explorer_hvplot_geo(): df = pd.DataFrame({"x": [-9796115.18980811], "y": [4838471.398061159]}) - explorer = hvplot.explorer(df, geo=True) + explorer = hvplot.explorer(df, x="x", geo=True, kind="points") assert explorer.geographic.geo assert explorer.geographic.global_extent assert explorer.geographic.features == ["coastline"] diff --git a/hvplot/ui.py b/hvplot/ui.py index 49f9143be..5d64aa6a1 100644 --- a/hvplot/ui.py +++ b/hvplot/ui.py @@ -507,8 +507,8 @@ def _plot(self, *events): if kwargs.get("geo"): if "crs" not in kwargs: xmax = np.max(np.abs(self.xlim())) - self.crs = "PlateCarree" if xmax <= 360 else "GOOGLE_MERCATOR" - kwargs["crs"] = self.crs + self.geographic.crs = "PlateCarree" if xmax <= 360 else "GOOGLE_MERCATOR" + kwargs["crs"] = self.geographic.crs for key in ["crs", "projection"]: crs_kwargs = kwargs.pop(f"{key}_kwargs", {}) kwargs[key] = instantiate_crs_str(kwargs.pop(key), **crs_kwargs) From 5b3d4089d2e484acca8daa0b09f1899ff77e0865 Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Wed, 27 Sep 2023 14:33:34 -0400 Subject: [PATCH 45/65] Tweaks to removing geo kwargs --- hvplot/ui.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/hvplot/ui.py b/hvplot/ui.py index 5d64aa6a1..08cf97e34 100644 --- a/hvplot/ui.py +++ b/hvplot/ui.py @@ -498,7 +498,7 @@ def _plot(self, *events): kwargs = {} for v in self.param.values().values(): # Geo is not enabled so not adding it to kwargs - if isinstance(v, Geo) and not v.geo: + if isinstance(v, Geographic) and not v.geo: continue if isinstance(v, Controls): @@ -515,12 +515,6 @@ def _plot(self, *events): feature_scale = kwargs.pop("feature_scale", None) kwargs['features'] = {feature: feature_scale for feature in kwargs.pop("features", [])} - else: - # Always remove these intermediate keys from kwargs - kwargs.pop('geo') - kwargs.pop('crs_kwargs', {}) - kwargs.pop('projection_kwargs', {}) - kwargs.pop('feature_scale', None) kwargs['min_height'] = 600 df = self._data From 0db60c93c196def92166c13ec058f1fbe122e0a8 Mon Sep 17 00:00:00 2001 From: Andrew <15331990+ahuang11@users.noreply.github.com> Date: Wed, 27 Sep 2023 14:38:22 -0400 Subject: [PATCH 46/65] Remove merge accident --- hvplot/ui.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/hvplot/ui.py b/hvplot/ui.py index 08f7bef18..554778d4c 100644 --- a/hvplot/ui.py +++ b/hvplot/ui.py @@ -521,12 +521,6 @@ def _plot(self, *events): feature_scale = kwargs.pop("feature_scale", None) kwargs['features'] = {feature: feature_scale for feature in kwargs.pop("features", [])} - else: - # Always remove these intermediate keys from kwargs - kwargs.pop('geo') - kwargs.pop('crs_kwargs', {}) - kwargs.pop('projection_kwargs', {}) - kwargs.pop('feature_scale', None) kwargs['min_height'] = 600 df = self._data From afb421cbd47f74da42bbaf7c3bfc989dbc6c478a Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Wed, 27 Sep 2023 14:53:22 -0400 Subject: [PATCH 47/65] Fix merge issues --- hvplot/tests/testui.py | 2 -- hvplot/tests/testutil.py | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/hvplot/tests/testui.py b/hvplot/tests/testui.py index 34e63d041..f0195e618 100644 --- a/hvplot/tests/testui.py +++ b/hvplot/tests/testui.py @@ -8,8 +8,6 @@ import pytest -import pytest - from bokeh.sampledata import penguins from hvplot.ui import hvDataFrameExplorer, hvGridExplorer diff --git a/hvplot/tests/testutil.py b/hvplot/tests/testutil.py index 726c2b6c9..036fef07f 100644 --- a/hvplot/tests/testutil.py +++ b/hvplot/tests/testutil.py @@ -323,7 +323,7 @@ def test_process_crs_pyproj_proj(): # Created with pyproj.CRS("EPSG:3857").to_wkt() 'PROJCRS["WGS 84 / Pseudo-Mercator",BASEGEOGCRS["WGS 84",ENSEMBLE["World Geodetic System 1984 ensemble",MEMBER["World Geodetic System 1984 (Transit)"],MEMBER["World Geodetic System 1984 (G730)"],MEMBER["World Geodetic System 1984 (G873)"],MEMBER["World Geodetic System 1984 (G1150)"],MEMBER["World Geodetic System 1984 (G1674)"],MEMBER["World Geodetic System 1984 (G1762)"],MEMBER["World Geodetic System 1984 (G2139)"],ELLIPSOID["WGS 84",6378137,298.257223563,LENGTHUNIT["metre",1]],ENSEMBLEACCURACY[2.0]],PRIMEM["Greenwich",0,ANGLEUNIT["degree",0.0174532925199433]],ID["EPSG",4326]],CONVERSION["Popular Visualisation Pseudo-Mercator",METHOD["Popular Visualisation Pseudo Mercator",ID["EPSG",1024]],PARAMETER["Latitude of natural origin",0,ANGLEUNIT["degree",0.0174532925199433],ID["EPSG",8801]],PARAMETER["Longitude of natural origin",0,ANGLEUNIT["degree",0.0174532925199433],ID["EPSG",8802]],PARAMETER["False easting",0,LENGTHUNIT["metre",1],ID["EPSG",8806]],PARAMETER["False northing",0,LENGTHUNIT["metre",1],ID["EPSG",8807]]],CS[Cartesian,2],AXIS["easting (X)",east,ORDER[1],LENGTHUNIT["metre",1]],AXIS["northing (Y)",north,ORDER[2],LENGTHUNIT["metre",1]],USAGE[SCOPE["Web mapping and visualisation."],AREA["World between 85.06°S and 85.06°N."],BBOX[-85.06,-180,85.06,180]],ID["EPSG",3857]]' ], ids=lambda x: str(x)[:20]) -def test_process_crs(input): +def test_process_crs_platecarree(input): pytest.importorskip("pyproj") ccrs = pytest.importorskip("cartopy.crs") crs = process_crs(input) From d93b211ce6570bbbda2cdd04358ca12260a1819b Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Wed, 27 Sep 2023 14:57:38 -0400 Subject: [PATCH 48/65] Fix tests --- hvplot/tests/testutil.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/hvplot/tests/testutil.py b/hvplot/tests/testutil.py index 036fef07f..6c005fc31 100644 --- a/hvplot/tests/testutil.py +++ b/hvplot/tests/testutil.py @@ -319,9 +319,9 @@ def test_process_crs_pyproj_proj(): 4326, "epsg:4326", "EPSG: 4326", - 'PROJCS["WGS 84 / Pseudo-Mercator",GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563,AUTHORITY["EPSG","7030"]],AUTHORITY["EPSG","6326"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.0174532925199433,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4326"]],PROJECTION["Mercator_1SP"],PARAMETER["central_meridian",0],PARAMETER["scale_factor",1],PARAMETER["false_easting",0],PARAMETER["false_northing",0],UNIT["metre",1,AUTHORITY["EPSG","9001"]],AXIS["Easting",EAST],AXIS["Northing",NORTH],EXTENSION["PROJ4","+proj=merc +a=6378137 +b=6378137 +lat_ts=0 +lon_0=0 +x_0=0 +y_0=0 +k=1 +units=m +nadgrids=@null +wktext +no_defs"],AUTHORITY["EPSG","3857"]]', - # Created with pyproj.CRS("EPSG:3857").to_wkt() - 'PROJCRS["WGS 84 / Pseudo-Mercator",BASEGEOGCRS["WGS 84",ENSEMBLE["World Geodetic System 1984 ensemble",MEMBER["World Geodetic System 1984 (Transit)"],MEMBER["World Geodetic System 1984 (G730)"],MEMBER["World Geodetic System 1984 (G873)"],MEMBER["World Geodetic System 1984 (G1150)"],MEMBER["World Geodetic System 1984 (G1674)"],MEMBER["World Geodetic System 1984 (G1762)"],MEMBER["World Geodetic System 1984 (G2139)"],ELLIPSOID["WGS 84",6378137,298.257223563,LENGTHUNIT["metre",1]],ENSEMBLEACCURACY[2.0]],PRIMEM["Greenwich",0,ANGLEUNIT["degree",0.0174532925199433]],ID["EPSG",4326]],CONVERSION["Popular Visualisation Pseudo-Mercator",METHOD["Popular Visualisation Pseudo Mercator",ID["EPSG",1024]],PARAMETER["Latitude of natural origin",0,ANGLEUNIT["degree",0.0174532925199433],ID["EPSG",8801]],PARAMETER["Longitude of natural origin",0,ANGLEUNIT["degree",0.0174532925199433],ID["EPSG",8802]],PARAMETER["False easting",0,LENGTHUNIT["metre",1],ID["EPSG",8806]],PARAMETER["False northing",0,LENGTHUNIT["metre",1],ID["EPSG",8807]]],CS[Cartesian,2],AXIS["easting (X)",east,ORDER[1],LENGTHUNIT["metre",1]],AXIS["northing (Y)",north,ORDER[2],LENGTHUNIT["metre",1]],USAGE[SCOPE["Web mapping and visualisation."],AREA["World between 85.06°S and 85.06°N."],BBOX[-85.06,-180,85.06,180]],ID["EPSG",3857]]' + "+init=epsg:4326", + # Created with pyproj.CRS("EPSG:4326").to_wkt() + 'GEOGCRS["WGS 84",ENSEMBLE["World Geodetic System 1984 ensemble",MEMBER["World Geodetic System 1984 (Transit)"],MEMBER["World Geodetic System 1984 (G730)"],MEMBER["World Geodetic System 1984 (G873)"],MEMBER["World Geodetic System 1984 (G1150)"],MEMBER["World Geodetic System 1984 (G1674)"],MEMBER["World Geodetic System 1984 (G1762)"],MEMBER["World Geodetic System 1984 (G2139)"],ELLIPSOID["WGS 84",6378137,298.257223563,LENGTHUNIT["metre",1]],ENSEMBLEACCURACY[2.0]],PRIMEM["Greenwich",0,ANGLEUNIT["degree",0.0174532925199433]],CS[ellipsoidal,2],AXIS["geodetic latitude (Lat)",north,ORDER[1],ANGLEUNIT["degree",0.0174532925199433]],AXIS["geodetic longitude (Lon)",east,ORDER[2],ANGLEUNIT["degree",0.0174532925199433]],USAGE[SCOPE["Horizontal component of 3D system."],AREA["World."],BBOX[-90,-180,90,180]],ID["EPSG",4326]]', ], ids=lambda x: str(x)[:20]) def test_process_crs_platecarree(input): pytest.importorskip("pyproj") @@ -336,7 +336,6 @@ def test_process_crs_platecarree(input): "epsg:3857", "EPSG: 3857", "+init=epsg:3857", - 'PROJCS["WGS 84 / Pseudo-Mercator",GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563,AUTHORITY["EPSG","7030"]],AUTHORITY["EPSG","6326"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.0174532925199433,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4326"]],PROJECTION["Mercator_1SP"],PARAMETER["central_meridian",0],PARAMETER["scale_factor",1],PARAMETER["false_easting",0],PARAMETER["false_northing",0],UNIT["metre",1,AUTHORITY["EPSG","9001"]],AXIS["Easting",EAST],AXIS["Northing",NORTH],EXTENSION["PROJ4","+proj=merc +a=6378137 +b=6378137 +lat_ts=0 +lon_0=0 +x_0=0 +y_0=0 +k=1 +units=m +nadgrids=@null +wktext +no_defs"],AUTHORITY["EPSG","3857"]]', # Created with pyproj.CRS("EPSG:3857").to_wkt() 'PROJCRS["WGS 84 / Pseudo-Mercator",BASEGEOGCRS["WGS 84",ENSEMBLE["World Geodetic System 1984 ensemble",MEMBER["World Geodetic System 1984 (Transit)"],MEMBER["World Geodetic System 1984 (G730)"],MEMBER["World Geodetic System 1984 (G873)"],MEMBER["World Geodetic System 1984 (G1150)"],MEMBER["World Geodetic System 1984 (G1674)"],MEMBER["World Geodetic System 1984 (G1762)"],MEMBER["World Geodetic System 1984 (G2139)"],ELLIPSOID["WGS 84",6378137,298.257223563,LENGTHUNIT["metre",1]],ENSEMBLEACCURACY[2.0]],PRIMEM["Greenwich",0,ANGLEUNIT["degree",0.0174532925199433]],ID["EPSG",4326]],CONVERSION["Popular Visualisation Pseudo-Mercator",METHOD["Popular Visualisation Pseudo Mercator",ID["EPSG",1024]],PARAMETER["Latitude of natural origin",0,ANGLEUNIT["degree",0.0174532925199433],ID["EPSG",8801]],PARAMETER["Longitude of natural origin",0,ANGLEUNIT["degree",0.0174532925199433],ID["EPSG",8802]],PARAMETER["False easting",0,LENGTHUNIT["metre",1],ID["EPSG",8806]],PARAMETER["False northing",0,LENGTHUNIT["metre",1],ID["EPSG",8807]]],CS[Cartesian,2],AXIS["easting (X)",east,ORDER[1],LENGTHUNIT["metre",1]],AXIS["northing (Y)",north,ORDER[2],LENGTHUNIT["metre",1]],USAGE[SCOPE["Web mapping and visualisation."],AREA["World between 85.06°S and 85.06°N."],BBOX[-85.06,-180,85.06,180]],ID["EPSG",3857]]', ], ids=lambda x: str(x)[:20]) From 9d2c122a0c4fbe506141732c0cf98174a834957f Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Wed, 27 Sep 2023 14:59:46 -0400 Subject: [PATCH 49/65] Add dropped test --- hvplot/tests/testutil.py | 1 + 1 file changed, 1 insertion(+) diff --git a/hvplot/tests/testutil.py b/hvplot/tests/testutil.py index 6c005fc31..58dad42a5 100644 --- a/hvplot/tests/testutil.py +++ b/hvplot/tests/testutil.py @@ -336,6 +336,7 @@ def test_process_crs_platecarree(input): "epsg:3857", "EPSG: 3857", "+init=epsg:3857", + 'PROJCS["WGS 84 / Pseudo-Mercator",GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563,AUTHORITY["EPSG","7030"]],AUTHORITY["EPSG","6326"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.0174532925199433,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4326"]],PROJECTION["Mercator_1SP"],PARAMETER["central_meridian",0],PARAMETER["scale_factor",1],PARAMETER["false_easting",0],PARAMETER["false_northing",0],UNIT["metre",1,AUTHORITY["EPSG","9001"]],AXIS["Easting",EAST],AXIS["Northing",NORTH],EXTENSION["PROJ4","+proj=merc +a=6378137 +b=6378137 +lat_ts=0 +lon_0=0 +x_0=0 +y_0=0 +k=1 +units=m +nadgrids=@null +wktext +no_defs"],AUTHORITY["EPSG","3857"]]', # Created with pyproj.CRS("EPSG:3857").to_wkt() 'PROJCRS["WGS 84 / Pseudo-Mercator",BASEGEOGCRS["WGS 84",ENSEMBLE["World Geodetic System 1984 ensemble",MEMBER["World Geodetic System 1984 (Transit)"],MEMBER["World Geodetic System 1984 (G730)"],MEMBER["World Geodetic System 1984 (G873)"],MEMBER["World Geodetic System 1984 (G1150)"],MEMBER["World Geodetic System 1984 (G1674)"],MEMBER["World Geodetic System 1984 (G1762)"],MEMBER["World Geodetic System 1984 (G2139)"],ELLIPSOID["WGS 84",6378137,298.257223563,LENGTHUNIT["metre",1]],ENSEMBLEACCURACY[2.0]],PRIMEM["Greenwich",0,ANGLEUNIT["degree",0.0174532925199433]],ID["EPSG",4326]],CONVERSION["Popular Visualisation Pseudo-Mercator",METHOD["Popular Visualisation Pseudo Mercator",ID["EPSG",1024]],PARAMETER["Latitude of natural origin",0,ANGLEUNIT["degree",0.0174532925199433],ID["EPSG",8801]],PARAMETER["Longitude of natural origin",0,ANGLEUNIT["degree",0.0174532925199433],ID["EPSG",8802]],PARAMETER["False easting",0,LENGTHUNIT["metre",1],ID["EPSG",8806]],PARAMETER["False northing",0,LENGTHUNIT["metre",1],ID["EPSG",8807]]],CS[Cartesian,2],AXIS["easting (X)",east,ORDER[1],LENGTHUNIT["metre",1]],AXIS["northing (Y)",north,ORDER[2],LENGTHUNIT["metre",1]],USAGE[SCOPE["Web mapping and visualisation."],AREA["World between 85.06°S and 85.06°N."],BBOX[-85.06,-180,85.06,180]],ID["EPSG",3857]]', ], ids=lambda x: str(x)[:20]) From 186900effade9ada175fcae518a9787c2b9dc44b Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Thu, 28 Sep 2023 08:24:55 -0400 Subject: [PATCH 50/65] Add var name suffix --- hvplot/tests/testui.py | 33 +++++++++++++++++++++++++++++++-- hvplot/ui.py | 11 ++++++++++- 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/hvplot/tests/testui.py b/hvplot/tests/testui.py index 6b67a164d..5f158fb26 100644 --- a/hvplot/tests/testui.py +++ b/hvplot/tests/testui.py @@ -218,7 +218,7 @@ def test_explorer_code_gridded(): explorer._code() code = explorer.code assert code == dedent("""\ - ds.hvplot( + ds['air'].hvplot( colorbar=True, groupby=['time'], kind='image', @@ -230,7 +230,36 @@ def test_explorer_code_gridded(): ```python import hvplot.xarray - ds.hvplot( + ds['air'].hvplot( + colorbar=True, + groupby=['time'], + kind='image', + x='lon', + y='lat' + ) + ```""" + ) + + +def test_explorer_code_gridded_dataarray(): + ds = xr.tutorial.open_dataset("air_temperature")["air"] + explorer = hvplot.explorer(ds, x="lon", y="lat", kind="image") + explorer._code() + code = explorer.code + assert code == dedent("""\ + da.hvplot( + colorbar=True, + groupby=['time'], + kind='image', + x='lon', + y='lat' + )""") + + assert explorer._code_pane.object == dedent("""\ + ```python + import hvplot.xarray + + da.hvplot( colorbar=True, groupby=['time'], kind='image', diff --git a/hvplot/ui.py b/hvplot/ui.py index 554778d4c..f254b4af7 100644 --- a/hvplot/ui.py +++ b/hvplot/ui.py @@ -412,6 +412,7 @@ def __init__(self, df, **params): super().__init__(**params) self._data = df self._converter = converter + self._var_name_suffix = "" groups = {group: KINDS[group] for group in self._groups} self._controls = pn.Param( self.param, parameters=['kind', 'x', 'y', 'groupby', 'by'], @@ -726,19 +727,27 @@ class hvGridExplorer(hvPlotExplorer): def __init__(self, ds, **params): import xarray as xr + var_name_suffix = "" if isinstance(ds, xr.Dataset): data_vars = list(ds.data_vars) if len(data_vars) == 1: ds = ds[data_vars[0]] + var_name_suffix = f"['{data_vars[0]}']" else: ds = ds.to_array('variable').transpose(..., "variable") + var_name_suffix = ".to_array('variable').transpose(..., 'variable')" if "kind" not in params: params["kind"] = "image" super().__init__(ds, **params) + self._var_name_suffix = var_name_suffix @property def _var_name(self): - return "ds" + print(self._var_name_suffix) + if self._var_name_suffix: + return f"ds{self._var_name_suffix}" + else: + return "da" @property def _backend(self): From aa827365d773a35fcca6a7c8c31b3f0a95ae3ee5 Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Thu, 28 Sep 2023 08:31:30 -0400 Subject: [PATCH 51/65] Remove print --- hvplot/ui.py | 1 - 1 file changed, 1 deletion(-) diff --git a/hvplot/ui.py b/hvplot/ui.py index f254b4af7..f4d9ff766 100644 --- a/hvplot/ui.py +++ b/hvplot/ui.py @@ -743,7 +743,6 @@ def __init__(self, ds, **params): @property def _var_name(self): - print(self._var_name_suffix) if self._var_name_suffix: return f"ds{self._var_name_suffix}" else: From 517bfbff849bc7ab3ddb3cf3f1ad9e2c094b9d23 Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Sat, 30 Sep 2023 23:21:24 -0400 Subject: [PATCH 52/65] Refactor refresh --- hvplot/ui.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/hvplot/ui.py b/hvplot/ui.py index f4d9ff766..ef5a6611b 100644 --- a/hvplot/ui.py +++ b/hvplot/ui.py @@ -77,6 +77,12 @@ def explorer(data, **kwargs): class Controls(Viewer): + refresh_plot = param.Boolean( + default=True, + precedence=0, + doc="Whether to automatically refresh the plot when a param is changed", + ) + explorer = param.ClassSelector(class_=Viewer, precedence=-1) __abstract = True @@ -352,6 +358,8 @@ def _update_options(self): class hvPlotExplorer(Viewer): + refresh_plot = param.Boolean(default=True) + kind = param.Selector() x = param.Selector() @@ -415,7 +423,7 @@ def __init__(self, df, **params): self._var_name_suffix = "" groups = {group: KINDS[group] for group in self._groups} self._controls = pn.Param( - self.param, parameters=['kind', 'x', 'y', 'groupby', 'by'], + self.param, parameters=['refresh_plot', 'kind', 'x', 'y', 'groupby', 'by'], sizing_mode='stretch_width', show_name=False, widgets={"kind": {"options": [], "groups": groups}} ) @@ -456,13 +464,11 @@ def __init__(self, df, **params): self._alert = pn.pane.Alert( alert_type='danger', visible=False, sizing_mode='stretch_width' ) - self._refresh_control = pn.widgets.Toggle(value=True, name="Auto-refresh plot", sizing_mode="stretch_width") - self._refresh_control.param.watch(self._refresh, 'value') + self._hv_pane = pn.pane.HoloViews(sizing_mode='stretch_width', margin=(5, 20, 5, 20)) self._code_pane = pn.pane.Markdown(sizing_mode='stretch_width', margin=(5, 20, 0, 20)) self._layout = pn.Column( self._alert, - self._refresh_control, pn.Row( self._tabs, pn.Tabs(("Plot", self._hv_pane), ("Code", self._code_pane)), @@ -497,8 +503,6 @@ def _populate(self): setattr(self, pname, p.objects[0]) def _plot(self, *events): - if not self._refresh_control.value: - return y = self.y_multi if 'y_multi' in self._controls.parameters else self.y if isinstance(y, list) and len(y) == 1: y = y[0] @@ -573,15 +577,15 @@ def _toggle_controls(self, event=None): # Control high-level parameters visible = True if event and event.new in ('table', 'dataset'): - parameters = ['kind', 'columns'] + parameters = ['refresh_plot', 'kind', 'columns'] visible = False elif event and event.new in KINDS['2d']: - parameters = ['kind', 'x', 'y', 'by', 'groupby'] + parameters = ['refresh_plot', 'kind', 'x', 'y', 'by', 'groupby'] elif event and event.new in ('hist', 'kde', 'density'): self.x = None - parameters = ['kind', 'y_multi', 'by', 'groupby'] + parameters = ['refresh_plot', 'kind', 'y_multi', 'by', 'groupby'] else: - parameters = ['kind', 'x', 'y_multi', 'by', 'groupby'] + parameters = ['refresh_plot', 'kind', 'x', 'y_multi', 'by', 'groupby'] self._controls.parameters = parameters # Control other tabs From 14ec6d6cfe0b2a5c6d802c90fba61dcf20aae1ab Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Sat, 30 Sep 2023 23:16:47 -0400 Subject: [PATCH 53/65] Fix auto refresh plot --- hvplot/ui.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/hvplot/ui.py b/hvplot/ui.py index ef5a6611b..6f15037f1 100644 --- a/hvplot/ui.py +++ b/hvplot/ui.py @@ -109,6 +109,10 @@ def kwargs(self): return {k: v for k, v in self.param.values().items() if k not in ('name', 'explorer') and v is not None and v != ''} + @param.depends("explorer.refresh_plot", watch=True) + def _update_refresh_plot(self): + self.refresh_plot = self.explorer.refresh_plot + class Colormapping(Controls): @@ -358,7 +362,10 @@ def _update_options(self): class hvPlotExplorer(Viewer): - refresh_plot = param.Boolean(default=True) + refresh_plot = param.Boolean( + default=True, + doc="Whether to automatically refresh the plot when a param is changed", + ) kind = param.Selector() @@ -460,7 +467,10 @@ def __init__(self, df, **params): params_to_watch.remove("code") self.param.watch(self._plot, params_to_watch) for controller in self._controllers.values(): - controller.param.watch(self._plot, list(controller.param)) + controller.param.watch(self._update_refresh_plot, "refresh_plot") + params_to_watch = list(controller.param) + params_to_watch.remove("refresh_plot") + controller.param.watch(self._plot, params_to_watch) self._alert = pn.pane.Alert( alert_type='danger', visible=False, sizing_mode='stretch_width' ) @@ -503,6 +513,9 @@ def _populate(self): setattr(self, pname, p.objects[0]) def _plot(self, *events): + if not self.refresh_plot: + return + y = self.y_multi if 'y_multi' in self._controls.parameters else self.y if isinstance(y, list) and len(y) == 1: y = y[0] @@ -563,6 +576,9 @@ def _var_name(self): def _backend(self): return "pandas" + def _update_refresh_plot(self, event): + self.refresh_plot = event.new + @property def _single_y(self): if self.kind in KINDS["2d"]: From 71d4fe1e0c4d72737c850fbf1dd38eefb63b2f9b Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Sat, 30 Sep 2023 23:22:31 -0400 Subject: [PATCH 54/65] Pop refresh plot --- hvplot/ui.py | 1 + 1 file changed, 1 insertion(+) diff --git a/hvplot/ui.py b/hvplot/ui.py index 6f15037f1..3ed34ca02 100644 --- a/hvplot/ui.py +++ b/hvplot/ui.py @@ -539,6 +539,7 @@ def _plot(self, *events): feature_scale = kwargs.pop("feature_scale", None) kwargs['features'] = {feature: feature_scale for feature in kwargs.pop("features", [])} + kwargs.pop('refresh_plot', None) kwargs['min_height'] = 600 df = self._data From ab7cb89b1d9bf664c3d0ae02900a01c5e689d1a1 Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Sat, 30 Sep 2023 23:26:11 -0400 Subject: [PATCH 55/65] Disable all geo keys --- hvplot/ui.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/hvplot/ui.py b/hvplot/ui.py index 3ed34ca02..bca0b5b46 100644 --- a/hvplot/ui.py +++ b/hvplot/ui.py @@ -36,6 +36,10 @@ 'states', 'grid' ] GEO_TILES = [None] + sorted(tile_sources) +GEO_KEYS = [ + 'crs', 'crs_kwargs', 'projection', 'projection_kwargs', + 'global_extent', 'project', 'features', 'feature_scale', 'tiles' +] AGGREGATORS = [None, 'count', 'min', 'max', 'mean', 'sum', 'any'] MAX_ROWS = 10000 @@ -289,10 +293,8 @@ class Geographic(Controls): @param.depends('geo', watch=True, on_init=True) def _update_crs_projection(self): enabled = bool(self.geo or self.project) - self.param.crs.constant = not enabled - self.param.crs_kwargs.constant = not enabled - self.param.projection.constant = not enabled - self.param.projection_kwargs.constant = not enabled + for key in GEO_KEYS: + getattr(self.param, key).constant = not enabled self.geo = enabled if not enabled: return From bdfcd3ff5ecd9ecf08dd0af393ad461b78596834 Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Sat, 30 Sep 2023 23:55:49 -0400 Subject: [PATCH 56/65] Fix test --- hvplot/tests/testui.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hvplot/tests/testui.py b/hvplot/tests/testui.py index 5f158fb26..01ca597bf 100644 --- a/hvplot/tests/testui.py +++ b/hvplot/tests/testui.py @@ -175,7 +175,7 @@ def test_explorer_hvplot_gridded_dataarray(): def test_explorer_hvplot_gridded_options(): ds = xr.tutorial.open_dataset("air_temperature") explorer = hvplot.explorer(ds) - assert explorer._controls[0].groups.keys() == {"dataframe", "gridded", "geom"} + assert explorer._controls[1].groups.keys() == {"dataframe", "gridded", "geom"} def test_explorer_hvplot_geo(): From 3cf77425babb3144be9d75038538010af1f58951 Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Sat, 30 Sep 2023 23:56:36 -0400 Subject: [PATCH 57/65] Watch _refresh instead of _plot --- hvplot/ui.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hvplot/ui.py b/hvplot/ui.py index bca0b5b46..d4c437556 100644 --- a/hvplot/ui.py +++ b/hvplot/ui.py @@ -472,7 +472,7 @@ def __init__(self, df, **params): controller.param.watch(self._update_refresh_plot, "refresh_plot") params_to_watch = list(controller.param) params_to_watch.remove("refresh_plot") - controller.param.watch(self._plot, params_to_watch) + controller.param.watch(self._refresh, params_to_watch) self._alert = pn.pane.Alert( alert_type='danger', visible=False, sizing_mode='stretch_width' ) From 35060b41e32b0097cb7c543241acf530ed79b11e Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Sun, 1 Oct 2023 00:07:13 -0400 Subject: [PATCH 58/65] Refactor refresh_plot and link --- hvplot/tests/testui.py | 25 ++++++++++++++++++++++++- hvplot/ui.py | 39 +++++++++++++++++++++++++++++---------- 2 files changed, 53 insertions(+), 11 deletions(-) diff --git a/hvplot/tests/testui.py b/hvplot/tests/testui.py index f0195e618..19b09fa4d 100644 --- a/hvplot/tests/testui.py +++ b/hvplot/tests/testui.py @@ -5,11 +5,12 @@ import hvplot.pandas import hvplot.xarray import xarray as xr +import param import pytest from bokeh.sampledata import penguins -from hvplot.ui import hvDataFrameExplorer, hvGridExplorer +from hvplot.ui import hvDataFrameExplorer, hvGridExplorer, Controls df = penguins.data @@ -185,3 +186,25 @@ def test_explorer_hvplot_geo(): assert explorer.geographic.features == ["coastline"] assert explorer.geographic.crs == "GOOGLE_MERCATOR" assert explorer.geographic.projection == "GOOGLE_MERCATOR" + +def test_explorer_refresh_plot_linked(): + explorer = hvplot.explorer(df) + controls = [ + p.name + for p in explorer.param.objects().values() + if isinstance(p, param.ClassSelector) + and issubclass(p.class_, Controls) + ] + # by default + for control in controls: + assert explorer.refresh_plot == getattr(explorer, control).refresh_plot + + # toggle top level + explorer.refresh_plot = False + for control in controls: + assert explorer.refresh_plot == getattr(explorer, control).refresh_plot + + # toggle axes + explorer.axes.refresh_plot = True + for control in controls: + assert explorer.refresh_plot == getattr(explorer, control).refresh_plot diff --git a/hvplot/ui.py b/hvplot/ui.py index 08cf97e34..f9c1e6a5d 100644 --- a/hvplot/ui.py +++ b/hvplot/ui.py @@ -77,6 +77,12 @@ def explorer(data, **kwargs): class Controls(Viewer): + refresh_plot = param.Boolean( + default=True, + precedence=0, + doc="Whether to automatically refresh the plot when a param is changed", + ) + explorer = param.ClassSelector(class_=Viewer, precedence=-1) __abstract = True @@ -103,6 +109,10 @@ def kwargs(self): return {k: v for k, v in self.param.values().items() if k not in ('name', 'explorer') and v is not None and v != ''} + @param.depends("explorer.refresh_plot", watch=True) + def _update_refresh_plot(self): + self.refresh_plot = self.explorer.refresh_plot + class Colormapping(Controls): @@ -352,6 +362,11 @@ def _update_options(self): class hvPlotExplorer(Viewer): + refresh_plot = param.Boolean( + default=True, + doc="Whether to automatically refresh the plot when a param is changed", + ) + kind = param.Selector() x = param.Selector() @@ -411,7 +426,7 @@ def __init__(self, df, **params): self._converter = converter groups = {group: KINDS[group] for group in self._groups} self._controls = pn.Param( - self.param, parameters=['kind', 'x', 'y', 'groupby', 'by'], + self.param, parameters=['refresh_plot', 'kind', 'x', 'y', 'groupby', 'by'], sizing_mode='stretch_width', show_name=False, widgets={"kind": {"options": [], "groups": groups}} ) @@ -446,16 +461,16 @@ def __init__(self, df, **params): self.param.update(**self._controllers) self.param.watch(self._plot, list(self.param)) for controller in self._controllers.values(): - controller.param.watch(self._plot, list(controller.param)) + params_to_watch = list(controller.param) + params_to_watch.remove("refresh_plot") + controller.param.watch(self._plot, params_to_watch) + controller.param.watch(self._update_refresh_plot, "refresh_plot") self._alert = pn.pane.Alert( alert_type='danger', visible=False, sizing_mode='stretch_width' ) - self._refresh_control = pn.widgets.Toggle(value=True, name="Auto-refresh plot", sizing_mode="stretch_width") - self._refresh_control.param.watch(self._refresh, 'value') self._hv_pane = pn.pane.HoloViews(sizing_mode='stretch_width', margin=(5, 20, 5, 20)) self._layout = pn.Column( self._alert, - self._refresh_control, pn.Row( self._tabs, self._hv_pane, @@ -490,7 +505,7 @@ def _populate(self): setattr(self, pname, p.objects[0]) def _plot(self, *events): - if not self._refresh_control.value: + if not self.refresh_plot: return y = self.y_multi if 'y_multi' in self._controls.parameters else self.y if isinstance(y, list) and len(y) == 1: @@ -515,6 +530,7 @@ def _plot(self, *events): feature_scale = kwargs.pop("feature_scale", None) kwargs['features'] = {feature: feature_scale for feature in kwargs.pop("features", [])} + kwargs.pop('refresh_plot', None) kwargs['min_height'] = 600 df = self._data @@ -539,6 +555,9 @@ def _refresh(self, event): if event.new: self._plot() + def _update_refresh_plot(self, event): + self.refresh_plot = event.new + @property def _single_y(self): if self.kind in KINDS["2d"]: @@ -553,15 +572,15 @@ def _toggle_controls(self, event=None): # Control high-level parameters visible = True if event and event.new in ('table', 'dataset'): - parameters = ['kind', 'columns'] + parameters = ['refresh_plot', 'kind', 'columns'] visible = False elif event and event.new in KINDS['2d']: - parameters = ['kind', 'x', 'y', 'by', 'groupby'] + parameters = ['refresh_plot', 'kind', 'x', 'y', 'by', 'groupby'] elif event and event.new in ('hist', 'kde', 'density'): self.x = None - parameters = ['kind', 'y_multi', 'by', 'groupby'] + parameters = ['refresh_plot', 'kind', 'y_multi', 'by', 'groupby'] else: - parameters = ['kind', 'x', 'y_multi', 'by', 'groupby'] + parameters = ['refresh_plot', 'kind', 'x', 'y_multi', 'by', 'groupby'] self._controls.parameters = parameters # Control other tabs From bb5a5d7d7e211b25825fe1af2ef765e4e2f8f745 Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Sun, 1 Oct 2023 00:07:37 -0400 Subject: [PATCH 59/65] Fix test --- hvplot/tests/testui.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hvplot/tests/testui.py b/hvplot/tests/testui.py index 19b09fa4d..385e2a906 100644 --- a/hvplot/tests/testui.py +++ b/hvplot/tests/testui.py @@ -175,7 +175,7 @@ def test_explorer_hvplot_gridded_dataarray(): def test_explorer_hvplot_gridded_options(): ds = xr.tutorial.open_dataset("air_temperature") explorer = hvplot.explorer(ds) - assert explorer._controls[0].groups.keys() == {"dataframe", "gridded", "geom"} + assert explorer._controls[1].groups.keys() == {"dataframe", "gridded", "geom"} def test_explorer_hvplot_geo(): From 7db9198dd699bc971ba44bfbad61d2c2f97b666d Mon Sep 17 00:00:00 2001 From: Andrew <15331990+ahuang11@users.noreply.github.com> Date: Sun, 1 Oct 2023 12:28:16 -0700 Subject: [PATCH 60/65] Update hvplot/ui.py --- hvplot/ui.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/hvplot/ui.py b/hvplot/ui.py index ba654b42d..5516f883c 100644 --- a/hvplot/ui.py +++ b/hvplot/ui.py @@ -580,9 +580,6 @@ def _backend(self): def _update_refresh_plot(self, event): self.refresh_plot = event.new - def _update_refresh_plot(self, event): - self.refresh_plot = event.new - @property def _single_y(self): if self.kind in KINDS["2d"]: From 8dee6600bc48dd2d5d38006b4527c675bdfe4192 Mon Sep 17 00:00:00 2001 From: Andrew <15331990+ahuang11@users.noreply.github.com> Date: Sun, 8 Oct 2023 16:51:33 -0700 Subject: [PATCH 61/65] Update hvplot/ui.py Co-authored-by: Maxime Liquet <35924738+maximlt@users.noreply.github.com> --- hvplot/ui.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hvplot/ui.py b/hvplot/ui.py index 5516f883c..3178b6995 100644 --- a/hvplot/ui.py +++ b/hvplot/ui.py @@ -294,7 +294,7 @@ class Geographic(Controls): def _update_crs_projection(self): enabled = bool(self.geo or self.project) for key in GEO_KEYS: - getattr(self.param, key).constant = not enabled + self.param[key]constant = not enabled self.geo = enabled if not enabled: return From 33e9c2c3a6fda6017b50825d0539add7a00b19af Mon Sep 17 00:00:00 2001 From: Maxime Liquet <35924738+maximlt@users.noreply.github.com> Date: Mon, 9 Oct 2023 17:36:55 +0200 Subject: [PATCH 62/65] Update hvplot/ui.py --- hvplot/ui.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hvplot/ui.py b/hvplot/ui.py index 3178b6995..b1f96c79b 100644 --- a/hvplot/ui.py +++ b/hvplot/ui.py @@ -294,7 +294,7 @@ class Geographic(Controls): def _update_crs_projection(self): enabled = bool(self.geo or self.project) for key in GEO_KEYS: - self.param[key]constant = not enabled + self.param[key].constant = not enabled self.geo = enabled if not enabled: return From 4e9a946986313b8cde04ce7ff2594465cdee4966 Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Mon, 9 Oct 2023 12:36:35 -0700 Subject: [PATCH 63/65] Fix tests --- hvplot/tests/testui.py | 26 +++++++++++++++++--------- hvplot/ui.py | 42 +++++++++++++++--------------------------- 2 files changed, 32 insertions(+), 36 deletions(-) diff --git a/hvplot/tests/testui.py b/hvplot/tests/testui.py index 024620e0e..00eb5b2a5 100644 --- a/hvplot/tests/testui.py +++ b/hvplot/tests/testui.py @@ -58,15 +58,27 @@ def test_explorer_plot_code(): hvplot_code = explorer.plot_code() assert ( - hvplot_code - == "df.hvplot(by=['species'], kind='scatter', x='bill_length_mm', y=['bill_depth_mm'])" + hvplot_code == ( + "df.hvplot(\n" + " by=['species'],\n" + " kind='scatter',\n" + " x='bill_length_mm',\n" + " y=['bill_depth_mm']\n" + ")" + ) ) hvplot_code = explorer.plot_code(var_name="othername") assert ( - hvplot_code - == "othername.hvplot(by=['species'], kind='scatter', x='bill_length_mm', y=['bill_depth_mm'])" + hvplot_code == ( + "othername.hvplot(\n" + " by=['species'],\n" + " kind='scatter',\n" + " x='bill_length_mm',\n" + " y=['bill_depth_mm']\n" + ")" + ) ) @@ -190,9 +202,7 @@ def test_explorer_hvplot_geo(): def test_explorer_code_dataframe(): explorer = hvplot.explorer(df, x="bill_length_mm", kind="points") - explorer._code() - code = explorer.code - assert code == dedent("""\ + assert explorer.code == dedent("""\ df.hvplot( kind='points', x='bill_length_mm', @@ -215,7 +225,6 @@ def test_explorer_code_dataframe(): def test_explorer_code_gridded(): ds = xr.tutorial.open_dataset("air_temperature") explorer = hvplot.explorer(ds, x="lon", y="lat", kind="image") - explorer._code() code = explorer.code assert code == dedent("""\ ds['air'].hvplot( @@ -244,7 +253,6 @@ def test_explorer_code_gridded(): def test_explorer_code_gridded_dataarray(): ds = xr.tutorial.open_dataset("air_temperature")["air"] explorer = hvplot.explorer(ds, x="lon", y="lat", kind="image") - explorer._code() code = explorer.code assert code == dedent("""\ da.hvplot( diff --git a/hvplot/ui.py b/hvplot/ui.py index b1f96c79b..13ad1ac9d 100644 --- a/hvplot/ui.py +++ b/hvplot/ui.py @@ -114,7 +114,7 @@ def kwargs(self): if k not in ('name', 'explorer') and v is not None and v != ''} @param.depends("explorer.refresh_plot", watch=True) - def _update_refresh_plot(self): + def _link_refresh_plot(self): self.refresh_plot = self.explorer.refresh_plot @@ -429,7 +429,6 @@ def __init__(self, df, **params): super().__init__(**params) self._data = df self._converter = converter - self._var_name_suffix = "" groups = {group: KINDS[group] for group in self._groups} self._controls = pn.Param( self.param, parameters=['refresh_plot', 'kind', 'x', 'y', 'groupby', 'by'], @@ -440,7 +439,7 @@ def __init__(self, df, **params): self.param.watch(self._check_y, 'y_multi') self.param.watch(self._check_by, 'by') self._populate() - self._tabs = pn.Tabs( + self._control_tabs = pn.Tabs( tabs_location='left', width=425 ) controls = [ @@ -467,12 +466,12 @@ def __init__(self, df, **params): self.param.update(**self._controllers) params_to_watch = list(self.param) params_to_watch.remove("code") - self.param.watch(self._plot, params_to_watch) + self.param.watch(self._refresh, params_to_watch) for controller in self._controllers.values(): + controller.param.watch(self._link_refresh_plot, "refresh_plot") params_to_watch = list(controller.param) params_to_watch.remove("refresh_plot") - controller.param.watch(self._plot, params_to_watch) - controller.param.watch(self._update_refresh_plot, "refresh_plot") + controller.param.watch(self._refresh, params_to_watch) self._alert = pn.pane.Alert( alert_type='danger', visible=False, sizing_mode='stretch_width' ) @@ -481,6 +480,7 @@ def __init__(self, df, **params): self._layout = pn.Column( self._alert, pn.Row( + self._control_tabs, pn.Tabs(("Plot", self._hv_pane), ("Code", self._code_pane)), sizing_mode="stretch_width", ), @@ -560,14 +560,11 @@ def _plot(self, *events): finally: self._layout.loading = False - def _code(self): - self.code = self._build_code_snippet() + def _refresh(self, *events): + self._plot() + self.code = self.plot_code() self._code_pane.object = f"""```python\nimport hvplot.{self._backend}\n\n{self.code}\n```""" - def _refresh(self, event): - if event.new: - self._plot() - self._code() @property def _var_name(self): @@ -577,7 +574,7 @@ def _var_name(self): def _backend(self): return "pandas" - def _update_refresh_plot(self, event): + def _link_refresh_plot(self, event): self.refresh_plot = event.new @property @@ -622,7 +619,7 @@ def _toggle_controls(self, event=None): ] if event and event.new not in ('area', 'kde', 'line', 'ohlc', 'rgb', 'step'): tabs.insert(5, ('Colormapping', self.colormapping)) - self._tabs[:] = tabs + self._control_tabs[:] = tabs def _check_y(self, event): if len(event.new) > 1 and self.by: @@ -632,15 +629,6 @@ def _check_by(self, event): if event.new and 'y_multi' in self._controls.parameters and self.y_multi and len(self.y_multi) > 1: self.by = [] - def _build_code_snippet(self): - settings = self.settings() - args = '' - if settings: - for k, v in settings.items(): - args += f' {k}={v!r},\n' - args = args[:-2] - return f'{self._var_name}.hvplot(\n{args}\n)' - #---------------------------------------------------------------- # Public API #---------------------------------------------------------------- @@ -650,7 +638,7 @@ def hvplot(self): """ return self._hvplot.clone() - def plot_code(self, var_name='df'): + def plot_code(self, var_name=None): """Return a string representation that can be easily copy-pasted in a notebook cell to create a plot from a call to the `.hvplot` accessor, and that includes all the customized settings of the explorer. @@ -667,9 +655,9 @@ def plot_code(self, var_name='df'): args = '' if settings: for k, v in settings.items(): - args += f'{k}={v!r}, ' + args += f' {k}={v!r},\n' args = args[:-2] - return f'{var_name}.hvplot({args})' + return f'{var_name or self._var_name}.hvplot(\n{args}\n)' def save(self, filename, **kwargs): """Save the plot to file. @@ -759,8 +747,8 @@ def __init__(self, ds, **params): var_name_suffix = ".to_array('variable').transpose(..., 'variable')" if "kind" not in params: params["kind"] = "image" - super().__init__(ds, **params) self._var_name_suffix = var_name_suffix + super().__init__(ds, **params) @property def _var_name(self): From bae32ef842f412d8784723ef676c2c8e3ec5ff49 Mon Sep 17 00:00:00 2001 From: maximlt Date: Thu, 12 Oct 2023 17:31:56 +0200 Subject: [PATCH 64/65] remove the import from code --- hvplot/tests/testui.py | 6 ------ hvplot/ui.py | 10 +--------- 2 files changed, 1 insertion(+), 15 deletions(-) diff --git a/hvplot/tests/testui.py b/hvplot/tests/testui.py index 8312cbe2c..463c72b03 100644 --- a/hvplot/tests/testui.py +++ b/hvplot/tests/testui.py @@ -279,8 +279,6 @@ def test_explorer_code_dataframe(): ) assert explorer._code_pane.object == dedent("""\ ```python - import hvplot.pandas - df.hvplot( kind='points', x='bill_length_mm', @@ -305,8 +303,6 @@ def test_explorer_code_gridded(): assert explorer._code_pane.object == dedent("""\ ```python - import hvplot.xarray - ds['air'].hvplot( colorbar=True, groupby=['time'], @@ -333,8 +329,6 @@ def test_explorer_code_gridded_dataarray(): assert explorer._code_pane.object == dedent("""\ ```python - import hvplot.xarray - da.hvplot( colorbar=True, groupby=['time'], diff --git a/hvplot/ui.py b/hvplot/ui.py index afc899282..a515b4b70 100644 --- a/hvplot/ui.py +++ b/hvplot/ui.py @@ -559,16 +559,12 @@ def _refresh(self, *events): self._plot() with param.parameterized.discard_events(self): self.code = self.plot_code() - self._code_pane.object = f"""```python\nimport hvplot.{self._backend}\n\n{self.code}\n```""" + self._code_pane.object = f"""```python\n{self.code}\n```""" @property def _var_name(self): return 'data' - @property - def _backend(self): - return 'pandas' - @property def _single_y(self): if self.kind in KINDS['2d']: @@ -749,10 +745,6 @@ def _var_name(self): else: return 'da' - @property - def _backend(self): - return 'xarray' - @property def _x(self): return (self._converter.x or self._converter.indexes[0]) if self.x is None else self.x From f67106b09e98322681dca46f702f8d5cc2497672 Mon Sep 17 00:00:00 2001 From: maximlt Date: Thu, 12 Oct 2023 17:34:53 +0200 Subject: [PATCH 65/65] docs cleanup after bad merge --- examples/user_guide/Explorer.ipynb | 87 +++++++++--------------------- 1 file changed, 24 insertions(+), 63 deletions(-) diff --git a/examples/user_guide/Explorer.ipynb b/examples/user_guide/Explorer.ipynb index 19bf29b88..484b6ef90 100644 --- a/examples/user_guide/Explorer.ipynb +++ b/examples/user_guide/Explorer.ipynb @@ -3,6 +3,7 @@ { "cell_type": "code", "execution_count": null, + "id": "cb9dcb54", "metadata": {}, "outputs": [], "source": [ @@ -47,6 +48,7 @@ }, { "cell_type": "markdown", + "id": "6485252a", "metadata": {}, "source": [ "The *Explorer* has a few useful methods:\n", @@ -59,6 +61,7 @@ }, { "cell_type": "markdown", + "id": "40c58e25", "metadata": {}, "source": [ "You will now see how to use the *Explorer* in combination with these methods.\n", @@ -69,6 +72,7 @@ { "cell_type": "code", "execution_count": null, + "id": "c25ee4e1", "metadata": {}, "outputs": [], "source": [ @@ -78,6 +82,7 @@ }, { "cell_type": "markdown", + "id": "e1a6104a", "metadata": {}, "source": [ "We may already be satisfied with the plot as is and decide to save a copy, in this case in an HTML file as we have created a Bokeh plot and would like to preserve its interactivity." @@ -86,6 +91,7 @@ { "cell_type": "code", "execution_count": null, + "id": "ac528bbd", "metadata": {}, "outputs": [], "source": [ @@ -94,6 +100,7 @@ }, { "cell_type": "markdown", + "id": "906a55c9", "metadata": {}, "source": [ "The options we have changed in the *Explorer* can obtained as a dictionary from the `settings()` method and be passed as kwargs to `hvplot.explorer` to initialize another *Explorer*, or directly to the `.hvplot()` data accessor. It means that in your workflow you can temporarily create *Explorers*, use them to quickly iterate on building new plots, record their settings, and finally replace the *Explorers* by shorter `.hvplot()` calls." @@ -102,6 +109,7 @@ { "cell_type": "code", "execution_count": null, + "id": "9cc7e7db", "metadata": {}, "outputs": [], "source": [ @@ -111,6 +119,7 @@ }, { "cell_type": "markdown", + "id": "81f80a7e", "metadata": {}, "source": [ "Note that for the next line to display a plot `hvplot.pandas` has to be imported, which we did at the beginning of this notebook." @@ -119,6 +128,7 @@ { "cell_type": "code", "execution_count": null, + "id": "5c212d28", "metadata": {}, "outputs": [], "source": [ @@ -135,6 +145,7 @@ }, { "cell_type": "markdown", + "id": "f6adc52e", "metadata": {}, "source": [ "Another practical way to create a plot from the recorded options is to use the `plot_code` method that generates a string ready to be executed after a copy/paste into a notebook code cell. `plot_code` assumes that you data variable is named `'df'`, you can change that by passing e.g. `var_name='df_othername'`." @@ -143,10 +154,11 @@ { "cell_type": "code", "execution_count": null, + "id": "c9acbdb1", "metadata": {}, "outputs": [], "source": [ - "hvexplorer.plot_code()\n" + "print(hvexplorer.plot_code())\n" ] }, { @@ -160,10 +172,17 @@ { "cell_type": "code", "execution_count": null, + "id": "afc6e1dc", "metadata": {}, "outputs": [], "source": [ - "df.hvplot(by=['species'], kind='scatter', title='Penguins Scatter', x='bill_length_mm', y=['bill_depth_mm'])\n" + "df.hvplot(\n", + " by=['species'],\n", + " kind='scatter',\n", + " title='Penguins Scatter',\n", + " x='bill_length_mm',\n", + " y=['bill_depth_mm']\n", + ")" ] }, { @@ -245,7 +264,7 @@ }, { "cell_type": "markdown", - "id": "b986b42d", + "id": "72e136ca", "metadata": {}, "source": [ "You may also call `hvplot` on the explorer instance." @@ -254,7 +273,7 @@ { "cell_type": "code", "execution_count": null, - "id": "db9ada50", + "id": "6d3606e8", "metadata": {}, "outputs": [], "source": [ @@ -263,65 +282,7 @@ }, { "cell_type": "markdown", - "id": "8c183122", - "metadata": {}, - "source": [ - "It also works for xarray objects." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "f15dde1f", - "metadata": {}, - "outputs": [], - "source": [ - "ds = xr.tutorial.open_dataset(\"air_temperature\")\n", - "\n", - "hvplot.explorer(ds, x=\"lon\", y=\"lat\")" - ] - }, - { - "cell_type": "markdown", - "id": "d6d90743", - "metadata": {}, - "source": [ - "It's also possible to geographically reference the data, with cartopy and geoviews installed." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "87d4ab4f", - "metadata": {}, - "outputs": [], - "source": [ - "hvexplorer = hvplot.explorer(ds, x=\"lon\", y=\"lat\", geo=True)\n", - "hvexplorer.geographic.param.update(crs=\"PlateCarree\", tiles=\"CartoDark\")\n", - "hvexplorer" - ] - }, - { - "cell_type": "markdown", - "id": "c4736831", - "metadata": {}, - "source": [ - "Large datasets can be plotted efficiently using Datashader's `rasterize`." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "d01077b1", - "metadata": {}, - "outputs": [], - "source": [ - "hvexplorer = hvplot.explorer(ds, x=\"lon\", y=\"lat\", rasterize=True)\n", - "hvexplorer" - ] - }, - { - "cell_type": "markdown", + "id": "145562f8", "metadata": {}, "source": [ "The *Explorer* makes it very easy to quickly spin up a small application in a notebook with which you can explore your data, generate the visualization that you want, record it in a simple way, and keep going with your analysis."