From f1001374fed97937d739e5af371a5f010c5d50f7 Mon Sep 17 00:00:00 2001 From: Demetris Roumis Date: Fri, 12 Apr 2024 00:45:18 -0700 Subject: [PATCH 01/43] minor typo in customizing plots (#6179) --- examples/user_guide/Customizing_Plots.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/user_guide/Customizing_Plots.ipynb b/examples/user_guide/Customizing_Plots.ipynb index ac76ff7839..2b519bee67 100644 --- a/examples/user_guide/Customizing_Plots.ipynb +++ b/examples/user_guide/Customizing_Plots.ipynb @@ -506,7 +506,7 @@ "source": [ "##### Dimension.soft_range\n", "\n", - "Declaringa ``soft_range`` on the other hand combines the data range and the supplied range, i.e. it will pick whichever extent is wider. Using the same example as above we can see it uses the -10 value supplied in the soft_range but also extends to 100, which is the upper bound of the actual data:" + "Declaring a ``soft_range`` on the other hand combines the data range and the supplied range, i.e. it will pick whichever extent is wider. Using the same example as above we can see it uses the -10 value supplied in the soft_range but also extends to 100, which is the upper bound of the actual data:" ] }, { From 2d6f46f2b42d3bca4b61dd8d97b03ae9fe297017 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Mon, 15 Apr 2024 16:10:39 +0200 Subject: [PATCH 02/43] Ensure plot ranges for all renderers are combined in auto-ranging (#6173) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Simon Høxbro Hansen --- holoviews/plotting/bokeh/element.py | 55 +++++--------- holoviews/tests/ui/bokeh/test_autorange.py | 86 ++++++++++++++++++++++ 2 files changed, 106 insertions(+), 35 deletions(-) create mode 100644 holoviews/tests/ui/bokeh/test_autorange.py diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py index 05972603ec..874132b499 100644 --- a/holoviews/plotting/bokeh/element.py +++ b/holoviews/plotting/bokeh/element.py @@ -1327,7 +1327,6 @@ def _setup_autorange(self): else: p0, p1 = self.padding, self.padding - # Clean this up in bokeh 3.0 using View.find_one API callback = CustomJS(code=f""" const cb = function() {{ @@ -1349,30 +1348,10 @@ def _setup_autorange(self): return invert ? [upper, lower] : [lower, upper] }} - const ref = plot.id - - const find = (view) => {{ - let iterable = view.child_views === undefined ? [] : view.child_views - for (const sv of iterable) {{ - if (sv.model.id == ref) - return sv - const obj = find(sv) - if (obj !== null) - return obj - }} - return null - }} - let plot_view = null; - for (const root of plot.document.roots()) {{ - const root_view = window.Bokeh.index[root.id] - if (root_view === undefined) - return - plot_view = find(root_view) - if (plot_view != null) - break - }} - if (plot_view == null) + let plot_view = Bokeh.index.find_one(plot) + if (plot_view == null) {{ return + }} let range_limits = {{}} for (const dr of plot.data_renderers) {{ @@ -1393,20 +1372,23 @@ def _setup_autorange(self): }} }} - if (y_range_name) {{ + if (y_range_name in range_limits) {{ + const [vmin_old, vmax_old] = range_limits[y_range_name] + range_limits[y_range_name] = [Math.min(vmin, vmin_old), Math.max(vmax, vmax_old)] + }} else {{ range_limits[y_range_name] = [vmin, vmax] }} }} - let range_tags_extras = plot.{dim}_range.tags[1] - if (range_tags_extras['autorange']) {{ - let lowerlim = range_tags_extras['y-lowerlim'] ?? null - let upperlim = range_tags_extras['y-upperlim'] ?? null - let [start, end] = get_padded_range('default', lowerlim, upperlim, range_tags_extras['invert_yaxis']) - if ((start != end) && window.Number.isFinite(start) && window.Number.isFinite(end)) {{ - plot.{dim}_range.setv({{start, end}}) - }} - }} + let range_tags_extras = plot.{dim}_range.tags[1] + if (range_tags_extras['autorange']) {{ + let lowerlim = range_tags_extras['y-lowerlim'] ?? null + let upperlim = range_tags_extras['y-upperlim'] ?? null + let [start, end] = get_padded_range('default', lowerlim, upperlim, range_tags_extras['invert_yaxis']) + if ((start != end) && window.Number.isFinite(start) && window.Number.isFinite(end)) {{ + plot.{dim}_range.setv({{start, end}}) + }} + }} for (let key in plot.extra_{dim}_ranges) {{ const extra_range = plot.extra_{dim}_ranges[key] @@ -2665,7 +2647,7 @@ class OverlayPlot(GenericOverlayPlot, LegendPlot): 'min_height', 'max_height', 'min_width', 'min_height', 'margin', 'aspect', 'data_aspect', 'frame_width', 'frame_height', 'responsive', 'fontscale', 'subcoordinate_y', - 'subcoordinate_scale'] + 'subcoordinate_scale', 'autorange'] def __init__(self, overlay, **kwargs): self._multi_y_propagation = self.lookup_options(overlay, 'plot').options.get('multi_y', False) @@ -2986,6 +2968,9 @@ def initialize_plot(self, ranges=None, plot=None, plots=None): if self.top_level: self.init_links() + if self.autorange: + self._setup_autorange() + self._execute_hooks(element) return self.handles['plot'] diff --git a/holoviews/tests/ui/bokeh/test_autorange.py b/holoviews/tests/ui/bokeh/test_autorange.py new file mode 100644 index 0000000000..787f3167a9 --- /dev/null +++ b/holoviews/tests/ui/bokeh/test_autorange.py @@ -0,0 +1,86 @@ +import numpy as np +import pytest + +from holoviews.element import Curve +from holoviews.plotting.bokeh.renderer import BokehRenderer + +from .. import expect, wait_until + +pytestmark = pytest.mark.ui + + +@pytest.mark.usefixtures("bokeh_backend") +def test_autorange_single(serve_hv): + curve = Curve(np.arange(1000)).opts(autorange='y', active_tools=['box_zoom']) + + plot = BokehRenderer.get_plot(curve) + + page = serve_hv(plot) + + hv_plot = page.locator('.bk-events') + + expect(hv_plot).to_have_count(1) + + bbox = hv_plot.bounding_box() + hv_plot.click() + + page.mouse.move(bbox['x']+100, bbox['y']+100) + page.mouse.down() + page.mouse.move(bbox['x']+150, bbox['y']+150, steps=5) + page.mouse.up() + + y_range = plot.handles['y_range'] + wait_until(lambda: y_range.start == 163.2 and y_range.end == 448.8, page) + + +@pytest.mark.usefixtures("bokeh_backend") +def test_autorange_single_in_overlay(serve_hv): + c1 = Curve(np.arange(1000)) + c2 = Curve(-np.arange(1000)).opts(autorange='y') + + overlay = (c1*c2).opts(active_tools=['box_zoom']) + + plot = BokehRenderer.get_plot(overlay) + + page = serve_hv(plot) + + hv_plot = page.locator('.bk-events') + + expect(hv_plot).to_have_count(1) + + bbox = hv_plot.bounding_box() + hv_plot.click() + + page.mouse.move(bbox['x']+100, bbox['y']+100) + page.mouse.down() + page.mouse.move(bbox['x']+150, bbox['y']+150, steps=5) + page.mouse.up() + + y_range = plot.handles['y_range'] + wait_until(lambda: y_range.start == -486 and y_range.end == 486, page) + +@pytest.mark.usefixtures("bokeh_backend") +def test_autorange_overlay(serve_hv): + c1 = Curve(np.arange(1000)) + c2 = Curve(-np.arange(1000)) + + overlay = (c1*c2).opts(active_tools=['box_zoom'], autorange='y') + + plot = BokehRenderer.get_plot(overlay) + + page = serve_hv(plot) + + hv_plot = page.locator('.bk-events') + + expect(hv_plot).to_have_count(1) + + bbox = hv_plot.bounding_box() + hv_plot.click() + + page.mouse.move(bbox['x']+100, bbox['y']+100) + page.mouse.down() + page.mouse.move(bbox['x']+150, bbox['y']+150, steps=5) + page.mouse.up() + + y_range = plot.handles['y_range'] + wait_until(lambda: y_range.start == -486 and y_range.end == 486, page) From c0dfc72486d614a9e96334bcaca171922a84ace2 Mon Sep 17 00:00:00 2001 From: Demetris Roumis Date: Mon, 15 Apr 2024 21:36:17 -0700 Subject: [PATCH 03/43] Fix name of tsdownsample (#6193) --- holoviews/operation/downsample.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/holoviews/operation/downsample.py b/holoviews/operation/downsample.py index 616434d386..575e8bce4e 100644 --- a/holoviews/operation/downsample.py +++ b/holoviews/operation/downsample.py @@ -165,7 +165,7 @@ def _min_max(x, y, n_out, **kwargs): from tsdownsample import MinMaxDownsampler except ModuleNotFoundError: raise NotImplementedError( - 'The min-max downsampling algorithm requires the tsdownsampler ' + 'The min-max downsampling algorithm requires the tsdownsample ' 'library to be installed.' ) from None return MinMaxDownsampler().downsample(x, y, n_out=n_out, **kwargs) @@ -175,7 +175,7 @@ def _min_max_lttb(x, y, n_out, **kwargs): from tsdownsample import MinMaxLTTBDownsampler except ModuleNotFoundError: raise NotImplementedError( - 'The minmax-lttb downsampling algorithm requires the tsdownsampler ' + 'The minmax-lttb downsampling algorithm requires the tsdownsample ' 'library to be installed.' ) from None return MinMaxLTTBDownsampler().downsample(x, y, n_out=n_out, **kwargs) @@ -185,7 +185,7 @@ def _m4(x, y, n_out, **kwargs): from tsdownsample import M4Downsampler except ModuleNotFoundError: raise NotImplementedError( - 'The m4 downsampling algorithm requires the tsdownsampler ' + 'The m4 downsampling algorithm requires the tsdownsample ' 'library to be installed.' ) from None return M4Downsampler().downsample(x, y, n_out=n_out, **kwargs) @@ -204,7 +204,7 @@ class downsample1d(ResampleOperation1D): """ Implements downsampling of a regularly sampled 1D dataset. - If available uses the `tsdownsampler` library to perform massively + If available uses the `tsdownsample` library to perform massively accelerated downsampling. """ @@ -214,14 +214,14 @@ class downsample1d(ResampleOperation1D): - `lttb`: Largest Triangle Three Buckets downsample algorithm. - `nth`: Selects every n-th point. - `viewport`: Selects all points in a given viewport. - - `minmax`: Selects the min and max value in each bin (requires tsdownsampler). - - `m4`: Selects the min, max, first and last value in each bin (requires tsdownsampler). + - `minmax`: Selects the min and max value in each bin (requires tsdownsample). + - `m4`: Selects the min, max, first and last value in each bin (requires tsdownsample). - `minmax-lttb`: First selects n_out * minmax_ratio min and max values, then further reduces these to n_out values using the - Largest Triangle Three Buckets algorithm (requires tsdownsampler).""") + Largest Triangle Three Buckets algorithm (requires tsdownsample).""") parallel = param.Boolean(default=False, doc=""" - The number of threads to use (if tsdownsampler is available).""") + The number of threads to use (if tsdownsample is available).""") minmax_ratio = param.Integer(default=4, bounds=(0, None), doc=""" For the minmax-lttb algorithm determines the ratio of candidate From 372eb6c0d7dd6b7694068375e04039f8156bdfc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Tue, 16 Apr 2024 09:11:32 +0200 Subject: [PATCH 04/43] Ensure that the downsample algorithm m4 n_out is always a multiple of 4 (#6195) --- holoviews/operation/downsample.py | 1 + 1 file changed, 1 insertion(+) diff --git a/holoviews/operation/downsample.py b/holoviews/operation/downsample.py index 575e8bce4e..50cdd7ff65 100644 --- a/holoviews/operation/downsample.py +++ b/holoviews/operation/downsample.py @@ -188,6 +188,7 @@ def _m4(x, y, n_out, **kwargs): 'The m4 downsampling algorithm requires the tsdownsample ' 'library to be installed.' ) from None + n_out = n_out - (n_out % 4) # n_out must be a multiple of 4 return M4Downsampler().downsample(x, y, n_out=n_out, **kwargs) From 1568bdda1a5646940afacf3fe84a7ef47de662e3 Mon Sep 17 00:00:00 2001 From: Maxime Liquet <35924738+maximlt@users.noreply.github.com> Date: Tue, 16 Apr 2024 14:17:33 +0200 Subject: [PATCH 05/43] subcoordinate_y: reverse the renderers by default (#6194) --- holoviews/plotting/bokeh/element.py | 20 +++++++++++++++---- .../tests/plotting/bokeh/test_subcoordy.py | 11 ++++++++++ 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py index 874132b499..659864924a 100644 --- a/holoviews/plotting/bokeh/element.py +++ b/holoviews/plotting/bokeh/element.py @@ -2201,7 +2201,6 @@ def _init_glyph(self, plot, mapping, properties, key): return renderer, renderer.glyph - class ColorbarPlot(ElementPlot): """ ColorbarPlot provides methods to create colormappers and colorbar @@ -2617,7 +2616,6 @@ def _process_legend(self, plot=None): r.muted = self.legend_muted - class AnnotationPlot: """ Mix-in plotting subclass for AnnotationPlots which do not have a legend. @@ -2782,7 +2780,6 @@ def _process_legend(self, overlay): for r in item.renderers: r.muted = self.legend_muted or r.muted - def _init_tools(self, element, callbacks=None): """ Processes the list of tools to be supplied to the plot. @@ -2825,7 +2822,6 @@ def _init_tools(self, element, callbacks=None): self.handles['hover_tools'] = hover_tools return init_tools - def _merge_tools(self, subplot): """ Merges tools on the overlay with those on the subplots. @@ -2923,6 +2919,7 @@ def initialize_plot(self, ranges=None, plot=None, plots=None): self._update_ranges(element, ranges) panels = [] + subcoord_y_glyph_renderers = [] for key, subplot in self.subplots.items(): frame = None if self.tabs: @@ -2941,6 +2938,21 @@ def initialize_plot(self, ranges=None, plot=None, plots=None): title = get_tab_title(key, frame, self.hmap.last) panels.append(TabPanel(child=child, title=title)) self._merge_tools(subplot) + if getattr(subplot, "subcoordinate_y", False) and ( + glyph_renderer := subplot.handles.get("glyph_renderer") + ): + subcoord_y_glyph_renderers.append(glyph_renderer) + + if self.subcoordinate_y: + # Reverse the subcoord-y renderers only. + reversed_renderers = subcoord_y_glyph_renderers[::-1] + reordered = [] + for item in plot.renderers: + if item not in subcoord_y_glyph_renderers: + reordered.append(item) + else: + reordered.append(reversed_renderers.pop(0)) + plot.renderers = reordered if self.tabs: self.handles['plot'] = Tabs( diff --git a/holoviews/tests/plotting/bokeh/test_subcoordy.py b/holoviews/tests/plotting/bokeh/test_subcoordy.py index 0009d00950..a31d9eead3 100644 --- a/holoviews/tests/plotting/bokeh/test_subcoordy.py +++ b/holoviews/tests/plotting/bokeh/test_subcoordy.py @@ -40,6 +40,17 @@ def test_bool_base(self): assert plot.state.yaxis.ticker.ticks == [0, 1] assert plot.state.yaxis.major_label_overrides == {0: 'Data 0', 1: 'Data 1'} + def test_renderers_reversed(self): + overlay = Overlay([Curve(range(10), label=f'Data {i}').opts(subcoordinate_y=True) for i in range(2)]) + overlay = VSpan(0, 1, label='back') * overlay * VSpan(2, 3, label='front') + plot = bokeh_renderer.get_plot(overlay) + renderers = plot.handles['plot'].renderers + assert (renderers[0].left, renderers[0].right) == (0, 1) + # Only the subcoord-y renderers are reversed by default. + assert renderers[1].name == 'Data 1' + assert renderers[2].name == 'Data 0' + assert (renderers[3].left, renderers[3].right) == (2, 3) + def test_bool_scale(self): test_data = [ (0.5, (-0.25, 0.25), (0.75, 1.25), (-0.25, 1.25)), From 8935f0dc8070932624b9442475e89447094768a5 Mon Sep 17 00:00:00 2001 From: Maxime Liquet <35924738+maximlt@users.noreply.github.com> Date: Tue, 16 Apr 2024 14:20:38 +0200 Subject: [PATCH 06/43] Add `xyzservices` as a test dependency (#6181) --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index d25e1c47ef..0edb3fd2ad 100644 --- a/setup.py +++ b/setup.py @@ -57,6 +57,7 @@ 'spatialpandas', 'datashader >=0.11.1', 'dash >=1.16', + 'xyzservices >=2022.9.0', ] if os.name != "nt": From 97d648be01826592ced72b87631afcaec505e12f Mon Sep 17 00:00:00 2001 From: Andrew <15331990+ahuang11@users.noreply.github.com> Date: Tue, 16 Apr 2024 09:24:07 -0700 Subject: [PATCH 07/43] Add hover_tooltips, hover_mode, hover_formatters opts to easily modify hover (#6180) --- .../demos/bokeh/html_hover_tooltips.ipynb | 113 +++++++++ examples/user_guide/Plotting_with_Bokeh.ipynb | 145 +++++++++-- holoviews/plotting/bokeh/element.py | 158 +++++++++++- holoviews/tests/ui/bokeh/test_hover.py | 234 ++++++++++++++++++ 4 files changed, 626 insertions(+), 24 deletions(-) create mode 100644 examples/gallery/demos/bokeh/html_hover_tooltips.ipynb create mode 100644 holoviews/tests/ui/bokeh/test_hover.py diff --git a/examples/gallery/demos/bokeh/html_hover_tooltips.ipynb b/examples/gallery/demos/bokeh/html_hover_tooltips.ipynb new file mode 100644 index 0000000000..767ed2b223 --- /dev/null +++ b/examples/gallery/demos/bokeh/html_hover_tooltips.ipynb @@ -0,0 +1,113 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import pandas as pd\n", + "import holoviews as hv\n", + "\n", + "hv.extension(\"bokeh\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This demo demonstrates how to build custom hover tooltips using HTML. The\n", + "tooltips are displayed when the user hovers over a point in the plot." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Declare data" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "df = pd.DataFrame(\n", + " dict(\n", + " x=[1, 2, 3, 4, 5],\n", + " y=[2, 5, 8, 2, 7],\n", + " desc=[\"A\", \"b\", \"C\", \"d\", \"E\"],\n", + " imgs=[\n", + " \"https://docs.bokeh.org/static/snake.jpg\",\n", + " \"https://docs.bokeh.org/static/snake2.png\",\n", + " \"https://docs.bokeh.org/static/snake3D.png\",\n", + " \"https://docs.bokeh.org/static/snake4_TheRevenge.png\",\n", + " \"https://docs.bokeh.org/static/snakebite.jpg\",\n", + " ],\n", + " fonts=[\n", + " \"italics\",\n", + " \"
pre
\",\n", + " \"bold\",\n", + " \"small\",\n", + " \"del\",\n", + " ],\n", + " )\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Declare plot\n", + "\n", + "Having declared the tooltips' columns, we can reference them in the tooltips with `@`. Just be sure to pass *all the relevant columns* as extra `vdims` ." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "TOOLTIPS = \"\"\"\n", + "
\n", + " $label\n", + "
\n", + " \n", + "
\n", + "
\n", + " @desc\n", + " [$index]\n", + "
\n", + "
\n", + " @fonts{safe}\n", + "
\n", + "
\n", + " Location\n", + " ($x, $y)\n", + "
\n", + "
\n", + "\"\"\"\n", + "\n", + "hv.Scatter(df, kdims=[\"x\"], vdims=[\"y\", \"desc\", \"imgs\", \"fonts\"], label=\"Pictures\").opts(\n", + " hover_tooltips=TOOLTIPS, size=20\n", + ")" + ] + } + ], + "metadata": { + "language_info": { + "name": "python", + "pygments_lexer": "ipython3" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/examples/user_guide/Plotting_with_Bokeh.ipynb b/examples/user_guide/Plotting_with_Bokeh.ipynb index 1e7fa587b6..b13c1edbed 100644 --- a/examples/user_guide/Plotting_with_Bokeh.ipynb +++ b/examples/user_guide/Plotting_with_Bokeh.ipynb @@ -652,7 +652,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Additionally, you can provide `'vline'`, the equivalent of passing `HoverTool(mode='vline')`, or `'hline'` to set the hit-testing behavior" + "Moreover, you can provide `'vline'`, the equivalent of passing `HoverTool(mode='vline')`, or `'hline'` to set the hit-testing behavior." ] }, { @@ -661,27 +661,142 @@ "metadata": {}, "outputs": [], "source": [ - "error = np.random.rand(100, 3)\n", - "heatmap_data = {(chr(65+i), chr(97+j)):i*j for i in range(5) for j in range(5) if i!=j}\n", - "data = [np.random.normal() for i in range(10000)]\n", - "hist = np.histogram(data, 20)\n", + "hv.Curve(np.arange(100)).opts(tools=[\"vline\"]) + hv.Curve(np.arange(100)).opts(tools=[\"hline\"])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Equivalently, you may say `tools=[\"hover\"]` alongside `hover_mode`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "(\n", + " hv.Curve(np.arange(100)).opts(tools=[\"hover\"], hover_mode=\"vline\")\n", + " + hv.Curve(np.arange(100)).opts(tools=[\"hover\"], hover_mode=\"hline\")\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "If you'd like finer control on the formatting, you may use `hover_tooltips` to declare the tooltips as a list of tuples of the labels and a specification of the dimension name and how to display it.\n", "\n", - "points = hv.Points(error)\n", - "heatmap = hv.HeatMap(heatmap_data).sort()\n", - "histogram = hv.Histogram(hist)\n", - "image = hv.Image(np.random.rand(50,50))\n", + "Behind the scenes, the `hover_tooltips` feature extends the capabilities of Bokeh's `HoverTool` tooltips by providing additional flexibility and customization options, so for a reference see the [bokeh user guide](https://bokeh.pydata.org/en/latest/docs/user_guide/tools.html#hovertool)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "hover_tooltips = [\n", + " ('Name', '@name'),\n", + " ('Symbol', '@symbol'),\n", + " ('CPK', '$color[hex, swatch]:CPK')\n", + "]\n", "\n", - "(points + heatmap + histogram + image).opts(\n", - " opts.Points(tools=['hline'], size=5), opts.HeatMap(tools=['hover']),\n", - " opts.Image(tools=['vline']), opts.Histogram(tools=['hover']),\n", - " opts.Layout(shared_axes=False)).cols(2)" + "points.clone().opts(\n", + " tools=[\"hover\"], hover_tooltips=hover_tooltips, color='metal', cmap='Category20',\n", + " line_color='black', size=dim('atomic radius')/10,\n", + " width=600, height=400, show_grid=True,\n", + " title='Chemical Elements by Type (scaled by atomic radius)')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Unique from Bokeh's `HoverTool`, the HoloViews' `hover_tooltips` also supports a mix of string and tuple formats for defining tooltips, allowing for both direct references to data columns and customized display options.\n", + "\n", + "Additionally, you can include as many, or as little, dimension names as desired." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "hover_tooltips = [\n", + " \"name\", # will assume @name\n", + " (\"Symbol\", \"@symbol\"), # @ still required if tuple\n", + " ('CPK', '$color[hex, swatch]:CPK'),\n", + " \"density\"\n", + "]\n", + "\n", + "points.clone().opts(\n", + " tools=[\"hover\"], hover_tooltips=hover_tooltips, color='metal', cmap='Category20',\n", + " line_color='black', size=dim('atomic radius')/10,\n", + " width=600, height=400, show_grid=True,\n", + " title='Chemical Elements by Type (scaled by atomic radius)')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "`hover_tooltips` also support displaying the HoloViews element's `label` and `group`.\n", + "\n", + "Keep in mind, to reference these special variables that are not based on the data, a prefix of `$` is required!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "a_curve = hv.Curve([0, 1, 2], label=\"A\", group=\"C\")\n", + "b_curve = hv.Curve([2, 1, 0], label=\"B\", group=\"C\")\n", + "(a_curve * b_curve).opts(\"Curve\", hover_tooltips=[\"$label\", \"$group\", \"@x\", \"y\"]) # $ is required, @ is not needed for string" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "If you need special formatting, you may also specify the formats inside `hover_tooltips` alongside `hover_formatters`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def datetime(x):\n", + " return np.array(x, dtype=np.datetime64)\n", + "\n", + "\n", + "df = pd.DataFrame(\n", + " {\n", + " \"date\": [\"2019-01-01\", \"2019-01-02\", \"2019-01-03\"],\n", + " \"adj_close\": [100, 101, 100000],\n", + " }\n", + ")\n", + "\n", + "curve = hv.Curve((datetime(df[\"date\"]), df[\"adj_close\"]), \"date\", \"adj close\")\n", + "curve.opts(\n", + " hover_tooltips=[\"date\", (\"Close\", \"$@{adj close}{0.2f}\")], # use @{ } for dims with spaces\n", + " hover_formatters={\"@{adj close}\": \"printf\"}, # use 'printf' formatter for '@{adj close}' field\n", + " hover_mode=\"vline\",\n", + ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "It is also possible to explicitly declare the columns to display by manually constructing a `HoverTool` and declaring the tooltips as a list of tuples of the labels and a specification of the dimension name and how to display it (for a complete reference see the [bokeh user guide](https://bokeh.pydata.org/en/latest/docs/user_guide/tools.html#hovertool))." + "You can provide HTML strings too! See a demo [here](../gallery/demos/bokeh/html_hover_tooltips.ipynb), or explicitly declare the columns to display by manually constructing a Bokeh [`HoverTool`](https://bokeh.pydata.org/en/latest/docs/user_guide/tools.html#hovertool)." ] }, { @@ -695,7 +810,7 @@ "\n", "points = hv.Points(\n", " elements, ['electronegativity', 'density'],\n", - " ['name', 'symbol', 'metal', 'CPK', 'atomic radius']\n", + " ['name', 'symbol', 'metal', 'CPK', 'atomic radius'],\n", ").sort('metal')\n", "\n", "tooltips = [\n", diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py index 659864924a..2b4dfcf9de 100644 --- a/holoviews/plotting/bokeh/element.py +++ b/holoviews/plotting/bokeh/element.py @@ -230,6 +230,15 @@ class ElementPlot(BokehPlot, GenericElementPlot): tools = param.List(default=[], doc=""" A list of plugin tools to use on the plot.""") + hover_tooltips = param.ClassSelector(class_=(list, str), doc=""" + A list of dimensions to be displayed in the hover tooltip.""") + + hover_formatters = param.Dict(doc=""" + A dict of formatting options for the hover tooltip.""") + + hover_mode = param.ObjectSelector(default='mouse', objects=['mouse', 'vline', 'hline'], doc=""" + The hover mode determines how the hover tool is activated.""") + toolbar = param.ObjectSelector(default='right', objects=["above", "below", "left", "right", "disable", None], @@ -286,16 +295,139 @@ def _hover_opts(self, element): dims += element.dimensions() return list(util.unique_iterator(dims)), {} + def _replace_hover_label_group(self, element, tooltip): + if isinstance(tooltip, tuple): + has_label = hasattr(element, 'label') and element.label + has_group = hasattr(element, 'group') and element.group != element.param.group.default + if not has_label and not has_group: + return tooltip + + if ("$label" in tooltip or "${label}" in tooltip): + tooltip = (tooltip[0], element.label) + elif ("$group" in tooltip or "${group}" in tooltip): + tooltip = (tooltip[0], element.group) + elif isinstance(tooltip, str): + if "$label" in tooltip: + tooltip = tooltip.replace("$label", element.label) + elif "${label}" in tooltip: + tooltip = tooltip.replace("${label}", element.label) + + if "$group" in tooltip: + tooltip = tooltip.replace("$group", element.group) + elif "${group}" in tooltip: + tooltip = tooltip.replace("${group}", element.group) + return tooltip + + def _replace_hover_value_aliases(self, tooltip, tooltips_dict): + for name, tuple_ in tooltips_dict.items(): + # some elements, like image, rename the tooltip, e.g. @y -> $y + # let's replace those, so the hover tooltip is discoverable + # ensure it works for `(@x, @y)` -> `($x, $y)` too + if isinstance(tooltip, tuple): + value_alias = tuple_[1] + if f"@{name}" in tooltip[1]: + tooltip = (tooltip[0], tooltip[1].replace(f"@{name}", value_alias)) + elif f"@{{{name}}}" in tooltip[1]: + tooltip = (tooltip[0], tooltip[1].replace(f"@{{{name}}}", value_alias)) + elif isinstance(tooltip, str): + if f"@{name}" in tooltip: + tooltip = tooltip.replace(f"@{name}", tuple_[1]) + elif f"@{{{name}}}" in tooltip: + tooltip = tooltip.replace(f"@{{{name}}}", tuple_[1]) + return tooltip + + def _prepare_hover_kwargs(self, element): + tooltips, hover_opts = self._hover_opts(element) + + dim_aliases = { + f"{dim.label} ({dim.unit})" if dim.unit else dim.label: dim.name + for dim in element.kdims + element.vdims + } + + # make dict so it's easy to get the tooltip for a given dimension; + tooltips_dict = {} + units_dict = {} + for ttp in tooltips: + if isinstance(ttp, tuple): + name = ttp[0] + tuple_ = (ttp[0], ttp[1]) + elif isinstance(ttp, Dimension): + name = ttp.name + # three brackets means replacing variable, + # and then wrapping in brackets, like @{air} + unit = f" ({ttp.unit})" if ttp.unit else "" + tuple_ = ( + ttp.pprint_label, + f"@{{{util.dimension_sanitizer(ttp.name)}}}" + ) + units_dict[name] = unit + elif isinstance(ttp, str): + name = ttp + # three brackets means replacing variable, + # and then wrapping in brackets, like @{air} + tuple_ = (ttp.name, f"@{{{util.dimension_sanitizer(ttp)}}}") + + if name in dim_aliases: + name = dim_aliases[name] + + # key is the vanilla data column/dimension name + # value should always be a tuple (label, value) + tooltips_dict[name] = tuple_ + + # subset the tooltips to only the ones user wants + if self.hover_tooltips: + # If hover tooltips are defined as a list of strings or tuples + if isinstance(self.hover_tooltips, list): + new_tooltips = [] + for tooltip in self.hover_tooltips: + if isinstance(tooltip, str): + # make into a tuple + new_tooltip = tooltips_dict.get(tooltip.lstrip("@")) + if new_tooltip is None: + label = tooltip.lstrip("$").lstrip("@") + value = tooltip if "$" in tooltip else f"@{{{tooltip.lstrip('@')}}}" + new_tooltip = (label, value) + new_tooltips.append(new_tooltip) + elif isinstance(tooltip, tuple): + unit = units_dict.get(tooltip[0]) + tooltip = self._replace_hover_value_aliases(tooltip, tooltips_dict) + if unit: + tooltip = (f"{tooltip[0]}{unit}", tooltip[1]) + new_tooltips.append(tooltip) + else: + raise ValueError('Hover tooltips must be a list with items of strings or tuples.') + tooltips = new_tooltips + else: + # Likely HTML str + tooltips = self._replace_hover_value_aliases(self.hover_tooltips, tooltips_dict) + else: + tooltips = list(tooltips_dict.values()) + + # replace the label and group in the tooltips + if isinstance(tooltips, list): + tooltips = [self._replace_hover_label_group(element, ttp) for ttp in tooltips] + elif isinstance(tooltips, str): + tooltips = self._replace_hover_label_group(element, tooltips) + + if self.hover_formatters: + hover_opts['formatters'] = self.hover_formatters + + if self.hover_mode: + hover_opts["mode"] = self.hover_mode + + return tooltips, hover_opts + def _init_tools(self, element, callbacks=None): """ Processes the list of tools to be supplied to the plot. """ if callbacks is None: callbacks = [] - tooltips, hover_opts = self._hover_opts(element) - tooltips = [(ttp.pprint_label, '@{%s}' % util.dimension_sanitizer(ttp.name)) - if isinstance(ttp, Dimension) else ttp for ttp in tooltips] - if not tooltips: tooltips = None + + tooltips, hover_opts = self._prepare_hover_kwargs(element) + + if not tooltips: + tooltips = None callbacks = callbacks+self.callbacks cb_tools, tool_names = [], [] @@ -314,13 +446,23 @@ def _init_tools(self, element, callbacks=None): cb_tools.append(tool) self.handles[handle] = tool + all_tools = cb_tools + self.default_tools + self.tools + if self.hover_tooltips: + no_hover = ( + "hover" not in all_tools and + not (any(isinstance(tool, tools.HoverTool) for tool in all_tools)) + ) + if no_hover: + all_tools.append("hover") + tool_list = [] - for tool in cb_tools + self.default_tools + self.tools: + for tool in all_tools: if tool in tool_names: continue if tool in ['vline', 'hline']: + tool_opts = dict(hover_opts, mode=tool) tool = tools.HoverTool( - tooltips=tooltips, tags=['hv_created'], mode=tool, **hover_opts + tooltips=tooltips, tags=['hv_created'], **tool_opts ) elif bokeh32 and isinstance(tool, str) and tool.endswith( ('wheel_zoom', 'zoom_in', 'zoom_out') @@ -392,9 +534,7 @@ def _init_tools(self, element, callbacks=None): def _update_hover(self, element): tool = self.handles['hover'] if 'hv_created' in tool.tags: - tooltips, hover_opts = self._hover_opts(element) - tooltips = [(ttp.pprint_label, '@{%s}' % util.dimension_sanitizer(ttp.name)) - if isinstance(ttp, Dimension) else ttp for ttp in tooltips] + tooltips, hover_opts = self._prepare_hover_kwargs(element) tool.tooltips = tooltips else: plot_opts = element.opts.get('plot', 'bokeh') diff --git a/holoviews/tests/ui/bokeh/test_hover.py b/holoviews/tests/ui/bokeh/test_hover.py new file mode 100644 index 0000000000..302599fe1f --- /dev/null +++ b/holoviews/tests/ui/bokeh/test_hover.py @@ -0,0 +1,234 @@ +import time + +import numpy as np +import pytest + +import holoviews as hv + +from .. import expect, wait_until + +pytestmark = pytest.mark.ui + + +def delay_rerun(*args): + time.sleep(2) + return True + + +@pytest.mark.usefixtures("bokeh_backend") +def test_hover_tooltips_list(serve_hv): + hv_image = hv.Image( + np.random.rand(10, 10), bounds=(0, 0, 1, 1), kdims=["xc", "yc"] + ).opts(hover_tooltips=["$x", "xc", "@yc", "@z"]) + + page = serve_hv(hv_image) + hv_plot = page.locator(".bk-events") + wait_until(lambda: expect(hv_plot).to_have_count(1), page=page) + bbox = hv_plot.bounding_box() + + # Hover over the plot + page.mouse.move(bbox["x"] + bbox["width"] / 2, bbox["y"] + bbox["height"] / 2) + page.mouse.up() + + wait_until(lambda: expect(page.locator(".bk-Tooltip")).to_have_count(1), page=page) + + expect(page.locator(".bk-Tooltip")).to_contain_text("x:") + expect(page.locator(".bk-Tooltip")).to_contain_text("xc:") + expect(page.locator(".bk-Tooltip")).to_contain_text("yc:") + expect(page.locator(".bk-Tooltip")).to_contain_text("z:") + expect(page.locator(".bk-Tooltip")).not_to_contain_text("?") + + +@pytest.mark.usefixtures("bokeh_backend") +def test_hover_tooltips_unit_format(serve_hv): + dim = hv.Dimension("Test", unit="Unit") + hv_image = hv.Image( + np.zeros((10, 10)), bounds=(0, 0, 1, 1), kdims=["xc", "yc"], vdims=[dim] + ).opts(hover_tooltips=[("Test", "@Test{%0.2f}")]) + + page = serve_hv(hv_image) + hv_plot = page.locator(".bk-events") + wait_until(lambda: expect(hv_plot).to_have_count(1), page=page) + bbox = hv_plot.bounding_box() + + # Hover over the plot + page.mouse.move(bbox["x"] + bbox["width"] / 2, bbox["y"] + bbox["height"] / 2) + page.mouse.up() + + wait_until(lambda: expect(page.locator(".bk-Tooltip")).to_have_count(1), page=page) + + expect(page.locator(".bk-Tooltip")).to_contain_text("Test: 0.00%") + + +@pytest.mark.usefixtures("bokeh_backend") +def test_hover_tooltips_list_mix_tuple_string(serve_hv): + hv_image = hv.Image( + np.random.rand(10, 10), bounds=(0, 0, 1, 1), kdims=["xc", "yc"] + ).opts(hover_tooltips=[("xs", "($x, @xc)"), "yc", "z"]) + + page = serve_hv(hv_image) + hv_plot = page.locator(".bk-events") + wait_until(lambda: expect(hv_plot).to_have_count(1), page=page) + bbox = hv_plot.bounding_box() + + # Hover over the plot + page.mouse.move(bbox["x"] + bbox["width"] / 2, bbox["y"] + bbox["height"] / 2) + page.mouse.up() + + wait_until(lambda: expect(page.locator(".bk-Tooltip")).to_have_count(1), page=page) + + expect(page.locator(".bk-Tooltip")).to_contain_text("xs:") + expect(page.locator(".bk-Tooltip")).to_contain_text("yc:") + expect(page.locator(".bk-Tooltip")).to_contain_text("z:") + expect(page.locator(".bk-Tooltip")).not_to_contain_text("?") + + +@pytest.mark.usefixtures("bokeh_backend") +def test_hover_tooltips_label_group(serve_hv): + hv_image = hv.Image( + np.random.rand(10, 10), + bounds=(0, 0, 1, 1), + kdims=["xc", "yc"], + label="Image Label", + group="Image Group", + ).opts( + hover_tooltips=[ + "$label", + "$group", + ("Plot Label", "$label"), + ("Plot Group", "$group"), + ] + ) + + page = serve_hv(hv_image) + hv_plot = page.locator(".bk-events") + wait_until(lambda: expect(hv_plot).to_have_count(1), page=page) + bbox = hv_plot.bounding_box() + + # Hover over the plot + page.mouse.move(bbox["x"] + bbox["width"] / 2, bbox["y"] + bbox["height"] / 2) + page.mouse.up() + + wait_until(lambda: expect(page.locator(".bk-Tooltip")).to_have_count(1), page=page) + + expect(page.locator(".bk-Tooltip")).to_contain_text("label:") + expect(page.locator(".bk-Tooltip")).to_contain_text("group:") + expect(page.locator(".bk-Tooltip")).to_contain_text("Plot Label:") + expect(page.locator(".bk-Tooltip")).to_contain_text("Plot Group:") + expect(page.locator(".bk-Tooltip")).not_to_contain_text("?") + + +@pytest.mark.usefixtures("bokeh_backend") +def test_hover_tooltips_missing(serve_hv): + hv_image = hv.Image( + np.random.rand(10, 10), bounds=(0, 0, 1, 1), kdims=["xc", "yc"] + ).opts(hover_tooltips=["abc"]) + + page = serve_hv(hv_image) + hv_plot = page.locator(".bk-events") + wait_until(lambda: expect(hv_plot).to_have_count(1), page=page) + bbox = hv_plot.bounding_box() + + # Hover over the plot + page.mouse.move(bbox["x"] + bbox["width"] / 2, bbox["y"] + bbox["height"] / 2) + page.mouse.up() + + wait_until(lambda: expect(page.locator(".bk-Tooltip")).to_have_count(1), page=page) + + expect(page.locator(".bk-Tooltip")).to_contain_text("?") + + +@pytest.mark.usefixtures("bokeh_backend") +def test_hover_tooltips_html_string(serve_hv): + hv_image = hv.Image( + np.random.rand(10, 10), bounds=(0, 0, 1, 1), kdims=["xc", "yc"] + ).opts(hover_tooltips="x: $x
y: @yc") + + page = serve_hv(hv_image) + hv_plot = page.locator(".bk-events") + wait_until(lambda: expect(hv_plot).to_have_count(1), page=page) + bbox = hv_plot.bounding_box() + + # Hover over the plot + page.mouse.move(bbox["x"] + bbox["width"] / 2, bbox["y"] + bbox["height"] / 2) + page.mouse.up() + + wait_until(lambda: expect(page.locator(".bk-Tooltip")).to_have_count(1), page=page) + + expect(page.locator(".bk-Tooltip")).to_contain_text("x:") + expect(page.locator(".bk-Tooltip")).to_contain_text("y:") + expect(page.locator(".bk-Tooltip")).not_to_contain_text("?") + + +@pytest.mark.usefixtures("bokeh_backend") +def test_hover_tooltips_formatters(serve_hv): + hv_image = hv.Image( + np.random.rand(10, 10), bounds=(0, 0, 1, 1), kdims=["xc", "yc"] + ).opts( + hover_tooltips=[("X", "($x, @xc{%0.3f})")], hover_formatters={"@xc": "printf"} + ) + + page = serve_hv(hv_image) + hv_plot = page.locator(".bk-events") + wait_until(lambda: expect(hv_plot).to_have_count(1), page=page) + bbox = hv_plot.bounding_box() + + # Hover over the plot + page.mouse.move(bbox["x"] + bbox["width"] / 2, bbox["y"] + bbox["height"] / 2) + page.mouse.up() + + wait_until(lambda: expect(page.locator(".bk-Tooltip")).to_have_count(1), page=page) + + expect(page.locator(".bk-Tooltip")).to_contain_text("X:") + expect(page.locator(".bk-Tooltip")).to_contain_text("%") + expect(page.locator(".bk-Tooltip")).not_to_contain_text("?") + + +@pytest.mark.usefixtures("bokeh_backend") +@pytest.mark.parametrize("hover_mode", ["hline", "vline"]) +def test_hover_mode(serve_hv, hover_mode): + hv_curve = hv.Curve([0, 10, 2]).opts(tools=["hover"], hover_mode=hover_mode) + + page = serve_hv(hv_curve) + hv_plot = page.locator(".bk-events") + wait_until(lambda: expect(hv_plot).to_have_count(1), page=page) + bbox = hv_plot.bounding_box() + + # Hover over the plot + page.mouse.move(bbox["x"] + bbox["width"] / 2, bbox["y"] + bbox["height"] / 2) + page.mouse.up() + + wait_until(lambda: expect(page.locator(".bk-Tooltip")).to_have_count(1), page=page) + + expect(page.locator(".bk-Tooltip")).to_contain_text("x:") + expect(page.locator(".bk-Tooltip")).to_contain_text("y:") + expect(page.locator(".bk-Tooltip")).not_to_contain_text("?") + + +@pytest.mark.usefixtures("bokeh_backend") +@pytest.mark.parametrize( + "hover_tooltip", + [ + "Amplitude", + "@Amplitude", + ("Amplitude", "@Amplitude"), + ], +) +def test_hover_tooltips_dimension_unit(serve_hv, hover_tooltip): + amplitude_dim = hv.Dimension("Amplitude", unit="µV") + hv_curve = hv.Curve([0, 10, 2], vdims=[amplitude_dim]).opts( + hover_tooltips=[hover_tooltip], hover_mode="vline" + ) + + page = serve_hv(hv_curve) + hv_plot = page.locator(".bk-events") + wait_until(lambda: expect(hv_plot).to_have_count(1), page=page) + bbox = hv_plot.bounding_box() + + # Hover over the plot + page.mouse.move(bbox["x"] + bbox["width"] / 2, bbox["y"] + bbox["height"] / 2) + page.mouse.up() + + wait_until(lambda: expect(page.locator(".bk-Tooltip")).to_have_count(1), page=page) + + expect(page.locator(".bk-Tooltip")).to_contain_text("Amplitude (µV): 10") From ffb12933935415368e0d71e0e5a3e0dd12858ab1 Mon Sep 17 00:00:00 2001 From: Demetris Roumis Date: Tue, 16 Apr 2024 14:03:06 -0700 Subject: [PATCH 08/43] Set hard navigable bounds (#6056) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Philipp Rudiger Co-authored-by: Simon Høxbro Hansen --- examples/user_guide/Plotting_with_Bokeh.ipynb | 72 ++++++++++++++ holoviews/plotting/bokeh/element.py | 38 ++++++++ holoviews/plotting/plot.py | 19 +++- .../tests/plotting/bokeh/test_elementplot.py | 97 ++++++++++++++++++- 4 files changed, 222 insertions(+), 4 deletions(-) diff --git a/examples/user_guide/Plotting_with_Bokeh.ipynb b/examples/user_guide/Plotting_with_Bokeh.ipynb index b13c1edbed..31e42ef7cf 100644 --- a/examples/user_guide/Plotting_with_Bokeh.ipynb +++ b/examples/user_guide/Plotting_with_Bokeh.ipynb @@ -296,6 +296,78 @@ " img.options(data_aspect=2, frame_width=300).relabel('data_aspect=2')).cols(2)" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Navigable Bounds\n", + "\n", + "Users may set the `apply_hard_bounds` option to constrain the navigable range (extent one could zoom or pan to). If `True`, the navigable bounds of the plot will be constrained to the range of the data. Go ahead and try to zoom in a out in the plot below, you should find that you cannot zoom beyond the extents of the data.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "x_values = np.linspace(0, 10, 100)\n", + "y_values = np.sin(x_values)\n", + "\n", + "hv.Curve((x_values, y_values)).opts(apply_hard_bounds=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "If `xlim` or `ylim` is set for an element, the navigable bounds of the plot will be set based\n", + "on the combined extremes of extents between the data and xlim/ylim ranges. In the plot below, the `xlim` constrains the initial view, but you should be able to pan to the x-range between 0 and 12 - the combined extremes of ranges between the data (0,10) and `xlim` (2,12)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "x_values = np.linspace(0, 10, 100)\n", + "y_values = np.sin(x_values)\n", + "\n", + "hv.Curve((x_values, y_values)).opts(\n", + " apply_hard_bounds=True,\n", + " xlim=(2, 12),\n", + " )" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "If a dimension range is specified (e.g. with `.redim.range`), this range will be used as the hard bounds, regardless of the data range or xlim/ylim. This is because the dimension range is itended to be an override on the minimum and maximum allowable values for the dimension. Read more in [Annotating your Data](./01-Annotating_Data.ipynb)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "x_values = np.linspace(0, 10, 100)\n", + "y_values = np.sin(x_values)\n", + "\n", + "hv.Curve((x_values, y_values)).opts(\n", + " apply_hard_bounds=True,\n", + ").redim.range(x=(4, 6))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In the plot above, you should not be able to navigate beyond the specified dimension ranges of `x` (4, 6). " + ] + }, { "cell_type": "markdown", "metadata": {}, diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py index 2b4dfcf9de..8b4d860ce7 100644 --- a/holoviews/plotting/bokeh/element.py +++ b/holoviews/plotting/bokeh/element.py @@ -105,6 +105,11 @@ class ElementPlot(BokehPlot, GenericElementPlot): align = param.ObjectSelector(default='start', objects=['start', 'center', 'end'], doc=""" Alignment (vertical or horizontal) of the plot in a layout.""") + apply_hard_bounds = param.Boolean(default=False, doc=""" + If True, the navigable bounds of the plot will be set based + on the more extreme of extents between the data or xlim/ylim ranges. + If dim ranges are set, the hard bounds will be set to the dim ranges.""") + autorange = param.ObjectSelector(default=None, objects=['x', 'y', None], doc=""" Whether to auto-range along either the x- or y-axis, i.e. when panning or zooming along the orthogonal axis it will @@ -2034,6 +2039,10 @@ def initialize_plot(self, ranges=None, plot=None, plots=None, source=None): if self._subcoord_overlaid: if style_element.label in plot.extra_y_ranges: self.handles['y_range'] = plot.extra_y_ranges.pop(style_element.label) + + if self.apply_hard_bounds: + self._apply_hard_bounds(element, ranges) + self.handles['plot'] = plot if self.autorange: @@ -2059,6 +2068,32 @@ def initialize_plot(self, ranges=None, plot=None, plots=None, source=None): return plot + def _apply_hard_bounds(self, element, ranges): + """ + Apply hard bounds to the x and y ranges of the plot. If xlim/ylim is set, limit the + initial viewable range to xlim/ylim, but allow navigation up to the abs max between + the data range and xlim/ylim. If dim range is set (e.g. via redim.range), enforce + as hard bounds. + + """ + + def validate_bound(bound): + return bound if util.isfinite(bound) else None + + min_extent_x, min_extent_y, max_extent_x, max_extent_y = map( + validate_bound, self.get_extents(element, ranges, range_type='combined', lims_as_soft_ranges=True) + ) + + def set_bounds(axis, min_extent, max_extent): + """Set the bounds for a given axis, using None if both extents are None or identical""" + try: + self.handles[axis].bounds = None if min_extent == max_extent else (min_extent, max_extent) + except ValueError: + self.handles[axis].bounds = None + + set_bounds('x_range', min_extent_x, max_extent_x) + set_bounds('y_range', min_extent_y, max_extent_y) + def _setup_data_callbacks(self, plot): if not self._js_on_data_callbacks: return @@ -2184,6 +2219,9 @@ def update_frame(self, key, ranges=None, plot=None, element=None): cds = self.handles['cds'] self._postprocess_hover(renderer, cds) + if self.apply_hard_bounds: + self._apply_hard_bounds(element, ranges) + self._update_glyphs(element, ranges, self.style[self.cyclic_index]) self._execute_hooks(element) diff --git a/holoviews/plotting/plot.py b/holoviews/plotting/plot.py index 612091659c..bcaedd2a50 100644 --- a/holoviews/plotting/plot.py +++ b/holoviews/plotting/plot.py @@ -1423,7 +1423,7 @@ def _get_range_extents(self, element, ranges, range_type, xdim, ydim, zdim): return (x0, y0, x1, y1) - def get_extents(self, element, ranges, range_type='combined', dimension=None, xdim=None, ydim=None, zdim=None, **kwargs): + def get_extents(self, element, ranges, range_type='combined', dimension=None, xdim=None, ydim=None, zdim=None, lims_as_soft_ranges=False, **kwargs): """ Gets the extents for the axes from the current Element. The globally computed ranges can optionally override the extents. @@ -1444,6 +1444,12 @@ def get_extents(self, element, ranges, range_type='combined', dimension=None, xd This allows Overlay plots to obtain each range and combine them appropriately for all the objects in the overlay. + + If lims_as_soft_ranges is set to True, the xlim and ylim will be treated as + soft ranges instead of the default case as hard ranges while computing the extents. + This is used e.g. when apply_hard_bounds is True and xlim/ylim is set, in which + case we limit the initial viewable range to xlim/ylim, but allow navigation up to + the abs max between the data range and xlim/ylim. """ num = 6 if (isinstance(self.projection, str) and self.projection == '3d') else 4 if self.apply_extents and range_type in ('combined', 'extents'): @@ -1486,8 +1492,15 @@ def get_extents(self, element, ranges, range_type='combined', dimension=None, xd else: x0, y0, x1, y1 = combined - x0, x1 = util.dimension_range(x0, x1, self.xlim, (None, None)) - y0, y1 = util.dimension_range(y0, y1, self.ylim, (None, None)) + if lims_as_soft_ranges: + # run x|ylim through max_range to ensure datetime-dtype matching with ranges + xlim_soft_ranges = util.max_range([self.xlim]) + ylim_soft_ranges = util.max_range([self.ylim]) + x0, x1 = util.dimension_range(x0, x1, (None, None), xlim_soft_ranges) + y0, y1 = util.dimension_range(y0, y1, (None, None), ylim_soft_ranges) + else: + x0, x1 = util.dimension_range(x0, x1, self.xlim, (None, None)) + y0, y1 = util.dimension_range(y0, y1, self.ylim, (None, None)) if not self.drawn: x_range, y_range = ((y0, y1), (x0, x1)) if self.invert_axes else ((x0, x1), (y0, y1)) diff --git a/holoviews/tests/plotting/bokeh/test_elementplot.py b/holoviews/tests/plotting/bokeh/test_elementplot.py index ab6e210382..9cef2f3b6c 100644 --- a/holoviews/tests/plotting/bokeh/test_elementplot.py +++ b/holoviews/tests/plotting/bokeh/test_elementplot.py @@ -3,6 +3,7 @@ import numpy as np import panel as pn +import param import pytest from bokeh.document import Document from bokeh.models import ( @@ -16,7 +17,8 @@ tools, ) -from holoviews.core import DynamicMap, HoloMap, NdOverlay +from holoviews import opts +from holoviews.core import DynamicMap, HoloMap, NdOverlay, Overlay from holoviews.core.util import dt_to_int from holoviews.element import Curve, HeatMap, Image, Labels, Scatter from holoviews.plotting.util import process_cmap @@ -993,3 +995,96 @@ def test_clim_percentile(self): low, high = plot.ranges[('Image',)]['z']['robust'] assert low > 0 assert high < 1 + +class TestApplyHardBounds(TestBokehPlot): + def test_apply_hard_bounds(self): + """Test `apply_hard_bounds` with a single element.""" + x_values = np.linspace(10, 50, 5) + y_values = np.array([10, 20, 30, 40, 50]) + curve = Curve((x_values, y_values)).opts(apply_hard_bounds=True) + plot = bokeh_renderer.get_plot(curve) + assert plot.handles['x_range'].bounds == (10, 50) + + def test_apply_hard_bounds_overlay(self): + """Test `apply_hard_bounds` with an overlay of curves.""" + x1_values = np.linspace(10, 50, 5) + x2_values = np.linspace(10, 90, 5) + y_values = np.array([10, 20, 30, 40, 50]) + curve1 = Curve((x1_values, y_values)) + curve2 = Curve((x2_values, y_values)) + overlay = Overlay([curve1, curve2]).opts(opts.Curve(apply_hard_bounds=True)) + plot = bokeh_renderer.get_plot(overlay) + # Check if the large of the data range can be navigated to + assert plot.handles['x_range'].bounds == (10, 90) + + def test_apply_hard_bounds_with_xlim(self): + """Test `apply_hard_bounds` with `xlim` set. Initial view should be within xlim but allow panning to data range.""" + x_values = np.linspace(10, 50, 5) + y_values = np.array([10, 20, 30, 40, 50]) + curve = Curve((x_values, y_values)).opts(apply_hard_bounds=True, xlim=(15, 35)) + plot = bokeh_renderer.get_plot(curve) + initial_view_range = (plot.handles['x_range'].start, plot.handles['x_range'].end) + assert initial_view_range == (15, 35) + # Check if data beyond xlim can be navigated to + assert plot.handles['x_range'].bounds == (10, 50) + + def test_apply_hard_bounds_with_redim_range(self): + """Test `apply_hard_bounds` with `.redim.range(x=...)`. Hard bounds should strictly apply.""" + x_values = np.linspace(10, 50, 5) + y_values = np.array([10, 20, 30, 40, 50]) + curve = Curve((x_values, y_values)).redim.range(x=(25, None)).opts(apply_hard_bounds=True) + plot = bokeh_renderer.get_plot(curve) + # Expected to strictly adhere to any redim.range bounds, otherwise the data range + assert (plot.handles['x_range'].start, plot.handles['x_range'].end) == (25, 50) + assert plot.handles['x_range'].bounds == (25, 50) + + def test_apply_hard_bounds_datetime(self): + """Test datetime axes with hard bounds.""" + target_xlim_l = dt.datetime(2020, 1, 3) + target_xlim_h = dt.datetime(2020, 1, 7) + dates = [dt.datetime(2020, 1, i) for i in range(1, 11)] + values = np.linspace(0, 100, 10) + curve = Curve((dates, values)).opts( + apply_hard_bounds=True, + xlim=(target_xlim_l, target_xlim_h) + ) + plot = bokeh_renderer.get_plot(curve) + initial_view_range = (dt_to_int(plot.handles['x_range'].start), dt_to_int(plot.handles['x_range'].end)) + assert initial_view_range == (dt_to_int(target_xlim_l), dt_to_int(target_xlim_h)) + # Validate navigation bounds include entire data range + hard_bounds = (dt_to_int(plot.handles['x_range'].bounds[0]), dt_to_int(plot.handles['x_range'].bounds[1])) + assert hard_bounds == (dt_to_int(dt.datetime(2020, 1, 1)), dt_to_int(dt.datetime(2020, 1, 10))) + + def test_dynamic_map_bounds_update(self): + """Test that `apply_hard_bounds` applies correctly when DynamicMap is updated.""" + + def curve_data(choice): + datasets = { + 'set1': (np.linspace(0, 5, 100), np.random.rand(100)), + 'set2': (np.linspace(0, 20, 100), np.random.rand(100)), + } + x, y = datasets[choice] + return Curve((x, y)) + + ChoiceStream = Stream.define( + 'Choice', + choice=param.ObjectSelector(default='set1', objects=['set1', 'set2']) + ) + choice_stream = ChoiceStream() + dmap = DynamicMap(curve_data, kdims=[], streams=[choice_stream]) + dmap = dmap.opts(opts.Curve(apply_hard_bounds=True, xlim=(2,3), framewise=True)) + dmap = dmap.redim.values(choice=['set1', 'set2']) + plot = bokeh_renderer.get_plot(dmap) + + # Keeping the xlim consistent between updates, and change data range bounds + # Initially select 'set1' + dmap.event(choice='set1') + assert plot.handles['x_range'].start == 2 + assert plot.handles['x_range'].end == 3 + assert plot.handles['x_range'].bounds == (0, 5) + + # Update to 'set2' + dmap.event(choice='set2') + assert plot.handles['x_range'].start == 2 + assert plot.handles['x_range'].end == 3 + assert plot.handles['x_range'].bounds == (0, 20) From 4042923d91707cadbcc188fe9511eb0ee4a9f5ec Mon Sep 17 00:00:00 2001 From: Maxime Liquet <35924738+maximlt@users.noreply.github.com> Date: Wed, 17 Apr 2024 13:38:30 +0200 Subject: [PATCH 09/43] Add a zoom tool per subcoordinate_y group (#6122) --- holoviews/plotting/bokeh/element.py | 74 +++++++++- .../tests/plotting/bokeh/test_subcoordy.py | 130 ++++++++++++++++++ 2 files changed, 203 insertions(+), 1 deletion(-) diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py index 8b4d860ce7..91a5c808da 100644 --- a/holoviews/plotting/bokeh/element.py +++ b/holoviews/plotting/bokeh/element.py @@ -1,4 +1,5 @@ import warnings +from collections import defaultdict from itertools import chain from types import FunctionType @@ -3025,9 +3026,77 @@ def _merge_tools(self, subplot): subplot.handles['zooms_subcoordy'].values(), self.handles['zooms_subcoordy'].values(), ): - renderers = list(util.unique_iterator(subplot_zoom.renderers + overlay_zoom.renderers)) + renderers = list(util.unique_iterator(overlay_zoom.renderers + subplot_zoom.renderers)) overlay_zoom.renderers = renderers + def _postprocess_subcoordinate_y_groups(self, overlay, plot): + """ + Add a zoom tool per group to the overlay. + """ + # First, just process and validate the groups and their content. + groups = defaultdict(list) + + # If there are groups AND there are subcoordinate_y elements without a group. + if any(el.group != type(el).__name__ for el in overlay) and any( + el.opts.get('plot').kwargs.get('subcoordinate_y', False) + and el.group == type(el).__name__ + for el in overlay + ): + raise ValueError( + 'The subcoordinate_y overlay contains elements with a defined group, each ' + 'subcoordinate_y element in the overlay must have a defined group.' + ) + + for el in overlay: + # group is the Element type per default (e.g. Curve, Spike). + if el.group == type(el).__name__: + continue + if not el.opts.get('plot').kwargs.get('subcoordinate_y', False): + raise ValueError( + f"All elements in group {el.group!r} must set the option " + f"'subcoordinate_y=True'. Not found for: {el}" + ) + groups[el.group].append(el) + + # No need to go any further if there's just one group. + if len(groups) <= 1: + return + + # At this stage, there's only one zoom tool (e.g. 1 wheel_zoom) that + # has all the renderers (e.g. all the curves in the overlay). + # We want to create as many zoom tools as groups, for each group + # the zoom tool must have the renderers of the elements of the group. + zoom_tools = self.handles['zooms_subcoordy'] + for zoom_tool_name, zoom_tool in zoom_tools.items(): + renderers_per_group = defaultdict(list) + # We loop through each overlay sub-elements and empty the list of + # renderers of the initial tool. + for el in overlay: + if el.group not in groups: + continue + renderers_per_group[el.group].append(zoom_tool.renderers.pop(0)) + + if zoom_tool.renderers: + raise RuntimeError(f'Found unexpected zoom renderers {zoom_tool.renderers}') + + new_ztools = [] + # Create a new tool per group with the right renderers and a custom description. + for grp, grp_renderers in renderers_per_group.items(): + new_tool = zoom_tool.clone() + new_tool.renderers = grp_renderers + new_tool.description = f"{zoom_tool_name.replace('_', ' ').title()} ({grp})" + new_ztools.append(new_tool) + # Revert tool order so the upper tool in the toolbar corresponds to the + # upper group in the overlay. + new_ztools = new_ztools[::-1] + + # Update the handle for good measure. + zoom_tools[zoom_tool_name] = new_ztools + + # Replace the original tool by the new ones + idx = plot.tools.index(zoom_tool) + plot.tools[idx:idx+1] = new_ztools + def _get_dimension_factors(self, overlay, ranges, dimension): factors = [] for k, sp in self.subplots.items(): @@ -3132,6 +3201,9 @@ def initialize_plot(self, ranges=None, plot=None, plots=None): reordered.append(reversed_renderers.pop(0)) plot.renderers = reordered + if self.subcoordinate_y: + self._postprocess_subcoordinate_y_groups(element, plot) + if self.tabs: self.handles['plot'] = Tabs( tabs=panels, width=self.width, height=self.height, diff --git a/holoviews/tests/plotting/bokeh/test_subcoordy.py b/holoviews/tests/plotting/bokeh/test_subcoordy.py index a31d9eead3..4bdf13c52e 100644 --- a/holoviews/tests/plotting/bokeh/test_subcoordy.py +++ b/holoviews/tests/plotting/bokeh/test_subcoordy.py @@ -261,3 +261,133 @@ def test_tools_instance_zoom_untouched(self): break else: raise AssertionError('Provided zoom not found.') + + def test_single_group(self): + # Same as test_bool_base, to check nothing is affected by defining + # a single group. + + overlay = Overlay([Curve(range(10), label=f'Data {i}', group='Group').opts(subcoordinate_y=True) for i in range(2)]) + plot = bokeh_renderer.get_plot(overlay) + # subcoordinate_y is propagated to the overlay + assert plot.subcoordinate_y is True + # the figure has only one yaxis + assert len(plot.state.yaxis) == 1 + # the overlay has two subplots + assert len(plot.subplots) == 2 + assert ('Group', 'Data_0') in plot.subplots + assert ('Group', 'Data_1') in plot.subplots + # the range per subplots are correctly computed + sp1 = plot.subplots[('Group', 'Data_0')] + assert sp1.handles['glyph_renderer'].coordinates.y_target.start == -0.5 + assert sp1.handles['glyph_renderer'].coordinates.y_target.end == 0.5 + sp2 = plot.subplots[('Group', 'Data_1')] + assert sp2.handles['glyph_renderer'].coordinates.y_target.start == 0.5 + assert sp2.handles['glyph_renderer'].coordinates.y_target.end == 1.5 + # y_range is correctly computed + assert plot.handles['y_range'].start == -0.5 + assert plot.handles['y_range'].end == 1.5 + # extra_y_range is empty + assert plot.handles['extra_y_ranges'] == {} + # the ticks show the labels + assert plot.state.yaxis.ticker.ticks == [0, 1] + assert plot.state.yaxis.major_label_overrides == {0: 'Data 0', 1: 'Data 1'} + + def test_multiple_groups(self): + overlay = Overlay([ + Curve(range(10), label=f'{group} / {i}', group=group).opts(subcoordinate_y=True) + for group in ['A', 'B'] + for i in range(2) + ]) + plot = bokeh_renderer.get_plot(overlay) + # subcoordinate_y is propagated to the overlay + assert plot.subcoordinate_y is True + # the figure has only one yaxis + assert len(plot.state.yaxis) == 1 + # the overlay has two subplots + assert len(plot.subplots) == 4 + assert ('A', 'A_over_0') in plot.subplots + assert ('A', 'A_over_1') in plot.subplots + assert ('B', 'B_over_0') in plot.subplots + assert ('B', 'B_over_1') in plot.subplots + # the range per subplots are correctly computed + sp1 = plot.subplots[('A', 'A_over_0')] + assert sp1.handles['glyph_renderer'].coordinates.y_target.start == -0.5 + assert sp1.handles['glyph_renderer'].coordinates.y_target.end == 0.5 + sp2 = plot.subplots[('A', 'A_over_1')] + assert sp2.handles['glyph_renderer'].coordinates.y_target.start == 0.5 + assert sp2.handles['glyph_renderer'].coordinates.y_target.end == 1.5 + sp3 = plot.subplots[('B', 'B_over_0')] + assert sp3.handles['glyph_renderer'].coordinates.y_target.start == 1.5 + assert sp3.handles['glyph_renderer'].coordinates.y_target.end == 2.5 + sp4 = plot.subplots[('B', 'B_over_1')] + assert sp4.handles['glyph_renderer'].coordinates.y_target.start == 2.5 + assert sp4.handles['glyph_renderer'].coordinates.y_target.end == 3.5 + # y_range is correctly computed + assert plot.handles['y_range'].start == -0.5 + assert plot.handles['y_range'].end == 3.5 + # extra_y_range is empty + assert plot.handles['extra_y_ranges'] == {} + # the ticks show the labels + assert plot.state.yaxis.ticker.ticks == [0, 1, 2, 3] + assert plot.state.yaxis.major_label_overrides == { + 0: 'A / 0', 1: 'A / 1', + 2: 'B / 0', 3: 'B / 1', + } + + def test_multiple_groups_wheel_zoom_configured(self): + # Same as test_tools_default_wheel_zoom_configured + + groups = ['A', 'B'] + overlay = Overlay([ + Curve(range(10), label=f'{group} / {i}', group=group).opts(subcoordinate_y=True) + for group in groups + for i in range(2) + ]) + plot = bokeh_renderer.get_plot(overlay) + zoom_tools = [tool for tool in plot.state.tools if isinstance(tool, WheelZoomTool)] + assert zoom_tools == plot.handles['zooms_subcoordy']['wheel_zoom'] + assert len(zoom_tools) == len(groups) + for zoom_tool, group in zip(zoom_tools, reversed(groups)): + assert len(zoom_tool.renderers) == 2 + assert len(set(zoom_tool.renderers)) == 2 + assert zoom_tool.dimensions == 'height' + assert zoom_tool.level == 1 + assert zoom_tool.description == f'Wheel Zoom ({group})' + + def test_single_group_overlaid_no_error(self): + overlay = Overlay([Curve(range(10), label=f'Data {i}', group='Group').opts(subcoordinate_y=True) for i in range(2)]) + with_span = VSpan(1, 2) * overlay * VSpan(3, 4) + bokeh_renderer.get_plot(with_span) + + def test_multiple_groups_overlaid_no_error(self): + overlay = Overlay([ + Curve(range(10), label=f'{group} / {i}', group=group).opts(subcoordinate_y=True) + for group in ['A', 'B'] + for i in range(2) + ]) + with_span = VSpan(1, 2) * overlay * VSpan(3, 4) + bokeh_renderer.get_plot(with_span) + + def test_missing_group_error(self): + curves = [] + for i, group in enumerate(['A', 'B', 'C']): + for i in range(2): + label = f'{group}{i}' + if group == "B": + curve = Curve(range(10), label=label, group=group).opts( + subcoordinate_y=True + ) + else: + curve = Curve(range(10), label=label).opts( + subcoordinate_y=True + ) + curves.append(curve) + + with pytest.raises( + ValueError, + match=( + 'The subcoordinate_y overlay contains elements with a defined group, each ' + 'subcoordinate_y element in the overlay must have a defined group.' + ) + ): + bokeh_renderer.get_plot(Overlay(curves)) From 3a2793f4d23186c954556f66fe38ff8fd71dc779 Mon Sep 17 00:00:00 2001 From: Theom <49269671+TheoMathurin@users.noreply.github.com> Date: Wed, 17 Apr 2024 18:22:29 +0200 Subject: [PATCH 10/43] Support all Bokeh Text style opts in hv.Labels and hv.Text (#6198) --- holoviews/plotting/bokeh/annotation.py | 17 +++++++++++++---- holoviews/plotting/bokeh/styles.py | 24 ++++++++++++++++-------- 2 files changed, 29 insertions(+), 12 deletions(-) diff --git a/holoviews/plotting/bokeh/annotation.py b/holoviews/plotting/bokeh/annotation.py index 56aca1f8d1..c97c44c95e 100644 --- a/holoviews/plotting/bokeh/annotation.py +++ b/holoviews/plotting/bokeh/annotation.py @@ -15,7 +15,14 @@ from .element import AnnotationPlot, ColorbarPlot, CompositeElementPlot, ElementPlot from .plot import BokehPlot from .selection import BokehOverlaySelectionDisplay -from .styles import base_properties, fill_properties, line_properties, text_properties +from .styles import ( + background_properties, + base_properties, + border_properties, + fill_properties, + line_properties, + text_properties, +) from .util import bokeh32, date_to_integer arrow_start = {'<->': NormalHead, '<|-|>': NormalHead} @@ -104,7 +111,8 @@ class VSpansAnnotationPlot(_SyntheticAnnotationPlot): class TextPlot(ElementPlot, AnnotationPlot): - style_opts = text_properties+['color', 'angle', 'visible'] + style_opts = (text_properties + background_properties + + border_properties + ['color', 'angle', 'visible']) _plot_methods = dict(single='text', batched='text') selection_display = None @@ -167,12 +175,13 @@ class LabelsPlot(ColorbarPlot, AnnotationPlot): selection_display = BokehOverlaySelectionDisplay() - style_opts = base_properties + text_properties + ['cmap', 'angle'] + style_opts = (base_properties + text_properties + + background_properties + border_properties + ['cmap', 'angle']) _nonvectorized_styles = base_properties + ['cmap'] _plot_methods = dict(single='text', batched='text') - _batched_style_opts = text_properties + _batched_style_opts = text_properties + background_properties + border_properties def get_data(self, element, ranges, style): style = self.style[self.cyclic_index] diff --git a/holoviews/plotting/bokeh/styles.py b/holoviews/plotting/bokeh/styles.py index 357127de04..b8fbded980 100644 --- a/holoviews/plotting/bokeh/styles.py +++ b/holoviews/plotting/bokeh/styles.py @@ -29,14 +29,22 @@ base_properties = ['visible', 'muted'] -line_properties = ['line_color', 'line_alpha', 'color', 'alpha', 'line_width', - 'line_join', 'line_cap', 'line_dash'] -line_properties += [f'{prefix}_{prop}' for prop in line_properties - for prefix in property_prefixes] - -fill_properties = ['fill_color', 'fill_alpha'] -fill_properties += [f'{prefix}_{prop}' for prop in fill_properties - for prefix in property_prefixes] +line_base_properties = ['line_color', 'line_alpha', 'color', 'alpha', 'line_width', + 'line_join', 'line_cap', 'line_dash', 'line_dash_offset'] +line_properties = line_base_properties + [f'{prefix}_{prop}' + for prop in line_base_properties + for prefix in property_prefixes] + +fill_base_properties = ['fill_color', 'fill_alpha'] +fill_properties = fill_base_properties + [f'{prefix}_{prop}' + for prop in fill_base_properties + for prefix in property_prefixes] + +border_properties = ['border_' + prop for prop in line_base_properties + ['radius']] + +hatch_properties = ['hatch_color', 'hatch_scale', 'hatch_weight', + 'hatch_extra', 'hatch_pattern', 'hatch_alpha'] +background_properties = ['background_' + prop for prop in fill_base_properties + hatch_properties] text_properties = ['text_font', 'text_font_size', 'text_font_style', 'text_color', 'text_alpha', 'text_align', 'text_baseline'] From 0025713dec7900116292cf2f3795976e7609995d Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Thu, 18 Apr 2024 14:02:57 +0200 Subject: [PATCH 11/43] Add support for popups on selection streams (#6168) --- .../user_guide/13-Custom_Interactivity.ipynb | 163 ++++++++- holoviews/plotting/bokeh/callbacks.py | 316 ++++++++++++++++-- holoviews/streams.py | 3 +- holoviews/tests/ui/bokeh/test_callback.py | 180 ++++++++++ 4 files changed, 633 insertions(+), 29 deletions(-) diff --git a/examples/user_guide/13-Custom_Interactivity.ipynb b/examples/user_guide/13-Custom_Interactivity.ipynb index 371d3f5749..a88758f47a 100644 --- a/examples/user_guide/13-Custom_Interactivity.ipynb +++ b/examples/user_guide/13-Custom_Interactivity.ipynb @@ -18,7 +18,7 @@ "import holoviews as hv\n", "from holoviews import opts\n", "\n", - "hv.extension('bokeh', 'matplotlib')" + "hv.extension('bokeh')" ] }, { @@ -410,6 +410,167 @@ "source": [ "taps" ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Pop-up panes\n", + "\n", + "Sometimes, you might want to display additional info, next to the selection, as a floating pane.\n", + "\n", + "To do this, specify `popup`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "points = hv.Points(np.random.randn(1000, 2))\n", + "\n", + "hv.streams.BoundsXY(source=points, popup=\"Used Box Select\")\n", + "hv.streams.Lasso(source=points, popup=\"Used Lasso Select\")\n", + "hv.streams.Tap(source=points, popup=\"Used Tap\")\n", + "\n", + "points.opts(\n", + " tools=[\"box_select\", \"lasso_select\", \"tap\"],\n", + " active_tools=[\"lasso_select\"],\n", + " size=6,\n", + " color=\"black\",\n", + " fill_color=None,\n", + " width=500,\n", + " height=500\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "An applicable example is using the `popup` to show stats of the selected points." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def popup_stats(index):\n", + " if not index:\n", + " return\n", + " return points.iloc[index].dframe().describe()\n", + "\n", + "\n", + "points = hv.Points(np.random.randn(1000, 2))\n", + "\n", + "hv.streams.Selection1D(\n", + " source=points,\n", + " popup=popup_stats\n", + "\n", + ")\n", + "\n", + "points.opts(\n", + " tools=[\"box_select\", \"lasso_select\", \"tap\"],\n", + " active_tools=[\"lasso_select\"],\n", + " size=6,\n", + " color=\"black\",\n", + " fill_color=None,\n", + " width=500,\n", + " height=500\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The contents of the `popup` can be another HoloViews object too, like the distribution of the selected points." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def popup_distribution(index):\n", + " x, y = points.iloc[index].data.T\n", + " return hv.Distribution((x, y)).opts(\n", + " width=100,\n", + " height=100,\n", + " toolbar=None,\n", + " yaxis=\"bare\",\n", + " xlabel=\"\",\n", + " xticks=[-1, 0, 1],\n", + " xlim=(-2, 2),\n", + " )\n", + "\n", + "\n", + "points = hv.Points(np.random.randn(1000, 2))\n", + "\n", + "hv.streams.Selection1D(\n", + " source=points,\n", + " popup=popup_distribution,\n", + ")\n", + "\n", + "points.opts(\n", + " tools=[\"box_select\", \"lasso_select\", \"tap\"],\n", + " active_tools=[\"lasso_select\"],\n", + " size=6,\n", + " color=\"black\",\n", + " fill_color=None,\n", + " width=500,\n", + " height=500\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "It can also be a object or any component that can be rendered with Panel, which is an open-source Python library built on top of Bokeh, with a variety of easy-to-use [widgets and panes](https://panel.holoviz.org/reference/index.html#), such as [`Image`](https://panel.holoviz.org/reference/panes/Image.html), [`Button`](https://panel.holoviz.org/reference/widgets/Button.html), [`TextInput`](https://panel.holoviz.org/reference/widgets/TextInput.html), and much more!\n", + "\n", + "To control the visibility of the `popup`, update `visible` parameter of the provided component." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import panel as pn\n", + "pn.extension()\n", + "\n", + "def popup_form(index):\n", + " def hide_popup(_):\n", + " layout.visible = False\n", + "\n", + " if not index:\n", + " return\n", + " df = points.iloc[index].dframe().describe()\n", + " button = pn.widgets.Button(name=\"Close\", sizing_mode=\"stretch_width\")\n", + " layout = pn.Column(button, df)\n", + " button.on_click(hide_popup)\n", + " return layout\n", + "\n", + "\n", + "points = hv.Points(np.random.randn(1000, 2))\n", + "hv.streams.Selection1D(source=points, popup=popup_form)\n", + "\n", + "points.opts(\n", + " tools=[\"box_select\", \"lasso_select\", \"tap\"],\n", + " active_tools=[\"lasso_select\"],\n", + " size=6,\n", + " color=\"black\",\n", + " fill_color=None,\n", + " width=500,\n", + " height=500\n", + ")" + ] } ], "metadata": { diff --git a/holoviews/plotting/bokeh/callbacks.py b/holoviews/plotting/bokeh/callbacks.py index 521970a0a3..02a5d8ccab 100644 --- a/holoviews/plotting/bokeh/callbacks.py +++ b/holoviews/plotting/bokeh/callbacks.py @@ -2,10 +2,12 @@ import base64 import time from collections import defaultdict +from functools import partial import numpy as np from bokeh.models import ( BoxEditTool, + Button, CustomJS, DataRange1d, DatetimeAxis, @@ -16,10 +18,24 @@ PolyEditTool, Range1d, ) +from panel.io.notebook import push_on_root from panel.io.state import set_curdoc, state +from panel.pane import panel +try: + from bokeh.models import XY, Panel +except Exception: + Panel = XY = None + +from ...core.data import Dataset from ...core.options import CallbackError -from ...core.util import datetime_types, dimension_sanitizer, dt64_to_dt, isequal +from ...core.util import ( + VersionError, + datetime_types, + dimension_sanitizer, + dt64_to_dt, + isequal, +) from ...element import Table from ...streams import ( BoundsX, @@ -308,9 +324,7 @@ async def on_change(self, attr, old, new): if not self._active and self.plot.document: self._active = True self._set_busy(True) - task = asyncio.create_task(self.process_on_change()) - self._background_task.add(task) - task.add_done_callback(self._background_task.discard) + await self.process_on_change() async def on_event(self, event): """ @@ -321,9 +335,7 @@ async def on_event(self, event): if not self._active and self.plot.document: self._active = True self._set_busy(True) - task = asyncio.create_task(self.process_on_event()) - self._background_task.add(task) - task.add_done_callback(self._background_task.discard) + await self.process_on_event() async def process_on_event(self, timeout=None): """ @@ -337,7 +349,7 @@ async def process_on_event(self, timeout=None): # Get unique event types in the queue events = list(dict([(event.event_name, event) - for event, dt in self._queue]).values()) + for event, dt in self._queue]).values()) self._queue = [] # Process event types @@ -349,9 +361,7 @@ async def process_on_event(self, timeout=None): model_obj = self.plot_handles.get(self.models[0]) msg[attr] = self.resolve_attr_spec(path, event, model_obj) self.on_msg(msg) - task = asyncio.create_task(self.process_on_event()) - self._background_task.add(task) - task.add_done_callback(self._background_task.discard) + await self.process_on_event() async def process_on_change(self): # Give on_change time to process new events @@ -387,30 +397,39 @@ async def process_on_change(self): if not equal or any(s.transient for s in self.streams): self.on_msg(msg) self._prev_msg = msg - task = asyncio.create_task(self.process_on_change()) - self._background_task.add(task) - task.add_done_callback(self._background_task.discard) + await self.process_on_change() + + def _schedule_event(self, event): + if self.plot.comm or not self.plot.document.session_context or state._is_pyodide: + task = asyncio.create_task(self.on_event(event)) + self._background_task.add(task) + task.add_done_callback(self._background_task.discard) + else: + self.plot.document.add_next_tick_callback(partial(self.on_event, event)) + + def _schedule_change(self, attr, old, new): + if not self.plot.document: + return + if self.plot.comm or not self.plot.document.session_context or state._is_pyodide: + task = asyncio.create_task(self.on_change(attr, old, new)) + self._background_task.add(task) + task.add_done_callback(self._background_task.discard) + else: + self.plot.document.add_next_tick_callback(partial(self.on_change, attr, old, new)) def set_callback(self, handle): """ Set up on_change events for bokeh server interactions. """ if self.on_events: - event_handler = lambda event: ( - asyncio.create_task(self.on_event(event)) - ) for event in self.on_events: - handle.on_event(event, event_handler) + handle.on_event(event, self._schedule_event) if self.on_changes: - change_handler = lambda attr, old, new: ( - asyncio.create_task(self.on_change(attr, old, new)) - if self.plot.document else None - ) for change in self.on_changes: if change in ['patching', 'streaming']: # Patch and stream events do not need handling on server continue - handle.on_change(change, change_handler) + handle.on_change(change, self._schedule_change) def initialize(self, plot_id=None): handles = self._init_plot_handles() @@ -547,7 +566,186 @@ def _process_msg(self, msg): return self._transform(dict(msg, stroke_count=self.stroke_count)) -class TapCallback(PointerXYCallback): +class PopupMixin: + + geom_type = 'any' + + def initialize(self, plot_id=None): + super().initialize(plot_id=plot_id) + if not self.streams: + return + + self._selection_event = None + self._processed_event = True + self._skipped_partial_event = False + self._existing_popup = None + stream = self.streams[0] + if not getattr(stream, 'popup', None): + return + elif Panel is None: + raise VersionError("Popup requires Bokeh >= 3.4") + + close_button = Button(label="", stylesheets=[r""" + :host(.bk-Button) { + width: 100%; + height: 100%; + top: -1em; + } + .bk-btn, .bk-btn:hover, .bk-btn:active, .bk-btn:focus { + background: none; + border: none; + color: inherit; + cursor: pointer; + padding: 0.5em; + margin: -0.5em; + outline: none; + box-shadow: none; + position: absolute; + top: 0; + right: 0; + } + .bk-btn::after { + content: '\2715'; + } + """], + css_classes=["popup-close-btn"]) + self._panel = Panel( + position=XY(x=np.nan, y=np.nan), + anchor="top_left", + elements=[close_button], + visible=False + ) + close_button.js_on_click(CustomJS(args=dict(panel=self._panel), code="panel.visible = false")) + + self.plot.state.elements.append(self._panel) + self._watch_position() + + def _watch_position(self): + geom_type = self.geom_type + self.plot.state.on_event('selectiongeometry', self._update_selection_event) + self.plot.state.js_on_event('selectiongeometry', CustomJS( + args=dict(panel=self._panel), + code=f""" + export default ({{panel}}, cb_obj, _) => {{ + const el = panel.elements[1] + if ((el && !el.visible) || !cb_obj.final || ({geom_type!r} !== 'any' && cb_obj.geometry.type !== {geom_type!r})) {{ + return + }} + let pos; + if (cb_obj.geometry.type === 'point') {{ + pos = {{x: cb_obj.geometry.x, y: cb_obj.geometry.y}} + }} else if (cb_obj.geometry.type === 'rect') {{ + pos = {{x: cb_obj.geometry.x1, y: cb_obj.geometry.y1}} + }} else if (cb_obj.geometry.type === 'poly') {{ + pos = {{x: Math.max(...cb_obj.geometry.x), y: Math.max(...cb_obj.geometry.y)}} + }} + if (pos) {{ + panel.position.setv(pos) + }} + }}""", + )) + + def _get_position(self, event): + if self.geom_type not in ('any', event.geometry['type']): + return + elif event.geometry['type'] == 'point': + return dict(x=event.geometry['x'], y=event.geometry['y']) + elif event.geometry['type'] == 'rect': + return dict(x=event.geometry['x1'], y=event.geometry['y1']) + elif event.geometry['type'] == 'poly': + return dict(x=np.max(event.geometry['x']), y=np.max(event.geometry['y'])) + + def _update_selection_event(self, event): + if (((prev:= self._selection_event) and prev.final and not self._processed_event) or + self.geom_type not in (event.geometry["type"], "any")): + return + self._selection_event = event + self._processed_event = not event.final + if event.final and self._skipped_partial_event: + self._process_selection_event() + self._skipped_partial_event = False + + def on_msg(self, msg): + super().on_msg(msg) + if hasattr(self, '_panel'): + self._process_selection_event() + + def _process_selection_event(self): + event = self._selection_event + if event is not None: + if self.geom_type not in (event.geometry["type"], "any"): + return + elif not event.final: + self._skipped_partial_event = True + return + + if event: + self._processed_event = True + for stream in self.streams: + popup = stream.popup + if popup is not None: + break + + if callable(popup): + popup = popup(**stream.contents) + + # If no popup is defined, hide the panel + if popup is None: + if self._panel.visible: + self._panel.visible = False + if self._existing_popup and not self._existing_popup.visible: + self._existing_popup.visible = False + return + + if event is not None: + position = self._get_position(event) + else: + position = None + popup_pane = panel(popup) + + if not popup_pane.stylesheets: + self._panel.stylesheets = [ + """ + :host { + padding: 1em; + border-radius: 0.5em; + border: 1px solid lightgrey; + } + """, + ] + else: + self._panel.stylesheets = [] + + self._panel.visible = True + # for existing popup, important to check if they're visible + # otherwise, UnknownReferenceError: can't resolve reference 'p...' + # meaning the popup has already been removed; we need to regenerate + if self._existing_popup and not self._existing_popup.visible: + if position: + self._panel.position = XY(**position) + self._existing_popup.visible = True + if self.plot.comm: + push_on_root(self.plot.root.ref['id']) + return + + model = popup_pane.get_root(self.plot.document, self.plot.comm) + model.js_on_change('visible', CustomJS( + args=dict(panel=self._panel), + code=""" + export default ({panel}, event, _) => { + if (!event.visible) { + panel.position.setv({x: NaN, y: NaN}) + } + }""", + )) + # the first element is the close button + self._panel.elements = [self._panel.elements[0], model] + if self.plot.comm: + push_on_root(self.plot.root.ref['id']) + self._existing_popup = popup_pane + + +class TapCallback(PopupMixin, PointerXYCallback): """ Returns the mouse x/y-position on tap event. @@ -555,6 +753,8 @@ class TapCallback(PointerXYCallback): individual tap events within a doubletap event. """ + geom_type = 'point' + on_events = ['tap', 'doubletap'] def _process_out_of_bounds(self, value, start, end): @@ -578,6 +778,7 @@ def _process_out_of_bounds(self, value, start, end): return value + class SingleTapCallback(TapCallback): """ Returns the mouse x/y-position on tap event. @@ -751,7 +952,7 @@ def _process_msg(self, msg): return msg -class BoundsCallback(Callback): +class BoundsCallback(PopupMixin, Callback): """ Returns the bounds of a box_select tool. """ @@ -759,6 +960,7 @@ class BoundsCallback(Callback): 'x1': 'cb_obj.geometry.x1', 'y0': 'cb_obj.geometry.y0', 'y1': 'cb_obj.geometry.y1'} + geom_type = 'rect' models = ['plot'] on_events = ['selectiongeometry'] @@ -870,11 +1072,13 @@ def _process_msg(self, msg): return {} -class LassoCallback(Callback): +class LassoCallback(PopupMixin, Callback): attributes = {'xs': 'cb_obj.geometry.x', 'ys': 'cb_obj.geometry.y'} + geom_type = 'poly' models = ['plot'] on_events = ['selectiongeometry'] + skip_events = [lambda event: event.geometry['type'] != 'poly', lambda event: not event.final] @@ -893,7 +1097,7 @@ def _process_msg(self, msg): return {'geometry': np.column_stack([xs, ys])} -class Selection1DCallback(Callback): +class Selection1DCallback(PopupMixin, Callback): """ Returns the current selection on a ColumnDataSource. """ @@ -902,6 +1106,64 @@ class Selection1DCallback(Callback): models = ['selected'] on_changes = ['indices'] + def _watch_position(self): + self.plot.state.on_event('selectiongeometry', self._update_selection_event) + source = self.plot.handles['source'] + renderer = self.plot.handles['glyph_renderer'] + selected = self.plot.handles['selected'] + self.plot.state.js_on_event('selectiongeometry', CustomJS( + args=dict(panel=self._panel, renderer=renderer, source=source, selected=selected), + code=""" + export default ({panel, renderer, source, selected}, cb_obj, _) => { + const el = panel.elements[1] + if ((el && !el.visible) || !cb_obj.final) { + return + } + let x, y, xs, ys; + let indices = selected.indices; + if (cb_obj.geometry.type == 'point') { + indices = indices.slice(-1) + } + if (renderer.glyph.x && renderer.glyph.y) { + xs = source.get_column(renderer.glyph.x.field) + ys = source.get_column(renderer.glyph.y.field) + } else if (renderer.glyph.right && renderer.glyph.top) { + xs = source.get_column(renderer.glyph.right.field) + ys = source.get_column(renderer.glyph.top.field) + } else if (renderer.glyph.x1 && renderer.glyph.y1) { + xs = source.get_column(renderer.glyph.x1.field) + ys = source.get_column(renderer.glyph.y1.field) + } else if (renderer.glyph.xs && renderer.glyph.ys) { + xs = source.get_column(renderer.glyph.xs.field) + ys = source.get_column(renderer.glyph.ys.field) + } + if (!xs || !ys) { return } + for (const i of indices) { + const tx = xs[i] + if (!x || (tx > x)) { + x = xs[i] + } + const ty = ys[i] + if (!y || (ty > y)) { + y = ys[i] + } + } + if (x && y) { + panel.position.setv({x, y}) + } + }""", + )) + + def _get_position(self, event): + el = self.plot.current_frame + if isinstance(el, Dataset): + s = self.streams[0] + sel = el.iloc[s.index] + # get the most top-right point + (_, x1), (_, y1) = sel.range(0), sel.range(1) + return dict(x=x1, y=y1) + return super()._get_position(event) + def _process_msg(self, msg): el = self.plot.current_frame if 'index' in msg: diff --git a/holoviews/streams.py b/holoviews/streams.py index 7a61a00cdf..12d3016958 100644 --- a/holoviews/streams.py +++ b/holoviews/streams.py @@ -1274,8 +1274,9 @@ class LinkedStream(Stream): supplying stream data. """ - def __init__(self, linked=True, **params): + def __init__(self, linked=True, popup=None, **params): super().__init__(linked=linked, **params) + self.popup = popup class PointerX(LinkedStream): diff --git a/holoviews/tests/ui/bokeh/test_callback.py b/holoviews/tests/ui/bokeh/test_callback.py index f08e056744..5efebd6d67 100644 --- a/holoviews/tests/ui/bokeh/test_callback.py +++ b/holoviews/tests/ui/bokeh/test_callback.py @@ -191,3 +191,183 @@ def range_function(x_range, y_range): wait_until(lambda: RANGE_COUNT[0] > 2, page) assert BOUND_COUNT[0] == 1 + +@pytest.mark.skipif(not bokeh34, reason="< Bokeh 3.4 does not support popup") +@pytest.mark.usefixtures("bokeh_backend") +def test_stream_popup(serve_hv): + def popup_form(name): + return f"# {name}" + + points = hv.Points(np.random.randn(10, 2)).opts(tools=["tap"]) + hv.streams.Tap(source=points, popup=popup_form("Tap")) + + page = serve_hv(points) + hv_plot = page.locator('.bk-events') + hv_plot.click() + expect(hv_plot).to_have_count(1) + + locator = page.locator("#tap") + expect(locator).to_have_count(1) + + +@pytest.mark.skipif(not bokeh34, reason="< Bokeh 3.4 does not support popup") +@pytest.mark.usefixtures("bokeh_backend") +def test_stream_popup_none(serve_hv): + def popup_form(name): + return + + points = hv.Points(np.random.randn(10, 2)) + hv.streams.Tap(source=points, popup=popup_form("Tap")) + + page = serve_hv(points) + hv_plot = page.locator('.bk-events') + expect(hv_plot).to_have_count(1) + + bbox = hv_plot.bounding_box() + hv_plot.click() + + page.mouse.move(bbox['x']+100, bbox['y']+100) + page.mouse.down() + page.mouse.move(bbox['x']+150, bbox['y']+150, steps=5) + page.mouse.up() + + locator = page.locator("#tap") + expect(locator).to_have_count(0) + + +@pytest.mark.skipif(not bokeh34, reason="< Bokeh 3.4 does not support popup") +@pytest.mark.usefixtures("bokeh_backend") +def test_stream_popup_callbacks(serve_hv): + def popup_form(x, y): + return pn.widgets.Button(name=f"{x},{y}") + + points = hv.Points(np.random.randn(10, 2)).opts(tools=["tap"]) + hv.streams.Tap(source=points, popup=popup_form) + + page = serve_hv(points) + hv_plot = page.locator('.bk-events') + hv_plot.click() + expect(hv_plot).to_have_count(1) + + locator = page.locator(".bk-btn") + expect(locator).to_have_count(2) + + +@pytest.mark.skipif(not bokeh34, reason="< Bokeh 3.4 does not support popup") +@pytest.mark.usefixtures("bokeh_backend") +def test_stream_popup_visible(serve_hv): + def popup_form(x, y): + def hide(_): + col.visible = False + button = pn.widgets.Button( + name=f"{x},{y}", + on_click=hide, + css_classes=["custom-button"] + ) + col = pn.Column(button) + return col + + points = hv.Points(np.random.randn(10, 2)).opts(tools=["tap"]) + hv.streams.Tap(source=points, popup=popup_form) + + page = serve_hv(points) + hv_plot = page.locator('.bk-events') + hv_plot.click() + expect(hv_plot).to_have_count(1) + + # initial appearance + locator = page.locator(".bk-btn") + expect(locator).to_have_count(2) + + # click custom button to hide + locator = page.locator(".custom-button") + locator.click() + locator = page.locator(".bk-btn") + expect(locator).to_have_count(0) + + + +@pytest.mark.skipif(not bokeh34, reason="< Bokeh 3.4 does not support popup") +@pytest.mark.usefixtures("bokeh_backend") +def test_stream_popup_close_button(serve_hv): + def popup_form(x, y): + return "Hello" + + points = hv.Points(np.random.randn(10, 2)).opts(tools=["tap", "box_select"]) + hv.streams.Tap(source=points, popup=popup_form) + hv.streams.BoundsXY(source=points, popup=popup_form) + + page = serve_hv(points) + hv_plot = page.locator('.bk-events') + expect(hv_plot).to_have_count(1) + hv_plot.click() + + locator = page.locator(".bk-btn.bk-btn-default") + expect(locator).to_have_count(1) + expect(locator).to_be_visible() + page.click(".bk-btn.bk-btn-default") + expect(locator).not_to_be_visible() + + +@pytest.mark.skipif(not bokeh34, reason="< Bokeh 3.4 does not support popup") +@pytest.mark.usefixtures("bokeh_backend") +def test_stream_popup_selection1d_undefined(serve_hv): + points = hv.Points(np.random.randn(10, 2)) + hv.streams.Selection1D(source=points) + + page = serve_hv(points) + hv_plot = page.locator('.bk-events') + expect(hv_plot).to_have_count(1) + hv_plot.click() # should not raise any error; properly guarded + + +@pytest.mark.skipif(not bokeh34, reason="< Bokeh 3.4 does not support popup") +@pytest.mark.usefixtures("bokeh_backend") +def test_stream_popup_selection1d(serve_hv): + def popup_form(index): + return "# Tap" + + points = hv.Points(np.random.randn(1000, 2)) + hv.streams.Selection1D(source=points, popup=popup_form) + points.opts(tools=["tap"], active_tools=["tap"]) + + page = serve_hv(points) + hv_plot = page.locator('.bk-events') + expect(hv_plot).to_have_count(1) + hv_plot.click() + + locator = page.locator("#tap") + expect(locator).to_have_count(1) + + +@pytest.mark.skipif(not bokeh34, reason="< Bokeh 3.4 does not support popup") +@pytest.mark.usefixtures("bokeh_backend") +def test_stream_popup_selection1d_lasso_select(serve_hv): + def popup_form(index): + if index: + return f"# lasso\n{len(index)}" + + points = hv.Points(np.random.randn(1000, 2)) + hv.streams.Selection1D(source=points, popup=popup_form) + points.opts(tools=["tap", "lasso_select"], active_tools=["lasso_select"]) + + page = serve_hv(points) + hv_plot = page.locator('.bk-events') + expect(hv_plot).to_have_count(1) + + box = hv_plot.bounding_box() + start_x, start_y = box['x'] + 10, box['y'] + box['height'] - 10 + mid_x, mid_y = box['x'] + 10, box['y'] + 10 + end_x, end_y = box['x'] + box['width'] - 10, box['y'] + 10 + + page.mouse.move(start_x, start_y) + hv_plot.click() + page.mouse.down() + page.mouse.move(mid_x, mid_y) + page.mouse.move(end_x, end_y) + page.mouse.up() + + wait_until(lambda: expect(page.locator("#lasso")).to_have_count(1), page) + locator = page.locator("#lasso") + expect(locator).to_have_count(1) + expect(locator).not_to_have_text("lasso\n0") From 39489c5d44176addb7cc8ebf852a0e4b4467a2fe Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Fri, 19 Apr 2024 15:22:40 +0200 Subject: [PATCH 12/43] Implement support for retaining Pandas index (#6061) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Simon Høxbro Hansen --- holoviews/core/data/ibis.py | 19 +- holoviews/core/data/pandas.py | 194 ++++++++++++---- .../tests/core/data/test_pandasinterface.py | 214 ++++++++++++++++++ holoviews/tests/operation/test_downsample.py | 2 - 4 files changed, 373 insertions(+), 56 deletions(-) diff --git a/holoviews/core/data/ibis.py b/holoviews/core/data/ibis.py index f904995d07..bc1268a687 100644 --- a/holoviews/core/data/ibis.py +++ b/holoviews/core/data/ibis.py @@ -8,8 +8,7 @@ from .. import util from ..element import Element from ..ndmapping import NdMapping, item_check, sorted_context -from . import pandas -from .interface import Interface +from .interface import DataError, Interface from .util import cached @@ -94,6 +93,17 @@ def init(cls, eltype, data, keys, values): values = list(data.columns[: nvdim if nvdim else None]) return data, dict(kdims=keys, vdims=values), {} + @classmethod + def validate(cls, dataset, vdims=True): + dim_types = 'all' if vdims else 'key' + dimensions = dataset.dimensions(dim_types, label='name') + cols = list(dataset.data.columns) + not_found = [d for d in dimensions if d not in cols] + if not_found: + raise DataError("Supplied data does not contain specified " + "dimensions, the following dimensions were " + "not found: %s" % repr(not_found), cls) + @classmethod def compute(cls, dataset): return dataset.clone(dataset.data.execute()) @@ -216,8 +226,9 @@ def redim(cls, dataset, dimensions): **{v.name: dataset.data[k] for k, v in dimensions.items()} ) - validate = pandas.PandasInterface.validate - reindex = pandas.PandasInterface.reindex + @classmethod + def reindex(cls, dataset, kdims=None, vdims=None): + return dataset.data @classmethod def _index_ibis_table(cls, data): diff --git a/holoviews/core/data/pandas.py b/holoviews/core/data/pandas.py index ce7b2fc086..2292e084b7 100644 --- a/holoviews/core/data/pandas.py +++ b/holoviews/core/data/pandas.py @@ -32,9 +32,7 @@ class PandasInterface(Interface, PandasAPI): @classmethod def dimension_type(cls, dataset, dim): - name = dataset.get_dimension(dim, strict=True).name - idx = list(dataset.data.columns).index(name) - return dataset.data.dtypes.iloc[idx].type + return cls.dtype(dataset, dim).type @classmethod def init(cls, eltype, data, kdims, vdims): @@ -46,9 +44,7 @@ def init(cls, eltype, data, kdims, vdims): data = data.to_frame(name=name) if util.is_dataframe(data): ncols = len(data.columns) - index_names = data.index.names if isinstance(data, pd.DataFrame) else [data.index.name] - if index_names == [None]: - index_names = ['index'] + index_names = cls.indexes(data) if eltype._auto_indexable_1d and ncols == 1 and kdims is None: kdims = list(index_names) @@ -74,17 +70,7 @@ def init(cls, eltype, data, kdims, vdims): "Having a non-string as a column name in a DataFrame is not supported." ) - # Handle reset of index if kdims reference index by name - for kd in kdims: - kd = dimension_name(kd) - if kd in data.columns: - continue - if any(kd == ('index' if name is None else name) - for name in index_names): - data = data.reset_index() - break - - if kdims: + if kdims and not (len(kdims) == len(index_names) and {dimension_name(kd) for kd in kdims} == set(index_names)): kdim = dimension_name(kdims[0]) if eltype._auto_indexable_1d and ncols == 1 and kdim not in data.columns: data = data.copy() @@ -147,31 +133,67 @@ def init(cls, eltype, data, kdims, vdims): raise ValueError('PandasInterface could not find specified dimensions in the data.') else: data = pd.DataFrame(data, columns=columns) - return data, {'kdims':kdims, 'vdims':vdims}, {} - + return data, {'kdims': kdims, 'vdims': vdims}, {} @classmethod def isscalar(cls, dataset, dim): name = dataset.get_dimension(dim, strict=True).name return len(dataset.data[name].unique()) == 1 + @classmethod + def dtype(cls, dataset, dimension): + dim = dataset.get_dimension(dimension, strict=True) + name = dim.name + df = dataset.data + if cls.isindex(dataset, dim): + data = cls.index_values(dataset, dim) + else: + data = df[name] + if util.isscalar(data): + return np.array([data]).dtype + else: + return data.dtype + + @classmethod + def indexes(cls, data): + index_names = data.index.names if isinstance(data, pd.DataFrame) else [data.index.name] + if index_names == [None]: + index_names = ['_index'] if 'index' in data.columns else ['index'] + return index_names + + @classmethod + def isindex(cls, dataset, dimension): + dimension = dataset.get_dimension(dimension, strict=True) + if dimension.name in dataset.data.columns: + return False + return dimension.name in cls.indexes(dataset.data) + + @classmethod + def index_values(cls, dataset, dimension): + dimension = dataset.get_dimension(dimension, strict=True) + index = dataset.data.index + if isinstance(index, pd.MultiIndex): + return index.get_level_values(dimension.name) + return index @classmethod def validate(cls, dataset, vdims=True): dim_types = 'all' if vdims else 'key' dimensions = dataset.dimensions(dim_types, label='name') - cols = list(dataset.data.columns) + cols = list(dataset.data.columns) + cls.indexes(dataset.data) not_found = [d for d in dimensions if d not in cols] if not_found: raise DataError("Supplied data does not contain specified " "dimensions, the following dimensions were " "not found: %s" % repr(not_found), cls) - @classmethod def range(cls, dataset, dimension): dimension = dataset.get_dimension(dimension, strict=True) - column = dataset.data[dimension.name] + if cls.isindex(dataset, dimension): + column = cls.index_values(dataset, dimension) + else: + column = dataset.data[dimension.name] if column.dtype.kind == 'O': if (not isinstance(dataset.data, pd.DataFrame) or util.pandas_version < Version('0.17.0')): @@ -184,6 +206,8 @@ def range(cls, dataset, dimension): pass if not len(column): return np.nan, np.nan + if isinstance(column, pd.Index): + return column[0], column[-1] return column.iloc[0], column.iloc[-1] else: if dimension.nodata is not None: @@ -246,10 +270,9 @@ def groupby(cls, dataset, dimensions, container_type, group_type, **kwargs): @classmethod def aggregate(cls, dataset, dimensions, function, **kwargs): - data = dataset.data cols = [d.name for d in dataset.kdims if d in dimensions] vdims = dataset.dimensions('value', label='name') - reindexed = data[cols+vdims] + reindexed = cls.dframe(dataset, dimensions=cols+vdims) if function in [np.std, np.var]: # Fix for consistency with other backend # pandas uses ddof=1 for std and var @@ -298,9 +321,11 @@ def unpack_scalar(cls, dataset, data): @classmethod def reindex(cls, dataset, kdims=None, vdims=None): - # DataFrame based tables don't need to be reindexed - return dataset.data - + data = dataset.data + if isinstance(data.index, pd.MultiIndex): + kdims = [kdims] if isinstance(kdims, (str, Dimension)) else kdims + data = data.reset_index().set_index(list(map(str, kdims)), drop=True) + return data @classmethod def mask(cls, dataset, mask, mask_value=np.nan): @@ -309,7 +334,6 @@ def mask(cls, dataset, mask, mask_value=np.nan): masked.loc[mask, cols] = mask_value return masked - @classmethod def redim(cls, dataset, dimensions): column_renames = {k: v.name for k, v in dimensions.items()} @@ -327,39 +351,94 @@ def sort(cls, dataset, by=None, reverse=False): return dataset.data.sort(columns=cols, ascending=not reverse) return dataset.data.sort_values(by=cols, ascending=not reverse) + @classmethod + def sorted_index(cls, df): + if hasattr(df.index, 'is_lexsorted'): + return df.index.is_lexsorted() + return df.index.is_monotonic_increasing + + @classmethod + def sort_depth(cls, df): + try: + from pandas.core.indexes.multi import _lexsort_depth + return _lexsort_depth(df.index.codes, df.index.nlevels) + except (ImportError, AttributeError): + return 0 + + @classmethod + def index_selection(cls, df, selection): + indexes = cls.indexes(df) + nindex = len(indexes) + sorted_index = cls.sorted_index(df) + if sorted_index: + depth = df.index.nlevels + else: + depth = cls.sort_depth(df) + index_sel = {} + skip_index = True + for level, idx in enumerate(indexes): + if idx not in selection: + index_sel[idx] = slice(None, None) + continue + skip_index = False + sel = selection[idx] + if isinstance(sel, tuple) and len(sel) < 4: + sel = slice(*sel) + elif not isinstance(sel, (list, slice)): + sel = [sel] + if isinstance(sel, slice) and nindex > 1 and not sorted_index and level>depth: + # If the index is not monotonic we cannot slice + # so return indexer up to the point it is valid + return index_sel + index_sel[idx] = sel + return {} if skip_index else index_sel @classmethod def select(cls, dataset, selection_mask=None, **selection): df = dataset.data if selection_mask is None: - selection_mask = cls.select_mask(dataset, selection) + if index_sel:= cls.index_selection(df, selection): + try: + if len(index_sel) == 1: + df = df[next(iter(index_sel.values()))] + else: + df = df.loc[tuple(index_sel.values()), :] + except KeyError: + # If index lookup fails we fall back to boolean indexing + index_sel = {} + column_sel = {k: v for k, v in selection.items() if k not in index_sel} + if column_sel: + selection_mask = cls.select_mask(dataset, column_sel) indexed = cls.indexed(dataset, selection) if isinstance(selection_mask, pd.Series): df = df[selection_mask] - else: + elif selection_mask is not None: df = df.iloc[selection_mask] if indexed and len(df) == 1 and len(dataset.vdims) == 1: return df[dataset.vdims[0].name].iloc[0] return df - @classmethod def values( - cls, - dataset, - dim, - expanded=True, - flat=True, - compute=True, - keep_index=False, + cls, + dataset, + dim, + expanded=True, + flat=True, + compute=True, + keep_index=False, ): dim = dataset.get_dimension(dim, strict=True) - data = dataset.data[dim.name] + isindex = cls.isindex(dataset, dim) + if isindex: + data = cls.index_values(dataset, dim) + else: + data = dataset.data[dim.name] if keep_index: return data if data.dtype.kind == 'M' and getattr(data.dtype, 'tz', None): - data = data.dt.tz_localize(None) + data = (data if isindex else data.dt).tz_localize(None) if not expanded: return pd.unique(data) return data.values if hasattr(data, 'values') else data @@ -405,38 +484,53 @@ def as_dframe(cls, dataset): if it already a dataframe type. """ if issubclass(dataset.interface, PandasInterface): + if any(cls.isindex(dataset, dim) for dim in dataset.dimensions()): + return dataset.data.reset_index() return dataset.data else: return dataset.dframe() - @classmethod def dframe(cls, dataset, dimensions): + data = dataset.data if dimensions: - return dataset.data[dimensions] + if any(cls.isindex(dataset, d) for d in dimensions): + data = data.reset_index() + return data[dimensions] else: - return dataset.data.copy() - + return data.copy() @classmethod def iloc(cls, dataset, index): rows, cols = index scalar = False - columns = list(dataset.data.columns) if isinstance(cols, slice): cols = [d.name for d in dataset.dimensions()][cols] elif np.isscalar(cols): scalar = np.isscalar(rows) - cols = [dataset.get_dimension(cols).name] + dim = dataset.get_dimension(cols) + if dim is None: + raise ValueError('column is out of bounds') + cols = [dim.name] else: - cols = [dataset.get_dimension(d).name for d in index[1]] - cols = [columns.index(c) for c in cols] + cols = [dataset.get_dimension(d).name for d in cols] if np.isscalar(rows): rows = [rows] + data = dataset.data + indexes = cls.indexes(data) + columns = list(data.columns) + id_cols = [columns.index(c) for c in cols if c not in indexes] + if not id_cols: + if len(indexes) > 1: + data = data.index.to_frame()[cols].iloc[rows].reset_index(drop=True) + data = data.values.ravel()[0] if scalar else data + else: + data = data.index.values[rows[0]] if scalar else data.index[rows] + return data if scalar: - return dataset.data.iloc[rows[0], cols[0]] - return dataset.data.iloc[rows, cols] + return data.iloc[rows[0], id_cols[0]] + return data.iloc[rows, id_cols] Interface.register(PandasInterface) diff --git a/holoviews/tests/core/data/test_pandasinterface.py b/holoviews/tests/core/data/test_pandasinterface.py index 415ec6acce..bb2e520d7e 100644 --- a/holoviews/tests/core/data/test_pandasinterface.py +++ b/holoviews/tests/core/data/test_pandasinterface.py @@ -1,5 +1,6 @@ import numpy as np import pandas as pd +import pytest from holoviews.core.data import Dataset from holoviews.core.data.interface import DataError @@ -163,6 +164,11 @@ def test_dataset_with_interface_column(self): ds = Dataset(df) self.assertEqual(list(ds.data.columns), ['interface']) + def test_dataset_range_with_object_index(self): + df = pd.DataFrame(range(4), columns=["values"], index=list("BADC")) + ds = Dataset(df, kdims='index') + assert ds.range('index') == ('A', 'D') + class PandasInterfaceTests(BasePandasInterfaceTests): @@ -177,3 +183,211 @@ def test_data_with_tz(self): df = pd.DataFrame({"dates": dates_tz}) data = Dataset(df).dimension_values("dates") np.testing.assert_equal(dates, data) + + @pytest.mark.xfail(reason="Breaks hvplot") + def test_reindex(self): + ds = Dataset(pd.DataFrame({'x': np.arange(10), 'y': np.arange(10), 'z': np.random.rand(10)})) + df = ds.interface.reindex(ds, ['x']) + assert df.index.names == ['x'] + df = ds.interface.reindex(ds, ['y']) + assert df.index.names == ['y'] + + +class PandasInterfaceMultiIndex(HeterogeneousColumnTests, InterfaceTests): + datatype = 'dataframe' + data_type = pd.DataFrame + + __test__ = True + + def setUp(self): + frame = pd.DataFrame({"number": [1, 1, 2, 2], "color": ["red", "blue", "red", "blue"]}) + index = pd.MultiIndex.from_frame(frame, names=("number", "color")) + self.df = pd.DataFrame(range(4), index=index, columns=["values"]) + super().setUp() + + def test_lexsort_depth_import(self): + # Indexing relies on knowing the lexsort_depth but this is a + # private import so we want to know should this import ever + # be changed + from pandas.core.indexes.multi import _lexsort_depth # noqa + + def test_no_kdims(self): + ds = Dataset(self.df) + assert ds.kdims == [Dimension("values")] + assert isinstance(ds.data.index, pd.MultiIndex) + + def test_index_kdims(self): + ds = Dataset(self.df, kdims=["number", "color"]) + assert ds.kdims == [Dimension("number"), Dimension("color")] + assert ds.vdims == [Dimension("values")] + assert isinstance(ds.data.index, pd.MultiIndex) + + def test_index_aggregate(self): + ds = Dataset(self.df, kdims=["number", "color"]) + expected = pd.DataFrame({'number': [1, 2], 'values': [0.5, 2.5], 'values_var': [0.25, 0.25]}) + agg = ds.aggregate("number", function=np.mean, spreadfn=np.var) + pd.testing.assert_frame_equal(agg.data, expected) + + def test_index_select_monotonic(self): + ds = Dataset(self.df, kdims=["number", "color"]) + selected = ds.select(number=1) + expected = pd.DataFrame({'color': ['red', 'blue'], 'values': [0, 1], 'number': [1, 1]}).set_index(['number', 'color']) + assert isinstance(selected.data.index, pd.MultiIndex) + pd.testing.assert_frame_equal(selected.data, expected) + + def test_index_select(self): + ds = Dataset(self.df, kdims=["number", "color"]) + selected = ds.select(number=1) + expected = pd.DataFrame({'color': ['red', 'blue'], 'values': [0, 1], 'number': [1, 1]}).set_index(['number', 'color']) + assert isinstance(selected.data.index, pd.MultiIndex) + pd.testing.assert_frame_equal(selected.data, expected) + + def test_index_select_all_indexes(self): + ds = Dataset(self.df, kdims=["number", "color"]) + selected = ds.select(number=1, color='red') + assert selected == 0 + + def test_index_select_all_indexes_lists(self): + ds = Dataset(self.df, kdims=["number", "color"]) + selected = ds.select(number=[1], color=['red']) + expected = pd.DataFrame({'color': ['red'], 'values': [0], 'number': [1]}).set_index(['number', 'color']) + assert isinstance(selected.data.index, pd.MultiIndex) + pd.testing.assert_frame_equal(selected.data, expected) + + def test_index_select_all_indexes_slice_and_scalar(self): + ds = Dataset(self.df, kdims=["number", "color"]) + selected = ds.select(number=(0, 1), color='red') + expected = pd.DataFrame({'color': ['red'], 'values': [0], 'number': [1]}).set_index(['number', 'color']) + assert isinstance(selected.data.index, pd.MultiIndex) + pd.testing.assert_frame_equal(selected.data, expected) + + def test_iloc_scalar_scalar_only_index(self): + ds = Dataset(self.df, kdims=["number", "color"]) + selected = ds.iloc[0, 0] + expected = 1 + assert selected == expected + + def test_iloc_slice_scalar_only_index(self): + ds = Dataset(self.df, kdims=["number", "color"]) + selected = ds.iloc[:, 0] + expected = self.df.reset_index()[["number"]] + pd.testing.assert_frame_equal(selected.data, expected) + + def test_iloc_slice_slice_only_index(self): + ds = Dataset(self.df, kdims=["number", "color"]) + selected = ds.iloc[:, :2] + expected = self.df.reset_index()[["number", "color"]] + pd.testing.assert_frame_equal(selected.data, expected) + + def test_iloc_scalar_slice_only_index(self): + ds = Dataset(self.df, kdims=["number", "color"]) + selected = ds.iloc[0, :2] + expected = pd.DataFrame({"number": 1, "color": "red"}, index=[0]) + pd.testing.assert_frame_equal(selected.data, expected) + + def test_iloc_scalar_scalar(self): + ds = Dataset(self.df, kdims=["number", "color"]) + selected = ds.iloc[0, 2] + expected = 0 + assert selected == expected + + def test_iloc_slice_scalar(self): + ds = Dataset(self.df, kdims=["number", "color"]) + selected = ds.iloc[:, 2] + expected = self.df.iloc[:, [0]] + pd.testing.assert_frame_equal(selected.data, expected) + + def test_iloc_slice_slice(self): + ds = Dataset(self.df, kdims=["number", "color"]) + selected = ds.iloc[:, :3] + expected = self.df.iloc[:, [0]] + pd.testing.assert_frame_equal(selected.data, expected) + + def test_iloc_scalar_slice(self): + ds = Dataset(self.df, kdims=["number", "color"]) + selected = ds.iloc[0, :3] + expected = self.df.iloc[[0], [0]] + pd.testing.assert_frame_equal(selected.data, expected) + + def test_out_of_bounds(self): + ds = Dataset(self.df, kdims=["number", "color"]) + with pytest.raises(ValueError, match="column is out of bounds"): + ds.iloc[0, 3] + + def test_sort(self): + ds = Dataset(self.df, kdims=["number", "color"]) + sorted_ds = ds.sort("color") + np.testing.assert_array_equal(sorted_ds.dimension_values("values"), [1, 3, 0, 2]) + np.testing.assert_array_equal(sorted_ds.dimension_values("number"), [1, 2, 1, 2]) + + def test_select_monotonic(self): + ds = Dataset(self.df.sort_index(), kdims=["number", "color"]) + selected = ds.select(color="red") + pd.testing.assert_frame_equal(selected.data, self.df.iloc[[0, 2], :]) + + selected = ds.select(number=1, color='red') + assert selected == 0 + + def test_select_not_monotonic(self): + frame = pd.DataFrame({"number": [1, 1, 2, 2], "color": [2, 1, 2, 1]}) + index = pd.MultiIndex.from_frame(frame, names=frame.columns) + df = pd.DataFrame(range(4), index=index, columns=["values"]) + ds = Dataset(df, kdims=list(frame.columns)) + + data = ds.select(color=slice(2, 3)).data + expected = pd.DataFrame({"number": [1, 2], "color": [2, 2], "values": [0, 2]}).set_index(['number', 'color']) + pd.testing.assert_frame_equal(data, expected) + + def test_select_not_in_index(self): + ds = Dataset(self.df, kdims=["number", "color"]) + selected = ds.select(number=[2, 3]) + expected = self.df.loc[[2]] + pd.testing.assert_frame_equal(selected.data, expected) + + def test_sample(self): + ds = Dataset(self.df, kdims=["number", "color"]) + sample = ds.interface.sample(ds, [1]) + assert sample.to_dict() == {'values': {(1, 'blue'): 1}} + + self.df.iloc[0, 0] = 1 + ds = Dataset(self.df, kdims=["number", "color"]) + sample = ds.interface.sample(ds, [1]) + assert sample.to_dict() == {'values': {(1, 'red'): 1, (1, 'blue'): 1}} + + def test_values(self): + ds = Dataset(self.df, kdims=["number", "color"]) + assert (ds.interface.values(ds, 'color') == ['red', 'blue', 'red', 'blue']).all() + assert (ds.interface.values(ds, 'number') == [1, 1, 2, 2]).all() + assert (ds.interface.values(ds, 'values') == [0, 1, 2, 3]).all() + + def test_reindex(self): + ds = Dataset(self.df, kdims=["number", "color"]) + df = ds.interface.reindex(ds, ['number', 'color']) + assert df.index.names == ['number', 'color'] + + df = ds.interface.reindex(ds, ['number']) + assert df.index.names == ['number'] + + df = ds.interface.reindex(ds, ['values']) + assert df.index.names == ['values'] + + def test_groupby_one_index(self): + ds = Dataset(self.df, kdims=["number", "color"]) + grouped = ds.groupby("number") + assert list(grouped.keys()) == [1, 2] + for k, v in grouped.items(): + pd.testing.assert_frame_equal(v.data, ds.select(number=k).data) + + def test_groupby_two_indexes(self): + ds = Dataset(self.df, kdims=["number", "color"]) + grouped = ds.groupby(["number", "color"]) + assert list(grouped.keys()) == list(self.df.index) + for k, v in grouped.items(): + pd.testing.assert_frame_equal(v.data, ds.select(number=[k[0]], color=[k[1]]).data) + + def test_groupby_one_index_one_column(self): + ds = Dataset(self.df, kdims=["number", "color"]) + grouped = ds.groupby('values') + assert list(grouped.keys()) == [0, 1, 2, 3] + for k, v in grouped.items(): + pd.testing.assert_frame_equal(v.data, ds.select(values=k).data) diff --git a/holoviews/tests/operation/test_downsample.py b/holoviews/tests/operation/test_downsample.py index 066a0dd6b8..3ca6a4c929 100644 --- a/holoviews/tests/operation/test_downsample.py +++ b/holoviews/tests/operation/test_downsample.py @@ -49,8 +49,6 @@ def _compute_mask(self, element): assert runs[0] == 1 -# Should be fixed when https://github.com/holoviz/holoviews/pull/6061 is merged -@pytest.mark.xfail(reason="This will make a copy of the data") def test_downsample1d_shared_data_index(): runs = [0] From 4727470006540e99a74ddf05315490cf6529d9d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Fri, 19 Apr 2024 17:29:48 +0200 Subject: [PATCH 13/43] Switch to Pixi for development / CI and hatchling for build system (#6182) --- .github/workflows/build.yaml | 44 +++--- .github/workflows/docs.yaml | 36 ++--- .github/workflows/nightly_lock.yaml | 14 ++ .github/workflows/test.yaml | 139 ++++++----------- .gitignore | 3 +- MANIFEST.in | 13 -- conda.recipe/README.md | 14 -- conda.recipe/meta.yaml | 46 ------ doc/conf.py | 2 + doc/developer_guide/index.md | 190 ++++++++++++++++++++++++ doc/index.rst | 1 + dodo.py | 15 -- holoviews/__init__.py | 58 ++++---- holoviews/__version.py | 44 ++++++ holoviews/tests/util/test_utils.py | 4 +- pixi.toml | 160 ++++++++++++++++++++ pyproject.toml | 76 +++++++++- scripts/build_conda.sh | 14 -- scripts/conda/build.sh | 21 +++ scripts/conda/recipe/meta.yaml | 47 ++++++ scripts/download_data.py | 13 ++ scripts/download_data.sh | 17 --- setup.cfg | 9 -- setup.py | 223 ---------------------------- tox.ini | 67 --------- 25 files changed, 677 insertions(+), 593 deletions(-) create mode 100644 .github/workflows/nightly_lock.yaml delete mode 100644 MANIFEST.in delete mode 100644 conda.recipe/README.md delete mode 100644 conda.recipe/meta.yaml create mode 100644 doc/developer_guide/index.md delete mode 100644 dodo.py create mode 100644 holoviews/__version.py create mode 100644 pixi.toml delete mode 100755 scripts/build_conda.sh create mode 100755 scripts/conda/build.sh create mode 100644 scripts/conda/recipe/meta.yaml create mode 100644 scripts/download_data.py delete mode 100755 scripts/download_data.sh delete mode 100644 setup.cfg delete mode 100644 setup.py delete mode 100644 tox.ini diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 519e489f61..2e7770ca24 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -15,7 +15,6 @@ defaults: shell: bash -el {0} env: - SETUPTOOLS_ENABLE_FEATURES: "legacy-editable" PYTHON_VERSION: "3.11" PACKAGE: "holoviews" @@ -30,31 +29,28 @@ jobs: steps: - run: echo "All builds have finished, have been approved, and ready to publish" + pixi_lock: + name: Pixi lock + runs-on: ubuntu-latest + steps: + - uses: holoviz-dev/holoviz_tasks/pixi_lock@pixi + conda_build: name: Build Conda + needs: [pixi_lock] runs-on: "ubuntu-latest" steps: - - uses: actions/checkout@v4 + - uses: holoviz-dev/holoviz_tasks/pixi_install@pixi with: - fetch-depth: "100" - - name: Fetch unshallow - run: git fetch --prune --tags --unshallow -f - - uses: conda-incubator/setup-miniconda@v3 - with: - miniconda-version: "latest" - - name: conda setup - run: | - # pyct is for running setup.py - conda install -y conda-build build pyct -c pyviz/label/dev + environments: "build" + download-data: false - name: conda build - run: | - source ./scripts/build_conda.sh - echo "CONDA_FILE="$CONDA_PREFIX/conda-bld/noarch/$PACKAGE-$VERSION-py_0.tar.bz2"" >> $GITHUB_ENV + run: pixi run -e build build-conda - uses: actions/upload-artifact@v4 if: always() with: name: conda - path: ${{ env.CONDA_FILE }} + path: dist/*tar.bz2 if-no-files-found: error conda_publish: @@ -88,21 +84,15 @@ jobs: pip_build: name: Build PyPI + needs: [pixi_lock] runs-on: "ubuntu-latest" steps: - - uses: actions/checkout@v4 - with: - fetch-depth: "100" - - name: Fetch unshallow - run: git fetch --prune --tags --unshallow -f - - uses: actions/setup-python@v5 + - uses: holoviz-dev/holoviz_tasks/pixi_install@pixi with: - python-version: ${{ env.PYTHON_VERSION }} - - name: Install build - run: | - python -m pip install build + environments: "build" + download-data: false - name: Build package - run: python -m build . + run: pixi run -e build build-pip - uses: actions/upload-artifact@v4 if: always() with: diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml index 578df685c2..d3d8a607f2 100644 --- a/.github/workflows/docs.yaml +++ b/.github/workflows/docs.yaml @@ -21,8 +21,15 @@ on: - cron: "0 14 * * SUN" jobs: + pixi_lock: + name: Pixi lock + runs-on: ubuntu-latest + steps: + - uses: holoviz-dev/holoviz_tasks/pixi_lock@pixi + build_docs: name: Documentation + needs: [pixi_lock] runs-on: "ubuntu-latest" timeout-minutes: 120 defaults: @@ -39,35 +46,16 @@ jobs: PANEL_EMBED: "true" PANEL_EMBED_JSON: "true" PANEL_EMBED_JSON_PREFIX: "json" + DASK_DATAFRAME__QUERY_PLANNING: false steps: - - uses: holoviz-dev/holoviz_tasks/install@v0 + - uses: holoviz-dev/holoviz_tasks/pixi_install@pixi with: - name: Documentation - python-version: "3.10" - channel-priority: flexible - channels: pyviz/label/dev,conda-forge,nodefaults - envs: "-o doc" - cache: true - conda-update: true + environments: docs - name: Set output id: vars run: echo "tag=${GITHUB_REF#refs/*/}" >> $GITHUB_OUTPUT - - name: Download data - run: | - conda activate test-environment - bash scripts/download_data.sh - - name: generate rst - run: | - conda activate test-environment - nbsite generate-rst --org holoviz --project-name holoviews - - name: refmanual - run: | - conda activate test-environment - python ./doc/generate_modules.py holoviews -d ./doc/reference_manual -n holoviews -e tests - - name: build docs - run: | - conda activate test-environment - nbsite build --what=html --output=builtdocs --org holoviz --project-name holoviews + - name: Build documentation + run: pixi run -e docs docs-build - name: upload dev if: | (github.event_name == 'workflow_dispatch' && github.event.inputs.target == 'dev') || diff --git a/.github/workflows/nightly_lock.yaml b/.github/workflows/nightly_lock.yaml new file mode 100644 index 0000000000..9d3379af2b --- /dev/null +++ b/.github/workflows/nightly_lock.yaml @@ -0,0 +1,14 @@ +name: nightly_lock +on: + workflow_dispatch: + schedule: + - cron: "0 0 * * *" + +jobs: + pixi_lock: + name: Pixi lock + runs-on: ubuntu-latest + steps: + - uses: holoviz-dev/holoviz_tasks/pixi_lock@pixi + + # TODO: Upload the lock-file diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 57e3226018..ef11a3ada8 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -33,17 +33,10 @@ defaults: shell: bash -el {0} env: - SETUPTOOLS_ENABLE_FEATURES: "legacy-editable" DISPLAY: ":99.0" PYTHONIOENCODING: "utf-8" GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - OMP_NUM_THREADS: 1 - OPENBLAS_NUM_THREADS: 1 - MKL_NUM_THREADS: 1 - VECLIB_MAXIMUM_THREADS: 1 - NUMEXPR_NUM_THREADS: 1 - NUMBA_NUM_THREADS: 1 - PYDEVD_DISABLE_FILE_VALIDATION: 1 + DASK_DATAFRAME__QUERY_PLANNING: false jobs: pre_commit: @@ -61,7 +54,7 @@ jobs: code_change: ${{ steps.filter.outputs.code }} matrix: ${{ env.MATRIX }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 if: github.event_name != 'pull_request' - name: Check for code changes uses: dorny/paths-filter@v3 @@ -71,7 +64,7 @@ jobs: code: - 'holoviews/**' - 'examples/**' - - 'setup.py' + - 'pixi.toml' - 'pyproject.toml' - '.github/workflows/test.yaml' - name: Set matrix option @@ -90,16 +83,16 @@ jobs: if: env.MATRIX_OPTION == 'default' run: | MATRIX=$(jq -nsc '{ - "os": ["ubuntu-latest", "macos-latest", "windows-latest"], - "python-version": ["3.9", "3.12"] + "os": ["ubuntu-latest", "macos-14", "windows-latest"], + "environment": ["test-39", "test-312"] }') echo "MATRIX=$MATRIX" >> $GITHUB_ENV - name: Set test matrix with 'full' option if: env.MATRIX_OPTION == 'full' run: | MATRIX=$(jq -nsc '{ - "os": ["ubuntu-latest", "macos-latest", "windows-latest"], - "python-version": ["3.9", "3.10", "3.11", "3.12"] + "os": ["ubuntu-latest", "macos-14", "windows-latest"], + "environment": ["test-39", "test-310", "test311", "test312"] }') echo "MATRIX=$MATRIX" >> $GITHUB_ENV - name: Set test matrix with 'downstream' option @@ -107,136 +100,94 @@ jobs: run: | MATRIX=$(jq -nsc '{ "os": ["ubuntu-latest"], - "python-version": ["3.11"] + "environment": ["test-311"] }') echo "MATRIX=$MATRIX" >> $GITHUB_ENV + pixi_lock: + name: Pixi lock + runs-on: ubuntu-latest + steps: + - uses: holoviz-dev/holoviz_tasks/pixi_lock@pixi + with: + cache: ${{ github.event.inputs.cache == 'true' || github.event.inputs.cache == '' }} + unit_test_suite: - name: Unit tests on Python ${{ matrix.python-version }}, ${{ matrix.os }} - needs: [pre_commit, setup] + name: unit:${{ matrix.environment }}:${{ matrix.os }} + needs: [pre_commit, setup, pixi_lock] runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: ${{ fromJson(needs.setup.outputs.matrix) }} timeout-minutes: 120 - env: - DESC: "Python ${{ matrix.python-version }}, ${{ matrix.os }} unit tests" - PYTHON_VERSION: ${{ matrix.python-version }} steps: - - uses: holoviz-dev/holoviz_tasks/install@v0 + - uses: holoviz-dev/holoviz_tasks/pixi_install@pixi if: needs.setup.outputs.code_change == 'true' with: - name: unit_test_suite - python-version: ${{ matrix.python-version }} - channel-priority: flexible - channels: pyviz/label/dev,conda-forge,nodefaults - envs: "-o flakes -o tests -o examples_tests -o tests_ci" - cache: ${{ github.event.inputs.cache || github.event.inputs.cache == '' }} - conda-update: true - id: install + environments: ${{ matrix.environment }} - name: Check packages latest version if: needs.setup.outputs.code_change == 'true' run: | - conda activate test-environment - python scripts/check_latest_packages.py bokeh panel param datashader - - name: Download data - if: needs.setup.outputs.code_change == 'true' - run: | - conda activate test-environment - bash scripts/download_data.sh - - name: doit test_unit + pixi run -e ${{ matrix.environment }} check-latest-packages bokeh panel param datashader + - name: Test Unit if: needs.setup.outputs.code_change == 'true' run: | - conda activate test-environment - doit test_unit - - name: test examples + pixi run -e ${{ matrix.environment }} test-unit --cov=./holoviews --cov-report=xml + - name: Test Examples if: needs.setup.outputs.code_change == 'true' run: | - conda activate test-environment - doit test_examples - - name: codecov + pixi run -e ${{ matrix.environment }} test-example + - uses: codecov/codecov-action@v4 if: needs.setup.outputs.code_change == 'true' - run: | - conda activate test-environment - codecov + with: + token: ${{ secrets.CODECOV_TOKEN }} ui_test_suite: - name: UI tests on Python ${{ matrix.python-version }}, ${{ matrix.os }} - needs: [pre_commit, setup] + name: ui:${{ matrix.environment }}:${{ matrix.os }} + needs: [pre_commit, setup, pixi_lock] runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: os: ["ubuntu-latest"] - python-version: ["3.9"] + environment: ["test-ui"] timeout-minutes: 60 env: - DESC: "Python ${{ matrix.python-version }}, ${{ matrix.os }} UI tests" PANEL_LOG_LEVEL: info - # Without this env var `doit env_create ...` uses by default - # the `pyviz` channel, except that we don't want to configure - # it as one of the sources. - PYCTDEV_SELF_CHANNEL: "pyviz/label/dev" steps: - - uses: holoviz-dev/holoviz_tasks/install@v0 + - uses: holoviz-dev/holoviz_tasks/pixi_install@pixi if: needs.setup.outputs.code_change == 'true' with: - name: ui_test_suite - python-version: ${{ matrix.python-version }} - channels: pyviz/label/dev,conda-forge,nodefaults - envs: "-o recommended -o tests -o build -o tests_ci" - cache: ${{ github.event.inputs.cache || github.event.inputs.cache == '' }} - playwright: true - id: install - - name: doit test_ui + environments: ${{ matrix.environment }} + - name: Test UI if: needs.setup.outputs.code_change == 'true' run: | - conda activate test-environment - doit test_ui - - name: Upload coverage to Codecov + pixi run -e ${{ matrix.environment }} test-ui --cov=./holoviews --cov-report=xml + - uses: codecov/codecov-action@v4 if: needs.setup.outputs.code_change == 'true' - uses: codecov/codecov-action@v3 with: - files: ./coverage.xml - flags: ui-tests - fail_ci_if_error: false # optional (default = false) + token: ${{ secrets.CODECOV_TOKEN }} core_test_suite: - name: Core tests on Python ${{ matrix.python-version }}, ${{ matrix.os }} - needs: [pre_commit, setup] + name: core:${{ matrix.environment }}:${{ matrix.os }} + needs: [pre_commit, setup, pixi_lock] runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: os: ["ubuntu-latest"] - python-version: ["3.12"] + environment: ["test-core"] timeout-minutes: 120 - env: - DESC: "Python ${{ matrix.python-version }}, ${{ matrix.os }} core tests" - PYTHON_VERSION: ${{ matrix.python-version }} steps: - - uses: holoviz-dev/holoviz_tasks/install@v0 + - uses: holoviz-dev/holoviz_tasks/pixi_install@pixi if: needs.setup.outputs.code_change == 'true' with: - name: core_test_suite - python-version: ${{ matrix.python-version }} - # channel-priority: strict - channels: pyviz/label/dev,conda-forge,nodefaults - envs: "-o tests_core -o tests_ci" - cache: ${{ github.event.inputs.cache || github.event.inputs.cache == '' }} - id: install - - name: Download data - if: needs.setup.outputs.code_change == 'true' - run: | - conda activate test-environment - bash scripts/download_data.sh + environments: ${{ matrix.environment }} - name: Check packages latest version if: needs.setup.outputs.code_change == 'true' run: | - conda activate test-environment - python scripts/check_latest_packages.py numpy pandas bokeh panel param - - name: doit test_unit + pixi run -e ${{ matrix.environment }} check-latest-packages numpy pandas bokeh panel param + - name: Test Unit if: needs.setup.outputs.code_change == 'true' run: | - conda activate test-environment - pytest holoviews + pixi run -e ${{ matrix.environment }} test-unit diff --git a/.gitignore b/.gitignore index a75807cd77..ae3111fef6 100644 --- a/.gitignore +++ b/.gitignore @@ -20,13 +20,14 @@ .ipynb_checkpoints .coverage .pytest_cache +.pixi +holoviews/_version.py /release /doc/Tutorials-WIP/*.ipynb .idea .vscode holoviews.rc -/examples/assets/ ghostdriver.log holoviews/.version .dir-locals.el diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 00ee15d10f..0000000000 --- a/MANIFEST.in +++ /dev/null @@ -1,13 +0,0 @@ -include README.rst -include LICENSE.txt -include CHANGELOG.md -include holoviews/.version -include holoviews/ipython/*.html -include holoviews/plotting/mpl/*.mplstyle -include holoviews/tests/ipython/notebooks/*.ipynb -global-exclude *.py[co] -global-exclude __pycache__ -global-exclude *~ -global-exclude *.ipynb_checkpoints/* -graft examples -graft holoviews/examples diff --git a/conda.recipe/README.md b/conda.recipe/README.md deleted file mode 100644 index 35638017b0..0000000000 --- a/conda.recipe/README.md +++ /dev/null @@ -1,14 +0,0 @@ -## Release Procedure - -- Ensure all tests pass. - -- Tag commit a PEP440 style tag (starting with the prefix 'v') and push to github - -```bash -git tag -a vx.x.x -m 'Version x.x.x' -git push --tags -``` - -Example tags might include v1.9.3 v1.10.0a1 or v1.11.3b3 - -- Build conda packages diff --git a/conda.recipe/meta.yaml b/conda.recipe/meta.yaml deleted file mode 100644 index 087efe849e..0000000000 --- a/conda.recipe/meta.yaml +++ /dev/null @@ -1,46 +0,0 @@ -{% set sdata = load_setup_py_data(setup_file="../setup.py", from_recipe_dir=True) %} - -package: - name: {{ sdata['name'] }} - version: {{ VERSION }} - -source: - url: ../dist/{{ sdata['name'] }}-{{ VERSION }}-py3-none-any.whl - -build: - noarch: python - script: {{ PYTHON }} -m pip install -vv {{ sdata['name'] }}-{{ VERSION }}-py3-none-any.whl - entry_points: - {% for group,epoints in sdata.get("entry_points",{}).items() %} - {% for entry_point in epoints %} - - {{ entry_point }} - {% endfor %} - {% endfor %} - -requirements: - build: - - python {{ sdata['python_requires'] }} - {% for dep in sdata['extras_require']['build'] %} - - {{ dep }} - {% endfor %} - run: - - python {{ sdata['python_requires'] }} - {% for dep in sdata.get('install_requires',{}) %} - - {{ dep }} - {% endfor %} - {% for dep in sdata['extras_require']['recommended'] %} - - {{ dep }} - {% endfor %} - -test: - imports: - - {{ sdata['name'] }} - commands: - - pip check - requires: - - pip - -about: - home: https://holoviews.org - summary: Stop plotting your data - annotate your data and let it visualize itself. - license: BSD 3-Clause diff --git a/doc/conf.py b/doc/conf.py index 63fe74c028..1c2cb85a52 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -61,6 +61,8 @@ 'nbsite.analytics', ] +myst_enable_extensions = ["colon_fence", "deflist"] + nbsite_analytics = { 'goatcounter_holoviz': True, } diff --git a/doc/developer_guide/index.md b/doc/developer_guide/index.md new file mode 100644 index 0000000000..c25212997e --- /dev/null +++ b/doc/developer_guide/index.md @@ -0,0 +1,190 @@ +# Setting up a development environment + +The HoloViews library is a project that provides a wide range of data interfaces and an extensible set of plotting backends, which means the development and testing process involves a broad set of libraries. + +This guide describes how to install and configure development environments. + +If you have any problems with the steps here, please reach out in the `dev` channel on [Discord](https://discord.gg/rb6gPXbdAr) or on [Discourse](https://discourse.holoviz.org/). + +## Preliminaries + +### Basic understanding of how to contribute to Open Source + +If this is your first open-source contribution, please study one +or more of the below resources. + +- [How to Get Started with Contributing to Open Source | Video](https://youtu.be/RGd5cOXpCQw) +- [Contributing to Open-Source Projects as a New Python Developer | Video](https://youtu.be/jTTf4oLkvaM) +- [How to Contribute to an Open Source Python Project | Blog post](https://www.educative.io/blog/contribue-open-source-python-project) + +### Git + +The HoloViews source code is stored in a [Git](https://git-scm.com) source control repository. The first step to working on HoloViews is to install Git onto your system. There are different ways to do this, depending on whether you use Windows, Mac, or Linux. + +To install Git on any platform, refer to the [Installing Git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) section of the [Pro Git Book](https://git-scm.com/book/en/v2). + +To contribute to HoloViews, you will also need [Github account](https://github.com/join) and knowledge of the [_fork and pull request workflow_](https://docs.github.com/en/get-started/quickstart/contributing-to-projects). + +### Pixi + +Developing all aspects of HoloViews requires a wide range of packages in different environments. To make this more manageable, Pixi manages the developer experience. To install Pixi, follow [this guide](https://prefix.dev/docs/pixi/overview#installation). + +#### Glossary + +- Tasks: A task is what can be run with `pixi run `. Tasks can be anything from installing packages to running tests. +- Environments: An environment is a set of packages installed in a virtual environment. Each environment has a name; you can run tasks in a specific environment with the `-e` flag. For example, `pixi run -e test-core test-unit` will run the `test-unit` task in the `test-core` environment. +- Lock-file: A lock-file is a file that contains all the information about the environments. + +For more information, see the [Pixi documentation](https://pixi.sh/latest/). + +:::{admonition} Note +:class: info + +The first time you run `pixi`, it will create a `.pixi` directory in the source directory. This directory will contain all the files needed for the virtual environments. The `.pixi` directory can be large, so don't accidentally put it into a cloud-synced directory. +::: + +## Installing the Project + +### Cloning the Project + +The source code for the HoloViews project is hosted on [GitHub](https://github.com/holoviz/holoviews). The first thing you need to do is clone the repository. + +1. Go to [github.com/holoviz/holoviews](https://github.com/holoviz/holoviews) +2. [Fork the repository](https://docs.github.com/en/get-started/quickstart/contributing-to-projects#forking-a-repository) +3. Run in your terminal: `git clone https://github.com//holoviews` + +The instructions for cloning above created a `holoviews` directory at your file system location. This `holoviews` directory is the _source checkout_ for the remainder of this document, and your current working directory is this directory. + +### Fetch tags from upstream + +The version number of the package depends on [`git tags`](https://git-scm.com/book/en/v2/Git-Basics-Tagging), so you need to fetch the tags from the upstream repository: + +```bash +git remote add upstream https://github.com/holoviz/holoviews.git +git fetch --tags upstream +git push --tags +``` + +## Start developing + +To start developing, run the following command + +```bash +pixi install +``` + +The first time you run it, it will create a `lock-file` with information for all available environments. This command will take a minute or so to run. When this is finished, it is possible to run the following command to download the data HoloViews tests and examples depend upon. + +```bash +pixi run download-data +``` + +### Editable install + +It can be advantageous to install the HoloViews in [editable mode](https://pip.pypa.io/en/stable/topics/local-project-installs/#editable-installs): + +```bash +pixi run install +``` + +:::{admonition} Note +:class: info + +Currently, this needs to be run for each environment. So, if you want to install in the `test-ui` environment, you can add `--environment` / `-e` to the command: + +```bash +pixi run -e test-ui install +``` + +::: + +## Linting + +HoloViews uses [pre-commit](https://pre-commit.com/) to apply linting to HoloViews code. Linting can be run for all the files with: + +```bash +pixi run lint +``` + +Linting can also be set up to run automatically with each commit; this is the recommended way because if linting is not passing, the [Continuous Integration](https://en.wikipedia.org/wiki/Continuous_integration) (CI) will also fail. + +```bash +pixi run lint-install +``` + +## Testing + +To help keep HoloViews maintainable, all Pull Requests (PR) with code changes should typically be accompanied by relevant tests. While exceptions may be made for specific circumstances, the default assumption should be that a Pull Request without tests will not be merged. + +There are three types of tasks and five environments related to tests. + +### Unit tests + +Unit tests are usually small tests executed with [pytest](https://docs.pytest.org). They can be found in `holoviews/tests/`. +Unit tests can be run with the `test-unit` task: + +```bash +pixi run test-unit +``` + +The task is available in the following environments: `test-39`, `test-310`, `test-311`, `test-312`, and `test-core`. Where the first ones have the same environments except for different Python versions, and `test-core` only has a core set of dependencies. + +If you haven't set the environment flag in the command, you need to select which one of the environments to use. + +### Example tests + +HoloViews's documentation consists mainly of Jupyter Notebooks. The example tests execute all the notebooks and fail if an error is raised. Example tests are possible thanks to [nbval](https://nbval.readthedocs.io/) and can be found in the `examples/` folder. +Example tests can be run with the following command: + +```bash +pixi run test-example +``` + +This task has the same environments as the unit tests except for `test-core`. + +### UI tests + +HoloViews provides web components that users can interact with through the browser. UI tests allow checking that these components get displayed as expected and that the backend <-> front-end bi-communication works correctly. UI tests are possible thanks to [Playwright](https://playwright.dev/python/). +The test can be found in the `holoviews/tests/ui/` folder. +UI tests can be run with the following task. This task is only available in the `test-ui` environment. The first time you run it, it will download the necessary browser files to run the tests in the Chrome browser. + +```bash +pixi run test-ui +``` + +## Documentation + +The documentation can be built with the command: + +```bash +pixi run docs-build +``` + +As HoloViews uses notebooks for much of the documentation, this will take significant time to run (around an hour). +If you want to run it locally, you can temporarily disable the gallery by setting the environment variable `export HV_DOC_GALLERY=False`. +You can also disable the reference gallery by setting the environment variable `export HV_DOC_REF_GALLERY=False`. + +A development version of HoloViews can be found [here](https://dev.holoviews.org/). You can ask a maintainer if they want to make a dev release for your PR, but there is no guarantee they will say yes. + +## Build + +HoloViews have to build tasks. One is for installing packages with Pip, and the other is for installing packages with Conda. + +```bash +pixi run build-pip +pixi run build-conda +``` + +## Continuous Integration + +Every push to the `main` branch or any PR branch on GitHub automatically triggers a test build with [GitHub Actions](https://github.com/features/actions). + +You can see the list of all current and previous builds at [this URL](https://github.com/holoviz/holoviews/actions) + +### Etiquette + +GitHub Actions provides free build workers for open-source projects. A few considerations will help you be considerate of others needing these limited resources: + +- Run the tests locally before opening or pushing to an opened PR. + +- Group commits to meaningful chunks of work before pushing to GitHub (i.e., don't push on every commit). diff --git a/doc/index.rst b/doc/index.rst index fdf3210fc9..74b7aa900d 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -132,6 +132,7 @@ After you have successfully installed and configured HoloViews, please see `Gett User Guide Gallery Reference Gallery + Developer Guide Releases API FAQ diff --git a/dodo.py b/dodo.py deleted file mode 100644 index b68ffd1e3a..0000000000 --- a/dodo.py +++ /dev/null @@ -1,15 +0,0 @@ -import os -if "PYCTDEV_ECOSYSTEM" not in os.environ: - os.environ["PYCTDEV_ECOSYSTEM"] = "conda" - -from pyctdev import * # noqa: api - - -def task_pip_on_conda(): - """Experimental: provide pip build env via conda""" - return {'actions':[ - # some ecosystem=pip build tools must be installed with conda when using conda... - 'conda install -y pip twine wheel rfc3986 keyring', - # ..and some are only available via conda-forge - 'conda install -y -c conda-forge tox virtualenv', - ]} diff --git a/holoviews/__init__.py b/holoviews/__init__.py index e3231dece9..1ca017cdf6 100644 --- a/holoviews/__init__.py +++ b/holoviews/__init__.py @@ -75,39 +75,47 @@ """ import os import sys +import warnings import param -__version__ = str(param.version.Version(fpath=__file__, archive_commit="$Format:%h$", - reponame="holoviews")) - -from . import util # noqa (API import) -from .core import archive, config # noqa (API import) -from .core.boundingregion import BoundingBox # noqa (API import) -from .core.dimension import Dimension # noqa (API import) -from .core.element import Element, Collator # noqa (API import) -from .core.layout import (Layout, NdLayout, Empty, # noqa (API import) - AdjointLayout) -from .core.ndmapping import NdMapping # noqa (API import) -from .core.options import (Options, Store, Cycle, # noqa (API import) - Palette, StoreOptions) -from .core.overlay import Overlay, NdOverlay # noqa (API import) -from .core.spaces import (HoloMap, Callable, DynamicMap, # noqa (API import) - GridSpace, GridMatrix) - -from .operation import Operation # noqa (API import) +from . import util # noqa (API import) +from .__version import __version__ +from .core import archive, config # noqa (API import) +from .core.boundingregion import BoundingBox # noqa (API import) +from .core.dimension import Dimension # noqa (API import) +from .core.element import Collator, Element # noqa (API import) +from .core.layout import AdjointLayout, Empty, Layout, NdLayout # noqa (API import) +from .core.ndmapping import NdMapping # noqa (API import) +from .core.options import ( # noqa (API import) + Cycle, + Options, + Palette, + Store, + StoreOptions, +) +from .core.overlay import NdOverlay, Overlay # noqa (API import) +from .core.spaces import ( # noqa (API import) + Callable, + DynamicMap, + GridMatrix, + GridSpace, + HoloMap, +) from .element import * from .element import __all__ as elements_list -from .selection import link_selections # noqa (API import) -from .util import (extension, renderer, output, opts, # noqa (API import) - render, save) -from .util.transform import dim # noqa (API import) -from .util.warnings import HoloviewsDeprecationWarning, HoloviewsUserWarning # noqa: F401 +from .operation import Operation # noqa (API import) +from .selection import link_selections # noqa (API import) +from .util import extension, opts, output, render, renderer, save # noqa (API import) from .util._versions import show_versions # noqa: F401 +from .util.transform import dim # noqa (API import) +from .util.warnings import ( # noqa: F401 + HoloviewsDeprecationWarning, + HoloviewsUserWarning, +) # Suppress warnings generated by NumPy in matplotlib # Expected to be fixed in next matplotlib release -import warnings warnings.filterwarnings("ignore", message="elementwise comparison failed; returning scalar instead") @@ -173,7 +181,7 @@ def help(obj, visualization=True, ansi=True, backend=None, pydoc.help(obj) -del os, rcfile, warnings +del os, sys, rcfile, warnings def __getattr__(name): if name == "annotate": diff --git a/holoviews/__version.py b/holoviews/__version.py new file mode 100644 index 0000000000..f01259f510 --- /dev/null +++ b/holoviews/__version.py @@ -0,0 +1,44 @@ +"""Define the package version. + +Called __version.py as setuptools_scm will create a _version.py +""" + +import os.path + +PACKAGE = "holoviews" + +try: + # For performance reasons on imports, avoid importing setuptools_scm + # if not in a .git folder + if os.path.exists(os.path.join(os.path.dirname(__file__), "..", ".git")): + # If setuptools_scm is installed (e.g. in a development environment with + # an editable install), then use it to determine the version dynamically. + from setuptools_scm import get_version + + # This will fail with LookupError if the package is not installed in + # editable mode or if Git is not installed. + __version__ = get_version(root="..", relative_to=__file__) + else: + raise FileNotFoundError +except (ImportError, LookupError, FileNotFoundError): + # As a fallback, use the version that is hard-coded in the file. + try: + # __version__ was added in _version in setuptools-scm 7.0.0, we rely on + # the hopefully stable version variable. + from ._version import version as __version__ + except (ModuleNotFoundError, ImportError): + # Either _version doesn't exist (ModuleNotFoundError) or version isn't + # in _version (ImportError). ModuleNotFoundError is a subclass of + # ImportError, let's be explicit anyway. + + # Try something else: + from importlib.metadata import PackageNotFoundError, version + + try: + __version__ = version(PACKAGE) + except PackageNotFoundError: + # The user is probably trying to run this without having installed + # the package. + __version__ = "0.0.0+unknown" + +__all__ = ("__version__",) diff --git a/holoviews/tests/util/test_utils.py b/holoviews/tests/util/test_utils.py index 9438f1856c..824031b131 100644 --- a/holoviews/tests/util/test_utils.py +++ b/holoviews/tests/util/test_utils.py @@ -5,7 +5,7 @@ from pyviz_comms import CommManager -from holoviews import Store, notebook_extension +from holoviews import Store from holoviews.core.options import OptionTree from holoviews.element.comparison import ComparisonTestCase from holoviews.plotting import bokeh, mpl @@ -26,6 +26,8 @@ class TestOutputUtil(ComparisonTestCase): def setUp(self): if notebook is None: raise SkipTest("Jupyter Notebook not available") + from holoviews.ipython import notebook_extension + notebook_extension(*BACKENDS) Store.current_backend = 'matplotlib' Store.renderers['matplotlib'] = mpl.MPLRenderer.instance() diff --git a/pixi.toml b/pixi.toml new file mode 100644 index 0000000000..acb8bf5d9e --- /dev/null +++ b/pixi.toml @@ -0,0 +1,160 @@ +[project] +name = "holoviews" +channels = ["conda-forge", "pyviz/label/dev"] +platforms = ["linux-64", "osx-arm64", "osx-64", "win-64"] + +[tasks] +check-latest-packages = 'python scripts/check_latest_packages.py' +download-data = 'python scripts/download_data.py' +install = 'python -m pip install --no-deps --disable-pip-version-check -e .' + +[environments] +test-39 = ["py39", "test-core", "test", "example", "test-example", "test-unit-task"] +test-310 = ["py310", "test-core", "test", "example", "test-example", "test-unit-task"] +test-311 = ["py311", "test-core", "test", "example", "test-example", "test-unit-task"] +test-312 = ["py312", "test-core", "test", "example", "test-example", "test-unit-task"] +test-ui = ["py312", "test-core", "test", "test-ui"] +test-core = ["py312", "test-core", "test-unit-task"] +docs = ["py311", "example", "doc"] +build = ["py311", "build"] +lint = ["py311", "lint"] + +[dependencies] +python = ">=3.9" +pip= "*" +param = ">=1.12.0,<3.0" +panel = ">=1.0" +pyviz_comms = ">=2.1" +colorcet = "*" +numpy = ">=1.0" +packaging = "*" +pandas = ">=0.20.0" +# Recommended +ipython = ">=5.4.0" +notebook = "*" +matplotlib-base = ">=3" +bokeh = ">=3.1" + +[feature.py39.dependencies] +python = "3.9.*" + +[feature.py310.dependencies] +python = "3.10.*" + +[feature.py311.dependencies] +python = "3.11.*" + +[feature.py312.dependencies] +python = "3.12.*" + +[feature.example.dependencies] +networkx = "*" +pillow = "*" +xarray = ">=0.10.4" +plotly = ">=4.0" +# dash >=1.16 +streamz = ">=0.5.0" +ffmpeg = "*" +cftime = "*" +netcdf4 = "*" +dask-core = "*" +scipy = "*" +shapely = "*" +scikit-image = "*" +pyarrow = "*" +pooch = "*" +datashader = ">=0.11.1" +notebook = ">=7.0" + +# ============================================= +# =================== TESTS =================== +# ============================================= +[feature.test-core.dependencies] +pytest = "*" +pytest-cov = "*" +pytest-github-actions-annotate-failures = "*" +pytest-rerunfailures = "*" +nbconvert = "*" +pillow = "*" +plotly = ">=4.0" +contourpy = "*" + +[feature.test-unit-task.tasks] +test-unit = 'pytest holoviews' # So it not showing up for UI tests + +[feature.test.dependencies] +dask-core = "*" +ibis-sqlite = "*" +xarray = ">=0.10.4" +networkx = "*" +shapely = "*" +ffmpeg = "*" +cftime = "*" +scipy = ">=1.10" # Python 3.9 + Windows downloads 1.9 +selenium = "*" +spatialpandas = "*" +datashader = ">=0.11.1" +xyzservices = "*" +# dash >=1.16 + +[feature.test.target.unix.dependencies] +tsdownsample = "*" # currently not available on Windows + +[feature.test-example.tasks] +test-example = 'pytest -n auto --dist loadscope --nbval-lax examples' + +[feature.test-example.dependencies] +nbval = "*" +pytest-xdist = "*" + +[feature.test-ui] +channels = ["conda-forge", "pyviz/label/dev", "microsoft"] + +[feature.test-ui.dependencies] +playwright = "*" +pytest-playwright = "*" + +[feature.test-ui.tasks] +install-ui = 'playwright install chromium' + +[feature.test-ui.tasks.test-ui] +cmd = 'pytest holoviews/tests/ui --ui --browser chromium' +depends_on = ["install-ui"] + +# ============================================= +# =================== DOCS ==================== +# ============================================= +[feature.doc.dependencies] +nbsite = ">=0.8.4,<0.9.0" +graphviz = "*" +pooch = "*" +selenium = "*" + +[feature.doc.tasks] +docs-generate-rst = 'nbsite generate-rst --org holoviz --project-name holoviews' +docs-refmanual = 'python ./doc/generate_modules.py holoviews -d ./doc/reference_manual -n holoviews -e tests' +docs-generate = 'nbsite build --what=html --output=builtdocs --org holoviz --project-name holoviews' + +[feature.doc.tasks.docs-build] +depends_on = ['docs-generate-rst', 'docs-refmanual', 'docs-generate'] + +# ============================================= +# ================== BUILD ==================== +# ============================================= +[feature.build.dependencies] +build = "*" +conda-build = "*" + +[feature.build.tasks] +build-conda = 'bash scripts/conda/build.sh' +build-pip = 'python -m build .' + +# ============================================= +# =================== lint ==================== +# ============================================= +[feature.lint.dependencies] +pre-commit = "*" + +[feature.lint.tasks] +lint = 'pre-commit run --all-files' +lint-install = 'pre-commit install' diff --git a/pyproject.toml b/pyproject.toml index 4338a5e691..1090553c9d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,10 +1,72 @@ [build-system] -requires = [ +requires = ["hatchling", "hatch-vcs"] +build-backend = "hatchling.build" + +[project] +name = "holoviews" +dynamic = ["version"] +description = "A high-level plotting API for the PyData ecosystem built on HoloViews." +readme = "README.md" +license = { text = "BSD" } +requires-python = ">=3.9" +authors = [ + { name = "Jean-Luc Stevens", email = "developers@holoviz.org" }, + { name = "Philipp Rudiger", email = "developers@holoviz.org" }, +] +maintainers = [ + { name = "HoloViz developers", email = "developers@holoviz.org" }, +] +classifiers = [ + "License :: OSI Approved :: BSD License", + "Development Status :: 5 - Production/Stable", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Operating System :: OS Independent", + "Intended Audience :: Science/Research", + "Intended Audience :: Developers", + "Natural Language :: English", + "Framework :: Matplotlib", + "Topic :: Scientific/Engineering", + "Topic :: Software Development :: Libraries", +] +dependencies = [ "param >=1.12.0,<3.0", - "pyct >=0.4.4", - "setuptools >=30.3.0", + "numpy >=1.0", + "pyviz_comms >=2.1", + "panel >=1.0", + "colorcet", + "packaging", + "pandas >=0.20.0", ] +[project.urls] +Homepage = "https://holoviews.org" +Source = "http://github.com/holoviz/holoviews" +HoloViz = "https://holoviz.org/" + +[project.optional-dependencies] +recommended = ["ipython >=5.4.0", "notebook", "matplotlib >=3", "bokeh >=3.1"] + +[project.scripts] +holoviews = "holoviews.util.command:main" + +[tool.hatch.version] +source = "vcs" + +[tool.hatch.build.targets.wheel] +include = ["holoviews"] + +[tool.hatch.build.targets.sdist] +include = ["holoviews", "CHANGELOG.md"] + +[tool.hatch.build.targets.sdist.force-include] +examples = "holoviews/examples" + +[tool.hatch.build.hooks.vcs] +version-file = "holoviews/_version.py" + [tool.pytest.ini_options] addopts = ["--strict-config", "--strict-markers", "--color=yes"] minversion = "7" @@ -51,10 +113,18 @@ filterwarnings = [ "ignore:Passing a (SingleBlockManager|BlockManager) to (Series|GeoSeries|DataFrame|GeoDataFrame) is deprecated:DeprecationWarning", # https://github.com/holoviz/spatialpandas/issues/137 # 2024-02 "ignore:The current Dask DataFrame implementation is deprecated:DeprecationWarning", # https://github.com/dask/dask/issues/10917 + # 2024-04 + "ignore:No data was collected:coverage.exceptions.CoverageWarning", # https://github.com/pytest-dev/pytest-cov/issues/627 ] [tool.coverage] run.concurrency = ["greenlet"] +omit = ["holoviews/__version.py"] +exclude_also = [ + "if __name__ == .__main__.:", + "if TYPE_CHECKING:", + "if ._pyodide. in sys.modules:", +] [tool.ruff] fix = true diff --git a/scripts/build_conda.sh b/scripts/build_conda.sh deleted file mode 100755 index 4b47650355..0000000000 --- a/scripts/build_conda.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env bash - -set -euxo pipefail - -git status - -export SETUPTOOLS_ENABLE_FEATURES="legacy-editable" -python -m build -w . - -git diff --exit-code - -VERSION=$(find dist -name "*.whl" -exec basename {} \; | cut -d- -f2) -export VERSION -conda build conda.recipe/ --no-anaconda-upload --no-verify diff --git a/scripts/conda/build.sh b/scripts/conda/build.sh new file mode 100755 index 0000000000..277ca7196b --- /dev/null +++ b/scripts/conda/build.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash + +set -euxo pipefail + +PACKAGE="holoviews" + +for file in dist/*.whl dist/*.tar.bz2; do + if [ -e "$file" ]; then + echo "dist folder already contains $(basename "$file"). Please delete it before running this script." + exit 1 + fi +done + +git diff --exit-code +python -m build . # Can add -w when this is solved: https://github.com/pypa/hatch/issues/1305 + +VERSION=$(find dist -name "*.whl" -exec basename {} \; | cut -d- -f2) +export VERSION +conda build scripts/conda/recipe --no-anaconda-upload --no-verify + +mv "$CONDA_PREFIX/conda-bld/noarch/$PACKAGE-$VERSION-py_0.tar.bz2" dist diff --git a/scripts/conda/recipe/meta.yaml b/scripts/conda/recipe/meta.yaml new file mode 100644 index 0000000000..ea83e2fd10 --- /dev/null +++ b/scripts/conda/recipe/meta.yaml @@ -0,0 +1,47 @@ +{% set pyproject = load_file_data('../../../pyproject.toml', from_recipe_dir=True) %} +{% set project = pyproject['project'] %} + +package: + name: {{ project["name"] }} + version: {{ VERSION }} + +source: + url: ../../../dist/{{ project["name"] }}-{{ VERSION }}-py3-none-any.whl + +build: + noarch: python + script: {{ PYTHON }} -m pip install -vv {{ project["name"] }}-{{ VERSION }}-py3-none-any.whl + entry_points: + {% for group,epoints in project.get("entry_points",{}).items() %} + {% for entry_point in epoints %} + - {{ entry_point }} + {% endfor %} + {% endfor %} + +requirements: + build: + - python {{ project['requires-python'] }} + {% for dep in pyproject['build-system']['requires'] %} + - {{ dep }} + {% endfor %} + run: + - python {{ project['requires-python'] }} + {% for dep in project.get('dependencies', []) %} + - {{ dep }} + {% endfor %} + {% for dep in project['optional-dependencies']['recommended'] %} + - {{ dep }} + {% endfor %} + +test: + imports: + - {{ project["name"] }} + commands: + - pip check + requires: + - pip + +about: + home: https://holoviews.org + summary: Stop plotting your data - annotate your data and let it visualize itself. + license: BSD 3-Clause diff --git a/scripts/download_data.py b/scripts/download_data.py new file mode 100644 index 0000000000..291c50ab83 --- /dev/null +++ b/scripts/download_data.py @@ -0,0 +1,13 @@ +import bokeh.sampledata + +bokeh.sampledata.download() + +try: + import pooch # noqa: F401 + import scipy # noqa: F401 + import xarray as xr +except ImportError: + pass +else: + xr.tutorial.open_dataset("air_temperature") + xr.tutorial.open_dataset("rasm") diff --git a/scripts/download_data.sh b/scripts/download_data.sh deleted file mode 100755 index 16754fee73..0000000000 --- a/scripts/download_data.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/usr/bin/env bash - -set -euxo pipefail - -bokeh sampledata - -python -c " -try: - import pooch - import scipy - import xarray as xr -except ImportError: - pass -else: - xr.tutorial.open_dataset('air_temperature') - xr.tutorial.open_dataset('rasm') -" diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index ee80d48e0f..0000000000 --- a/setup.cfg +++ /dev/null @@ -1,9 +0,0 @@ -[metadata] -license_files = LICENSE.txt - -[tool:pyctdev.conda] -namespace_map = - ibis-framework=ibis-sqlite - dask=dask-core - geoviews=geoviews-core - matplotlib=matplotlib-base diff --git a/setup.py b/setup.py deleted file mode 100644 index 0edb3fd2ad..0000000000 --- a/setup.py +++ /dev/null @@ -1,223 +0,0 @@ -#!/usr/bin/env python - -import json -import os -import shutil -import sys - -import pyct.build -from setuptools import find_packages, setup - -setup_args = {} -install_requires = [ - "param >=1.12.0,<3.0", - "numpy >=1.0", - "pyviz_comms >=2.1", - "panel >=1.0", - "colorcet", - "packaging", - "pandas >=0.20.0", -] - -extras_require = {} - -extras_require['lint'] = [ - 'ruff', - 'pre-commit', -] - -# Test requirements -extras_require['tests_core'] = [ - 'pytest', - 'pytest-cov', - 'pytest-xdist', - 'pytest-rerunfailures', - 'matplotlib >=3', - 'nbconvert', - 'bokeh >=3.1', - 'pillow', - 'plotly >=4.0', - 'ipython >=5.4.0', - 'contourpy', -] - -# Optional tests dependencies, i.e. one should be able -# to run and pass the test suite without installing any -# of those. -extras_require['tests'] = extras_require['tests_core'] + [ - 'dask', - 'ibis-framework', # Mapped to ibis-sqlite in setup.cfg for conda - 'xarray >=0.10.4', - 'networkx', - 'shapely', - 'ffmpeg', - 'cftime', - 'scipy >=1.10', # Python 3.9 + Windows downloads 1.9 - 'selenium', - 'spatialpandas', - 'datashader >=0.11.1', - 'dash >=1.16', - 'xyzservices >=2022.9.0', -] - -if os.name != "nt": - # Currently not available on Windows on conda-forge - extras_require['tests'] += ['tsdownsample'] - -extras_require['tests_ci'] = [ - 'codecov', - "pytest-github-actions-annotate-failures", -] - -extras_require['tests_gpu'] = extras_require['tests'] + [ - 'cudf', -] - -extras_require['tests_nb'] = ['nbval'] -extras_require['ui'] = ['playwright', 'pytest-playwright'] - -# Notebook dependencies -extras_require["notebook"] = ["ipython >=5.4.0", "notebook"] - -# IPython Notebook + pandas + matplotlib + bokeh -extras_require["recommended"] = extras_require["notebook"] + [ - "matplotlib >=3", - "bokeh >=3.1", -] - -# Requirements to run all examples -extras_require["examples"] = extras_require["recommended"] + [ - "networkx", - "pillow", - "xarray >=0.10.4", - "plotly >=4.0", - 'dash >=1.16', - "streamz >=0.5.0", - "ffmpeg", - "cftime", - "netcdf4", - "dask", - "scipy", - "shapely", - "scikit-image", - "pyarrow", - "pooch", - "datashader >=0.11.1", - "notebook >=7.0", -] - - -extras_require["examples_tests"] = extras_require["examples"] + extras_require['tests_nb'] - -# Not used in tox.ini or elsewhere, kept for backwards compatibility. -extras_require["unit_tests"] = extras_require["examples"] + extras_require["tests"] + extras_require['lint'] - -extras_require['doc'] = extras_require['examples'] + [ - 'nbsite >=0.8.4,<0.9.0', - 'myst-nb <1', - 'graphviz', - 'bokeh >=3.1', - 'pooch', - 'selenium', -] - -extras_require['all'] = sorted(set(sum(extras_require.values(), []))) - -extras_require["build"] = [ - "param >=1.7.0", - "setuptools >=30.3.0", - "pyct >=0.4.4", -] - -def get_setup_version(reponame): - """ - Helper to get the current version from either git describe or the - .version file (if available). - """ - basepath = os.path.split(__file__)[0] - version_file_path = os.path.join(basepath, reponame, ".version") - try: - from param import version - except ImportError: - version = None - if version is not None: - return version.Version.setup_version( - basepath, reponame, archive_commit="$Format:%h$" - ) - else: - print( - "WARNING: param>=1.6.0 unavailable. If you are installing a package, this warning can safely be ignored. If you are creating a package or otherwise operating in a git repository, you should install param>=1.6.0." - ) - return json.load(open(version_file_path))["version_string"] - - -setup_args.update( - dict( - name="holoviews", - version=get_setup_version("holoviews"), - python_requires=">=3.9", - install_requires=install_requires, - extras_require=extras_require, - description="Stop plotting your data - annotate your data and let it visualize itself.", - long_description=open("README.md").read(), - long_description_content_type="text/markdown", - author="Jean-Luc Stevens and Philipp Rudiger", - author_email="holoviews@gmail.com", - maintainer="HoloViz Developers", - maintainer_email="developers@pyviz.org", - platforms=["Windows", "Mac OS X", "Linux"], - license="BSD", - url="https://www.holoviews.org", - project_urls={ - "Source": "https://github.com/holoviz/holoviews", - }, - entry_points={"console_scripts": ["holoviews = holoviews.util.command:main"]}, - packages=find_packages(), - include_package_data=True, - classifiers=[ - "License :: OSI Approved :: BSD License", - "Development Status :: 5 - Production/Stable", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - "Operating System :: OS Independent", - "Intended Audience :: Science/Research", - "Intended Audience :: Developers", - "Natural Language :: English", - "Framework :: Matplotlib", - "Topic :: Scientific/Engineering", - "Topic :: Software Development :: Libraries", - ], - ) -) - - -if __name__ == "__main__": - example_path = os.path.join( - os.path.dirname(os.path.abspath(__file__)), "holoviews/examples" - ) - - if "develop" not in sys.argv and "egg_info" not in sys.argv: - pyct.build.examples(example_path, __file__, force=True) - - if "install" in sys.argv: - header = "HOLOVIEWS INSTALLATION INFORMATION" - bars = "=" * len(header) - - extras = "\n".join("holoviews[%s]" % e for e in setup_args["extras_require"]) - - print(f"{bars}\n{header}\n{bars}") - - print("\nHoloViews supports the following installation types:\n") - print("%s\n" % extras) - print("Users should consider using one of these options.\n") - print("By default only a core installation is performed and ") - print("only the minimal set of dependencies are fetched.\n\n") - print("For more information please visit http://holoviews.org/install.html\n") - print(bars + "\n") - - setup(**setup_args) - - if os.path.isdir(example_path): - shutil.rmtree(example_path) diff --git a/tox.ini b/tox.ini deleted file mode 100644 index b5a5876ed0..0000000000 --- a/tox.ini +++ /dev/null @@ -1,67 +0,0 @@ -# For use with pyct (https://github.com/pyviz/pyct), but just standard -# tox config (works with tox alone). - -[tox] -# python version test group extra envs extra commands -envlist = {py39,py310,py311,py312}-{unit,ui,examples,all_recommended,simple}-{default}-{dev,pkg} - -[_simple] -description = Install holoviews without any optional dependencies -deps = . -commands = python -c "import holoviews as hv; print(hv.__version__)" - -[_unit_core] -description = Run unit tests with coverage but no optional test dependency -deps = .[tests_core] -commands = pytest holoviews --cov=./holoviews - -[_unit] -description = Run unit tests with coverage and all the optional test dependencies -deps = .[tests] -commands = pytest holoviews --cov=./holoviews - -[_unit_gpu] -description = Run unit tests with coverage and all the optional test dependencies -deps = .[tests_gpu] -commands = pytest holoviews --cov=./holoviews - -[_ui] -description = Run UI tests -deps = .[tests, ui] -commands = pytest holoviews --cov=./holoviews --cov-report=xml --ui --browser chromium - -[_examples] -description = Test that default examples run -deps = .[examples_tests] -commands = pytest -n auto --dist loadscope --nbval-lax examples - -[_all_recommended] -description = Run all recommended tests -deps = .[tests, examples_tests] -commands = {[_unit]commands} - {[_examples]commands} - -[_pkg] -commands = holoviews --install-examples - -[testenv] -sitepackages = True -install_command = pip install --no-deps {opts} pytest {packages} - -changedir = {envtmpdir} - -commands = examples-pkg: {[_pkg]commands} - unit: {[_unit]commands} - unit_core: {[_unit_core]commands} - unit_gpu: {[_unit_gpu]commands} - ui: {[_ui]commands} - simple: {[_simple]commands} - examples: {[_examples]commands} - all_recommended: {[_all_recommended]commands} - -deps = unit: {[_unit]deps} - unit_core: {[_unit_core]deps} - unit_gpu: {[_unit_gpu]deps} - ui: {[_ui]commands} - examples: {[_examples]deps} - all_recommended: {[_all_recommended]deps} From 8507aab7b217152181ecf0d181eea32ba5225bb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Fri, 19 Apr 2024 19:04:23 +0200 Subject: [PATCH 14/43] Fix flaky pop up ui tests (#6199) --- holoviews/tests/ui/bokeh/test_callback.py | 44 ++++++++++++----------- 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/holoviews/tests/ui/bokeh/test_callback.py b/holoviews/tests/ui/bokeh/test_callback.py index 5efebd6d67..b1cb47d4d4 100644 --- a/holoviews/tests/ui/bokeh/test_callback.py +++ b/holoviews/tests/ui/bokeh/test_callback.py @@ -11,6 +11,12 @@ pytestmark = pytest.mark.ui +skip_popup = pytest.mark.skipif(not bokeh34, reason="Pop ups needs Bokeh 3.4") + +@pytest.fixture +def points(): + rng = np.random.default_rng(10) + return hv.Points(rng.normal(size=(1000, 2))) @pytest.mark.usefixtures("bokeh_backend") @pytest.mark.parametrize( @@ -192,7 +198,8 @@ def range_function(x_range, y_range): assert BOUND_COUNT[0] == 1 -@pytest.mark.skipif(not bokeh34, reason="< Bokeh 3.4 does not support popup") + +@skip_popup @pytest.mark.usefixtures("bokeh_backend") def test_stream_popup(serve_hv): def popup_form(name): @@ -210,13 +217,12 @@ def popup_form(name): expect(locator).to_have_count(1) -@pytest.mark.skipif(not bokeh34, reason="< Bokeh 3.4 does not support popup") +@skip_popup @pytest.mark.usefixtures("bokeh_backend") -def test_stream_popup_none(serve_hv): +def test_stream_popup_none(serve_hv, points): def popup_form(name): return - points = hv.Points(np.random.randn(10, 2)) hv.streams.Tap(source=points, popup=popup_form("Tap")) page = serve_hv(points) @@ -235,7 +241,7 @@ def popup_form(name): expect(locator).to_have_count(0) -@pytest.mark.skipif(not bokeh34, reason="< Bokeh 3.4 does not support popup") +@skip_popup @pytest.mark.usefixtures("bokeh_backend") def test_stream_popup_callbacks(serve_hv): def popup_form(x, y): @@ -253,9 +259,9 @@ def popup_form(x, y): expect(locator).to_have_count(2) -@pytest.mark.skipif(not bokeh34, reason="< Bokeh 3.4 does not support popup") +@skip_popup @pytest.mark.usefixtures("bokeh_backend") -def test_stream_popup_visible(serve_hv): +def test_stream_popup_visible(serve_hv, points): def popup_form(x, y): def hide(_): col.visible = False @@ -267,7 +273,7 @@ def hide(_): col = pn.Column(button) return col - points = hv.Points(np.random.randn(10, 2)).opts(tools=["tap"]) + points = points.opts(tools=["tap"]) hv.streams.Tap(source=points, popup=popup_form) page = serve_hv(points) @@ -287,13 +293,13 @@ def hide(_): -@pytest.mark.skipif(not bokeh34, reason="< Bokeh 3.4 does not support popup") +@skip_popup @pytest.mark.usefixtures("bokeh_backend") -def test_stream_popup_close_button(serve_hv): +def test_stream_popup_close_button(serve_hv, points): def popup_form(x, y): return "Hello" - points = hv.Points(np.random.randn(10, 2)).opts(tools=["tap", "box_select"]) + points = points.opts(tools=["tap", "box_select"]) hv.streams.Tap(source=points, popup=popup_form) hv.streams.BoundsXY(source=points, popup=popup_form) @@ -309,10 +315,9 @@ def popup_form(x, y): expect(locator).not_to_be_visible() -@pytest.mark.skipif(not bokeh34, reason="< Bokeh 3.4 does not support popup") +@skip_popup @pytest.mark.usefixtures("bokeh_backend") -def test_stream_popup_selection1d_undefined(serve_hv): - points = hv.Points(np.random.randn(10, 2)) +def test_stream_popup_selection1d_undefined(serve_hv, points): hv.streams.Selection1D(source=points) page = serve_hv(points) @@ -321,13 +326,13 @@ def test_stream_popup_selection1d_undefined(serve_hv): hv_plot.click() # should not raise any error; properly guarded -@pytest.mark.skipif(not bokeh34, reason="< Bokeh 3.4 does not support popup") +@skip_popup @pytest.mark.usefixtures("bokeh_backend") -def test_stream_popup_selection1d(serve_hv): +def test_stream_popup_selection1d_tap(serve_hv, points): def popup_form(index): return "# Tap" - points = hv.Points(np.random.randn(1000, 2)) + points = points.opts(hit_dilation=5) hv.streams.Selection1D(source=points, popup=popup_form) points.opts(tools=["tap"], active_tools=["tap"]) @@ -340,14 +345,13 @@ def popup_form(index): expect(locator).to_have_count(1) -@pytest.mark.skipif(not bokeh34, reason="< Bokeh 3.4 does not support popup") +@skip_popup @pytest.mark.usefixtures("bokeh_backend") -def test_stream_popup_selection1d_lasso_select(serve_hv): +def test_stream_popup_selection1d_lasso_select(serve_hv, points): def popup_form(index): if index: return f"# lasso\n{len(index)}" - points = hv.Points(np.random.randn(1000, 2)) hv.streams.Selection1D(source=points, popup=popup_form) points.opts(tools=["tap", "lasso_select"], active_tools=["lasso_select"]) From c1824f751c16d888900ac9bc4de829a08201b7d6 Mon Sep 17 00:00:00 2001 From: Maxime Liquet <35924738+maximlt@users.noreply.github.com> Date: Tue, 23 Apr 2024 11:30:36 +0200 Subject: [PATCH 15/43] subcoordinate_y: respect `ylim` (#6190) Co-authored-by: Philipp Rudiger --- holoviews/plotting/bokeh/element.py | 13 ++++++++----- holoviews/plotting/plot.py | 2 +- holoviews/tests/plotting/bokeh/test_subcoordy.py | 8 ++++++++ 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py index 91a5c808da..fc8918f3f5 100644 --- a/holoviews/plotting/bokeh/element.py +++ b/holoviews/plotting/bokeh/element.py @@ -654,11 +654,14 @@ def _axis_props(self, plots, subplots, element, ranges, pos, *, dim=None, l, b, r, t = b, l, t, r if pos == 1 and self._subcoord_overlaid: if isinstance(self.subcoordinate_y, bool): - offset = self.subcoordinate_scale / 2. - # This sum() is equal to n+1, n being the number of elements contained - # in the overlay with subcoordinate_y=True, as the traversal goes through - # the root overlay that has subcoordinate_y=True too since it's propagated. - v0, v1 = 0-offset, sum(self.traverse(lambda p: p.subcoordinate_y))-2+offset + if self.ylim and all(np.isfinite(val) for val in self.ylim): + v0, v1 = self.ylim + else: + offset = self.subcoordinate_scale / 2. + # This sum() is equal to n+1, where n is the number of elements contained + # in the overlay with subcoordinate_y=True (including the the root overlay, + # which has subcoordinate_y=True due to option propagation) + v0, v1 = 0-offset, sum(self.traverse(lambda p: p.subcoordinate_y))-2+offset else: v0, v1 = 0, 1 else: diff --git a/holoviews/plotting/plot.py b/holoviews/plotting/plot.py index bcaedd2a50..84baf122f9 100644 --- a/holoviews/plotting/plot.py +++ b/holoviews/plotting/plot.py @@ -1118,7 +1118,7 @@ class GenericElementPlot(DimensionedPlot): If specified, takes precedence over data and dimension ranges.""") ylim = param.Tuple(default=(np.nan, np.nan), length=2, doc=""" - User-specified x-axis range limits for the plot, as a tuple (low,high). + User-specified y-axis range limits for the plot, as a tuple (low,high). If specified, takes precedence over data and dimension ranges.""") zlim = param.Tuple(default=(np.nan, np.nan), length=2, doc=""" diff --git a/holoviews/tests/plotting/bokeh/test_subcoordy.py b/holoviews/tests/plotting/bokeh/test_subcoordy.py index 4bdf13c52e..11388ce733 100644 --- a/holoviews/tests/plotting/bokeh/test_subcoordy.py +++ b/holoviews/tests/plotting/bokeh/test_subcoordy.py @@ -138,6 +138,14 @@ def test_invisible_yaxis(self): plot = bokeh_renderer.get_plot(overlay) assert not plot.state.yaxis.visible + def test_overlay_set_ylim(self): + ylim = (1, 2.5) + overlay = Overlay([Curve(range(10), label=f'Data {i}').opts(subcoordinate_y=True) for i in range(2)]) + overlay.opts(ylim=ylim) + plot = bokeh_renderer.get_plot(overlay) + y_range = plot.handles['y_range'] + assert y_range.start, y_range.end == ylim + def test_axis_labels(self): overlay = Overlay([Curve(range(10), label=f'Data {i}').opts(subcoordinate_y=True) for i in range(2)]) plot = bokeh_renderer.get_plot(overlay) From eb7ae9d58bf655d29b6c7d78bb977bc4d73ca52f Mon Sep 17 00:00:00 2001 From: Maxime Liquet <35924738+maximlt@users.noreply.github.com> Date: Tue, 23 Apr 2024 14:05:30 +0200 Subject: [PATCH 16/43] Add operation for group-wise normalisation (#6124) --- holoviews/operation/normalization.py | 62 ++++++++++++++++++- holoviews/plotting/bokeh/element.py | 7 ++- .../tests/plotting/bokeh/test_subcoordy.py | 31 ++++++++++ 3 files changed, 98 insertions(+), 2 deletions(-) diff --git a/holoviews/operation/normalization.py b/holoviews/operation/normalization.py index ffa6bac238..e685ad6d69 100644 --- a/holoviews/operation/normalization.py +++ b/holoviews/operation/normalization.py @@ -12,13 +12,15 @@ normalizations result in transformations to the stored data within each element. """ +from collections import defaultdict +import numpy as np import param from ..core import Overlay from ..core.operation import Operation from ..core.util import match_spec -from ..element import Raster +from ..element import Chart, Raster class Normalization(Operation): @@ -175,3 +177,61 @@ def _normalize_raster(self, raster, key): if range: norm_raster.data[:,:,depth] /= range return norm_raster + + +class subcoordinate_group_ranges(Operation): + """ + Compute the data range group-wise in a subcoordinate_y overlay, + and set the dimension range of each Chart element based on the + value computed for its group. + + This operation is useful to visually apply a group-wise min-max + normalisation. + """ + + def _process(self, overlay, key=None): + # If there are groups AND there are subcoordinate_y elements without a group. + if any(el.group != type(el).__name__ for el in overlay) and any( + el.opts.get('plot').kwargs.get('subcoordinate_y', False) + and el.group == type(el).__name__ + for el in overlay + ): + self.param.warning( + 'The subcoordinate_y overlay contains elements with a defined group, each ' + 'subcoordinate_y element in the overlay must have a defined group.' + ) + + vmins = defaultdict(list) + vmaxs = defaultdict(list) + include_chart = False + for el in overlay: + # Only applies to Charts. + # `group` is the Element type per default (e.g. Curve, Spike). + if not isinstance(el, Chart) or el.group == type(el).__name__: + continue + if not el.opts.get('plot').kwargs.get('subcoordinate_y', False): + self.param.warning( + f"All elements in group {el.group!r} must set the option " + f"'subcoordinate_y=True'. Not found for: {el}" + ) + vmin, vmax = el.range(1) + vmins[el.group].append(vmin) + vmaxs[el.group].append(vmax) + include_chart = True + + if not include_chart or not vmins: + return overlay + + minmax = { + group: (np.min(vmins[group]), np.max(vmaxs[group])) + for group in vmins + } + new = [] + for el in overlay: + if not isinstance(el, Chart): + new.append(el) + continue + y_dimension = el.vdims[0] + y_dimension = y_dimension.clone(range=minmax[el.group]) + new.append(el.redim(**{y_dimension.name: y_dimension})) + return overlay.clone(data=new) diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py index fc8918f3f5..4ed11a8a5b 100644 --- a/holoviews/plotting/bokeh/element.py +++ b/holoviews/plotting/bokeh/element.py @@ -632,10 +632,15 @@ def _axis_props(self, plots, subplots, element, ranges, pos, *, dim=None, range_el = el if self.batched and not isinstance(self, OverlayPlot) else element + if pos == 1 and 'subcoordinate_y' in range_tags_extras and dim and dim.range != (None, None): + dims = [dim] + v0, v1 = dim.range + axis_label = str(dim) + specs = ((dim.name, dim.label, dim.unit),) # For y-axes check if we explicitly passed in a dimension. # This is used by certain plot types to create an axis from # a synthetic dimension and exclusively supported for y-axes. - if pos == 1 and dim: + elif pos == 1 and dim: dims = [dim] v0, v1 = util.max_range([ elrange.get(dim.name, {'combined': (None, None)})['combined'] diff --git a/holoviews/tests/plotting/bokeh/test_subcoordy.py b/holoviews/tests/plotting/bokeh/test_subcoordy.py index 11388ce733..c047fd1e3f 100644 --- a/holoviews/tests/plotting/bokeh/test_subcoordy.py +++ b/holoviews/tests/plotting/bokeh/test_subcoordy.py @@ -5,6 +5,7 @@ from holoviews.core import Overlay from holoviews.element import Curve from holoviews.element.annotation import VSpan +from holoviews.operation.normalization import subcoordinate_group_ranges from .test_plot import TestBokehPlot, bokeh_renderer @@ -399,3 +400,33 @@ def test_missing_group_error(self): ) ): bokeh_renderer.get_plot(Overlay(curves)) + + def test_norm_subcoordinate_group_ranges(self): + x = np.linspace(0, 10 * np.pi, 21) + curves = [] + j = 0 + for group in ['A', 'B']: + for i in range(2): + yvals = j * np.sin(x) + curves.append( + Curve((x + np.pi/2, yvals), label=f'{group}{i}', group=group).opts(subcoordinate_y=True) + ) + j += 1 + + overlay = Overlay(curves) + noverlay = subcoordinate_group_ranges(overlay) + + expected = [ + (-1.0, 1.0), + (-1.0, 1.0), + (-3.0, 3.0), + (-3.0, 3.0), + ] + for i, el in enumerate(noverlay): + assert el.get_dimension('y').range == expected[i] + + plot = bokeh_renderer.get_plot(noverlay) + + for i, sp in enumerate(plot.subplots.values()): + y_source = sp.handles['glyph_renderer'].coordinates.y_source + assert (y_source.start, y_source.end) == expected[i] From 1dbea2c6ebf9413a613e2e57a0dc4b8d0235d93c Mon Sep 17 00:00:00 2001 From: mirage007 Date: Mon, 29 Apr 2024 02:32:25 -0400 Subject: [PATCH 17/43] Bug fix for player example pause button (#6212) --- examples/reference/apps/bokeh/player.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/examples/reference/apps/bokeh/player.py b/examples/reference/apps/bokeh/player.py index b7b034f74d..0893f29622 100644 --- a/examples/reference/apps/bokeh/player.py +++ b/examples/reference/apps/bokeh/player.py @@ -38,13 +38,16 @@ def slider_update(attrname, old, new): slider = Slider(start=start, end=end, value=0, step=1, title="Year") slider.on_change('value', slider_update) +callback_tasks = {} + def animate(): - if button.label == '► Play': - button.label = '❚❚ Pause' - curdoc().add_periodic_callback(animate_update, 200) + if button.label == "► Play": + button.label = "❚❚ Pause" + callback_tasks["animate"] = curdoc().add_periodic_callback(animate_update, 400) else: - button.label = '► Play' - curdoc().remove_periodic_callback(animate_update) + button.label = "► Play" + curdoc().remove_periodic_callback(callback_tasks["animate"]) + button = Button(label='► Play', width=60) button.on_click(animate) From b1fe1cf06e8cffa101dca7b06c96cf09b2169aab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Fri, 3 May 2024 07:48:07 -0700 Subject: [PATCH 18/43] Update documentation and small Pixi improvements (#6200) --- .gitattributes | 2 - .github/workflows/build.yaml | 8 +- .github/workflows/docs.yaml | 51 +++-- .github/workflows/nightly_lock.yaml | 12 +- .github/workflows/test.yaml | 21 ++- README.md | 81 +++----- doc/developer_guide/index.md | 20 +- doc/index.md | 122 ++++++++++++ doc/index.rst | 140 -------------- doc/install.md | 52 ++++++ doc/install.rst | 94 ---------- doc/user_guide/Configuring.md | 60 ++++++ doc/user_guide/Installing_and_Configuring.rst | 5 - doc/user_guide/index.rst | 4 +- examples/README.md | 12 +- .../Installing_and_Configuring.ipynb | 175 ------------------ holoviews/core/data/interface.py | 2 +- holoviews/core/pprint.py | 4 +- holoviews/tests/conftest.py | 2 + .../tests/core/data/test_ibisinterface.py | 13 +- holoviews/tests/core/test_options.py | 33 +++- holoviews/tests/core/test_storeoptions.py | 11 +- holoviews/tests/ipython/test_displayhooks.py | 4 + holoviews/tests/ipython/test_notebooks.py | 4 + holoviews/tests/operation/test_operation.py | 14 +- .../tests/plotting/bokeh/test_callbacks.py | 3 +- .../tests/plotting/bokeh/test_renderer.py | 1 + holoviews/tests/plotting/bokeh/test_utils.py | 2 +- .../tests/plotting/matplotlib/__init__.py | 3 + holoviews/tests/plotting/plotly/__init__.py | 3 + holoviews/tests/plotting/test_plotutils.py | 15 +- holoviews/tests/test_selection.py | 7 +- holoviews/tests/util/test_help.py | 5 +- holoviews/tests/util/test_utils.py | 12 +- pixi.toml | 84 ++++----- pyproject.toml | 6 +- scripts/conda/recipe/meta.yaml | 2 +- scripts/download_data.py | 8 +- 38 files changed, 508 insertions(+), 589 deletions(-) delete mode 100644 .gitattributes create mode 100644 doc/index.md delete mode 100644 doc/index.rst create mode 100644 doc/install.md delete mode 100644 doc/install.rst create mode 100644 doc/user_guide/Configuring.md delete mode 100644 doc/user_guide/Installing_and_Configuring.rst delete mode 100644 examples/user_guide/Installing_and_Configuring.ipynb diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index 83c4418e26..0000000000 --- a/.gitattributes +++ /dev/null @@ -1,2 +0,0 @@ - __init__.py export-subst - setup.py export-subst diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 2e7770ca24..4af98850c5 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -44,6 +44,7 @@ jobs: with: environments: "build" download-data: false + install: false - name: conda build run: pixi run -e build build-conda - uses: actions/upload-artifact@v4 @@ -91,6 +92,7 @@ jobs: with: environments: "build" download-data: false + install: false - name: Build package run: pixi run -e build build-pip - uses: actions/upload-artifact@v4 @@ -114,8 +116,12 @@ jobs: path: dist/ - name: Install package run: python -m pip install dist/*.whl - - name: Test package + - name: Import package run: python -c "import $PACKAGE; print($PACKAGE.__version__)" + - name: Install test dependencies + run: python -m pip install "$(ls dist/*whl)[tests]" + - name: Test package + run: python -m pytest --pyargs $PACKAGE --color=yes pip_publish: name: Publish PyPI diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml index d3d8a607f2..bca34dfa84 100644 --- a/.github/workflows/docs.yaml +++ b/.github/workflows/docs.yaml @@ -20,6 +20,10 @@ on: schedule: - cron: "0 14 * * SUN" +defaults: + run: + shell: bash -el {0} + jobs: pixi_lock: name: Pixi lock @@ -27,21 +31,16 @@ jobs: steps: - uses: holoviz-dev/holoviz_tasks/pixi_lock@pixi - build_docs: - name: Documentation + docs_build: + name: Build Documentation needs: [pixi_lock] - runs-on: "ubuntu-latest" - timeout-minutes: 120 - defaults: - run: - shell: bash -el {0} + runs-on: "macos-latest" + timeout-minutes: 180 + outputs: + tag: ${{ steps.vars.outputs.tag }} env: - DESC: "Documentation build" MPLBACKEND: "Agg" GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - AWS_DEFAULT_REGION: ${{ secrets.AWS_DEFAULT_REGION }} MOZ_HEADLESS: 1 PANEL_EMBED: "true" PANEL_EMBED_JSON: "true" @@ -51,22 +50,44 @@ jobs: - uses: holoviz-dev/holoviz_tasks/pixi_install@pixi with: environments: docs + - name: Build documentation + run: pixi run -e docs docs-build + - uses: actions/upload-artifact@v4 + if: always() + with: + name: docs + if-no-files-found: error + path: builtdocs - name: Set output id: vars run: echo "tag=${GITHUB_REF#refs/*/}" >> $GITHUB_OUTPUT - - name: Build documentation - run: pixi run -e docs docs-build + + docs_publish: + name: Publish Documentation + runs-on: "ubuntu-latest" + needs: [docs_build] + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + AWS_DEFAULT_REGION: ${{ secrets.AWS_DEFAULT_REGION }} + steps: + - uses: actions/download-artifact@v4 + with: + name: docs + path: builtdocs/ + - name: Set output + id: vars + run: echo "tag=${{ needs.docs_build.outputs.tag }}" >> $GITHUB_OUTPUT - name: upload dev if: | (github.event_name == 'workflow_dispatch' && github.event.inputs.target == 'dev') || (github.event_name == 'push' && (contains(steps.vars.outputs.tag, 'a') || contains(steps.vars.outputs.tag, 'b') || contains(steps.vars.outputs.tag, 'rc'))) run: | - pipx install awscli aws s3 sync --quiet ./builtdocs s3://dev.holoviews.org/ - name: upload main if: | (github.event_name == 'workflow_dispatch' && github.event.inputs.target == 'main') || (github.event_name == 'push' && !(contains(steps.vars.outputs.tag, 'a') || contains(steps.vars.outputs.tag, 'b') || contains(steps.vars.outputs.tag, 'rc'))) run: | - pipx install awscli aws s3 sync --quiet ./builtdocs s3://holoviews.org/ diff --git a/.github/workflows/nightly_lock.yaml b/.github/workflows/nightly_lock.yaml index 9d3379af2b..bea5345e9a 100644 --- a/.github/workflows/nightly_lock.yaml +++ b/.github/workflows/nightly_lock.yaml @@ -8,7 +8,15 @@ jobs: pixi_lock: name: Pixi lock runs-on: ubuntu-latest + timeout-minutes: 5 steps: - uses: holoviz-dev/holoviz_tasks/pixi_lock@pixi - - # TODO: Upload the lock-file + - name: Upload lock-file to S3 + env: + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + AWS_DEFAULT_REGION: "eu-west-1" + PACKAGE: "holoviews" + run: | + cp pixi.lock $(date +%Y-%m-%d)-pixi.lock + aws s3 cp ./$(date +%Y-%m-%d)-pixi.lock s3://assets.holoviz.org/lock/$PACKAGE/ diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index ef11a3ada8..b16390f645 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -37,6 +37,7 @@ env: PYTHONIOENCODING: "utf-8" GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} DASK_DATAFRAME__QUERY_PLANNING: false + COV: "--cov=./holoviews --cov-report=xml" jobs: pre_commit: @@ -83,7 +84,7 @@ jobs: if: env.MATRIX_OPTION == 'default' run: | MATRIX=$(jq -nsc '{ - "os": ["ubuntu-latest", "macos-14", "windows-latest"], + "os": ["ubuntu-latest", "macos-latest", "windows-latest"], "environment": ["test-39", "test-312"] }') echo "MATRIX=$MATRIX" >> $GITHUB_ENV @@ -91,8 +92,8 @@ jobs: if: env.MATRIX_OPTION == 'full' run: | MATRIX=$(jq -nsc '{ - "os": ["ubuntu-latest", "macos-14", "windows-latest"], - "environment": ["test-39", "test-310", "test311", "test312"] + "os": ["ubuntu-latest", "macos-latest", "windows-latest"], + "environment": ["test-39", "test-310", "test-311", "test-312"] }') echo "MATRIX=$MATRIX" >> $GITHUB_ENV - name: Set test matrix with 'downstream' option @@ -132,7 +133,7 @@ jobs: - name: Test Unit if: needs.setup.outputs.code_change == 'true' run: | - pixi run -e ${{ matrix.environment }} test-unit --cov=./holoviews --cov-report=xml + pixi run -e ${{ matrix.environment }} test-unit $COV - name: Test Examples if: needs.setup.outputs.code_change == 'true' run: | @@ -162,7 +163,17 @@ jobs: - name: Test UI if: needs.setup.outputs.code_change == 'true' run: | - pixi run -e ${{ matrix.environment }} test-ui --cov=./holoviews --cov-report=xml + # Create a .uicoveragerc file to set the concurrency library to greenlet + # https://github.com/microsoft/playwright-python/issues/313 + echo "[run]\nconcurrency = greenlet" > .uicoveragerc + FAIL="--screenshot only-on-failure --full-page-screenshot --output ui_screenshots --tracing retain-on-failure" + pixi run -e ${{ matrix.environment }} test-ui $COV --cov-config=.uicoveragerc $FAIL + - uses: actions/upload-artifact@v4 + if: always() + with: + name: ui_screenshots_${{ runner.os }} + path: ./ui_screenshots + if-no-files-found: ignore - uses: codecov/codecov-action@v4 if: needs.setup.outputs.code_change == 'true' with: diff --git a/README.md b/README.md index eaf2ec464e..b7fc145336 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ itself.** | Downloads | ![https://pypistats.org/packages/holoviews](https://img.shields.io/pypi/dm/holoviews?label=pypi) ![https://anaconda.org/pyviz/holoviews](https://pyviz.org/_static/cache/holoviews_conda_downloads_badge.svg) | Build Status | [![Build Status](https://github.com/holoviz/holoviews/actions/workflows/test.yaml/badge.svg?branch=main)](https://github.com/holoviz/holoviews/actions/workflows/test.yaml?query=branch%3Amain) | | Coverage | [![codecov](https://codecov.io/gh/holoviz/holoviews/branch/main/graph/badge.svg)](https://codecov.io/gh/holoviz/holoviews) | -| Latest dev release | [![Github tag](https://img.shields.io/github/tag/holoviz/holoviews.svg?label=tag&colorB=11ccbb)](https://github.com/holoviz/holoviews/tags) [![dev-site](https://img.shields.io/website-up-down-green-red/http/dev.holoviews.org.svg?label=dev%20website)](http://dev.holoviews.org) | +| Latest dev release | [![Github tag](https://img.shields.io/github/tag/holoviz/holoviews.svg?label=tag&colorB=11ccbb)](https://github.com/holoviz/holoviews/tags) [![dev-site](https://img.shields.io/website-up-down-green-red/http/dev.holoviews.org.svg?label=dev%20website)](https://dev.holoviews.org) | | Latest release | [![Github release](https://img.shields.io/github/release/holoviz/holoviews.svg?label=tag&colorB=11ccbb)](https://github.com/holoviz/holoviews/releases) [![PyPI version](https://img.shields.io/pypi/v/holoviews.svg?colorB=cc77dd)](https://pypi.python.org/pypi/holoviews) [![holoviews version](https://img.shields.io/conda/v/pyviz/holoviews.svg?colorB=4488ff&style=flat)](https://anaconda.org/pyviz/holoviews) [![conda-forge version](https://img.shields.io/conda/v/conda-forge/holoviews.svg?label=conda%7Cconda-forge&colorB=4488ff)](https://anaconda.org/conda-forge/holoviews) [![defaults version](https://img.shields.io/conda/v/anaconda/holoviews.svg?label=conda%7Cdefaults&style=flat&colorB=4488ff)](https://anaconda.org/anaconda/holoviews) | | Python | [![Python support](https://img.shields.io/pypi/pyversions/holoviews.svg)](https://pypi.org/project/holoviews/) | | Docs | [![DocBuildStatus](https://github.com/holoviz/holoviews/workflows/docs/badge.svg?query=branch%3Amain)](https://github.com/holoviz/holoviews/actions?query=workflow%3Adocs+branch%3Amain) [![site](https://img.shields.io/website-up-down-green-red/https/holoviews.org.svg)](https://holoviews.org) | @@ -22,80 +22,59 @@ and simple. With HoloViews, you can usually express what you want to do in very few lines of code, letting you focus on what you are trying to explore and convey, not on the process of plotting. -Check out the [HoloViews web site](http://holoviews.org) for extensive examples and documentation. +Check out the [HoloViews web site](https://holoviews.org) for extensive examples and documentation. # Installation -HoloViews works with -[Python](https://github.com/holoviz/holoviews/actions/workflows/test.yaml) -on Linux, Windows, or Mac, and works seamlessly with -[Jupyter Notebook and JupyterLab](https://jupyter.org). +HoloViews works with [Python](https://github.com/holoviz/holoviews/actions/workflows/test.yaml) +on Linux, Windows, or Mac, and works seamlessly with [Jupyter Notebook and JupyterLab](https://jupyter.org). -The recommended way to install HoloViews is using the -[conda](https://docs.conda.io/projects/conda/en/latest/index.html) command provided by -[Anaconda](https://docs.anaconda.com/free/anaconda/install/) or -[Miniconda](https://docs.conda.io/en/latest/miniconda.html): +You can install HoloViews either with `conda` or `pip`, for more information see the [install guide](https://holoviews.org/install.html). conda install holoviews -This command will install the typical packages most useful with -HoloViews, though HoloViews itself depends only on -[Numpy](https://numpy.org) [Pandas](https://pandas.pydata.org) and [Param](https://param.holoviz.org). -Additional installation and configuration options are described in the -[user guide](https://holoviews.org/user_guide/Installing_and_Configuring.html). + pip install holoviews -You can also clone holoviews directly from GitHub and install it with: +# Developer Guide - git clone git://github.com/holoviz/holoviews.git - cd holoviews - pip install -e . +If you want to help develop HoloViews, you can checkout the [developer guide](https://dev.holoviews.org/developer_guide/index.html), +this guide will help you get set-up. Making it easy to contribute. -## Usage +# Support & Feedback -Once you've installed HoloViews, you can get a copy of all the examples shown on this website: - - holoviews --install-examples - cd holoviews-examples - -Now you can launch Jupyter Notebook or JupyterLab to explore them: - - jupyter notebook - - jupyter lab +If you find any bugs or have any feature suggestions please file a GitHub +[issue](https://github.com/holoviz/holoviews/issues). -For more details on setup and configuration see [our website](https://holoviews.org/user_guide/Installing_and_Configuring.html). +If you have any usage questions, please ask them on [HoloViz Discourse](https://discourse.holoviz.org/), -For general discussion, we have a [discord channel](https://discord.gg/AXRHnJU6sP). -If you find any bugs or have any feature suggestions please file a GitHub -[issue](https://github.com/holoviz/holoviews/issues) -or submit a [pull request](https://help.github.com/articles/about-pull-requests). +For general discussion, we have a [Discord channel](https://discord.gg/AXRHnJU6sP). diff --git a/doc/developer_guide/index.md b/doc/developer_guide/index.md index c25212997e..ed10b2eabf 100644 --- a/doc/developer_guide/index.md +++ b/doc/developer_guide/index.md @@ -27,7 +27,7 @@ To contribute to HoloViews, you will also need [Github account](https://github.c ### Pixi -Developing all aspects of HoloViews requires a wide range of packages in different environments. To make this more manageable, Pixi manages the developer experience. To install Pixi, follow [this guide](https://prefix.dev/docs/pixi/overview#installation). +Developing all aspects of HoloViews requires a wide range of packages in different environments. To make this more manageable, Pixi manages the developer experience. To install Pixi, follow [this guide](https://pixi.sh/latest/#installation). #### Glossary @@ -40,7 +40,9 @@ For more information, see the [Pixi documentation](https://pixi.sh/latest/). :::{admonition} Note :class: info -The first time you run `pixi`, it will create a `.pixi` directory in the source directory. This directory will contain all the files needed for the virtual environments. The `.pixi` directory can be large, so don't accidentally put it into a cloud-synced directory. +The first time you run `pixi`, it will create a `.pixi` directory in the source directory. +This directory will contain all the files needed for the virtual environments. +The `.pixi` directory can be large, so it is advised not to put the source directory into a cloud-synced directory. ::: ## Installing the Project @@ -51,9 +53,10 @@ The source code for the HoloViews project is hosted on [GitHub](https://github.c 1. Go to [github.com/holoviz/holoviews](https://github.com/holoviz/holoviews) 2. [Fork the repository](https://docs.github.com/en/get-started/quickstart/contributing-to-projects#forking-a-repository) -3. Run in your terminal: `git clone https://github.com//holoviews` +3. Run in your terminal: `git clone https://github.com//holoviews` -The instructions for cloning above created a `holoviews` directory at your file system location. This `holoviews` directory is the _source checkout_ for the remainder of this document, and your current working directory is this directory. +The instructions for cloning above created a `holoviews` directory at your file system location. +This `holoviews` directory is the _source checkout_ for the remainder of this document, and your current working directory is this directory. ### Fetch tags from upstream @@ -73,12 +76,15 @@ To start developing, run the following command pixi install ``` -The first time you run it, it will create a `lock-file` with information for all available environments. This command will take a minute or so to run. When this is finished, it is possible to run the following command to download the data HoloViews tests and examples depend upon. +The first time you run it, it will create a `pixi.lock` file with information for all available environments. This command will take a minute or so to run. +When this is finished, it is possible to run the following command to download the data HoloViews tests and examples depend upon. ```bash pixi run download-data ``` +All available tasks can be found by running `pixi task list`, the following sections will give a brief introduction to the most common tasks. + ### Editable install It can be advantageous to install the HoloViews in [editable mode](https://pip.pypa.io/en/stable/topics/local-project-installs/#editable-installs): @@ -129,7 +135,7 @@ pixi run test-unit The task is available in the following environments: `test-39`, `test-310`, `test-311`, `test-312`, and `test-core`. Where the first ones have the same environments except for different Python versions, and `test-core` only has a core set of dependencies. -If you haven't set the environment flag in the command, you need to select which one of the environments to use. +If you haven't set the environment flag in the command, a menu will help you select which one of the environments to use. ### Example tests @@ -168,7 +174,7 @@ A development version of HoloViews can be found [here](https://dev.holoviews.org ## Build -HoloViews have to build tasks. One is for installing packages with Pip, and the other is for installing packages with Conda. +HoloViews have two build tasks. One is for building packages for Pip, and the other is for building packages for Conda. ```bash pixi run build-pip diff --git a/doc/index.md b/doc/index.md new file mode 100644 index 0000000000..16f412bd05 --- /dev/null +++ b/doc/index.md @@ -0,0 +1,122 @@ +

+ +**Stop plotting your data - annotate your data and let it visualize +itself.** + +
+
+ +HoloViews is an +[open-source](https://github.com/holoviz/holoviews/blob/main/LICENSE.txt) +Python library designed to make data analysis and visualization seamless +and simple. With HoloViews, you can usually express what you want to do +in very few lines of code, letting you focus on what you are trying to +explore and convey, not on the process of plotting. + +For examples, check out the thumbnails below and the other items in the +[Gallery](gallery/index) of demos and apps and the [Reference Gallery](reference/index) +that shows every HoloViews component. Be sure to +look at the code, not just the pictures, to appreciate how easy it is to +create such plots yourself! + +The [Getting-Started](getting_started/index) guide explains the basic concepts +and how to start using HoloViews, and is the recommended way to +understand how everything works. + +The [User Guide](user_guide/index) goes more deeply into key concepts from +HoloViews, when you are ready for further study. + +The [API](reference_manual) is the definitive guide to each HoloViews +object, but the same information is available more conveniently via the +`hv.help()` command and tab completion in the Jupyter notebook. + +If you have any [issues](https://github.com/holoviz/holoviews/issues) or +wish to [contribute code](https://help.github.com/articles/about-pull-requests), you can +visit our [GitHub site](https://github.com/holoviz/holoviews), file a +topic on the [HoloViz Discourse](https://discourse.holoviz.org/), or ask a quick question +on [Holoviz Discord](https://discord.gg/AXRHnJU6sP). + +
+ +
+
+ +
+ + + + +
+ +# Installation + +[![CondaPkg](https://img.shields.io/conda/v/anaconda/holoviews.svg?label=conda%7Cdefaults&style=flat&colorB=4488ff)](https://anaconda.org/pyviz/holoviews) +[![PyPI](https://img.shields.io/pypi/v/holoviews.svg)](https://pypi.python.org/pypi/holoviews) +[![License](https://img.shields.io/pypi/l/holoviews.svg)](https://github.com/holoviz/holoviews/blob/main/LICENSE.txt) +[![Coverage](https://codecov.io/gh/holoviz/holoviews/branch/main/graph/badge.svg)](https://codecov.io/gh/holoviz/holoviews) + +HoloViews works with Python 3 on Linux, Windows, or Mac, and works +seamlessly with [Jupyter Notebook and JupyterLab](https://jupyter.org). + +You can install HoloViews either with `conda` or `pip`, for more information see the [install guide](install). + + conda install holoviews + + pip install holoviews + +# Usage + +Once you've installed HoloViews, you can get a copy of all the examples +shown on this website: + + holoviews --install-examples + cd holoviews-examples + +Now you can launch Jupyter Notebook or JupyterLab to explore them: + + jupyter notebook + + jupyter lab + +After you have successfully installed and configured HoloViews, please +see [Getting Started](getting_started/index). + +```{toctree} +:titlesonly: +:hidden: +:maxdepth: 2 + +Home +Getting Started +User Guide +Gallery +Reference Gallery +Developer Guide +Releases +API +FAQ Roadmap +About +``` diff --git a/doc/index.rst b/doc/index.rst deleted file mode 100644 index 74b7aa900d..0000000000 --- a/doc/index.rst +++ /dev/null @@ -1,140 +0,0 @@ -.. HoloViews documentation main file - -.. raw:: html - -

- -**Stop plotting your data - annotate your data and let it visualize itself.** - -.. raw:: html - -
-
- -HoloViews is an `open-source `_ Python library designed to make data analysis and visualization seamless and simple. With HoloViews, you can usually express what you want to do in very few lines of code, letting you focus on what you are trying to explore and convey, not on the process of plotting. - -For examples, check out the thumbnails below and the other items in the `Gallery `_ of demos and apps and the `Reference Gallery `_ that shows every HoloViews component. Be sure to look at the code, not just the pictures, to appreciate how easy it is to create such plots yourself! - -The `Getting-Started `_ guide explains the basic concepts and how to start using HoloViews, and is the recommended way to understand how everything works. - -The `User Guide `_ goes more deeply into key concepts from HoloViews, when you are ready for further study. - -The `API `_ is the definitive guide to each HoloViews object, but the same information is available more conveniently via the ``hv.help()`` command and tab completion in the Jupyter notebook. - -If you have any `issues `_ or wish to `contribute code `_, you can visit our `GitHub site `_ or file a topic on the `HoloViz Discourse `_. - -.. raw:: html - -
- -.. raw:: html - -
-
- -.. raw:: html - -
-
- - - - - - -
- - -
- - - - - - -
-
- - -Installation ------------- - -|CondaPkg|_ |PyPI|_ |License|_ |Coverage|_ - - -HoloViews works with Python 3 on Linux, Windows, or Mac, and works seamlessly with `Jupyter Notebook and JupyterLab `_. - -The recommended way to install HoloViews is using the `conda `_ command provided by `Anaconda `_ or `Miniconda `_:: - - conda install -c pyviz holoviews bokeh - -This command will install the typical packages most useful with HoloViews, though HoloViews itself -directly depends only on `Numpy `_, `Pandas `_ and `Param `_. - -Additional installation and configuration options are described in the -`user guide `_. - -Additional methods of installation, including different ways to use -``pip`` can be found in the `installation guide `_. - -Usage ------ - -Once you've installed HoloViews, you can get a copy of all the examples shown on this website:: - - holoviews --install-examples - cd holoviews-examples - -Now you can launch Jupyter Notebook or JupyterLab to explore them:: - - jupyter notebook - - jupyter lab - -If you are working with a JupyterLab version <2.0 you will also need the PyViz JupyterLab -extension:: - - jupyter labextension install @pyviz/jupyterlab_pyviz - -For more details on installing and configuring HoloViews see `the installing and configuring guide `_. - -After you have successfully installed and configured HoloViews, please see `Getting Started `_. - - -.. |PyPI| image:: https://img.shields.io/pypi/v/holoviews.svg -.. _PyPI: https://pypi.python.org/pypi/holoviews - -.. |CondaPkg| image:: https://img.shields.io/conda/v/anaconda/holoviews.svg?label=conda%7Cdefaults&style=flat&colorB=4488ff -.. _CondaPkg: https://anaconda.org/pyviz/holoviews - -.. |License| image:: https://img.shields.io/pypi/l/holoviews.svg -.. _License: https://github.com/holoviz/holoviews/blob/main/LICENSE.txt - -.. |Coverage| image:: https://codecov.io/gh/holoviz/holoviews/branch/main/graph/badge.svg -.. _Coverage: https://codecov.io/gh/holoviz/holoviews - -.. toctree:: - :titlesonly: - :hidden: - :maxdepth: 2 - - Home - Getting Started - User Guide - Gallery - Reference Gallery - Developer Guide - Releases - API - FAQ - Roadmap - About diff --git a/doc/install.md b/doc/install.md new file mode 100644 index 0000000000..fd1ca54b99 --- /dev/null +++ b/doc/install.md @@ -0,0 +1,52 @@ +# Installing HoloViews + +The quickest and easiest way to get the latest version of all the +recommended packages for working with HoloViews on Linux, Windows, or +Mac systems is via the +[conda](https://docs.conda.io/projects/conda/en/latest/) command +provided by the [Anaconda](https://docs.anaconda.com/anaconda/install/) +or [Miniconda](https://docs.conda.io/en/latest/miniconda.html) +scientific Python distributions: + + conda install -c pyviz holoviews + +This recommended installation includes the default +[Matplotlib](http://matplotlib.org) plotting library backend, the more +interactive [Bokeh](http://bokeh.pydata.org) plotting library backend, +and the [Jupyter Notebook](http://jupyter.org). + +A similar set of packages can be installed using `pip`, if that command +is available on your system: + + pip install "holoviews[recommended]" + +`pip` also supports other installation options, including a minimal +install of only the packages necessary to generate and manipulate +HoloViews objects without visualization: + + pip install holoviews + +This minimal install will install only the required packages, for HoloViews to run. +This makes it very easy to integrate HoloViews into your workflow or as part of another project. + +Now that you are set up you can get a copy of all the examples shown on +this website: + + holoviews --install-examples + cd holoviews-examples + +Once you've installed HoloViews examples, you can get started by launching +Jupyter Notebook + + jupyter notebook + +Or JupyterLab + + jupyter lab + +Both can be installed with pip or conda: + + pip install jupyterlab + conda install jupyterlab + +For helping develop HoloViews see the [developer guide](developer_guide/index). diff --git a/doc/install.rst b/doc/install.rst deleted file mode 100644 index 96cb2c360f..0000000000 --- a/doc/install.rst +++ /dev/null @@ -1,94 +0,0 @@ -Installing HoloViews -==================== - -The quickest and easiest way to get the latest version of all the -recommended packages for working with HoloViews on Linux, Windows, or -Mac systems is via the -`conda `_ command provided by -the -`Anaconda `_ or -`Miniconda `_ scientific -Python distributions:: - - conda install -c pyviz holoviews - -This recommended installation includes the default `Matplotlib -`_ plotting library backend, the -more interactive `Bokeh `_ plotting library -backend, and the `Jupyter Notebook `_. - -A similar set of packages can be installed using ``pip``, if that -command is available on your system:: - - pip install "holoviews[recommended]" - -``pip`` also supports other installation options, including a minimal -install of only the packages necessary to generate and manipulate -HoloViews objects without visualization:: - - pip install holoviews - -This minimal install includes only three required libraries `Param -`_, `Numpy `_ and, -`pandas `_, which makes it very easy to -integrate HoloViews into your workflow or as part of another project. - -Alternatively, you can ask ``pip`` to install a larger set of -packages that provide additional functionality in HoloViews:: - - pip install "holoviews[examples]" - -This option installs all the required and recommended packages, in -addition to all all libraries required for running all the examples. - -Lastly, to get *everything* including the test dependencies, you can use:: - - pip install "holoviews[all]" - -Between releases, development snapshots are made available on conda and -can be installed using:: - - conda install -c pyviz/label/dev holoviews - -To get the very latest development version using ``pip``, you can use:: - - pip install git+https://github.com/holoviz/holoviews.git - -The alternative approach using git archive (e.g ``pip install -https://github.com/holoviz/holoviews/archive/main.zip``) is *not* -recommended as you will have incomplete version strings. - -Anyone interested in following development can get the very latest -version by cloning the git repository:: - - git clone https://github.com/holoviz/holoviews.git - -To make this code available for import you then need to run:: - - python setup.py develop - -And you can then update holoviews at any time to the latest version by -running:: - - git pull - -Once you've installed HoloViews, you can get started by launching -Jupyter Notebook:: - - jupyter notebook - -To work with JupyterLab>2.0 you won't need to install anything else, -however for older versions you should also install the PyViz -extension:: - - jupyter labextension install @pyviz/jupyterlab_pyviz - -Once you have installed JupyterLab and the extension launch it with:: - - jupyter lab - -Now that you are set up you can get a copy of all the examples shown -on this website:: - - holoviews --install-examples - cd holoviews-examples diff --git a/doc/user_guide/Configuring.md b/doc/user_guide/Configuring.md new file mode 100644 index 0000000000..abe36bb88b --- /dev/null +++ b/doc/user_guide/Configuring.md @@ -0,0 +1,60 @@ +# Configuring Holoviews + +## `hv.config` settings + +The default HoloViews installation will use the latest defaults and options available, which is appropriate for new users. +If you want to work with code written for older HoloViews versions, you can use the top-level `hv.config` object to control various backwards-compatibility options: + +- `future_deprecations`: Enables warnings about future deprecations. +- `warn_options_call`: Warn when using the to-be-deprecated `__call__` syntax for specifying options, instead of the recommended `.opts` method. + +It is recommended you set `warn_options_call` to `True` in your holoviews.rc file (see section below). + +It is possible to set the configuration using `hv.config` directly: + +```python +import holoviews as hv + +hv.config(future_deprecations=True) +``` + +However, because in some cases this configuration needs to be declared before the plotting extensions are imported, the recommended way of setting configuration options is: + +```python +hv.extension("bokeh", config=dict(future_deprecations=True)) +``` + +In addition to backwards-compatibility options, `hv.config` holds some global options: + +- `image_rtol`: The tolerance used to enforce regular sampling for regular, gridded data. Used to validate `Image` data. + +This option allows you to set the `rtol` parameter of [`Image`](../reference/elements/bokeh/Image.ipynb) elements globally. + +## Improved tab-completion + +Both `Layout` and `Overlay` are designed around convenient tab-completion, with the expectation of upper-case names being listed first. + +```python +import holoviews as hv + +hv.extension(case_sensitive_completion=True) +``` + +## The holoviews.rc file + +HoloViews searches for the first rc file it finds in the following places (in order): + +1. `holoviews.rc` in the parent directory of the top-level `__init__.py` file (useful for developers working out of the HoloViews git repo) +2. `~/.holoviews.rc` +3. `~/.config/holoviews/holoviews.rc` + +The rc file location can be overridden via the `HOLOVIEWSRC` environment variable. + +The rc file is a Python script, executed as HoloViews is imported. An example rc file to include various options discussed above might look like this: + +```python +import holoviews as hv + +hv.config(warn_options_call=True) +hv.extension.case_sensitive_completion = True +``` diff --git a/doc/user_guide/Installing_and_Configuring.rst b/doc/user_guide/Installing_and_Configuring.rst deleted file mode 100644 index 35187406eb..0000000000 --- a/doc/user_guide/Installing_and_Configuring.rst +++ /dev/null @@ -1,5 +0,0 @@ -Installing and Configuring HoloViews -____________________________________ - -.. notebook:: holoviews ../../examples/user_guide/Installing_and_Configuring.ipynb - :offset: 1 diff --git a/doc/user_guide/index.rst b/doc/user_guide/index.rst index 616965873c..252b653934 100644 --- a/doc/user_guide/index.rst +++ b/doc/user_guide/index.rst @@ -78,8 +78,8 @@ Supplementary guides These guides provide detail about specific additional features in HoloViews: -`Installing and Configuring HoloViews `_ - Additional information about installation and configuration options. +`Configuring HoloViews `_ + Information about configuration options. `Customizing Plots `_ How to customize plots including their titles, axis labels, ranges, ticks and more. diff --git a/examples/README.md b/examples/README.md index e4d1b36b69..c8a747e9d0 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,16 +1,16 @@ # Examples This directory contains all the notebooks built as part of the -[holoviews.org](http://holoviews.org) website. +[holoviews.org](https://holoviews.org) website. ## Directory structure - `assets`: Files used by the examples. -- `gallery`: Examples shown on the [gallery page](http://holoviews.org/gallery/index.html). -- `getting_started`: Notebooks used in the [getting started](http://holoviews.org/getting_started/index.html) guide. -- `reference`: Notebooks shown in the website [reference gallery](http://holoviews.org/reference/index.html) -- `topics`: Notebooks shown in the [showcase](http://holoviews.org/reference/showcase/index.html) -- `user_guide`: Notebooks used in the [user guide](http://holoviews.org/user_guide/index.html). +- `gallery`: Examples shown on the [gallery page](https://holoviews.org/gallery/index.html). +- `getting_started`: Notebooks used in the [getting started](https://holoviews.org/getting_started/index.html) guide. +- `reference`: Notebooks shown in the website [reference gallery](https://holoviews.org/reference/index.html) +- `topics`: Notebooks shown in the [showcase](https://holoviews.org/reference/showcase/index.html) +- `user_guide`: Notebooks used in the [user guide](https://holoviews.org/user_guide/index.html). ## Contributing to examples diff --git a/examples/user_guide/Installing_and_Configuring.ipynb b/examples/user_guide/Installing_and_Configuring.ipynb deleted file mode 100644 index fd002182d8..0000000000 --- a/examples/user_guide/Installing_and_Configuring.ipynb +++ /dev/null @@ -1,175 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Installing and Configuring Holoviews" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "HoloViews can be installed on any platform where [NumPy](http://numpy.org) and Python 3 are available.\n", - "\n", - "That said, HoloViews is designed to work closely with many other libraries, which can make installation and configuration more complicated. This user guide page describes some of these less-common or not-required options that may be helpful for some users." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Other installation options\n", - "\n", - "The main [installation instructions](http://holoviews.org/#installation) should be sufficient for most users, but you may also want the [Matplotlib](http://matplotlib.org) and [Plotly](https://plot.ly/python/) backends, which are required for some of the examples:\n", - "\n", - " conda install matplotlib plotly\n", - "\n", - "HoloViews can also be installed using one of these `pip` commands:\n", - "\n", - " pip install holoviews\n", - " pip install 'holoviews[recommended]'\n", - " pip install 'holoviews[extras]'\n", - " pip install 'holoviews[all]'\n", - "\n", - "The first option installs just the bare library and the [NumPy](http://numpy.org) and [Param](https://github.com/holoviz/param) libraries, which is all you need on your system to generate and work with HoloViews objects without visualizing them. The other options install additional libraries that are often useful, with the `recommended` option being similar to the `conda` install command above.\n", - "\n", - "Between releases, development snapshots are made available as conda packages:\n", - "\n", - " conda install -c pyviz/label/dev holoviews\n", - "\n", - "To get the very latest development version you can clone our git\n", - "repository and put it on the Python path:\n", - "\n", - " git clone https://github.com/holoviz/holoviews.git\n", - " cd holoviews\n", - " pip install -e ." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## JupyterLab configuration\n", - "\n", - "To work with JupyterLab you will also need the HoloViews JupyterLab\n", - "extension:\n", - "\n", - "```\n", - "conda install -c conda-forge jupyterlab\n", - "jupyter labextension install @pyviz/jupyterlab_pyviz\n", - "```\n", - "\n", - "Once you have installed JupyterLab and the extension launch it with:\n", - "\n", - "```\n", - "jupyter-lab\n", - "```" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## ``hv.config`` settings\n", - "\n", - "The default HoloViews installation will use the latest defaults and options available, which is appropriate for new users. If you want to work with code written for older HoloViews versions, you can use the top-level ``hv.config`` object to control various backwards-compatibility options:\n", - "\n", - "* ``future_deprecations``: Enables warnings about future deprecations (introduced in 1.11).\n", - "* ``warn_options_call``: Warn when using the to-be-deprecated ``__call__`` syntax for specifying options, instead of the recommended ``.opts`` method.\n", - "\n", - "It is recommended you set ``warn_options_call`` to ``True`` in your holoviews.rc file (see section below).\n", - "\n", - "It is possible to set the configuration using `hv.config` directly:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import holoviews as hv\n", - "hv.config(future_deprecations=True)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "However, because in some cases this configuration needs to be declared before the plotting extensions are imported, the recommended way of setting configuration options is:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "hv.extension('bokeh', config=dict(future_deprecations=True))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "In addition to backwards-compatibility options, ``hv.config`` holds some global options:\n", - "\n", - "* ``image_rtol``: The tolerance used to enforce regular sampling for regular, gridded data. Used to validate ``Image`` data.\n", - "\n", - "This option allows you to set the ``rtol`` parameter of [``Image``](../reference/elements/bokeh/Image.ipynb) elements globally.\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Improved tab-completion\n", - "\n", - "Both ``Layout`` and ``Overlay`` are designed around convenient tab-completion, with the expectation of upper-case names being listed first. In recent versions of Jupyter/IPython there has been a regression whereby the tab-completion is no longer case-sensitive. This can be fixed with:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import holoviews as hv\n", - "hv.extension(case_sensitive_completion=True)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## The holoviews.rc file\n", - "\n", - "HoloViews searches for the first rc file it finds in the following places (in order): \n", - "\n", - "1. ``holoviews.rc`` in the parent directory of the top-level ``__init__.py`` file (useful for developers working out of the HoloViews git repo)\n", - "2. ``~/.holoviews.rc``\n", - "3. ``~/.config/holoviews/holoviews.rc``\n", - "\n", - "The rc file location can be overridden via the ``HOLOVIEWSRC`` environment variable.\n", - "\n", - "The rc file is a Python script, executed as HoloViews is imported. An example rc file to include various options discussed above might look like this:\n", - "\n", - "```\n", - "import holoviews as hv\n", - "hv.config(warn_options_call=True)\n", - "hv.extension.case_sensitive_completion=True\n", - "```\n" - ] - } - ], - "metadata": { - "language_info": { - "name": "python", - "pygments_lexer": "ipython3" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/holoviews/core/data/interface.py b/holoviews/core/data/interface.py index cdd3e151a2..9758554f05 100644 --- a/holoviews/core/data/interface.py +++ b/holoviews/core/data/interface.py @@ -178,7 +178,7 @@ def cast(cls, datasets, datatype=None, cast_type=None): @classmethod def error(cls): info = dict(interface=cls.__name__) - url = "http://holoviews.org/user_guide/%s_Datasets.html" + url = "https://holoviews.org/user_guide/%s_Datasets.html" if cls.multi: datatype = 'a list of tabular' info['url'] = url % 'Tabular' diff --git a/holoviews/core/pprint.py b/holoviews/core/pprint.py index 03a8993cc1..c3eee05c1f 100644 --- a/holoviews/core/pprint.py +++ b/holoviews/core/pprint.py @@ -215,8 +215,8 @@ def target_info(cls, obj, ansi=False): @classmethod def object_info(cls, obj, name, backend, ansi=False): element = not getattr(obj, '_deep_indexable', False) - element_url ='http://holoviews.org/reference/elements/{backend}/{obj}.html' - container_url ='http://holoviews.org/reference/containers/{backend}/{obj}.html' + element_url ='https://holoviews.org/reference/elements/{backend}/{obj}.html' + container_url ='https://holoviews.org/reference/containers/{backend}/{obj}.html' url = element_url if element else container_url link = url.format(obj=name, backend=backend) diff --git a/holoviews/tests/conftest.py b/holoviews/tests/conftest.py index 7d79f2379b..6ab08c282e 100644 --- a/holoviews/tests/conftest.py +++ b/holoviews/tests/conftest.py @@ -74,6 +74,7 @@ def bokeh_backend(): @pytest.fixture def mpl_backend(): + pytest.importorskip("matplotlib") hv.renderer("matplotlib") prev_backend = hv.Store.current_backend hv.Store.current_backend = "matplotlib" @@ -83,6 +84,7 @@ def mpl_backend(): @pytest.fixture def plotly_backend(): + pytest.importorskip("plotly") hv.renderer("plotly") prev_backend = hv.Store.current_backend hv.Store.current_backend = "plotly" diff --git a/holoviews/tests/core/data/test_ibisinterface.py b/holoviews/tests/core/data/test_ibisinterface.py index 2b488ec7a6..907b038f39 100644 --- a/holoviews/tests/core/data/test_ibisinterface.py +++ b/holoviews/tests/core/data/test_ibisinterface.py @@ -10,9 +10,10 @@ import numpy as np import pandas as pd +from packaging.version import Version from holoviews.core.data import Dataset -from holoviews.core.data.ibis import IbisInterface +from holoviews.core.data.ibis import IbisInterface, ibis_version from holoviews.core.spaces import HoloMap from .base import HeterogeneousColumnTests, InterfaceTests, ScalarColumnTests @@ -157,18 +158,20 @@ def test_dataset_add_dimensions_values_ht(self): raise SkipTest("Not supported") def test_dataset_dataset_ht_dtypes(self): + int_dtype = "int64" if ibis_version() >= Version("9.0") else "int32" ds = self.table self.assertEqual(ds.interface.dtype(ds, "Gender"), np.dtype("object")) - self.assertEqual(ds.interface.dtype(ds, "Age"), np.dtype("int32")) - self.assertEqual(ds.interface.dtype(ds, "Weight"), np.dtype("int32")) + self.assertEqual(ds.interface.dtype(ds, "Age"), np.dtype(int_dtype)) + self.assertEqual(ds.interface.dtype(ds, "Weight"), np.dtype(int_dtype)) self.assertEqual(ds.interface.dtype(ds, "Height"), np.dtype("float64")) def test_dataset_dtypes(self): + int_dtype = "int64" if ibis_version() >= Version("9.0") else "int32" self.assertEqual( - self.dataset_hm.interface.dtype(self.dataset_hm, "x"), np.dtype("int32") + self.dataset_hm.interface.dtype(self.dataset_hm, "x"), np.dtype(int_dtype) ) self.assertEqual( - self.dataset_hm.interface.dtype(self.dataset_hm, "y"), np.dtype("int32") + self.dataset_hm.interface.dtype(self.dataset_hm, "y"), np.dtype(int_dtype) ) def test_dataset_reduce_ht(self): diff --git a/holoviews/tests/core/test_options.py b/holoviews/tests/core/test_options.py index 3df5c6e243..3ba86ad92b 100644 --- a/holoviews/tests/core/test_options.py +++ b/holoviews/tests/core/test_options.py @@ -1,5 +1,7 @@ +import contextlib import os import pickle +from unittest import SkipTest import numpy as np import pytest @@ -24,13 +26,17 @@ options_policy, ) from holoviews.element.comparison import ComparisonTestCase +from holoviews.plotting import bokeh # noqa: F401 Options.skip_invalid = False # Needed a backend to register backend and options -from holoviews.plotting import mpl # noqa -from holoviews.plotting import bokeh # noqa -from holoviews.plotting import plotly # noqa +try: + from holoviews.plotting import mpl +except ImportError: + mpl = None +with contextlib.suppress(ImportError): + from holoviews.plotting import plotly # noqa : F401 class TestOptions(ComparisonTestCase): @@ -283,6 +289,8 @@ class TestStoreInheritanceDynamic(ComparisonTestCase): """ def setUp(self): + if mpl is None: + raise SkipTest("Matplotlib required to test Store inheritance") self.backend = 'matplotlib' Store.set_current_backend(self.backend) options = Store.options() @@ -471,6 +479,8 @@ class TestStoreInheritance(ComparisonTestCase): """ def setUp(self): + if mpl is None: + raise SkipTest("Matplotlib required to test Store inheritance") self.backend = 'matplotlib' Store.set_current_backend(self.backend) self.store_copy = OptionTree(sorted(Store.options().items()), @@ -554,6 +564,8 @@ def test_style_transfer(self): class TestOptionsMethod(ComparisonTestCase): def setUp(self): + if mpl is None: + raise SkipTest("Matplotlib required to test Store inheritance") self.backend = 'matplotlib' Store.set_current_backend(self.backend) self.store_copy = OptionTree(sorted(Store.options().items()), @@ -605,6 +617,8 @@ def test_plot_options_object_list(self): class TestOptsMethod(ComparisonTestCase): def setUp(self): + if mpl is None: + raise SkipTest("Matplotlib required to test Store inheritance") self.backend = 'matplotlib' Store.set_current_backend(self.backend) self.store_copy = OptionTree(sorted(Store.options().items()), @@ -767,6 +781,8 @@ class TestCrossBackendOptions(ComparisonTestCase): """ def setUp(self): + if mpl is None: + raise SkipTest("Matplotlib required to test Store inheritance") # Some tests require that plotly isn't loaded self.plotly_options = Store._options.pop('plotly', None) self.store_mpl = OptionTree( @@ -906,9 +922,12 @@ class TestLookupOptions(ComparisonTestCase): def test_lookup_options_honors_backend(self): points = Points([[1, 2], [3, 4]]) - import holoviews.plotting.bokeh - import holoviews.plotting.mpl - import holoviews.plotting.plotly # noqa + try: + import holoviews.plotting.bokeh + import holoviews.plotting.mpl + import holoviews.plotting.plotly # noqa + except ImportError: + raise SkipTest("Matplotlib or Plotly not installed") backends = Store.loaded_backends() @@ -952,6 +971,8 @@ class TestCrossBackendOptionSpecification(ComparisonTestCase): """ def setUp(self): + if mpl is None: + raise SkipTest("Matplotlib required to test Store inheritance") # Some tests require that plotly isn't loaded self.plotly_options = Store._options.pop('plotly', None) self.store_mpl = OptionTree( diff --git a/holoviews/tests/core/test_storeoptions.py b/holoviews/tests/core/test_storeoptions.py index 29a4c67aef..0355386da6 100644 --- a/holoviews/tests/core/test_storeoptions.py +++ b/holoviews/tests/core/test_storeoptions.py @@ -2,13 +2,19 @@ Unit tests of the StoreOptions class used to control custom options on Store as used by the %opts magic. """ +from unittest import SkipTest + import numpy as np from holoviews import Curve, HoloMap, Image, Overlay from holoviews.core.options import Store, StoreOptions from holoviews.element.comparison import ComparisonTestCase -from holoviews.plotting import mpl # noqa Register backend +from holoviews.plotting import bokeh # noqa: F401 +try: + from holoviews.plotting import mpl +except ImportError: + mpl = None class TestStoreOptionsMerge(ComparisonTestCase): @@ -43,6 +49,9 @@ class TestStoreOptsMethod(ComparisonTestCase): """ def setUp(self): + if mpl is None: + raise SkipTest("Matplotlib required to test Store inheritance") + Store.current_backend = 'matplotlib' def test_overlay_options_partitioned(self): diff --git a/holoviews/tests/ipython/test_displayhooks.py b/holoviews/tests/ipython/test_displayhooks.py index 086bbb9b79..cf1a7ba3ed 100644 --- a/holoviews/tests/ipython/test_displayhooks.py +++ b/holoviews/tests/ipython/test_displayhooks.py @@ -1,3 +1,7 @@ +import pytest + +pytest.importorskip("IPython") + from holoviews import Curve, Store from holoviews.ipython import IPTestCase, notebook_extension diff --git a/holoviews/tests/ipython/test_notebooks.py b/holoviews/tests/ipython/test_notebooks.py index fe31f6dbda..d23e5d0b6c 100644 --- a/holoviews/tests/ipython/test_notebooks.py +++ b/holoviews/tests/ipython/test_notebooks.py @@ -3,6 +3,10 @@ """ import os +import pytest + +pytest.importorskip("nbconvert") + import nbconvert import nbformat diff --git a/holoviews/tests/operation/test_operation.py b/holoviews/tests/operation/test_operation.py index 763c5f81ad..942ea6de1b 100644 --- a/holoviews/tests/operation/test_operation.py +++ b/holoviews/tests/operation/test_operation.py @@ -1,5 +1,6 @@ import datetime as dt -from unittest import skipIf +from importlib.util import find_spec +from unittest import SkipTest, skipIf import numpy as np import pandas as pd @@ -15,6 +16,7 @@ except ImportError: ibis = None + from holoviews import ( Area, Contours, @@ -44,6 +46,7 @@ transform, ) +mpl = find_spec("matplotlib") da_skip = skipIf(da is None, "dask.array is not available") ibis_skip = skipIf(ibis is None, "ibis is not available") @@ -127,6 +130,9 @@ def test_image_contours_no_range(self): self.assertEqual(op_contours, contour) def test_image_contours_x_datetime(self): + if mpl is None: + raise SkipTest("Matplotlib required to test datetime axes") + x = np.array(['2023-09-01', '2023-09-03', '2023-09-05'], dtype='datetime64') y = [14, 15] z = np.array([[0, 1, 0], [0, 1, 0]]) @@ -150,6 +156,8 @@ def test_image_contours_x_datetime(self): np.testing.assert_array_almost_equal(op_contours.dimension_values('z'), [0.5]*5) def test_image_contours_y_datetime(self): + if mpl is None: + raise SkipTest("Matplotlib required to test datetime axes") x = [14, 15, 16] y = np.array(['2023-09-01', '2023-09-03'], dtype='datetime64') z = np.array([[0, 1, 0], [0, 1, 0]]) @@ -174,6 +182,8 @@ def test_image_contours_y_datetime(self): np.testing.assert_array_almost_equal(op_contours.dimension_values('z'), [0.5]*5) def test_image_contours_xy_datetime(self): + if mpl is None: + raise SkipTest("Matplotlib required to test datetime axes") x = np.array(['2023-09-01', '2023-09-03', '2023-09-05'], dtype='datetime64') y = np.array(['2023-10-07', '2023-10-08'], dtype='datetime64') z = np.array([[0, 1, 0], [0, 1, 0]]) @@ -204,6 +214,8 @@ def test_image_contours_xy_datetime(self): np.testing.assert_array_almost_equal(op_contours.dimension_values('z'), [0.5]*5) def test_image_contours_z_datetime(self): + if mpl is None: + raise SkipTest("Matplotlib required to test datetime axes") z = np.array([['2023-09-10', '2023-09-10'], ['2023-09-10', '2023-09-12']], dtype='datetime64') img = Image(z) op_contours = contours(img, levels=[np.datetime64('2023-09-11')]) diff --git a/holoviews/tests/plotting/bokeh/test_callbacks.py b/holoviews/tests/plotting/bokeh/test_callbacks.py index 889740757c..29d7c63a70 100644 --- a/holoviews/tests/plotting/bokeh/test_callbacks.py +++ b/holoviews/tests/plotting/bokeh/test_callbacks.py @@ -467,13 +467,14 @@ def test_msg_with_base64_array(): assert np.equal(data_expected, data_after).all() +@pytest.mark.usefixtures('bokeh_backend') def test_rangexy_multi_yaxes(): c1 = Curve(np.arange(100).cumsum(), vdims='y') c2 = Curve(-np.arange(100).cumsum(), vdims='y2') RangeXY(source=c1) RangeXY(source=c2) - overlay = (c1 * c2).opts(backend='bokeh', multi_y=True) + overlay = (c1 * c2).opts(multi_y=True) plot = bokeh_server_renderer.get_plot(overlay) p1, p2 = plot.subplots.values() diff --git a/holoviews/tests/plotting/bokeh/test_renderer.py b/holoviews/tests/plotting/bokeh/test_renderer.py index 2e69a49906..8282bc9409 100644 --- a/holoviews/tests/plotting/bokeh/test_renderer.py +++ b/holoviews/tests/plotting/bokeh/test_renderer.py @@ -17,6 +17,7 @@ from holoviews.streams import Stream +@pytest.mark.usefixtures("bokeh_backend") class BokehRendererTest(ComparisonTestCase): def setUp(self): diff --git a/holoviews/tests/plotting/bokeh/test_utils.py b/holoviews/tests/plotting/bokeh/test_utils.py index 927ffa7101..0136d9f27a 100644 --- a/holoviews/tests/plotting/bokeh/test_utils.py +++ b/holoviews/tests/plotting/bokeh/test_utils.py @@ -82,7 +82,7 @@ def test_glyph_order(self): ['scatter', 'patch']) self.assertEqual(order, ['scatter_1', 'patch_1', 'rect_1']) - +@pytest.mark.usefixtures("bokeh_backend") @pytest.mark.parametrize( "figure_index,expected", [ diff --git a/holoviews/tests/plotting/matplotlib/__init__.py b/holoviews/tests/plotting/matplotlib/__init__.py index e69de29bb2..5b27c23897 100644 --- a/holoviews/tests/plotting/matplotlib/__init__.py +++ b/holoviews/tests/plotting/matplotlib/__init__.py @@ -0,0 +1,3 @@ +import pytest + +pytest.importorskip("matplotlib") diff --git a/holoviews/tests/plotting/plotly/__init__.py b/holoviews/tests/plotting/plotly/__init__.py index e69de29bb2..f27f7a60e7 100644 --- a/holoviews/tests/plotting/plotly/__init__.py +++ b/holoviews/tests/plotting/plotly/__init__.py @@ -0,0 +1,3 @@ +import pytest + +pytest.importorskip("plotly") diff --git a/holoviews/tests/plotting/test_plotutils.py b/holoviews/tests/plotting/test_plotutils.py index 305296f8a3..1f402691ad 100644 --- a/holoviews/tests/plotting/test_plotutils.py +++ b/holoviews/tests/plotting/test_plotutils.py @@ -1,8 +1,8 @@ - import numpy as np +import pytest from holoviews import Dimension, NdOverlay, Overlay -from holoviews.core.options import Cycle, Store +from holoviews.core.options import Cycle from holoviews.core.spaces import DynamicMap, HoloMap from holoviews.element import ( Area, @@ -31,8 +31,6 @@ ) from holoviews.streams import PointerX -bokeh_renderer = Store.renderers['bokeh'] - class TestOverlayableZorders(ComparisonTestCase): @@ -472,11 +470,9 @@ def test_process_cmap_invalid_type(self): process_cmap({'A', 'B', 'C'}, 3) +@pytest.mark.usefixtures("mpl_backend") class TestMPLColormapUtils(ComparisonTestCase): - def setUp(self): - import holoviews.plotting.mpl # noqa - def test_mpl_colormap_fire(self): colors = process_cmap('fire', 3, provider='matplotlib') self.assertEqual(colors, ['#000000', '#ed1400', '#ffffff']) @@ -535,12 +531,9 @@ def test_mpl_colormap_perceptually_uniform_reverse(self): self.assertEqual(colors, ['#440154', '#30678d', '#35b778', '#fde724'][::-1]) +@pytest.mark.usefixtures("bokeh_backend") class TestBokehPaletteUtils(ComparisonTestCase): - def setUp(self): - import bokeh.palettes # noqa - import holoviews.plotting.bokeh # noqa - def test_bokeh_palette_categorical_palettes_not_interpolated(self): # Ensure categorical palettes are not expanded categorical = ('accent', 'category20', 'dark2', 'colorblind', 'pastel1', diff --git a/holoviews/tests/test_selection.py b/holoviews/tests/test_selection.py index 570491b092..2512a218a2 100644 --- a/holoviews/tests/test_selection.py +++ b/holoviews/tests/test_selection.py @@ -1,4 +1,4 @@ -from unittest import skip, skipIf +from unittest import SkipTest, skip, skipIf import pandas as pd import panel as pn @@ -703,7 +703,10 @@ class TestLinkSelectionsPlotly(TestLinkSelections): __test__ = True def setUp(self): - import holoviews.plotting.plotly # noqa + try: + import holoviews.plotting.plotly # noqa: F401 + except ImportError: + raise SkipTest("Plotly required to test plotly backend") super().setUp() self._backend = Store.current_backend Store.set_current_backend('plotly') diff --git a/holoviews/tests/util/test_help.py b/holoviews/tests/util/test_help.py index 6a596a8e34..9b19751686 100644 --- a/holoviews/tests/util/test_help.py +++ b/holoviews/tests/util/test_help.py @@ -1,8 +1,11 @@ +import pytest + import holoviews as hv +@pytest.mark.usefixtures("bokeh_backend") def test_help_pattern(capsys): - import holoviews.plotting.bokeh # noqa + pytest.importorskip("IPython") hv.help(hv.Curve, pattern='border') captured = capsys.readouterr() assert '\x1b[43;1;30mborder\x1b[0m' in captured.out diff --git a/holoviews/tests/util/test_utils.py b/holoviews/tests/util/test_utils.py index 824031b131..4355ebcb46 100644 --- a/holoviews/tests/util/test_utils.py +++ b/holoviews/tests/util/test_utils.py @@ -8,7 +8,7 @@ from holoviews import Store from holoviews.core.options import OptionTree from holoviews.element.comparison import ComparisonTestCase -from holoviews.plotting import bokeh, mpl +from holoviews.plotting import bokeh from holoviews.util import Options, OutputSettings, opts, output BACKENDS = ['matplotlib', 'bokeh'] @@ -20,12 +20,20 @@ except ImportError: notebook = None +try: + from holoviews.plotting import mpl +except ImportError: + mpl = None + + class TestOutputUtil(ComparisonTestCase): def setUp(self): if notebook is None: raise SkipTest("Jupyter Notebook not available") + if mpl is None: + raise SkipTest("Matplotlib not available") from holoviews.ipython import notebook_extension notebook_extension(*BACKENDS) @@ -74,6 +82,8 @@ class TestOptsUtil(LoggingComparisonTestCase): """ def setUp(self): + if mpl is None: + raise SkipTest("Matplotlib not available") self.backend = Store.current_backend Store.current_backend = 'matplotlib' self.store_copy = OptionTree(sorted(Store.options().items()), diff --git a/pixi.toml b/pixi.toml index acb8bf5d9e..e50865b22b 100644 --- a/pixi.toml +++ b/pixi.toml @@ -1,6 +1,6 @@ [project] name = "holoviews" -channels = ["conda-forge", "pyviz/label/dev"] +channels = ["pyviz/label/dev", "conda-forge"] platforms = ["linux-64", "osx-arm64", "osx-64", "win-64"] [tasks] @@ -20,20 +20,20 @@ build = ["py311", "build"] lint = ["py311", "lint"] [dependencies] -python = ">=3.9" -pip= "*" -param = ">=1.12.0,<3.0" -panel = ">=1.0" -pyviz_comms = ">=2.1" colorcet = "*" numpy = ">=1.0" packaging = "*" pandas = ">=0.20.0" +panel = ">=1.0" +param = ">=1.12.0,<3.0" +pip= "*" +pyviz_comms = ">=2.1" # Recommended +bokeh = ">=3.1" ipython = ">=5.4.0" -notebook = "*" matplotlib-base = ">=3" -bokeh = ">=3.1" +notebook = "*" +plotly = ">=4.0" [feature.py39.dependencies] python = "3.9.*" @@ -48,23 +48,21 @@ python = "3.11.*" python = "3.12.*" [feature.example.dependencies] -networkx = "*" -pillow = "*" -xarray = ">=0.10.4" -plotly = ">=4.0" -# dash >=1.16 -streamz = ">=0.5.0" -ffmpeg = "*" cftime = "*" -netcdf4 = "*" dask-core = "*" +datashader = ">=0.11.1" +ffmpeg = "*" +netcdf4 = "*" +networkx = "*" +notebook = "*" +pillow = "*" +pooch = "*" +pyarrow = "*" +scikit-image = "*" scipy = "*" shapely = "*" -scikit-image = "*" -pyarrow = "*" -pooch = "*" -datashader = ">=0.11.1" -notebook = ">=7.0" +streamz = ">=0.5.0" +xarray = ">=0.10.4" # ============================================= # =================== TESTS =================== @@ -74,28 +72,27 @@ pytest = "*" pytest-cov = "*" pytest-github-actions-annotate-failures = "*" pytest-rerunfailures = "*" -nbconvert = "*" -pillow = "*" -plotly = ">=4.0" -contourpy = "*" -[feature.test-unit-task.tasks] -test-unit = 'pytest holoviews' # So it not showing up for UI tests +[feature.test-unit-task.tasks] # So it is not showing up in the test-ui environment +test-unit = 'pytest holoviews/tests' [feature.test.dependencies] +cftime = "*" +contourpy = "*" +# dash >=1.16 dask-core = "*" +datashader = ">=0.11.1" +ffmpeg = "*" ibis-sqlite = "*" -xarray = ">=0.10.4" +nbconvert = "*" networkx = "*" -shapely = "*" -ffmpeg = "*" -cftime = "*" +pillow = "*" scipy = ">=1.10" # Python 3.9 + Windows downloads 1.9 selenium = "*" +shapely = "*" spatialpandas = "*" -datashader = ">=0.11.1" +xarray = ">=0.10.4" xyzservices = "*" -# dash >=1.16 [feature.test.target.unix.dependencies] tsdownsample = "*" # currently not available on Windows @@ -108,41 +105,42 @@ nbval = "*" pytest-xdist = "*" [feature.test-ui] -channels = ["conda-forge", "pyviz/label/dev", "microsoft"] +channels = ["pyviz/label/dev", "microsoft", "conda-forge"] [feature.test-ui.dependencies] playwright = "*" pytest-playwright = "*" [feature.test-ui.tasks] -install-ui = 'playwright install chromium' +_install-ui = 'playwright install chromium' [feature.test-ui.tasks.test-ui] cmd = 'pytest holoviews/tests/ui --ui --browser chromium' -depends_on = ["install-ui"] +depends_on = ["_install-ui"] # ============================================= # =================== DOCS ==================== # ============================================= [feature.doc.dependencies] -nbsite = ">=0.8.4,<0.9.0" graphviz = "*" +nbsite = ">=0.8.4,<0.9.0" pooch = "*" +python-kaleido = "*" selenium = "*" [feature.doc.tasks] -docs-generate-rst = 'nbsite generate-rst --org holoviz --project-name holoviews' -docs-refmanual = 'python ./doc/generate_modules.py holoviews -d ./doc/reference_manual -n holoviews -e tests' -docs-generate = 'nbsite build --what=html --output=builtdocs --org holoviz --project-name holoviews' +_docs-generate-rst = 'nbsite generate-rst --org holoviz --project-name holoviews' +_docs-refmanual = 'python ./doc/generate_modules.py holoviews -d ./doc/reference_manual -n holoviews -e tests' +_docs-generate = 'nbsite build --what=html --output=builtdocs --org holoviz --project-name holoviews' [feature.doc.tasks.docs-build] -depends_on = ['docs-generate-rst', 'docs-refmanual', 'docs-generate'] +depends_on = ['_docs-generate-rst', '_docs-refmanual', '_docs-generate'] # ============================================= # ================== BUILD ==================== # ============================================= [feature.build.dependencies] -build = "*" +python-build = "*" conda-build = "*" [feature.build.tasks] @@ -150,7 +148,7 @@ build-conda = 'bash scripts/conda/build.sh' build-pip = 'python -m build .' # ============================================= -# =================== lint ==================== +# =================== LINT ==================== # ============================================= [feature.lint.dependencies] pre-commit = "*" diff --git a/pyproject.toml b/pyproject.toml index 1090553c9d..9fa06e809f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,11 +43,12 @@ dependencies = [ [project.urls] Homepage = "https://holoviews.org" -Source = "http://github.com/holoviz/holoviews" +Source = "https://github.com/holoviz/holoviews" HoloViz = "https://holoviz.org/" [project.optional-dependencies] -recommended = ["ipython >=5.4.0", "notebook", "matplotlib >=3", "bokeh >=3.1"] +recommended = ["ipython >=5.4.0", "notebook", "matplotlib >=3", "bokeh >=3.1", "plotly >=4.0"] +tests = ["pytest", "pytest-rerunfailures"] [project.scripts] holoviews = "holoviews.util.command:main" @@ -118,7 +119,6 @@ filterwarnings = [ ] [tool.coverage] -run.concurrency = ["greenlet"] omit = ["holoviews/__version.py"] exclude_also = [ "if __name__ == .__main__.:", diff --git a/scripts/conda/recipe/meta.yaml b/scripts/conda/recipe/meta.yaml index ea83e2fd10..5b1968deed 100644 --- a/scripts/conda/recipe/meta.yaml +++ b/scripts/conda/recipe/meta.yaml @@ -10,7 +10,7 @@ source: build: noarch: python - script: {{ PYTHON }} -m pip install -vv {{ project["name"] }}-{{ VERSION }}-py3-none-any.whl + script: {{ PYTHON }} -m pip install --no-deps -vv {{ project["name"] }}-{{ VERSION }}-py3-none-any.whl entry_points: {% for group,epoints in project.get("entry_points",{}).items() %} {% for entry_point in epoints %} diff --git a/scripts/download_data.py b/scripts/download_data.py index 291c50ab83..cb987a4026 100644 --- a/scripts/download_data.py +++ b/scripts/download_data.py @@ -1,13 +1,13 @@ +from contextlib import suppress + import bokeh.sampledata bokeh.sampledata.download() -try: +with suppress(ImportError): import pooch # noqa: F401 import scipy # noqa: F401 import xarray as xr -except ImportError: - pass -else: + xr.tutorial.open_dataset("air_temperature") xr.tutorial.open_dataset("rasm") From c98871ba6d040b92e59277fc3d1e9824b4737b5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Fri, 3 May 2024 09:03:15 -0700 Subject: [PATCH 19/43] Use psutil for pytest-xdist logical option (#6213) --- pixi.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pixi.toml b/pixi.toml index e50865b22b..0b42ef1724 100644 --- a/pixi.toml +++ b/pixi.toml @@ -98,9 +98,10 @@ xyzservices = "*" tsdownsample = "*" # currently not available on Windows [feature.test-example.tasks] -test-example = 'pytest -n auto --dist loadscope --nbval-lax examples' +test-example = 'pytest -n logical --dist loadscope --nbval-lax examples' [feature.test-example.dependencies] +psutil = "*" nbval = "*" pytest-xdist = "*" From bc684e348fa8a6460268c26a43b0d597f289c10a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Sat, 4 May 2024 01:42:37 -0700 Subject: [PATCH 20/43] General maintenance (#6214) --- .github/workflows/nightly_lock.yaml | 4 ++-- .pre-commit-config.yaml | 4 ++-- holoviews/annotators.py | 7 +++---- holoviews/core/accessors.py | 5 ++--- holoviews/core/data/array.py | 2 +- holoviews/core/data/cudf.py | 4 ++-- holoviews/core/data/dictionary.py | 4 ++-- holoviews/core/data/grid.py | 5 ++--- holoviews/core/data/ibis.py | 10 ++++------ holoviews/core/data/interface.py | 8 ++++---- holoviews/core/data/multipath.py | 4 ++-- holoviews/core/data/pandas.py | 6 +++--- holoviews/core/data/spatialpandas.py | 2 +- holoviews/core/data/xarray.py | 20 ++++++++------------ holoviews/core/dimension.py | 22 +++++++++------------- holoviews/core/element.py | 3 +-- holoviews/core/io.py | 2 +- holoviews/core/ndmapping.py | 11 ++++------- holoviews/core/options.py | 2 +- holoviews/core/pprint.py | 4 ++-- holoviews/core/spaces.py | 16 ++++++---------- holoviews/core/tree.py | 2 +- holoviews/core/util.py | 5 ++--- holoviews/element/annotation.py | 2 +- holoviews/element/graphs.py | 5 ++--- holoviews/element/sankey.py | 3 +-- holoviews/ipython/__init__.py | 7 +++---- holoviews/ipython/archive.py | 4 ++-- holoviews/operation/datashader.py | 13 ++++++------- holoviews/operation/stats.py | 5 ++--- holoviews/plotting/bokeh/chart.py | 5 ++--- holoviews/plotting/bokeh/element.py | 13 ++++++------- holoviews/plotting/bokeh/graphs.py | 2 +- holoviews/plotting/bokeh/links.py | 4 ++-- holoviews/plotting/bokeh/plot.py | 12 ++++++------ holoviews/plotting/bokeh/tiles.py | 2 +- holoviews/plotting/bokeh/util.py | 4 ++-- holoviews/plotting/mpl/element.py | 2 +- holoviews/plotting/plot.py | 13 +++++++------ holoviews/plotting/plotly/__init__.py | 3 +-- holoviews/plotting/plotly/plot.py | 8 ++++---- holoviews/plotting/plotly/tiles.py | 2 +- holoviews/plotting/util.py | 10 +++++----- holoviews/selection.py | 6 +++--- holoviews/streams.py | 11 +++++------ holoviews/util/__init__.py | 13 ++++++------- holoviews/util/parser.py | 5 ++--- holoviews/util/settings.py | 11 ++++------- holoviews/util/transform.py | 8 ++++---- 49 files changed, 145 insertions(+), 180 deletions(-) diff --git a/.github/workflows/nightly_lock.yaml b/.github/workflows/nightly_lock.yaml index bea5345e9a..efae867a46 100644 --- a/.github/workflows/nightly_lock.yaml +++ b/.github/workflows/nightly_lock.yaml @@ -18,5 +18,5 @@ jobs: AWS_DEFAULT_REGION: "eu-west-1" PACKAGE: "holoviews" run: | - cp pixi.lock $(date +%Y-%m-%d)-pixi.lock - aws s3 cp ./$(date +%Y-%m-%d)-pixi.lock s3://assets.holoviz.org/lock/$PACKAGE/ + zip $(date +%Y-%m-%d).zip pixi.lock + aws s3 cp ./$(date +%Y-%m-%d).zip s3://assets.holoviz.org/lock/$PACKAGE/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 602951f640..b122f3f2cd 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -5,7 +5,7 @@ exclude: (\.min\.js$|\.svg$|\.html$) default_stages: [commit] repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.5.0 + rev: v4.6.0 hooks: - id: end-of-file-fixer - id: trailing-whitespace @@ -20,7 +20,7 @@ repos: - id: check-json - id: detect-private-key - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.3.4 + rev: v0.4.3 hooks: - id: ruff files: holoviews/|scripts/ diff --git a/holoviews/annotators.py b/holoviews/annotators.py index 3505f93ee2..b31842324b 100644 --- a/holoviews/annotators.py +++ b/holoviews/annotators.py @@ -120,8 +120,7 @@ def compose(cls, *annotators): elif isinstance(annotator, (HoloMap, ViewableElement)): layers.append(annotator) else: - raise ValueError("Cannot compose %s type with annotators." % - type(annotator).__name__) + raise ValueError(f"Cannot compose {type(annotator).__name__} type with annotators.") tables = Overlay(tables, group='Annotator') return (Overlay(layers).collate() + tables) @@ -153,7 +152,7 @@ def __call__(self, element, **params): if annotator_type is None: obj = overlay if isinstance(overlay, Overlay) else element raise ValueError('Could not find an Element to annotate on' - '%s object.' % type(obj).__name__) + f'{type(obj).__name__} object.') if len(layers) == 1: return layers[0] @@ -386,7 +385,7 @@ def _process_element(self, element=None): if validate and len({len(v) for v in poly_data.values()}) != 1: raise ValueError('annotations must refer to value dimensions ' 'which vary per path while at least one of ' - '%s varies by vertex.' % validate) + f'{validate} varies by vertex.') # Add options to element tools = [tool() for tool in self._tools] diff --git a/holoviews/core/accessors.py b/holoviews/core/accessors.py index a6d8b56805..b5b2e15680 100644 --- a/holoviews/core/accessors.py +++ b/holoviews/core/accessors.py @@ -162,11 +162,10 @@ def __call__(self, apply_function, streams=None, link_inputs=True, def apply_function(object, **kwargs): method = getattr(object, method_name, None) if method is None: - raise AttributeError('Applied method %s does not exist.' + raise AttributeError(f'Applied method {method_name} does not exist.' 'When declaring a method to apply ' 'as a string ensure a corresponding ' - 'method exists on the object.' % - method_name) + 'method exists on the object.') return method(*args, **kwargs) if 'panel' in sys.modules: diff --git a/holoviews/core/data/array.py b/holoviews/core/data/array.py index b5a1c0791e..af16e13fbf 100644 --- a/holoviews/core/data/array.py +++ b/holoviews/core/data/array.py @@ -76,7 +76,7 @@ def validate(cls, dataset, vdims=True): ncols = dataset.data.shape[1] if dataset.data.ndim > 1 else 1 if ncols < ndims: raise DataError("Supplied data does not match specified " - "dimensions, expected at least %s columns." % ndims, cls) + f"dimensions, expected at least {ndims} columns.", cls) @classmethod diff --git a/holoviews/core/data/cudf.py b/holoviews/core/data/cudf.py index 455a177183..0c88d4ec2a 100644 --- a/holoviews/core/data/cudf.py +++ b/holoviews/core/data/cudf.py @@ -108,9 +108,9 @@ def init(cls, eltype, data, kdims, vdims): d = dimension_name(d) if len([c for c in columns if c == d]) > 1: raise DataError('Dimensions may not reference duplicated DataFrame ' - 'columns (found duplicate %r columns). If you want to plot ' + f'columns (found duplicate {d!r} columns). If you want to plot ' 'a column against itself simply declare two dimensions ' - 'with the same name. '% d, cls) + 'with the same name.', cls) return data, {'kdims':kdims, 'vdims':vdims}, {} diff --git a/holoviews/core/data/dictionary.py b/holoviews/core/data/dictionary.py index edbd403342..4c2b95e7a0 100644 --- a/holoviews/core/data/dictionary.py +++ b/holoviews/core/data/dictionary.py @@ -124,13 +124,13 @@ def validate(cls, dataset, vdims=True): not_found = [d for d in dimensions if d not in dataset.data] if not_found: raise DataError('Following columns specified as dimensions ' - 'but not found in data: %s' % not_found, cls) + f'but not found in data: {not_found}', cls) lengths = [(dim, 1 if isscalar(dataset.data[dim]) else len(dataset.data[dim])) for dim in dimensions] if len({l for d, l in lengths if l > 1}) > 1: lengths = ', '.join(['%s: %d' % l for l in sorted(lengths)]) raise DataError('Length of columns must be equal or scalar, ' - 'columns have lengths: %s' % lengths, cls) + f'columns have lengths: {lengths}', cls) @classmethod diff --git a/holoviews/core/data/grid.py b/holoviews/core/data/grid.py index 617f07ad12..1fb2e79109 100644 --- a/holoviews/core/data/grid.py +++ b/holoviews/core/data/grid.py @@ -187,7 +187,7 @@ def validate(cls, dataset, vdims=True): if not_found and tuple(not_found) not in dataset.data: raise DataError("Supplied data does not contain specified " "dimensions, the following dimensions were " - "not found: %s" % repr(not_found), cls) + f"not found: {not_found!r}", cls) @classmethod @@ -446,8 +446,7 @@ def groupby(cls, dataset, dim_names, container_type, group_type, **kwargs): invalid = [d for d in dimensions if dataset.data[d.name].ndim > 1] if invalid: if len(invalid) == 1: invalid = f"'{invalid[0]}'" - raise ValueError("Cannot groupby irregularly sampled dimension(s) %s." - % invalid) + raise ValueError(f"Cannot groupby irregularly sampled dimension(s) {invalid}.") # Update the kwargs appropriately for Element group types group_kwargs = {} diff --git a/holoviews/core/data/ibis.py b/holoviews/core/data/ibis.py index bc1268a687..fd3c11ac00 100644 --- a/holoviews/core/data/ibis.py +++ b/holoviews/core/data/ibis.py @@ -102,7 +102,7 @@ def validate(cls, dataset, vdims=True): if not_found: raise DataError("Supplied data does not contain specified " "dimensions, the following dimensions were " - "not found: %s" % repr(not_found), cls) + f"not found: {not_found!r}", cls) @classmethod def compute(cls, dataset): @@ -235,8 +235,7 @@ def _index_ibis_table(cls, data): import ibis if not cls.has_rowid(): raise ValueError( - "iloc expressions are not supported for ibis version %s." - % ibis.__version__ + f"iloc expressions are not supported for ibis version {ibis.__version__}." ) if "hv_row_id__" in data.columns: @@ -358,9 +357,8 @@ def add_dimension(cls, dataset, dimension, dim_pos, values, vdim): data = dataset.data if dimension.name not in data.columns: if not isinstance(values, ibis.Expr) and not np.isscalar(values): - raise ValueError("Cannot assign %s type as a Ibis table column, " - "expecting either ibis.Expr or scalar." - % type(values).__name__) + raise ValueError(f"Cannot assign {type(values).__name__} type as a Ibis table column, " + "expecting either ibis.Expr or scalar.") data = data.mutate(**{dimension.name: values}) return data diff --git a/holoviews/core/data/interface.py b/holoviews/core/data/interface.py index 9758554f05..d5d868d1ac 100644 --- a/holoviews/core/data/interface.py +++ b/holoviews/core/data/interface.py @@ -278,7 +278,7 @@ def validate(cls, dataset, vdims=True): if not_found: raise DataError("Supplied data does not contain specified " "dimensions, the following dimensions were " - "not found: %s" % repr(not_found), cls) + f"not found: {not_found!r}", cls) @classmethod def persist(cls, dataset): @@ -440,7 +440,7 @@ def concatenate(cls, datasets, datatype=None, new_type=None): dimensions, keys = [], [()]*len(datasets) else: raise DataError('Concatenation only supported for NdMappings ' - 'and lists of Datasets, found %s.' % type(datasets).__name__) + f'and lists of Datasets, found {type(datasets).__name__}.') template = datasets[0] datatype = datatype or template.interface.datatype @@ -452,10 +452,10 @@ def concatenate(cls, datasets, datatype=None, new_type=None): datatype = 'grid' if len(datasets) > 1 and not dimensions and cls.interfaces[datatype].gridded: - raise DataError('Datasets with %s datatype cannot be concatenated ' + raise DataError(f'Datasets with {datatype} datatype cannot be concatenated ' 'without defining the dimensions to concatenate along. ' 'Ensure you pass in a NdMapping (e.g. a HoloMap) ' - 'of Dataset types, not a list.' % datatype) + 'of Dataset types, not a list.') datasets = template.interface.cast(datasets, datatype) template = datasets[0] diff --git a/holoviews/core/data/multipath.py b/holoviews/core/data/multipath.py index 22999f5267..6a1b7f0d30 100644 --- a/holoviews/core/data/multipath.py +++ b/holoviews/core/data/multipath.py @@ -276,8 +276,8 @@ def groupby(cls, dataset, dimensions, container_type, group_type, **kwargs): for d in dimensions: if not cls.isscalar(dataset, d, True): raise ValueError('MultiInterface can only apply groupby ' - 'on scalar dimensions, %s dimension ' - 'is not scalar' % d) + f'on scalar dimensions, {d} dimension ' + 'is not scalar') vals = cls.values(dataset, d, False, True) values.append(vals) values = tuple(values) diff --git a/holoviews/core/data/pandas.py b/holoviews/core/data/pandas.py index 2292e084b7..ea552194fc 100644 --- a/holoviews/core/data/pandas.py +++ b/holoviews/core/data/pandas.py @@ -80,9 +80,9 @@ def init(cls, eltype, data, kdims, vdims): d = dimension_name(d) if len([c for c in data.columns if c == d]) > 1: raise DataError('Dimensions may not reference duplicated DataFrame ' - 'columns (found duplicate %r columns). If you want to plot ' + f'columns (found duplicate {d!r} columns). If you want to plot ' 'a column against itself simply declare two dimensions ' - 'with the same name. '% d, cls) + 'with the same name.', cls) else: # Check if data is of non-numeric type # Then use defined data type @@ -185,7 +185,7 @@ def validate(cls, dataset, vdims=True): if not_found: raise DataError("Supplied data does not contain specified " "dimensions, the following dimensions were " - "not found: %s" % repr(not_found), cls) + f"not found: {not_found!r}", cls) @classmethod def range(cls, dataset, dimension): diff --git a/holoviews/core/data/spatialpandas.py b/holoviews/core/data/spatialpandas.py index 1d5e696181..44fc86c96b 100644 --- a/holoviews/core/data/spatialpandas.py +++ b/holoviews/core/data/spatialpandas.py @@ -128,7 +128,7 @@ def validate(cls, dataset, vdims=True): if not_found: raise DataError("Supplied data does not contain specified " "dimensions, the following dimensions were " - "not found: %s" % repr(not_found), cls) + f"not found: {not_found!r}", cls) @classmethod def dtype(cls, dataset, dimension): diff --git a/holoviews/core/data/xarray.py b/holoviews/core/data/xarray.py index c1cad1a16e..4daefd7785 100644 --- a/holoviews/core/data/xarray.py +++ b/holoviews/core/data/xarray.py @@ -108,15 +108,14 @@ def retrieve_unit_and_label(dim): vdim = asdim(vdim_param.default[0]) if vdim.name in data.dims: raise DataError("xarray DataArray does not define a name, " - "and the default of '%s' clashes with a " + f"and the default of '{vdim.name}' clashes with a " "coordinate dimension. Give the DataArray " - "a name or supply an explicit value dimension." - % vdim.name, cls) + "a name or supply an explicit value dimension.", cls) else: raise DataError("xarray DataArray does not define a name " - "and %s does not define a default value " + f"and {eltype.__name__} does not define a default value " "dimension. Give the DataArray a name or " - "supply an explicit vdim." % eltype.__name__, + "supply an explicit vdim.", cls) if not packed: if vdim in data.dims: @@ -213,8 +212,7 @@ def retrieve_unit_and_label(dim): raise TypeError('Data must be be an xarray Dataset type.') elif not_found: raise DataError("xarray Dataset must define coordinates " - "for all defined kdims, %s coordinates not found." - % not_found, cls) + f"for all defined kdims, {not_found} coordinates not found.", cls) for vdim in vdims: if packed: @@ -257,7 +255,7 @@ def validate(cls, dataset, vdims=True): if not_found: raise DataError("Supplied data does not contain specified " "dimensions, the following dimensions were " - "not found: %s" % repr(not_found), cls) + f"not found: {not_found!r}", cls) # Check whether irregular (i.e. multi-dimensional) coordinate # array dimensionality matches @@ -273,8 +271,7 @@ def validate(cls, dataset, vdims=True): raise DataError("The dimensions of coordinate arrays " "on irregular data must match. The " "following kdims were found to have " - "non-matching array dimensions:\n\n%s" - % ('\n'.join(nonmatching)), cls) + "non-matching array dimensions:\n\n{}".format('\n'.join(nonmatching)), cls) @classmethod def compute(cls, dataset): @@ -329,8 +326,7 @@ def groupby(cls, dataset, dimensions, container_type, group_type, **kwargs): invalid = [d for d in index_dims if dataset.data[d.name].ndim > 1] if invalid: if len(invalid) == 1: invalid = f"'{invalid[0]}'" - raise ValueError("Cannot groupby irregularly sampled dimension(s) %s." - % invalid) + raise ValueError(f"Cannot groupby irregularly sampled dimension(s) {invalid}.") group_kwargs = {} if group_type != 'raw' and issubclass(group_type, Element): diff --git a/holoviews/core/dimension.py b/holoviews/core/dimension.py index 59218185f9..99dd590562 100644 --- a/holoviews/core/dimension.py +++ b/holoviews/core/dimension.py @@ -81,10 +81,9 @@ def dimension_name(dimension): elif dimension is None: return None else: - raise ValueError('%s type could not be interpreted as Dimension. ' + raise ValueError(f'{type(dimension).__name__} type could not be interpreted as Dimension. ' 'Dimensions must be declared as a string, tuple, ' - 'dictionary or Dimension type.' - % type(dimension).__name__) + 'dictionary or Dimension type.') def process_dimensions(kdims, vdims): @@ -252,7 +251,7 @@ def __init__(self, spec, **params): except ValueError as exc: raise ValueError( "Dimensions specified as a tuple must be a tuple " - "consisting of the name and label not: %s" % str(spec) + f"consisting of the name and label not: {spec}" ) from exc if 'label' in params and params['label'] != all_params['label']: self.param.warning( @@ -268,9 +267,8 @@ def __init__(self, spec, **params): ) from exc else: raise ValueError( - '%s type could not be interpreted as Dimension. Dimensions must be ' + f'{type(spec).__name__} type could not be interpreted as Dimension. Dimensions must be ' 'declared as a string, tuple, dictionary or Dimension type.' - % type(spec).__name__ ) all_params.update(params) @@ -501,11 +499,9 @@ def __init__(self, data, id=None, plot_id=None, **params): super().__init__(**params) if not util.group_sanitizer.allowable(self.group): - raise ValueError("Supplied group %r contains invalid characters." % - self.group) + raise ValueError(f"Supplied group {self.group!r} contains invalid characters.") elif not util.label_sanitizer.allowable(self.label): - raise ValueError("Supplied label %r contains invalid characters." % - self.label) + raise ValueError(f"Supplied label {self.label!r} contains invalid characters.") @property def id(self): @@ -930,8 +926,8 @@ def dimensions(self, selection='all', label=False): key_traversal = self.traverse(lmbd, **kwargs) dims = [dim for keydims in key_traversal for dim in keydims] else: - raise KeyError("Invalid selection %r, valid selections include" - "'all', 'value' and 'key' dimensions" % repr(selection)) + raise KeyError(f"Invalid selection {repr(selection)!r}, valid selections include" + "'all', 'value' and 'key' dimensions") return [(dim.label if label == 'long' else dim.name) if label else dim for dim in dims] @@ -950,7 +946,7 @@ def get_dimension(self, dimension, default=None, strict=False): if dimension is not None and not isinstance(dimension, (int, str, Dimension)): raise TypeError('Dimension lookup supports int, string, ' 'and Dimension instances, cannot lookup ' - 'Dimensions using %s type.' % type(dimension).__name__) + f'Dimensions using {type(dimension).__name__} type.') all_dims = self.dimensions() if isinstance(dimension, int): if 0 <= dimension < len(all_dims): diff --git a/holoviews/core/element.py b/holoviews/core/element.py index 6a0b9654b0..c463254b9a 100644 --- a/holoviews/core/element.py +++ b/holoviews/core/element.py @@ -75,8 +75,7 @@ def __getitem__(self, key): if key == (): return self else: - raise NotImplementedError("%s currently does not support getitem" % - type(self).__name__) + raise NotImplementedError(f"{type(self).__name__} currently does not support getitem") def __bool__(self): """Indicates whether the element is empty. diff --git a/holoviews/core/io.py b/holoviews/core/io.py index 774238ece4..b88c2a3e73 100644 --- a/holoviews/core/io.py +++ b/holoviews/core/io.py @@ -482,7 +482,7 @@ def collect(self_or_cls, files, drop=None, metadata=True): kval = key[files.get_dimension_index(odim)] if kval != mdata[odim]: raise KeyError("Metadata supplies inconsistent " - "value for dimension %s" % odim) + f"value for dimension {odim}") mkey = tuple(mdata.get(d, None) for d in added_dims) key = mkey if aslist else key + mkey if isinstance(fname, tuple) and len(fname) == 1: diff --git a/holoviews/core/ndmapping.py b/holoviews/core/ndmapping.py index 6541f85528..30545841b6 100644 --- a/holoviews/core/ndmapping.py +++ b/holoviews/core/ndmapping.py @@ -942,8 +942,7 @@ def group(self): @group.setter def group(self, group): if group is not None and not sanitize_identifier.allowable(group): - raise ValueError("Supplied group %s contains invalid " - "characters." % self.group) + raise ValueError(f"Supplied group {self.group} contains invalid characters.") self._group = group @@ -962,8 +961,7 @@ def label(self): @label.setter def label(self, label): if label is not None and not sanitize_identifier.allowable(label): - raise ValueError("Supplied group %s contains invalid " - "characters." % self.group) + raise ValueError(f"Supplied group {self.group} contains invalid characters.") self._label = label @property @@ -991,9 +989,8 @@ def __mul__(self, other, reverse=False): from .overlay import Overlay if isinstance(other, type(self)): if self.kdims != other.kdims: - raise KeyError("Can only overlay two %ss with " - "non-matching key dimensions." - % type(self).__name__) + raise KeyError(f"Can only overlay two {type(self).__name__}s with " + "non-matching key dimensions.") items = [] self_keys = list(self.data.keys()) other_keys = list(other.data.keys()) diff --git a/holoviews/core/options.py b/holoviews/core/options.py index 1ae4b1199b..1bb4a7a127 100644 --- a/holoviews/core/options.py +++ b/holoviews/core/options.py @@ -1294,7 +1294,7 @@ def lookup(cls, backend, obj): elif len(ids) != 1: idlist = ",".join([str(el) for el in sorted(ids)]) raise Exception("Object contains elements combined across " - "multiple custom trees (ids %s)" % idlist) + f"multiple custom trees (ids {idlist})") return cls._custom_options[backend][next(iter(ids))] @classmethod diff --git a/holoviews/core/pprint.py b/holoviews/core/pprint.py index c3eee05c1f..958aa3882c 100644 --- a/holoviews/core/pprint.py +++ b/holoviews/core/pprint.py @@ -191,13 +191,13 @@ def target_info(cls, obj, ansi=False): if len(element_set) == 1: element_info = f'Element: {next(iter(element_set))}' elif len(element_set) > 1: - element_info = 'Elements:\n %s' % '\n '.join(sorted(element_set)) + element_info = 'Elements:\n {}'.format('\n '.join(sorted(element_set))) container_info = None if len(container_set) == 1: container_info = f'Container: {next(iter(container_set))}' elif len(container_set) > 1: - container_info = 'Containers:\n %s' % '\n '.join(sorted(container_set)) + container_info = 'Containers:\n {}'.format('\n '.join(sorted(container_set))) heading = cls.heading('Target Specifications', ansi=ansi, char="-") target_header = '\nTargets in this object available for customization:\n' diff --git a/holoviews/core/spaces.py b/holoviews/core/spaces.py index cd9cac8b39..8eddb146f0 100644 --- a/holoviews/core/spaces.py +++ b/holoviews/core/spaces.py @@ -570,8 +570,7 @@ def __call__(self, *args, **kwargs): pos_kwargs = {k:v for k,v in zip(self.argspec.args, args)} ignored = range(len(self.argspec.args),len(args)) if len(ignored): - self.param.warning('Ignoring extra positional argument %s' - % ', '.join('%s' % i for i in ignored)) + self.param.warning('Ignoring extra positional argument {}'.format(', '.join(f'{i}' for i in ignored))) clashes = set(pos_kwargs.keys()) & set(kwargs.keys()) if clashes: self.param.warning( @@ -1456,8 +1455,7 @@ def collation_cb(*args, **kwargs): layout_type = type(layout).__name__ if len(container.keys()) != len(layout.keys()): raise ValueError('Collated DynamicMaps must return ' - '%s with consistent number of items.' - % layout_type) + f'{layout_type} with consistent number of items.') key = kwargs['selection_key'] index = kwargs['selection_index'] @@ -1470,10 +1468,10 @@ def collation_cb(*args, **kwargs): dyn_type_counter = {t: len(vals) for t, vals in dyn_type_map.items()} if dyn_type_counter != type_counter: - raise ValueError('The objects in a %s returned by a ' + raise ValueError(f'The objects in a {layout_type} returned by a ' 'DynamicMap must consistently return ' 'the same number of items of the ' - 'same type.' % layout_type) + 'same type.') return dyn_type_map[obj_type][index] callback = Callable(partial(collation_cb, selection_key=k, @@ -1499,8 +1497,7 @@ def collation_cb(*args, **kwargs): raise ValueError( 'The following streams are set to be automatically ' 'linked to a plot, but no stream_mapping specifying ' - 'which item in the (Nd)Layout to link it to was found:\n%s' - % ', '.join(unmapped_streams) + 'which item in the (Nd)Layout to link it to was found:\n{}'.format(', '.join(unmapped_streams)) ) return container @@ -1882,6 +1879,5 @@ class GridMatrix(GridSpace): def _item_check(self, dim_vals, data): if not traversal.uniform(NdMapping([(0, self), (1, data)])): - raise ValueError("HoloMaps dimensions must be consistent in %s." % - type(self).__name__) + raise ValueError(f"HoloMaps dimensions must be consistent in {type(self).__name__}.") NdMapping._item_check(self, dim_vals, data) diff --git a/holoviews/core/tree.py b/holoviews/core/tree.py index d202b309b3..339cd66a61 100644 --- a/holoviews/core/tree.py +++ b/holoviews/core/tree.py @@ -119,7 +119,7 @@ def set_path(self, path, val): disallowed = [p for p in path if not type(self)._sanitizer.allowable(p)] if any(disallowed): raise Exception("Attribute strings in path elements cannot be " - "correctly escaped : %s" % ','.join(repr(el) for el in disallowed)) + "correctly escaped : {}".format(','.join(repr(el) for el in disallowed))) if len(path) > 1: attrtree = self.__getattr__(path[0]) attrtree.set_path(path[1:], val) diff --git a/holoviews/core/util.py b/holoviews/core/util.py index b389c884e9..3f325ddc8d 100644 --- a/holoviews/core/util.py +++ b/holoviews/core/util.py @@ -82,7 +82,7 @@ masked_types = (BaseMaskedArray,) except Exception as e: param.main.param.warning('pandas could not register all extension types ' - 'imports failed with the following error: %s' % e) + f'imports failed with the following error: {e}') try: import cftime @@ -807,8 +807,7 @@ def sanitize(self, name, valid_fn): "Accumulate blocks of hex and separate blocks by underscores" invalid = {'\a':'a','\b':'b', '\v':'v','\f':'f','\r':'r'} for cc in filter(lambda el: el in name, invalid.keys()): - raise Exception(r"Please use a raw string or escape control code '\%s'" - % invalid[cc]) + raise Exception(rf"Please use a raw string or escape control code '\{invalid[cc]}'") sanitized, chars = [], '' for split in name.split(): for c in split: diff --git a/holoviews/element/annotation.py b/holoviews/element/annotation.py index 0d06fcc3c4..8396c9ef35 100644 --- a/holoviews/element/annotation.py +++ b/holoviews/element/annotation.py @@ -478,7 +478,7 @@ def __init__(self, data, **params): data = '' if not isinstance(data, str): raise ValueError("Div element html data must be a string " - "type, found %s type." % type(data).__name__) + f"type, found {type(data).__name__} type.") super().__init__(data, **params) diff --git a/holoviews/element/graphs.py b/holoviews/element/graphs.py index e46fc14f42..30578b86ce 100644 --- a/holoviews/element/graphs.py +++ b/holoviews/element/graphs.py @@ -209,7 +209,7 @@ def _validate(self): mismatch.append(f'{kd1} != {kd2}') if mismatch: raise ValueError('Ensure that the first two key dimensions on ' - 'Nodes and EdgePaths match: %s' % ', '.join(mismatch)) + 'Nodes and EdgePaths match: {}'.format(', '.join(mismatch))) npaths = len(self._edgepaths.data) nedges = len(self) if nedges != npaths: @@ -780,8 +780,7 @@ def __init__(self, data, kdims=None, vdims=None, compute=True, **params): raise TypeError(f"Expected Nodes object in data, found {type(nodes)}.") self._nodes = nodes if not isinstance(edgepaths, EdgePaths): - raise TypeError("Expected EdgePaths object in data, found %s." - % type(edgepaths)) + raise TypeError(f"Expected EdgePaths object in data, found {type(edgepaths)}.") self._edgepaths = edgepaths self._validate() diff --git a/holoviews/element/sankey.py b/holoviews/element/sankey.py index fe0219ef8a..87f43e15c3 100644 --- a/holoviews/element/sankey.py +++ b/holoviews/element/sankey.py @@ -444,8 +444,7 @@ def __init__(self, data, kdims=None, vdims=None, **params): raise TypeError(f"Expected Nodes object in data, found {type(nodes)}.") self._nodes = nodes if not isinstance(edgepaths, self.edge_type): - raise TypeError("Expected EdgePaths object in data, found %s." - % type(edgepaths)) + raise TypeError(f"Expected EdgePaths object in data, found {type(edgepaths)}.") self._edgepaths = edgepaths self._sankey = sankey_graph self._validate() diff --git a/holoviews/ipython/__init__.py b/holoviews/ipython/__init__.py index 4f2a3bf68a..7727ee55ce 100644 --- a/holoviews/ipython/__init__.py +++ b/holoviews/ipython/__init__.py @@ -153,7 +153,7 @@ def __call__(self, *args, **params): if 'html' not in p.display_formats and len(p.display_formats) > 1: msg = ('Output magic unable to control displayed format ' 'as IPython notebook uses fixed precedence ' - 'between %r' % p.display_formats) + f'between {p.display_formats!r}') display(HTML(f'Warning: {msg}')) loaded = notebook_extension._loaded @@ -168,7 +168,7 @@ def __call__(self, *args, **params): css = '' if p.width is not None: - css += '' % p.width + css += f'' if p.css: css += f'' @@ -239,8 +239,7 @@ def _get_resources(self, args, params): unmatched_args = set(args) - set(resources) if unmatched_args: - display(HTML("Warning: Unrecognized resources '%s'" - % "', '".join(unmatched_args))) + display(HTML("Warning: Unrecognized resources '{}'".format("', '".join(unmatched_args)))) resources = [r for r in resources if r not in disabled] if ('holoviews' not in disabled) and ('holoviews' not in resources): diff --git a/holoviews/ipython/archive.py b/holoviews/ipython/archive.py index e6acd4d67f..504247f41e 100644 --- a/holoviews/ipython/archive.py +++ b/holoviews/ipython/archive.py @@ -109,7 +109,7 @@ def auto(self, enabled=True, clear=False, **kwargs): self._timestamp = tuple(time.localtime()) kernel = r'var kernel = IPython.notebook.kernel; ' nbname = r"var nbname = IPython.notebook.get_notebook_name(); " - nbcmd = (r"var name_cmd = '%s.notebook_name = \"' + nbname + '\"'; " % self.namespace) + nbcmd = (rf"var name_cmd = '{self.namespace}.notebook_name = \"' + nbname + '\"'; ") cmd = (kernel + nbname + nbcmd + "kernel.execute(name_cmd); ") display(Javascript(cmd)) time.sleep(0.5) @@ -136,7 +136,7 @@ def export(self, timestamp=None): self.export_success = None name = self.get_namespace() # Unfortunate javascript hacks to get at notebook data - capture_cmd = ((r"var capture = '%s._notebook_data=r\"\"\"'" % name) + capture_cmd = ((rf"var capture = '{name}._notebook_data=r\"\"\"'") + r"+json_string+'\"\"\"'; ") cmd = (r'var kernel = IPython.notebook.kernel; ' + r'var json_data = IPython.notebook.toJSON(); ' diff --git a/holoviews/operation/datashader.py b/holoviews/operation/datashader.py index cd7ac77a2a..869bb16933 100644 --- a/holoviews/operation/datashader.py +++ b/holoviews/operation/datashader.py @@ -133,7 +133,7 @@ def _get_aggregator(cls, element, agg, add_field=True): not isinstance(agg, agg_types)): if not elements: raise ValueError('Could not find any elements to apply ' - '%s operation to.' % cls.__name__) + f'{cls.__name__} operation to.') inner_element = elements[0] if isinstance(inner_element, TriMesh) and inner_element.nodes.vdims: field = inner_element.nodes.vdims[0].name @@ -143,9 +143,9 @@ def _get_aggregator(cls, element, agg, add_field=True): field = element.kdims[0].name else: raise ValueError("Could not determine dimension to apply " - "'%s' operation to. Declare the dimension " + f"'{cls.__name__}' operation to. Declare the dimension " "to aggregate as part of the datashader " - "aggregator." % cls.__name__) + "aggregator.") agg = type(agg)(field) return agg @@ -1521,8 +1521,7 @@ def _process(self, element, key=None): unused_params = list(all_supplied_kws - all_allowed_kws) if unused_params: - self.param.warning('Parameter(s) [%s] not consumed by any element rasterizer.' - % ', '.join(unused_params)) + self.param.warning('Parameter(s) [{}] not consumed by any element rasterizer.'.format(', '.join(unused_params))) return element @@ -1576,7 +1575,7 @@ def _process(self, overlay, key=None): for rgb in overlay: if not isinstance(rgb, RGB): raise TypeError("The stack operation expects elements of type RGB, " - "not '%s'." % type(rgb).__name__) + f"not '{type(rgb).__name__}'.") rgb = rgb.rgb dims = [kd.name for kd in rgb.kdims][::-1] coords = {kd.name: rgb.dimension_values(kd, False) @@ -1647,7 +1646,7 @@ def _process(self, element, key=None): data = element.clone(datatype=['xarray']).data[element.vdims[0].name] else: raise ValueError('spreading can only be applied to Image or RGB Elements. ' - 'Received object of type %s' % str(type(element))) + f'Received object of type {type(element)!s}') kwargs = {} array = self._apply_spreading(data) diff --git a/holoviews/operation/stats.py b/holoviews/operation/stats.py index 32b7585771..79dca831f2 100644 --- a/holoviews/operation/stats.py +++ b/holoviews/operation/stats.py @@ -85,9 +85,8 @@ def _process(self, element, key=None): else: dimensions = element.vdims+element.kdims if not dimensions: - raise ValueError("%s element does not declare any dimensions " - "to compute the kernel density estimate on." % - type(element).__name__) + raise ValueError(f"{type(element).__name__} element does not declare any dimensions " + "to compute the kernel density estimate on.") selected_dim = dimensions[0] vdim_name = f'{selected_dim.name}_density' vdims = [Dimension(vdim_name, label='Density')] diff --git a/holoviews/plotting/bokeh/chart.py b/holoviews/plotting/bokeh/chart.py index db03780ef5..40fbc7bf18 100644 --- a/holoviews/plotting/bokeh/chart.py +++ b/holoviews/plotting/bokeh/chart.py @@ -933,9 +933,8 @@ def get_data(self, element, ranges, style): style_mapping = [v for k, v in style.items() if 'color' in k and (isinstance(v, dim) or v in element)] if style_mapping and not no_cidx and self.color_index is not None: - self.param.warning("Cannot declare style mapping for '%s' option " - "and declare a color_index; ignoring the color_index." - % style_mapping[0]) + self.param.warning(f"Cannot declare style mapping for '{style_mapping[0]}' option " + "and declare a color_index; ignoring the color_index.") cdim = None cvals = element.dimension_values(cdim, expanded=False) if cdim else None diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py index 4ed11a8a5b..20071e0610 100644 --- a/holoviews/plotting/bokeh/element.py +++ b/holoviews/plotting/bokeh/element.py @@ -1292,8 +1292,8 @@ def _update_main_ranges(self, element, x_range, y_range, ranges): if data_aspect and (categorical or datetime): ax_type = 'categorical' if categorical else 'datetime axes' self.param.warning('Cannot set data_aspect if one or both ' - 'axes are %s, the option will ' - 'be ignored.' % ax_type) + f'axes are {ax_type}, the option will ' + 'be ignored.') elif data_aspect: plot = self.handles['plot'] xspan = r-l if util.is_number(l) and util.is_number(r) else None @@ -1438,7 +1438,7 @@ def _update_range(self, axis_range, low, high, factors, invert, shared, log, str self.param.warning( "Logarithmic axis range encountered value less " "than or equal to zero, please supply explicit " - "lower bound to override default of %.3f." % low) + f"lower bound to override default of {low:.3f}.") updates = {} if util.isfinite(low): updates['start'] = (axis_range.start, low) @@ -1941,7 +1941,7 @@ def _postprocess_hover(self, renderer, source): return if not isinstance(hover.tooltips, str) and 'hv_created' in hover.tags: for k, values in source.data.items(): - key = '@{%s}' % k + key = f'@{{{k}}}' if ( (len(values) and isinstance(values[0], util.datetime_types)) or (len(values) and isinstance(values[0], np.ndarray) and values[0].dtype.kind == 'M') @@ -2639,9 +2639,8 @@ def _get_color_data(self, element, ranges, style, name='color', factors=None, co color = style.get(name, None) if cdim and ((isinstance(color, str) and color in element) or isinstance(color, dim)): self.param.warning( - "Cannot declare style mapping for '%s' option and " - "declare a color_index; ignoring the color_index." - % name) + f"Cannot declare style mapping for '{name}' option and " + "declare a color_index; ignoring the color_index.") cdim = None if not cdim: return data, mapping diff --git a/holoviews/plotting/bokeh/graphs.py b/holoviews/plotting/bokeh/graphs.py index 0e95b249a6..c9201d6560 100644 --- a/holoviews/plotting/bokeh/graphs.py +++ b/holoviews/plotting/bokeh/graphs.py @@ -94,7 +94,7 @@ def _hover_opts(self, element): dims = element.nodes.dimensions() dims = [(dims[2].pprint_label, '@{index_hover}')]+dims[3:] elif self.inspection_policy == 'edges': - kdims = [(kd.pprint_label, '@{%s_values}' % kd) + kdims = [(kd.pprint_label, f'@{{{kd}_values}}') if kd in ('start', 'end') else kd for kd in element.kdims] dims = kdims+element.vdims else: diff --git a/holoviews/plotting/bokeh/links.py b/holoviews/plotting/bokeh/links.py index bae1590569..1145b98ea9 100644 --- a/holoviews/plotting/bokeh/links.py +++ b/holoviews/plotting/bokeh/links.py @@ -189,8 +189,8 @@ def __init__(self, root_model, link, source_plot, target_plot): (v.dtype.kind not in 'iufc' and (v==col).all()) or np.allclose(v, np.asarray(src_cds.data[k]), equal_nan=True)): raise ValueError('DataLink can only be applied if overlapping ' - 'dimension values are equal, %s column on source ' - 'does not match target' % k) + f'dimension values are equal, {k} column on source ' + 'does not match target') src_cds.data.update(tgt_cds.data) renderer = target_plot.handles.get('glyph_renderer') diff --git a/holoviews/plotting/bokeh/plot.py b/holoviews/plotting/bokeh/plot.py index 6a97643d63..2f74b52359 100644 --- a/holoviews/plotting/bokeh/plot.py +++ b/holoviews/plotting/bokeh/plot.py @@ -160,10 +160,10 @@ def _postprocess_data(self, data): if any(v.calendar not in _STANDARD_CALENDARS for v in values): self.param.warning( 'Converting cftime.datetime from a non-standard ' - 'calendar (%s) to a standard calendar for plotting. ' + f'calendar ({values[0].calendar}) to a standard calendar for plotting. ' 'This may lead to subtle errors in formatting ' 'dates, for accurate tick formatting switch to ' - 'the matplotlib backend.' % values[0].calendar) + 'the matplotlib backend.') values = cftime_to_timestamp(values, 'ms') new_data[k] = values return new_data @@ -574,8 +574,8 @@ def _create_subplots(self, layout, ranges): if plotting_class is None: if view is not None: self.param.warning( - "Bokeh plotting class for %s type not found, " - "object will not be rendered." % vtype.__name__) + f"Bokeh plotting class for {vtype.__name__} type not found, " + "object will not be rendered.") else: subplot = plotting_class(view, dimensions=self.dimensions, show_title=False, subplot=True, @@ -850,8 +850,8 @@ def _create_subplots(self, layout, positions, layout_dimensions, ranges, num=0): continue elif plot_type is None: self.param.warning( - "Bokeh plotting class for %s type not found, object " - " will not be rendered." % vtype.__name__) + f"Bokeh plotting class for {vtype.__name__} type not found, object " + " will not be rendered.") continue num = num if len(self.coords) > 1 else 0 subplot = plot_type(element, keys=self.keys, diff --git a/holoviews/plotting/bokeh/tiles.py b/holoviews/plotting/bokeh/tiles.py index 441e3a8bbf..51b8b274d1 100644 --- a/holoviews/plotting/bokeh/tiles.py +++ b/holoviews/plotting/bokeh/tiles.py @@ -25,7 +25,7 @@ def get_data(self, element, ranges, style): if not isinstance(element.data, (str, dict)): SkipRendering("WMTS element data must be a URL string, dictionary, or " "xyzservices.TileProvider, bokeh cannot " - "render %r" % element.data) + f"render {element.data!r}") if element.data is None: raise ValueError("Tile source URL may not be None with the bokeh backend") elif isinstance(element.data, dict): diff --git a/holoviews/plotting/bokeh/util.py b/holoviews/plotting/bokeh/util.py index 1bc7427fa2..9b5a662b90 100644 --- a/holoviews/plotting/bokeh/util.py +++ b/holoviews/plotting/bokeh/util.py @@ -298,11 +298,11 @@ def compute_layout_properties( aspect = None if logger: logger.warning( - "%s value was ignored because absolute width and " + f"{aspect_type} value was ignored because absolute width and " "height values were provided. Either supply " "explicit frame_width and frame_height to achieve " "desired aspect OR supply a combination of width " - "or height and an aspect value." % aspect_type) + "or height and an aspect value.") elif fixed_width and responsive: height = None responsive = False diff --git a/holoviews/plotting/mpl/element.py b/holoviews/plotting/mpl/element.py index 7f797ff47e..a3cd123406 100644 --- a/holoviews/plotting/mpl/element.py +++ b/holoviews/plotting/mpl/element.py @@ -416,7 +416,7 @@ def _compute_limits(self, low, high, log, invert, low_key, high_key): self.param.warning( "Logarithmic axis range encountered value less " "than or equal to zero, please supply explicit " - "lower-bound to override default of %.3f." % low) + f"lower-bound to override default of {low:.3f}.") if invert: high, low = low, high if isinstance(low, util.cftime_types) or low != high: diff --git a/holoviews/plotting/plot.py b/holoviews/plotting/plot.py index 84baf122f9..4bd69f6756 100644 --- a/holoviews/plotting/plot.py +++ b/holoviews/plotting/plot.py @@ -349,8 +349,9 @@ def __setattr__(self, label, value): try: return super().__setattr__(label, value) except Exception as e: - raise Exception("Please set class parameters directly on classes %s" - % ', '.join(str(cls) for cls in self.__dict__['plot_classes'].values())) from e + plot_cls_str = ', '.join(str(cls) for cls in self.__dict__['plot_classes'].values()) + msg = f"Please set class parameters directly on classes {plot_cls_str}" + raise Exception(msg) from e def params(self): return self.plot_options @@ -1809,8 +1810,8 @@ def _create_subplots(self, ranges): self.zoffset += len(subplot.subplots.keys()) - 1 if not subplots: - raise SkipRendering("%s backend could not plot any Elements " - "in the Overlay." % self.renderer.backend) + raise SkipRendering(f"{self.renderer.backend} backend could not plot any Elements " + "in the Overlay.") return subplots def _create_subplot(self, key, obj, streams, ranges): @@ -1859,8 +1860,8 @@ def _create_subplot(self, key, obj, streams, ranges): opts['group_counter'] = self.group_counter opts['show_legend'] = self.show_legend if not any(len(frame) for frame in obj): - self.param.warning('%s is empty and will be skipped ' - 'during plotting' % obj.last) + self.param.warning(f'{obj.last} is empty and will be skipped ' + 'during plotting') return None elif self.batched and 'batched' in plottype._plot_methods: param_vals = self.param.values() diff --git a/holoviews/plotting/plotly/__init__.py b/holoviews/plotting/plotly/__init__.py index 00dae53961..58a6761604 100644 --- a/holoviews/plotting/plotly/__init__.py +++ b/holoviews/plotting/plotly/__init__.py @@ -24,8 +24,7 @@ if Version(plotly.__version__) < Version('4.0.0'): raise VersionError( "The plotly extension requires a plotly version >=4.0.0, " - "please upgrade from plotly %s to a more recent version." - % plotly.__version__, plotly.__version__, '4.0.0') + f"please upgrade from plotly {plotly.__version__} to a more recent version.", plotly.__version__, '4.0.0') Store.renderers['plotly'] = PlotlyRenderer.instance() diff --git a/holoviews/plotting/plotly/plot.py b/holoviews/plotting/plotly/plot.py index ea77114517..c46d659700 100644 --- a/holoviews/plotting/plotly/plot.py +++ b/holoviews/plotting/plotly/plot.py @@ -173,8 +173,8 @@ def _create_subplots(self, layout, positions, layout_dimensions, ranges, num=0): if plot_type is None: self.param.warning( - "Plotly plotting class for %s type not found, " - "object will not be rendered." % vtype.__name__) + f"Plotly plotting class for {vtype.__name__} type not found, " + "object will not be rendered.") continue num = num if len(self.coords) > 1 else 0 subplot = plot_type(element, keys=self.keys, @@ -332,8 +332,8 @@ def _create_subplots(self, layout, ranges): if plotting_class is None: if view is not None: self.param.warning( - "Plotly plotting class for %s type not found, " - "object will not be rendered." % vtype.__name__) + f"Plotly plotting class for {vtype.__name__} type not found, " + "object will not be rendered.") else: subplot = plotting_class(view, dimensions=self.dimensions, show_title=False, subplot=True, diff --git a/holoviews/plotting/plotly/tiles.py b/holoviews/plotting/plotly/tiles.py index cfa6b6a079..88d0cebbbb 100644 --- a/holoviews/plotting/plotly/tiles.py +++ b/holoviews/plotting/plotly/tiles.py @@ -40,7 +40,7 @@ def graph_options(self, element, ranges, style, **kwargs): layer['maxzoom'] = element.data.get("max_zoom", 20) else: for v in ["X", "Y", "Z"]: - url = url.replace("{%s}" % v, "{%s}" % v.lower()) + url = url.replace(f"{{{v}}}", f"{{{v.lower()}}}") layer["source"] = [url] for key, attribution in _ATTRIBUTIONS.items(): diff --git a/holoviews/plotting/util.py b/holoviews/plotting/util.py index 3cc2947bae..6dcd6bb2fd 100644 --- a/holoviews/plotting/util.py +++ b/holoviews/plotting/util.py @@ -65,21 +65,21 @@ def collate(obj): nested_type = next(type(o).__name__ for o in obj if isinstance(o, (HoloMap, GridSpace, AdjointLayout))) display_warning.param.warning( - "Nesting %ss within an Overlay makes it difficult to " + f"Nesting {nested_type}s within an Overlay makes it difficult to " "access your data or control how it appears; we recommend " "calling .collate() on the Overlay in order to follow the " "recommended nesting structure shown in the Composing Data " - "user guide (http://goo.gl/2YS8LJ)" % nested_type) + "user guide (http://goo.gl/2YS8LJ)") return obj.collate() if isinstance(obj, DynamicMap): if obj.type in [DynamicMap, HoloMap]: obj_name = obj.type.__name__ - raise Exception("Nesting a %s inside a DynamicMap is not " + raise Exception(f"Nesting a {obj_name} inside a DynamicMap is not " "supported. Ensure that the DynamicMap callback " "returns an Element or (Nd)Overlay. If you have " "applied an operation ensure it is not dynamic by " - "setting dynamic=False." % obj_name) + "setting dynamic=False.") return obj.collate() if isinstance(obj, HoloMap): display_warning.param.warning( @@ -346,7 +346,7 @@ def undisplayable_info(obj, html=False): return f'{error}\n{remedy}\n{info}' else: return "
{msg}
".format(msg=('
'.join( - ['%s' % error, remedy, '%s' % info]))) + [f'{error}', remedy, f'{info}']))) def compute_sizes(sizes, size_fn, scaling_factor, scaling_method, base_size): diff --git a/holoviews/selection.py b/holoviews/selection.py index cc7685b7e8..1120473c4f 100644 --- a/holoviews/selection.py +++ b/holoviews/selection.py @@ -141,10 +141,10 @@ def __call__(self, hvobj, **kwargs): if Store.current_backend not in Store.renderers: raise RuntimeError("Cannot perform link_selections operation " - "since the selected backend %r is not " + f"since the selected backend {Store.current_backend!r} is not " "loaded. Load the plotting extension with " "hv.extension or import the plotting " - "backend explicitly." % Store.current_backend) + "backend explicitly.") # Perform transform return self._selection_transform(hvobj.clone()) @@ -523,7 +523,7 @@ def _select(element, selection_expr, cache=None): f"display selection for all elements: {key_error} on '{element!r}'.") from e except Exception as e: raise CallbackError("linked_selection aborted because it could not " - "display selection for all elements: %s." % e) from e + f"display selection for all elements: {e}.") from e ds_cache[selection_expr] = mask else: selection = element diff --git a/holoviews/streams.py b/holoviews/streams.py index 12d3016958..e004aaaccc 100644 --- a/holoviews/streams.py +++ b/holoviews/streams.py @@ -361,8 +361,8 @@ def _validate_rename(self, mapping): if k not in param_names: raise KeyError(f'Cannot rename {k!r} as it is not a stream parameter') if k != v and v in param_names: - raise KeyError('Cannot rename to %r as it clashes with a ' - 'stream parameter of the same name' % v) + raise KeyError(f'Cannot rename to {v!r} as it clashes with a ' + 'stream parameter of the same name') return mapping @@ -768,8 +768,8 @@ def _validate_rename(self, mapping): if n not in pnames: raise KeyError(f'Cannot rename {n!r} as it is not a stream parameter') if n != v and v in pnames: - raise KeyError('Cannot rename to %r as it clashes with a ' - 'stream parameter of the same name' % v) + raise KeyError(f'Cannot rename to {v!r} as it clashes with a ' + 'stream parameter of the same name') return mapping def _watcher(self, *events): @@ -843,8 +843,7 @@ class ParamMethod(Params): def __init__(self, parameterized, parameters=None, watch=True, **params): if not util.is_param_method(parameterized): raise ValueError('ParamMethod stream expects a method on a ' - 'parameterized class, found %s.' - % type(parameterized).__name__) + f'parameterized class, found {type(parameterized).__name__}.') method = parameterized parameterized = util.get_method_owner(parameterized) if not parameters: diff --git a/holoviews/util/__init__.py b/holoviews/util/__init__.py index fd11c49441..9ea64c5a19 100644 --- a/holoviews/util/__init__.py +++ b/holoviews/util/__init__.py @@ -128,8 +128,9 @@ def _group_kwargs_to_options(cls, obj, kwargs): raise Exception("Keyword options {} must be one of {}".format(groups, ','.join(repr(g) for g in groups))) elif not all(isinstance(v, dict) for v in kwargs.values()): - raise Exception("The %s options must be specified using dictionary groups" % - ','.join(repr(k) for k in kwargs.keys())) + options_str = ','.join([repr(k) for k in kwargs.keys()]) + msg = f"The {options_str} options must be specified using dictionary groups" + raise Exception(msg) # Check whether the user is specifying targets (such as 'Image.Foo') targets = [grp and all(k[0].isupper() for k in grp) for grp in kwargs.values()] @@ -305,7 +306,7 @@ def _expand_by_backend(cls, options, backend): if backend and not used_fallback: cls.param.warning("All supplied Options objects already define a backend, " - "backend override %r will be ignored." % backend) + f"backend override {backend!r} will be ignored.") return [(bk, cls._expand_options(o, bk)) for (bk, o) in groups.items()] @@ -926,8 +927,7 @@ def _get_streams(self, map_obj, watch=True): for name, p in self.p.streams.items(): if not isinstance(p, param.Parameter): raise ValueError("Stream dictionary must map operation keywords " - "to parameter names. Cannot handle %r type." - % type(p)) + f"to parameter names. Cannot handle {type(p)!r} type.") if inspect.isclass(p.owner) and issubclass(p.owner, Stream): if p.name != name: streams[p.owner][p.name] = name @@ -949,8 +949,7 @@ def _get_streams(self, map_obj, watch=True): if inspect.isclass(stream) and issubclass(stream, Stream): stream = stream() elif not (isinstance(stream, Stream) or util.is_param_method(stream)): - raise ValueError('Streams must be Stream classes or instances, found %s type' % - type(stream).__name__) + raise ValueError(f'Streams must be Stream classes or instances, found {type(stream).__name__} type') if isinstance(op, Operation): updates = {k: op.p.get(k) for k, v in stream.contents.items() if v is None and k in op.p} diff --git a/holoviews/util/parser.py b/holoviews/util/parser.py index ec6af789d3..6d7ec7906e 100644 --- a/holoviews/util/parser.py +++ b/holoviews/util/parser.py @@ -233,7 +233,7 @@ def process_normalization(cls, parse_group): for normopt in options: if opts.count(normopt) > 1: raise SyntaxError("Normalization specification must not" - " contain repeated %r" % normopt) + f" contain repeated {normopt!r}") if not all(opt in options for opt in opts): raise SyntaxError(f"Normalization option not one of {', '.join(options)}") @@ -434,8 +434,7 @@ def parse(cls, line, ns=None): spec = ' '.join(group['spec'].asList()[0]) if group['op'] not in opmap: - raise SyntaxError("Operation %s not available for use with compositors." - % group['op']) + raise SyntaxError("Operation {} not available for use with compositors.".format(group['op'])) if 'op_settings' in group: kwargs = cls.todict(group['op_settings'][0], 'brackets', ns=ns) diff --git a/holoviews/util/settings.py b/holoviews/util/settings.py index 547beefba3..7ca0ef342a 100644 --- a/holoviews/util/settings.py +++ b/holoviews/util/settings.py @@ -216,14 +216,11 @@ def _generate_docstring(cls, signature=False): holomap = "holomap : The display type for holomaps" widgets = "widgets : The widget mode for widgets" fps = "fps : The frames per second used for animations" - max_frames= ("max_frames : The max number of frames rendered (default %r)" - % cls.defaults['max_frames']) + max_frames= ("max_frames : The max number of frames rendered (default {!r})".format(cls.defaults['max_frames'])) size = "size : The percentage size of displayed output" dpi = "dpi : The rendered dpi of the figure" - filename = ("filename : The filename of the saved output, if any (default %r)" - % cls.defaults['filename']) - info = ("info : The information to page about the displayed objects (default %r)" - % cls.defaults['info']) + filename = ("filename : The filename of the saved output, if any (default {!r})".format(cls.defaults['filename'])) + info = ("info : The information to page about the displayed objects (default {!r})".format(cls.defaults['info'])) css = ("css : Optional css style attributes to apply to the figure image tag") widget_location = "widget_location : The position of the widgets relative to the plot" @@ -232,7 +229,7 @@ def _generate_docstring(cls, signature=False): keywords = ['backend', 'fig', 'holomap', 'widgets', 'fps', 'max_frames', 'size', 'dpi', 'filename', 'info', 'css', 'widget_location'] if signature: - doc_signature = '\noutput(%s)\n' % ', '.join('%s=None' % kw for kw in keywords) + doc_signature = '\noutput({})\n'.format(', '.join(f'{kw}=None' for kw in keywords)) return '\n'.join([doc_signature] + intro + descriptions) else: return '\n'.join(intro + descriptions) diff --git a/holoviews/util/transform.py b/holoviews/util/transform.py index 9ecbd2470e..63444f67a2 100644 --- a/holoviews/util/transform.py +++ b/holoviews/util/transform.py @@ -274,7 +274,7 @@ def __init__(self, obj, *args, **kwargs): if not (isinstance(fn, function_types+(str,)) or any(fn in funcs for funcs in self._all_funcs)): raise ValueError('Second argument must be a function, ' - 'found %s type' % type(fn)) + f'found {type(fn)} type') self.ops = self.ops + [{'args': args[1:], 'fn': fn, 'kwargs': kwargs, 'reverse': kwargs.pop('reverse', False)}] @@ -292,10 +292,10 @@ def _current_accessor(self): def __call__(self, *args, **kwargs): if (not self.ops or not isinstance(self.ops[-1]['fn'], str) or 'accessor' not in self.ops[-1]['kwargs']): - raise ValueError("Cannot call method on %r expression. " + raise ValueError(f"Cannot call method on {self!r} expression. " "Only methods accessed via namespaces, " "e.g. dim(...).df or dim(...).xr), " - "can be called. " % self) + "can be called.") op = self.ops[-1] if op['fn'] == 'str': new_op = dict(op, fn=astype, args=(str,), kwargs={}) @@ -795,7 +795,7 @@ def __repr__(self): prev_accessor = accessor accessor = kwargs.pop('accessor', None) kwargs = sorted(kwargs.items(), key=operator.itemgetter(0)) - kwargs = '%s' % ', '.join(['{}={!r}'.format(*item) for item in kwargs]) if kwargs else '' + kwargs = ', '.join(['{}={!r}'.format(*item) for item in kwargs]) if kwargs else '' if fn in self._binary_funcs: fn_name = self._binary_funcs[o['fn']] if o['reverse']: From ca56d1d87f2468dbd9c21d58fe0c3b3af05e28f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Tue, 7 May 2024 17:43:21 +0200 Subject: [PATCH 21/43] Clone model if Tool is model (#6220) --- holoviews/plotting/bokeh/element.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py index 20071e0610..e46be71504 100644 --- a/holoviews/plotting/bokeh/element.py +++ b/holoviews/plotting/bokeh/element.py @@ -8,6 +8,7 @@ import numpy as np import param from bokeh.document.events import ModelChangedEvent +from bokeh.model import Model from bokeh.models import ( BinnedTicker, ColorBar, @@ -501,7 +502,10 @@ def _init_tools(self, element, callbacks=None): copied_tools = [] for tool in tool_list: if isinstance(tool, tools.Tool): - properties = tool.properties_with_values(include_defaults=False) + properties = { + p: v.clone() if isinstance(v, Model) else v + for p, v in tool.properties_with_values(include_defaults=False).items() + } tool = type(tool)(**properties) copied_tools.append(tool) From 85564f19c5556daacad8ab467c424af4ceae1473 Mon Sep 17 00:00:00 2001 From: Andrew <15331990+ahuang11@users.noreply.github.com> Date: Tue, 7 May 2024 09:03:08 -0700 Subject: [PATCH 22/43] Cleanup popup (#6207) --- holoviews/plotting/bokeh/callbacks.py | 12 ++++++------ holoviews/tests/ui/bokeh/test_callback.py | 3 ++- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/holoviews/plotting/bokeh/callbacks.py b/holoviews/plotting/bokeh/callbacks.py index 02a5d8ccab..82698dbf73 100644 --- a/holoviews/plotting/bokeh/callbacks.py +++ b/holoviews/plotting/bokeh/callbacks.py @@ -693,15 +693,16 @@ def _process_selection_event(self): if popup is None: if self._panel.visible: self._panel.visible = False - if self._existing_popup and not self._existing_popup.visible: - self._existing_popup.visible = False return if event is not None: position = self._get_position(event) else: position = None + popup_pane = panel(popup) + if not popup_pane.visible: + return if not popup_pane.stylesheets: self._panel.stylesheets = [ @@ -723,8 +724,7 @@ def _process_selection_event(self): if self._existing_popup and not self._existing_popup.visible: if position: self._panel.position = XY(**position) - self._existing_popup.visible = True - if self.plot.comm: + if self.plot.comm: # update Jupyter Notebook push_on_root(self.plot.root.ref['id']) return @@ -734,13 +734,13 @@ def _process_selection_event(self): code=""" export default ({panel}, event, _) => { if (!event.visible) { - panel.position.setv({x: NaN, y: NaN}) + panel.visible = false; } }""", )) # the first element is the close button self._panel.elements = [self._panel.elements[0], model] - if self.plot.comm: + if self.plot.comm: # update Jupyter Notebook push_on_root(self.plot.root.ref['id']) self._existing_popup = popup_pane diff --git a/holoviews/tests/ui/bokeh/test_callback.py b/holoviews/tests/ui/bokeh/test_callback.py index b1cb47d4d4..93eb11de3a 100644 --- a/holoviews/tests/ui/bokeh/test_callback.py +++ b/holoviews/tests/ui/bokeh/test_callback.py @@ -284,12 +284,13 @@ def hide(_): # initial appearance locator = page.locator(".bk-btn") expect(locator).to_have_count(2) + expect(locator.first).to_be_visible() # click custom button to hide locator = page.locator(".custom-button") locator.click() locator = page.locator(".bk-btn") - expect(locator).to_have_count(0) + expect(locator.first).not_to_be_visible() From 1c8b0885e5ca4e2357d9416e64cbe1bcf124115d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Tue, 7 May 2024 21:21:31 +0200 Subject: [PATCH 23/43] Merge CHANGELOG.md and releases.rst (#6218) --- CHANGELOG.md => doc/releases.md | 196 +- doc/releases.rst | 4510 ------------------------------- 2 files changed, 118 insertions(+), 4588 deletions(-) rename CHANGELOG.md => doc/releases.md (99%) delete mode 100644 doc/releases.rst diff --git a/CHANGELOG.md b/doc/releases.md similarity index 99% rename from CHANGELOG.md rename to doc/releases.md index e42d6f75da..f9ccd401f1 100644 --- a/CHANGELOG.md +++ b/doc/releases.md @@ -1,4 +1,8 @@ -# Version 1.18.3 +# Releases + +## Version 1.18 + +### Version 1.18.3 **February 12, 2024** @@ -11,7 +15,7 @@ Bug fixes: - Fix link selection for empty Layout ([#6110](https://github.com/holoviz/holoviews/pull/6110)) - Don't pin notebook in conda recipe for pyviz channel ([#6108](https://github.com/holoviz/holoviews/pull/6108)) -# Version 1.18.2 +### Version 1.18.2 **February 5, 2024** @@ -66,7 +70,7 @@ Maintenance: - Skip Deploying_Bokeh_Apps.ipynb on Windows ([#6070](https://github.com/holoviz/holoviews/pull/6070)) - Fix failing Windows tests ([#6087](https://github.com/holoviz/holoviews/pull/6087)) -# Version 1.18.1 +### Version 1.18.1 **November 8, 2023** @@ -89,7 +93,7 @@ Maintenance: - General maintenance ([#5955](https://github.com/holoviz/holoviews/pull/5955)) -# Version 1.18.0 +### Version 1.18.0 **October 18, 2023** @@ -150,7 +154,9 @@ Maintenance: - Add sorting of imports ([#5937](https://github.com/holoviz/holoviews/pull/5937)) - Enable Bugbear lint ([#5861](https://github.com/holoviz/holoviews/pull/5861)) -# Version 1.17.1 +## Version 1.17 + +### Version 1.17.1 **August 16, 2023** @@ -181,7 +187,7 @@ Maintenance: - Update Ruff in pre-commit and report panel communication in `hv.show_versions` ([#5853](https://github.com/holoviz/holoviews/pull/5853)) - Cleanup imports ([#5846](https://github.com/holoviz/holoviews/pull/5846)) -# Version 1.17.0 +### Version 1.17.0 **July 24, 2023** @@ -242,7 +248,9 @@ Maintenance: - Update pre-commit and lint ([#5747](https://github.com/holoviz/holoviews/pull/5747), [#5768](https://github.com/holoviz/holoviews/pull/5768), [#5777](https://github.com/holoviz/holoviews/pull/5777)) - Setup infrastructure for UI tests and add first linked streams tests ([#5764](https://github.com/holoviz/holoviews/pull/5764)) -# Version 1.16.2 +## Version 1.16 + +### Version 1.16.2 **June 8, 2023** @@ -262,7 +270,7 @@ Maintenance: - Fix failing tests ([#5742](https://github.com/holoviz/holoviews/pull/5742)) - Misc. mainteance ([#5717](https://github.com/holoviz/holoviews/pull/5717)) -# Version 1.16.1 +### Version 1.16.1 **June 2, 2023** @@ -285,7 +293,7 @@ Documentation: - Upgrade to latest `nbsite` and `pydata-sphinx-theme` ([#5724](https://github.com/holoviz/holoviews/pull/5724), [#5735](https://github.com/holoviz/holoviews/pull/5735)) -# Version 1.16.0 +### Version 1.16.0 **May 9, 2023** @@ -355,7 +363,9 @@ Removals: - Remove deprecated tile sources ([#5654](https://github.com/holoviz/holoviews/pull/5654)) - Removed support for `apply_groups` for old option groups signature ([#5497](https://github.com/holoviz/holoviews/pull/5497)) -# Version 1.15.4 +## Version 1.15 + +### Version 1.15.4 **January 16, 2023** @@ -401,7 +411,7 @@ Maintenance: - Update binder link and dependency pinning ([#5583](https://github.com/holoviz/holoviews/pull/5583)) - Update copyright to only contain start year ([#5580](https://github.com/holoviz/holoviews/pull/5580)) -# Version 1.15.3 +### Version 1.15.3 **December 6, 2022** @@ -428,7 +438,7 @@ Documentation: - Fixes to release notes and CHANGELOG ([#5506](https://github.com/holoviz/holoviews/pull/5506)) -# Version 1.15.2 +### Version 1.15.2 **November 3, 2022** @@ -466,7 +476,7 @@ be deprecated in future. - Upgrade warning for invalid dataframe column names ([#5472](https://github.com/holoviz/holoviews/pull/5472)) -# Version 1.15.1 +### Version 1.15.1 **October 4, 2022** @@ -547,7 +557,7 @@ issuing a `DeprecationWarning` that should not be visible to users. - Issue DeprecationWarning for invalid DataFrame column types ([#5457](https://github.com/holoviz/holoviews/pull/5457)) -# Version 1.15.0 +### Version 1.15.0 **July 6, 2022** @@ -712,7 +722,9 @@ infrastructure across too many PRs to list here. - Test updates following release of datashader 0.14.1 ([#5344](https://github.com/holoviz/holoviews/pull/5344)) -# Version 1.14.9 +## Version 1.14 + +### Version 1.14.9 **May 6, 2022** @@ -760,7 +772,7 @@ Documentation: ([#5267](https://github.com/holoviz/holoviews/pull/5267), [#5290](https://github.com/holoviz/holoviews/pull/5290)) -# Version 1.14.8 +### Version 1.14.8 **February 15, 2022** @@ -794,7 +806,7 @@ Bug fixes: [#5201](https://github.com/holoviz/holoviews/pull/5201), [#5206](https://github.com/holoviz/holoviews/pull/5206)) -# Version 1.14.7 +### Version 1.14.7 **December 16, 2021** @@ -857,7 +869,7 @@ Bug fixes: - Switch to the Pydata Sphinx theme ([#5163](https://github.com/holoviz/holoviews/pull/5163)) -# Version 1.14.6 +### Version 1.14.6 **September 16, 2021** @@ -888,7 +900,7 @@ Bug fixes: - Apply hover postprocessor on updates ([#5039](https://github.com/holoviz/holoviews/pull/5039)) -# Version 1.14.5 +### Version 1.14.5 **July 16, 2021** @@ -907,7 +919,7 @@ Bug fixes: [#5001](https://github.com/holoviz/holoviews/pull/5001), [#5005](https://github.com/holoviz/holoviews/pull/5005)) -# Version 1.14.4 +### Version 1.14.4 **May 18, 2021** @@ -965,7 +977,7 @@ unless `hv.config.raise_deprecated_tilesource_exception` is set to available. Attempting to use these tile sources will result in a deprecation warning. -# Version 1.14.3 +### Version 1.14.3 **April 8, 2021** @@ -996,7 +1008,7 @@ Compatibility: - Support matplotlib versions >=3.4 ([#4878](https://github.com/holoviz/holoviews/pull/4878)) -# Version 1.14.2 +### Version 1.14.2 **March 2, 2021** @@ -1066,7 +1078,7 @@ Documentation: [#4844](https://github.com/holoviz/holoviews/pull/4844), [#4811](https://github.com/holoviz/holoviews/pull/4811)) -# Version 1.14.1 +### Version 1.14.1 **December 28, 2020** @@ -1091,7 +1103,7 @@ Documentation: - Warn about disabled interactive features on website ([#4762](https://github.com/holoviz/holoviews/pull/4762)) -# Version 1.14.0 +### Version 1.14.0 **December 1, 2020** @@ -1197,7 +1209,9 @@ Compatibility: set to 'kbc_r' for consistency and can be set back to the old value of 'RdYlBu_r' via `hv.config.default_heatmap_cmap`. -# Version 1.13.5 +## Version 1.13 + +### Version 1.13.5 **October 23, 2020** @@ -1240,7 +1254,7 @@ Documentation: - Various documentation fixes ([#4628](https://github.com/holoviz/holoviews/pull/4628)) -# Version 1.13.4 +### Version 1.13.4 **September 8, 2020** @@ -1312,7 +1326,7 @@ Enhancements: - Allow rendering to pgf in matplotlib ([#4577](https://github.com/holoviz/holoviews/pull/4577)) -# Version 1.13.3 +### Version 1.13.3 **June 23, 2020** @@ -1382,7 +1396,7 @@ Bug fixes: - Handle missing categories on split Violin plot ([#4482](https://github.com/holoviz/holoviews/pull/4482)) -# Version 1.13.2 +### Version 1.13.2 **April 2, 2020** @@ -1409,7 +1423,7 @@ Bug fixes: - Fix handling of document in server mode ([#4355](https://github.com/holoviz/holoviews/pull/4355)) -# Version 1.13.1 +### Version 1.13.1 **March 25, 2020** @@ -1455,7 +1469,7 @@ Documentation: - Update API reference manual ([#4316](https://github.com/holoviz/holoviews/pull/4316)) -# Version 1.13.0 +### Version 1.13.0 **March 20, 2020** @@ -1605,7 +1619,9 @@ Migration notes: - `hv.output` `filename` argument is deprecated; use `hv.save` instead ([#3985](https://github.com/holoviz/holoviews/pull/3985)) -# Version 1.12.7 +## Version 1.12 + +### Version 1.12.7 **November 22, 2019** @@ -1624,7 +1640,7 @@ Bug fixes: - Fixed shared_axes/axiswise regression ([#4097](https://github.com/holoviz/holoviews/pull/4097)) -# Version 1.12.6 +### Version 1.12.6 **October 8, 2019** @@ -1690,7 +1706,7 @@ Compatibility: - Ensure compatibility with new legend options in bokeh 1.4.0 ([#4036](https://github.com/pyviz/holoviews/issues/4036)) -# Version 1.12.5 +### Version 1.12.5 **August 14, 2019** @@ -1705,7 +1721,7 @@ Compatibility: - Fix for rendering Scatter3D with matplotlib 3.1 ([#3898](https://github.com/pyviz/holoviews/issues/3898)) -# Version 1.12.4 +### Version 1.12.4 **August 4, 2019** @@ -1774,7 +1790,7 @@ Backwards incompatible changes: box-whisker plots was fixed resulting in different results going forward. -# Version 1.12.3 +### Version 1.12.3 **May 20, 2019** @@ -1809,7 +1825,7 @@ Bug fixes: - Ensure that param streams handle subobjects ([#3728](https://github.com/pyviz/holoviews/pull/3728)) -# Version 1.12.2 +### Version 1.12.2 **May 1, 2019** @@ -1856,7 +1872,7 @@ Backward compatibility: - Added color cycles on Violin and BoxWhisker elements due to earlier regression ([#3592](https://github.com/pyviz/holoviews/pull/3592)) -# Version 1.12.1 +### Version 1.12.1 **April 10, 2019** @@ -1868,7 +1884,7 @@ Enhancements: - Add support for passing in parameter instances as streams ([#3616](https://github.com/pyviz/holoviews/pull/3616)) -# Version 1.12.0 +### Version 1.12.0 **April 2, 2019** @@ -1920,7 +1936,9 @@ Bug fixes: [#3585](https://github.com/pyviz/holoviews/pull/3585), [#3594](https://github.com/pyviz/holoviews/pull/3594)) -# Version 1.11.3 +## Version 1.11 + +### Version 1.11.3 **February 25, 2019** @@ -1965,7 +1983,7 @@ Enhancements: - Added Tiles element from GeoViews ([#3515](https://github.com/pyviz/holoviews/pull/3515)) -# Version 1.11.2 +### Version 1.11.2 **January 28, 2019** @@ -1990,7 +2008,7 @@ Enhancements: - Improvements for handling graph attributes in Graph.from_networkx ([#3432](https://github.com/pyviz/holoviews/pull/3432)) -# Version 1.11.1 +### Version 1.11.1 **January 17, 2019** @@ -2032,7 +2050,7 @@ Documentation: ([#3364]((https://github.com/pyviz/holoviews/pull/3364), [#3367](<(https://github.com/pyviz/holoviews/pull/3367)>) -# Version 1.11.0 +### Version 1.11.0 **December 24, 2018** @@ -2134,7 +2152,9 @@ Deprecations: marked for deprecation ([#3128](https://github.com/pyviz/holoviews/pull/3128)) -# Version 1.10.8 +## Version 1.10 + +### Version 1.10.8 **October 29, 2018** @@ -2209,7 +2229,7 @@ Documentation: [#2959](https://github.com/pyviz/holoviews/pull/2959), [#3025](https://github.com/pyviz/holoviews/pull/3025)) -# Version 1.10.7 +### Version 1.10.7 **July 8, 2018** @@ -2228,7 +2248,7 @@ Fixes: - Fixed ticks on log Colorbar if low value <= 0 ([#2865](https://github.com/pyviz/holoviews/pull/2865)) -# Version 1.10.6 +### Version 1.10.6 **June 29, 2018** @@ -2260,7 +2280,7 @@ Fixes: zero range ([#2829](https://github.com/pyviz/holoviews/pull/2829), [#2842](https://github.com/pyviz/holoviews/pull/2842)) -# Version 1.10.5 +### Version 1.10.5 **June 5, 2018** @@ -2305,7 +2325,7 @@ Compatibility: [#2725](https://github.com/pyviz/holoviews/pull/2725), [#2767](https://github.com/pyviz/holoviews/pull/2767)) -# Version 1.10.4 +### Version 1.10.4 **May 14, 2018** @@ -2326,7 +2346,7 @@ Fixes: - Fixed bug slicing xarray with tuples ([#2674](https://github.com/pyviz/holoviews/pull/2674)) -# Version 1.10.3 +### Version 1.10.3 **May 8, 2018** @@ -2361,7 +2381,7 @@ API: keys for consistency ([#2650](https://github.com/pyviz/holoviews/issues/2650)) -# Version 1.10.2 +### Version 1.10.2 **April 30, 2018** @@ -2394,7 +2414,7 @@ Deprecations: matplotlib `fontsize` option are deprecated ([#2411](https://github.com/pyviz/holoviews/issues/2411)) -# Version 1.10.1 +### Version 1.10.1 **April 20, 2018** @@ -2412,7 +2432,7 @@ Fixes: - Fixed Selection1D stream on bokeh server after changes in bokeh 0.12.15 ([#2586](https://github.com/pyviz/holoviews/pull/2586)) -# Version 1.10.0 +### Version 1.10.0 **April 17, 2018** @@ -2551,7 +2571,9 @@ Changes affecting backwards compatibility: single column is supplied no integer index column is added automatically ([#2522](https://github.com/pyviz/holoviews/pull/2522)) -# Version 1.9.5 +## Version 1.9 + +### Version 1.9.5 **March 2, 2018** @@ -2572,7 +2594,7 @@ Fixes: - Fixed bug streaming data containing datetimes using bokeh>=0.12.14 ([\#2383](https://github.com/pyviz/holoviews/pull/2383)) -# Version 1.9.4 +### Version 1.9.4 **February 16, 2018** @@ -2587,7 +2609,7 @@ This release contains a small number of important bug fixes: - Fixed issue when using datetimes with datashader when processing ranges ([\#2344](https://github.com/pyviz/holoviews/pull/2344)) -# Version 1.9.3 +### Version 1.9.3 **February 11, 2018** @@ -2642,7 +2664,7 @@ API Changes: - Renamed `Trisurface` to `TriSurface` for future consistency ([\#2219](https://github.com/pyviz/holoviews/pull/2219)) -# Version 1.9.2 +### Version 1.9.2 **December 11, 2017** @@ -2676,7 +2698,7 @@ Fixes: - Fixed bug attaching streams to (Nd)Overlay types ([\#2194](https://github.com/pyviz/holoviews/pull/2194)) -# Version 1.9.1 +### Version 1.9.1 **November 13, 2017** @@ -2702,7 +2724,7 @@ Fixes: and bivariate_kde operations ([\#2103](https://github.com/pyviz/holoviews/pull/2103)) -# Version 1.9.0 +### Version 1.9.0 **November 3, 2017** @@ -2788,7 +2810,9 @@ Changes affecting backwards compatibility: generic value dimension and customizable label ([\#1836](https://github.com/pyviz/holoviews/pull/1836)) -# Version 1.8.4 +## Version 1.8 + +### Version 1.8.4 **September 13, 2017** @@ -2814,7 +2838,7 @@ Fixes: - Fixes for inverting Image/RGB/Raster axes in Bokeh. ([\#1872](https://github.com/pyviz/holoviews/pull/1872)) -# Version 1.8.3 +### Version 1.8.3 **August 21, 2017** @@ -2841,7 +2865,7 @@ Fixes: ([\#1664](https://github.com/pyviz/holoviews/pull/1664), [\#1796](https://github.com/pyviz/holoviews/pull/1796)) -# Version 1.8.2 +### Version 1.8.2 **August 4, 2017** @@ -2868,7 +2892,7 @@ Fixes: [\#1739](https://github.com/pyviz/holoviews/pull/1739), [\#1711](https://github.com/pyviz/holoviews/pull/1711)) -# Version 1.8.1 +### Version 1.8.1 **July 7, 2017** @@ -2909,7 +2933,7 @@ Fixes: [\#1692](https://github.com/pyviz/holoviews/pull/1692), [\#1658](https://github.com/pyviz/holoviews/pull/1658)) -# Version 1.8.0 +### Version 1.8.0 **June 29, 2017** @@ -3030,7 +3054,9 @@ Changes affecting backwards compatibility: instance, `hv.extension('bokeh', config=dict(style_17=True))` ([\#1518](https://github.com/pyviz/holoviews/pull/1518)) -# Version 1.7.0 +## Version 1.7 + +### Version 1.7.0 **April 25, 2017** @@ -3405,7 +3431,9 @@ Changes affecting backwards compatibility: in 1.6.2 (PR [\#826](https://github.com/pyviz/holoviews/pull/826)), now enabled by default. -# Version 1.6.2 +## Version 1.6 + +### Version 1.6.2 **August 23, 2016** @@ -3440,7 +3468,7 @@ LayoutPlot.v17_layout_format = True LayoutPlot.vspace = 0.3 ``` -# Version 1.6.1 +### Version 1.6.1 **July 27, 2016** @@ -3460,7 +3488,7 @@ the grid data interfaces and improvements to the options system. and transposed correctly (PR [\#794](https://github.com/pyviz/holoviews/pull/794)). -# Version 1.6 +### Version 1.6.0 **July 14, 2016** @@ -3492,7 +3520,9 @@ Features and improvements: Dimension value_format (PR [\#728](https://github.com/pyviz/holoviews/pull/728)). -# Version 1.5 +## Version 1.5 + +### Version 1.5.0 **May 12, 2016** @@ -3547,7 +3577,9 @@ Backwards compatibility: - Renamed `Columns` type to `Dataset` (PR [\#620](https://github.com/pyviz/holoviews/issues/620)). -# Version 1.4.3 +## Version 1.4 + +### Version 1.4.3 **February 11, 2016** @@ -3587,7 +3619,7 @@ Backwards compatibility: - Renamed the DynamicMap mode `closed` to `bounded` ([PR \#477](https://github.com/pyviz/holoviews/pull/485)) -# Version 1.4.2 +### Version 1.4.2 **February 7, 2016** @@ -3633,7 +3665,7 @@ Fixes and improvements: - Compatibility with the latest Bokeh 0.11 release ([PR \#393](https://github.com/pyviz/holoviews/pull/393)) -# Version 1.4.1 +### Version 1.4.1 **December 22, 2015** @@ -3681,7 +3713,7 @@ Notable bug fixes: labels and values ([PR \#376](https://github.com/pyviz/holoviews/pull/376)). -# Version 1.4.0 +### Version 1.4.0 **December 4, 2015** @@ -3755,7 +3787,9 @@ API Changes: - DFrame conversion interface deprecated in favor of Columns pandas interface. -# Version 1.3.2 +## Version 1.3 + +### Version 1.3.2 **July 6, 2015** @@ -3773,7 +3807,7 @@ Bug fixes: - Ensuring that underscore.js is loaded in widgets (f2f6378). - Fixed Python3 issue in Overlay.get (8ceabe3). -# Version 1.3.1 +### Version 1.3.1 **July 1, 2015** @@ -3796,7 +3830,7 @@ Bug fixes: - Fix for multiple and animated colorbars (5e1e4b5). - Fix to Chart slices starting or ending at zero (edd0039). -# Version 1.3.0 +### Version 1.3.0 **June 27, 2015** @@ -3861,7 +3895,9 @@ API Changes should be customized instead. There is no longer a need to call the deprecated `Store.register_plots` method. -# Version 1.2.0 +## Version 1.2 + +### Version 1.2.0 **May 27, 2015** @@ -3923,7 +3959,9 @@ Important bug fixes: - Fixed plot ordering of overlaid elements across a `HoloMap` (c4f1685) -# Version 1.1.0 +## Version 1.1 + +### Version 1.1.0 **April 15, 2015** @@ -3950,7 +3988,9 @@ API changes (not backward compatible): In addition to the above improvements, many miscellaneous bug fixes were made. -# Version 1.0.1 +## Version 1.0 + +### Version 1.0.1 **March 26, 2015** @@ -3972,7 +4012,7 @@ Highlights: - Miscellaneous bug fixes, including Python 3 compatibility improvements. -# Version 1.0.0 +### Version 1.0.0 **March 16, 2015** diff --git a/doc/releases.rst b/doc/releases.rst deleted file mode 100644 index 216b89a563..0000000000 --- a/doc/releases.rst +++ /dev/null @@ -1,4510 +0,0 @@ -Releases -======== - -Version 1.18 -~~~~~~~~~~~~ - -Version 1.18.3 -************** - -**February 12, 2024** - -This micro release includes bug fixes. - -Bug fixes: - -- Fix BoundsX and BoundsY regression - (`#6099 `__) -- Fix rasterize regression - (`#6102 `__) -- Fix link selection for empty Layout - (`#6110 `__) -- Don’t pin notebook in conda recipe for pyviz channel - (`#6108 `__) - - -Version 1.18.2 -************** - -**February 5, 2024** - -This micro release includes a number of bug fixes and documentation -updates, as well as compatibility updates for xarray 2023.12 and Pandas -2.2. Many thanks to the new contributors @junietoc, @JulianGiles, and -@magic-lantern, as well as the returning contributors @ianthomas23, -@maximlt, @TheoMathurin, @philippjfr, @ahuang11, and @Hoxbro. - -Enhancements: - -- Update contour line calculations to use ContourPy’s - ``LineType.ChunkCombinedNan`` - (`#5985 `__) -- Use sys.executable for ``check_output`` - (`#5983 `__) -- Updates to ``show_versions`` - (`#6072 `__, - `#6081 `__) - -Bug fixes: - -- Support ``color_key`` in ``datashade`` when intermediate step is an - ``ImageStack`` - (`#5994 `__) -- Fix hist on overlay - (`#5995 `__) -- Set proper context before triggering streams - (`#6000 `__) -- Support partial bound function - (`#6009 `__) -- Add ``norm`` in ``init_artists`` in holoviews/plotting/mpl/raster.py - (`#6029 `__) -- Fix linking elements that are transformed by a Compositor - (`#6003 `__) -- Add datetime hover information for selector - (`#6023 `__, - `#6039 `__) -- Only evaluate ``rx`` if it is a Reactive Expression - (`#6014 `__) -- Ensure partial methods can be used as dmap callables - (`#6063 `__) -- Del frame after stack level is found in ``deprecated`` - (`#6085 `__) - -Compatibility: - -- Compatibility updates with xarray 2023.12 - (`#6026 `__) -- Add extra check to detect if we are in jupyterlite - (`#6007 `__) -- Compatibility updates with Pandas 2.2 - (`#6074 `__, - `#6078 `__) -- Add Comm ``on_open`` handler to initialize the server comm - (`#6076 `__) - -Documentation: - -- Fix docs (`#5996 `__) -- Fix Param usage in the Plot and Renderers guide - (`#6001 `__) -- Fixing URLs to bokeh project - (`#6005 `__) -- Fix to broken urls in example gallery pages - (`#6038 `__) -- Replace Google Analytics with GoatCounter - (`#6048 `__) -- Add downloads badges - (`#6088 `__) - -Maintenance: - -- Change to pytest-rerunfailures - (`#5984 `__) -- Holoviews maintenance - (`#5987 `__) -- Add ``log_cli_level = "INFO"`` to pytest - (`#5989 `__) -- Add shell pre-commit hook - (`#5991 `__) -- Enable Bugbear 904 - (`#5992 `__) -- Part 1, modernize test suite - (`#5954 `__) -- Enforce labels - (`#5996 `__) -- Add lower pin to scipy - (`#6032 `__) -- Skip Deploying_Bokeh_Apps.ipynb on Windows - (`#6070 `__) -- Fix failing Windows tests - (`#6087 `__) - - -Version 1.18.1 -************** - -**November 8, 2023** - -This release contains a small number of bug fixes and compatibility -updates — many thanks to @philippjfr and @Hoxbro for their -contributions. - -Bug fixes: - -- Account for overlaid elements when using ``subcoordinates_y`` - (`#5950 `__) -- Fix ``groupby`` option for vectorized annotations - (`#5956 `__) -- Fix and improvements to ``ImageStack`` - (`#5961 `__) -- Do not allow partial matches when updating ``OverlayPlot`` - (`#5962 `__) -- Always ravel array in ``unique_array`` - (`#5969 `__) - -Compatibility: - -- Update Stamen maps with new URL - (`#5967 `__) -- Compatibility updates for Numpy 2.0 - (`#5979 `__) - -Maintenance: - -- General maintenance - (`#5955 `__) - -Version 1.18.0 -************** - -**October 18, 2023** - -This release includes new features, improvements, and bug fixes. Among -these are the new elements. First is the ``ImageStack`` element allows -you to easily visualize a 3D array, while the ``VLines``, ``HLines``, -``VSpans``, and ``HSpans`` elements allow you to visualize vertical and -horizontal lines and spans easily. In addition, this release includes -support for subcoordinate systems in the y-axis and various other -enhancements and bug fixes. This release adds support for the newest -Python 3.12 and Bokeh 3.3 and drops support for Python 3.8 and Bokeh 2. - -Many thanks to the new contributors @MeggyCal, along with our returning -contributors @ahuang11, @ianthomas23, @jlstevens, @maximlt, @philippjfr, -and @Hoxbro. - -New features: - -- Implementation of ``ImageStack`` - (`#5751 `__, - `#5945 `__) -- Adding vectorized ``VLines``, ``HLines``, ``VSpans``, and ``HSpans`` - elements (`#5845 `__, - `#5911 `__, - `#5940 `__) -- Implement support for subcoordinate systems in the y-axis - (`#5840 `__) - -Enhancements: - -- Cycle through ``text_color`` when overlaying Labels - (`#5888 `__) -- Drop requirements for ``OrderedDict`` - (`#5867 `__, - `#5890 `__, - `#5925 `__) -- Allow to link to an ``Overlay`` - (`#5881 `__) -- Use contourpy for contour calculations - (`#5910 `__) -- Use browser information to set ``pixel_density`` in - ``ResampleOperation2D`` - (`#5947 `__) -- Avoid bounce back of events for ``Range{X,Y,XY}`` streams - (`#5946 `__) - -Bug fixes: - -- Fix overlaying labels in Sankey diagram - (`#5864 `__) -- Ensure the ``PlotSize`` stream works with undefined width/height - (`#5868 `__) -- Fix test when only ``python3`` command is available - (`#5874 `__) -- Try and except ``get_extents`` without the ``dimension`` argument and - add kwargs to all ``get_extents`` - (`#5872 `__) -- Enable ``Mathjax`` with ``enable_mathjax`` - (`#5904 `__) -- Fix ``histogram`` operation on Ibis data - (`#5929 `__) -- Raise exceptions in ``compare_dataset`` - (`#5932 `__) -- Don’t overlap objects in overlaid plot - (`#5942 `__) - -Compatibility: - -- Param 2.0 support - (`#5865 `__, - `#5897 `__, - `#5906 `__, - `#5918 `__) -- Pandas 2.1 support - (`#5877 `__, - `#5898 `__, - `#5880 `__) -- Numpy 1.25 support - (`#5870 `__) -- Replace ``np.NaN`` with ``np.nan`` for Numpy 2.0 - (`#5938 `__) -- Bokeh 3.3 support - (`#5873 `__, - `#5923 `__, - `#5935 `__) -- Dropping support for Bokeh 2 - (`#5891 `__) -- Python 3.12 support - (`#5909 `__) -- Dropping support for Python 3.8 - (`#5936 `__) -- Matplotlib 3.8 compatibility - (`#5910 `__, - `#5924 `__) -- Remove deprecations functions - (`#5915 `__) - -Documentation: - -- Add *Linking Bokeh plots* guide to the table of contents - (`#5900 `__) - -Maintenance: - -- Remove warnings - (`#5854 `__, - `#5894 `__) -- Add output of pre-commit hook in summary and add environment artifact - (`#5905 `__) -- Improvements to test CI - (`#5917 `__, - `#5892 `__) -- General maintenance update - (`#5889 `__, - `#5907 `__, - `#5934 `__, - `#5943 `__) -- Update build of conda package - (`#5921 `__, - `#5922 `__) -- Add sorting of imports - (`#5937 `__) -- Enable Bugbear lint - (`#5861 `__) - - -Version 1.17 -~~~~~~~~~~~~ - -Version 1.17.1 -************** - -**August 16, 2023** - -This release contains a small number of important bug fixes and -regressions — many thanks to @ianthomas23, @maximlt, @philippjfr, and -@Hoxbro for their contributions. - -Enhancements: - -- Improve support for ``hv.NdOverlay`` and ``hv.Overlay`` in - downsample1d - (`#5856 `__) -- Improve legend support for ``hv.Layout`` and add documentation for - ``sync_legends`` and ``show_legends`` - (`#5852 `__) - -Bug fixes: - -- ``RangeToolLink`` now correctly reset to the predefined ``boundsx`` - and ``boundsy`` values - (`#5848 `__) -- Fix regressions with Bokeh’s axis - (`#5838 `__, `#5850 `__, `#5851 `__) - -Compatibility: - -- Pin Param version for Bokeh 2 - (`#5844 `__) -- Explicitly clear Matplotlib figure to remove warning about - auto-removal of overlapping axes - (`#5857 `__) - -Documentation: - -- Set ``autohide`` example to ``True`` as in the hook - (`#5832 `__) - -Maintenance: - -- Add `OpenCollective `__ sponsor - link on the repo page - (`#5839 `__) -- Update Ruff in pre-commit and report panel communication in - ``hv.show_versions`` - (`#5853 `__) -- Cleanup imports - (`#5846 `__) - - -Version 1.17.0 -************** - -**July 24, 2023** - -This release brings one of the most requested features - interactive -twin-axis support! Another feature added in this release is the ability -to easily set custom options on plot components with ``backend_opts``, -making it much easier to customize your plots. Datashaders ``where`` and -``summary`` aggregators are now supported, and ``rasterize`` now has a -``selector`` option, making it easy to get extra information about your -rasterized data. Lastly, Bokeh figures with the same labels will -synchronize muteness or visibility across different figures. - -In addition, this release includes several enhancements and bug fixes. - -Many thanks to the new contributors @alfredocarella and @RaulPL, as well -as the returning contributors @ahuang11, @droumis, @jbednar, @jlstevens, -@maximlt, @philippjfr, @TheoMathurin and @Hoxbro. - -New features: - -- Multi-yaxis support in the Bokeh backend - (`#5621 `__, `#5826 `__, `#5827 `__) -- Allow modifying the underlying Bokeh or Matplotlib figure, axes, - etc. using ``backend_opts`` - (`#4463 `__) -- Support Datashaders ``where`` and ``summary`` aggregators and add - ``selector`` option to ``rasterize`` enabling instant hover - inspection of value dimensions - (`#5805 `__) -- Synchronize muteness or visibility across Bokeh figures to support - linked legends - (`#5763 `__) - -Enhancements: - -- Add option for initial ranges to RangeToolLink - (`#5800 `__) -- Allow resample’s ``pixel_ratio`` to go below 1 - (`#5813 `__, - `#5817 `__) Add the - ability for \`VectorField`\` to instantiate from UV coordinates - (`#5797 `__) -- Handle the ``nodata`` option for rasterized RGB image - (`#5774 `__) - -Bug fixes: - -- Fix bins option in the autompg_histogram demo - (`#5750 `__) -- Fix 0pt bug in safari - (`#5755 `__) -- Disable pan if ``active_tools=[]`` - (`#5754 `__) -- Better handling of inputs to ``rasterize.instance()`` - (`#5767 `__, - `#5811 `__) -- Fix class variable being used as instance ``vdims`` in ``hv.RGB`` - (`#5773 `__, - `#5775 `__) -- Improve notebook detection in VSCode and Google Colab - (`#5792 `__) -- Don’t warn when running ``rasterize`` in VSCode - (`#5780 `__) -- Add ``__init__`` to ``hv.Output`` to not overwrite its parent - signature - (`#5799 `__) -- Fix ``XArrayInterface`` crashing when input is an empty array - (`#5809 `__) -- Avoid setting ``batched`` before the class is initialized - (`#5814 `__) -- Fix aspect handling when plot size is still unknown - (`#5808 `__) -- Update callbacks to use Bokeh’s ``quad`` instead of ``quad`` - (`#5760 `__) -- Update ``hv.Image``/``hv.RGB`` ``invert_{x,y}axis`` to work with - Bokeh 3 (`#5796 `__) -- ``strip_magics`` should also strip IPython line magic - (`#5794 `__) -- Fix ``HoloMap.collapse`` for ``(Nd)Overlay`` - (`#5825 `__) - -Compatibility: - -- Implement HEP1 - Drop support for Python 3.7 - (`#5695 `__) -- Replace deprecated ``np.product`` with ``np.prod`` - (`#5787 `__) -- Update ``FileArchive`` repr for Param 2.0 - (`#5791 `__) -- Deprecate functionality - (`#5776 `__) - -Documentation: - -- Fix typo in Getting Started section text - (`#5759 `__) -- Add sep keyword to ``pd.read_csv`` in documentation page - (`#5798 `__) - -Maintenance: - -- General maintenance - (`#5758 `__, - `#5783 `__, - `#5802 `__, - `#5804 `__, - `#5806 `__, - `#5819 `__) -- Correctly check the version for deprecation - (`#5772 `__) -- Update pre-commit and lint - (`#5747 `__, - `#5768 `__, - `#5777 `__) -- Setup infrastructure for UI tests and add first linked streams tests - (`#5764 `__) - - -Version 1.16 -~~~~~~~~~~~~ - -Version 1.16.2 -************** - -**June 8, 2023** - -This release includes a breaking change as notebooks will no longer be -inlining as default. This change will reduce the size of the notebook -files and, more importantly, address an upstream issue in Jupyter where -running ``hv.extension`` would give an error when used in a notebook. - -Critical bug fixes and compatibility: - -- Correctly set ``inline=False`` when running ``hv.extension`` in a - Jupyter Notebook - (`#5748 `__) -- Adding more Param 2 support - (`#5717 `__) - -Enhancements: - -- Speed up import time of Holoviews - (`#5719 `__) - -Maintenance: - -- Fix failing tests - (`#5742 `__) -- Misc. mainteance - (`#5717 `__) - - -Version 1.16.1 -************** - -**June 2, 2023** - -This release contains a small number of important bug fixes and -enhancements. Many thanks to @philippjfr and @Hoxbro. - -This release includes a breaking change as notebooks will no longer be -inlining as default. This change will reduce the size of the notebook -files and, more importantly, address an upstream issue in Jupyter where -running ``hv.extension`` would give an error when used in a notebook. - -Critical bug fixes and compatibility: - -- Add ``enable_mathjax`` and set it and inline to ``False`` - (`#5729 `__) -- Update to support Datashader on Python 3.11 - (`#5720 `__) - -Enhancements: - -- Add ``show_versions`` helper function - (`#5725 `__) -- Ignore known model warning in VS Code - (`#5734 `__) -- Add workaround for plots where the objects in a figure would overlap - (`#5733 `__) - -Documentation: - -- Upgrade to latest ``nbsite`` and ``pydata-sphinx-theme`` - (`#5724 `__, - `#5735 `__) - - -Version 1.16.0 -************** - -**May 9, 2023** - -This release includes many new features, improvements, and bug fixes. -Among the highlights are support for Bokeh 3.1 and Panel 1.0, both of -which come with a wide range of new features and enhancements. Time -series support has also been improved, with auto-ranging along one axis, -a new downsample algorithm, and having WebGL enabled by default. In -addition, the release includes various other enhancements and bug fixes. - -We would like to thank the many users who contributed to this release by -filing bug reports, providing new features, and bug fixes. We want to -give a special shout-out to existing contributors @ianthomas23, -@jlstevens, @jordansamuels, @maximlt, @philippjfr, @TheoMathurin, -@Yura52, and @Hoxbro, as well as new contributors @GeoVizNow, @JRRudy1, -@keewis, @michaelaye, and @wendrul. - -This minor version will be the last to support Python 3.7. The next -minor version will require Python 3.8 or higher. In the next release, -``holoviews.annotate`` will start giving a deprecation warning about its -future move to the new package -`HoloNote `__. - -New features: - -- Support for Bokeh 3.1 and Panel 1.0 - (`#5388 `__, - `#5620 `__, - `#5640 `__, - `#5679 `__, - `#5683 `__, - `#5692 `__, - `#5703 `__) -- Implement auto-ranging support along one axis - (`#5554 `__, - `#5609 `__) -- Add Largest Triangle Three Buckets (LTTB) as a downsample algorithm - (`#5552 `__) -- Enable WebGL by default - (`#5708 `__) - -Enhancements: - -- Improve ``legend_cols`` support for Bokeh 3 - (`#5669 `__) -- Add convenience ``getter`` interface to ``opts`` - (`#5606 `__) -- Ensure ``.stack`` works on areas with different ``vdims`` - (`#5693 `__) -- Add ``muted`` support to ``PointPlot`` like ``hv.Scatter`` - (`#5705 `__) -- Automatic detect ``comms`` without calling ``pn.extension()`` - (`#5645 `__) -- Add support for extra ``Hovertool`` variables in a Bokeh’s - ``quadmesh`` with 2D coordinates (with tests) - (`#5638 `__) -- Change ``hv.Rectangles`` to internally use Bokeh ``Quad`` and not - ``Rect`` to support logarithmic axis in WebGL - (`#5664 `__, - `#5702 `__) - -Bug fixes: - -- Ensure ``spatial_select`` in non-zero indexed DataFrame is applied - right (`#5625 `__) -- Fix error handling for plotting class lookups on empty - ``DynamicMap``/``HoloMap`` - (`#5604 `__) -- Fix ``active_tools`` to only be set for enabled tools - (`#5616 `__) -- Fix legend display when using categorical ``datashade`` on GPU - (`#5631 `__) -- Adding ``GeoDataFrame`` to ``DataConversion`` - (`#5325 `__) -- Don’t emit warnings when the toolbar is disabled - (`#5691 `__) -- Don’t try to find the closest match if the input is empty - (`#5700 `__) -- Only use ``long_name`` if it is a string - (`#5646 `__) -- Use Matplotlib’s public API to list the colormaps - (`#5598 `__) - -Compatibility: - -- Add Param 2.0 support - (`#5667 `__, - `#5641 `__, - `#5680 `__, - `#5704 `__) -- Add Pandas 2.0 support - (`#5662 `__) -- Update ``types.functiontype`` to ``collections.abc.callable`` - (`#5636 `__) -- Improve error message for categorical data when used with - ``datashade`` - (`#5643 `__) -- Don’t disable Jedi completion by default - (`#5701 `__) - -Documentation: - -- Fix an incorrect number stated regarding available axis types - (`#5623 `__) -- Fix ``BoundsY`` example - (`#5629 `__) -- Fix formatting on FAQ - (`#5630 `__) -- Fix anchor links - (`#5677 `__) - -Maintenance: - -- Use ``clean-notebook``, ``codespell``, and ``ruff`` in ``pre-commit`` - (`#5594 `__, - `#5627 `__, - `#5653 `__) -- General maintenance - (`#5607 `__, - `#5611 `__, - `#5612 `__, - `#5649 `__) - -Known issues: - ``BoxEditTool`` is not yet supported with the new -internals of ``hv.Rectangle``. - -Removals: - -- Raise ``DataError`` for non-string column names in DataFrame - (`#5654 `__) -- Remove deprecated tile sources - (`#5654 `__) -- Removed support for ``apply_groups`` for old option groups signature - (`#5497 `__) - -Version 1.15 -~~~~~~~~~~~~ - -Version 1.15.4 -************** - -**January 16, 2023** - -This release contains a small number of enhancements and important bug -fixes. Many thanks to our new contributors @mmorys, @jj-github-jj, and -@sandhujasmine, but also our returning contributors @droumis, -@jlstevens, @MarcSkovMadsen, @maximlt, @philippjfr, @stanwest, and -@Hoxbro. - -Enhancements: - -- Make lasso select mask values using a Dask-compatible method - (`#5568 `__) -- Make plotly legend group unique - (`#5570 `__) -- Set pan and wheel_zoom as the default Bokeh active tools - (`#5480 `__) -- Enable rendering colorbars on bokeh ``GraphPlot``\ s - (`#5585 `__) -- Add Plotly ``Scatter3d`` documentation and fix colorbar title - (`#5418 `__) - -Bug fixes: - -- Only trigger range-update once in callbacks - (`#5558 `__) -- Ensure dynamically created subplots can be updated - (`#5555 `__) -- Fix start of stack-level in deprecations - (`#5569 `__) -- When sorting colormap records, replace None with an empty string - (`#5539 `__) -- Fix annotator in Geoviews by adding deserialization of - non-deserialized base64 data - (`#5587 `__) -- Fix ``hv.Empty`` not working in ``AdjointLayout`` plot - (`#5584 `__) -- Check for categorical data to histogram - (`#5540 `__) -- Fix ``clim_percentile`` - (`#5495 `__) - -Compatibility: - -- Compatibility with Shapely 2.0 - (`#5561 `__) -- Compatibility with Numpy 1.24 - (`#5581 `__) -- Compatibility with Ibis 4.0 - (`#5588 `__) - -Documentation: - -- Installation instructions update - (`#5562 `__) -- Use OSM for reference tile source in notebook documentation - (`#5536 `__) -- Enhance Tiles example notebook - (`#5563 `__) - -Maintenance: - -- Various fixes and general maintenance of the CI - (`#5384 `__, - `#5573 `__, - `#5576 `__, - `#5582 `__) -- Updated codebase to modern Python conventions - (`#5509 `__, - `#5577 `__) -- Renamed ``master`` branch to ``main`` - (`#5579 `__) -- Update binder link and dependency pinning - (`#5583 `__) -- Update copyright to only contain start year - (`#5580 `__) - -Version 1.15.3 -************** - -**December 6, 2022** - -This release contains a small number of important bug fixes and -adds support for Python 3.11. Many thanks to our maintainers -@Hoxbro, @maximlt and @jlstevens. - -Bug Fixes: - -- Fix for empty opts warning and incorrect clearing semantics - (`#5496 `__) -- Fix potential race condition in the Options system - (`#5535 `__) - -Enhancements: - -- Add support to Python 3.11 - (`#5513 `__) -- Cleanup the top ``__init__`` module - (`#5516 `__) - -Documentation: - -- Fixes to release notes and CHANGELOG - (`#5506 `__) - - -Version 1.15.2 -************** - -**November 3, 2022** - -This release contains a small number of important bug fixes. Many thanks -to @stanwest for his contribution and thank you to our maintainers -@Hoxbro, @maximlt, @jlstevens, @jbednar, and @philippjfr. - -Bug fixes: - -- Fix support for jupyterlite - (`#5502 `__) -- Improve error message for ``hv.opts`` without a plotting backend - (`#5494 `__) -- Fix warnings exposed in CI logs - (`#5470 `__) -- Thanks to @maximlt for various CI fixes - (`#5484 `__, - `#5498 `__, - `#5485 `__) - -Enhancement: - -- Allow Dimension objects to accept a dictionary specification - (`#5333 `__) -- Refactor to remove iterrows for loop from ``connect_edges_pd`` - (`#5473 `__) - -Deprecations: - -Promoted ``DeprecationWarning`` to ``FutureWarning`` when using pandas -``DataFrame``\ s with non-string column names. This will not change any -functionality but will start warning users about functionality that will -be deprecated in future. - -- Upgrade warning for invalid dataframe column names - (`#5472 `__) - - -Version 1.15.1 -************** - -**October 4, 2022** - -This release contains a small number of important bug fixes. Many thanks -to all our new contributors @MarcSkovMadsen, @j-svensmark, @ceball, -@droumis, @ddrinka, @Jhsmit and @stanwest as well as a special thanks to -@Hoxbro for his many bug fixes. An additional thank you goes out to -@maximlt, @philippjfr, @jbednar and @jlstevens. - -Enhancements: - -- Sort output of ``decimate`` operation so that it can be used with - connected Elements (Curve, Area, etc.) - (`#5452 `__) -- Ensure HoloViews is importable from a pyodide webworker - (`#5410 `__) -- Add support for stepwise Area plots - (`#5390 `__) -- Better error message for ``hv.Cycle`` when incompatible backend - activated - (`#5379 `__) -- Improvements to VSCode notebook support - (`#5398 `__) -- Protect matplotlib tests from global styles - (`#5311 `__) -- Faster hashing for arrays and pandas objects - (`#5455 `__) -- Add pre-commit hooks to CI actions and fixes to pytest configuration - (`#5385 `__, - `#5440 `__) - -Bug Fixes: - -- Allow import of numpy 1.12 - (`#5367 `__) -- Fixes handling of iterables in Overlays - (`#5320 `__) -- Always return a string when using ``hv.Dimension.pprint_value`` - (`#5383 `__) -- Support widgets in slices for ``loc`` and ``iloc`` - (`#5352 `__) -- Take account of labeled dimension in Bokeh plotting classes - (`#5404 `__) -- Fix handling of pandas ``Period`` ranges - (`#5393 `__) -- Fixed declaration of ``Scatter`` to ``Selection1DExpr`` - (`#5413 `__) -- Ensure rangesupdate event fires on all plots with linked axes - (`#5465 `__) -- Fixed fallback to shapely spatial select - (`#5468 `__) -- Many thanks to @Hoxbro for many miscellaneous plotting fixes, - including fixes to plotting of ``BoxWhisker``, ``VectorField`` - elements (`#5397 `__, - `#5450 `__, - `#5400 `__, - `#5409 `__, - `#5460 `__)) -- Fixes to documentation building GitHub Action - (`#5320 `__, - (`#5320 `__)) - -Documentation: - -- Introduced module documentation - (`#5362 `__) -- Remove Python 2 references from README - (`#5365 `__) -- Update call to panel add_periodic_callback in Bokeh gallery example - (`#5436 `__) -- Added reference to example in ``RangeToolLink`` - (`#5435 `__) - -API: - -In future, HoloViews will not allow non-string values for pandas -DataFrame column names. This deprecation cycle starts by issuing a -``DeprecationWarning`` that should not be visible to users. - -- Issue DeprecationWarning for invalid DataFrame column types - (`#5457 `__) - - -Version 1.15.0 -************** - -**July 6, 2022** - -This is a major release with a large number of new features and bug -fixes, as well as updates to Python and Panel compatibility. - -Many thanks to the numerous users who filed bug reports, tested -development versions, and contributed a number of new features and bug -fixes, including special thanks to @ablythed @ahuang11 -@douglas-raillard-arm @FloLangenfeld @HoxBro @ianthomas23 @jenssss -@pepijndevos @peterroelants @stas-sl @Yura52 for their contributions. In -addition, thanks to the maintainers @jbednar, @maximlt, @jlstevens and -@philippjfr for contributing to this release. - -Compatibility: - -- Python 2 support has finally been dropped with 1.14.9 as the last - release supporting Python 2. -- HoloViews now requires panel >0.13.1 - (`#4329 `__) -- Colormaps for the output of the datashade operation have changed to - address - `holoviz/datashader#357 `__; - see ``rescale_discrete_levels`` below. To revert to the old colorbar - behavior, set ``ColorbarPlot.rescale_discrete_levels = False`` in the - ``bokeh`` or ``mpl`` plotting modules as appropriate. -- Updated Sankey algorithm means that some users may need to update the - ``node_padding`` parameter for plots generated with earlier releases. - -Major features: - -After a long period of hotfix releases for the 1.14.9 series, many new -features on the main branch have been released. Features relating to -datashader support, linked selection and improvements to the Bokeh -plotting backend are called out in their own sections. - -- Support constructor interface from a spatialpandas GeometryArray - (`#5281 `__) -- Allow plotting anonymous pandas.Series - (`#5015 `__) -- Add support for rendering in pyodide/pyscript - (`#5338 `__, - `#5321 `__, - `#5275 `__) - -Datashader features: - -The following new features have been added to the datashader support in -HoloViews, mainly focused on Datashader's new support for antialiasing -lines as well as the new ``rescale_discrete_levels`` colormapping -option. - -- Add automatic categorical legend for datashaded plots - (`#4806 `__) -- Implement ``line_width`` support when rasterizing spatialpandas paths - (`#5280 `__) -- Expose ``rescale_discrete_levels`` in the Bokeh backend - (`#5312 `__) -- Set ``rescale_discrete_levels=True`` by default - (`#5268 `__) - -New linked selection features: - -- Implement ``linked_selection.filter`` method - (`#4999 `__) -- Allow passing custom ``selection_expr`` to linked selections filter - (`#5012 `__) -- Fix ``AdjointLayout`` in ``link_selections`` - (`#5030 `__) - -New features for the Bokeh plotting backend: - -- Add ``legend_labels`` option to allow overriding legend labels - (`#5342 `__) -- Updated sankey algorithm to ``d3-sankey-v0.12.3`` - (`#4707 `__) - -Other enhancements: - -- Optimize and clean up options system - (`#4954 `__) -- Optimize lasso selection by applying box-select first - (`#5061 `__) - https://github.com/holoviz/holoviews/pull/5061 -- Support ibis-framework version 3 - (`#5292 `__) -- Add ``OpenTopoMap`` as a tile source - (`#5052 `__) -- Show all histograms of an ``Overlay`` - (`#5031 `__) - -Bug fixes: - -- Fix batch watching and linking of parameters in Params stream - (`#4960 `__, - `#4956 `__) -- Ensure ``Plot.refresh`` is dispatched immediately if possible - (`#5348 `__) -- Fix datashader empty overlay aggregation - (`#5334 `__) -- Fixed missing handling of nodata for count aggregator with column - (`#4951 `__) -- Handle ``pd.NA`` as missing data in dtype=object column - (`#5323 `__) -- Forward ``DynamicMap.hist`` dimension parameter to histogram creation - (`#5037 `__) -- Remove numpy pin from examples - (`#5285 `__) -- Fix vmin/vmax deprecation on matplotlib HeatMapPlot - (`#5300 `__) -- Don't skip each renderer's ``load_nb call`` when multiple extension - calls are made in a single cell - (`#5302 `__) -- Set plotly range correctly for log axis - (`#5272 `__) -- Sanitize uses of ``contextlib.contextmanager`` - (`#5018 `__) -- Ensure ``overlay_aggregate`` is not applied for anti-aliased lines - (`#5266 `__) -- Switch to using bokeh ``rangesupdate`` event for ``Range`` streams - (`#5265 `__) -- Fixes for bokeh ``Callbacks`` - (`#5040 `__) -- Fix for attribute error in matplotlib ``CompositePlot`` - (`#4969 `__) -- Silenced inappropriate deprecation warnings and updated deprecation - settings in options system - (`#5345 `__, - `#5346 `__) - -Documentation: - -The following improvements to the documentation have been made: - -- Fix ``hv.help`` when pattern is set - (`#5330 `__) -- Added release dates to changelog and releases - (`#5027 `__, - `#5035 `__) -- Removed unneeded list from dynamic map example - (`#4953 `__) -- Added FAQ about sharing only a single axis - (`#5278 `__) -- Miscellaneous fixes to Heatmap reference notebook and Continuous - Coordinates user guide - (`#5262 `__) -- Added example of multiple RGB images as glyphs - (`#5172 `__) -- Trim trailing whitespaces - (`#5019 `__) -- Update outdated IOAM references - (`#4985 `__) - -Testing infrastructure: - -Many thanks to @maximlt for his work maintaining and fixing the testing -infrastructure across too many PRs to list here. - -- Switch to pytest - (`#4949 `__) -- Test suite clean up and fix for the pip build - (`#5326 `__) -- Test updates following release of datashader 0.14.1 - (`#5344 `__) - - - -Version 1.14 -~~~~~~~~~~~~ - -Version 1.14.9 -************** - -**May 6, 2022** - -This release contains a small number of important bug fixes as well as -support for the newly added antialiasing option for line rendering in -datashader. Many thanks to @andriyot, @Hoxbro, @pepijndevos, @stas-sl, -@TheoMathurin, @maximlt, @jlstevens, @jbednar, and @philippjfr. - -Enhancements: - -- Improvements to extension loading, improving visual appearance in - JupyterLab when no logo is used and a check to avoid loading - unnecessary JavaScript. - (`#5216 `__, - `#5249 `__) -- Add support for setting antialiased line_width on datashader line - aggregation as well as pixel_ratio setting - (`#5264 `__, - `#5288 `__) -- Added options to customize hover line_(width|join|cap|dash) - properties - (`#5211 `__) -- Restored Python 2 compatibility that lapsed due to lack of CI testing - since 1.14.3. This is expected to be the last release with Python 2 - support. (`#5298 `__) - -Bug fixes: - -- Fix to respect series order in stacked area plot - (`#5236 `__) -- Support buffer streams of unspecified length (#5247) - (`#5247 `__) -- Fixed log axis lower bound when data minimum is <= 0 - (`#5246 `__) -- Declared GitHub project URL in setup.py - (`#5227 `__) -- Fixed streaming Psutil example application - (`#5243 `__) -- Respecting Renderer’s center property for HoloViews pane - (`#5197 `__) - -Documentation: - -- Updated Large data guide to reflect changes in Datashader and - antialiasing support - (`#5267 `__, - `#5290 `__) - - -Version 1.14.8 -************** - -**February 15, 2022** - -This release contains a small number of important bug fixes as well as -fixes required for Python 3.9 and 3.10 support. Many thanks to @Hoxbro, -@maximlt, @jlstevens, @jbednar, and @philippjfr. - -Bug fixes: - -- Fixed xarray validation for aliased coordinate - (`#5169 `__) -- Fixed xaxis/yaxis options with Matplotlib - (`#5200 `__) -- Fixed nested widgets by handling list or tuple values in - ``resolve_dependent_value`` utility - (`#5184 `__) -- Fixed issue handling multiple widgets without names - (`#5185 `__) -- Fix overlay of two-level categorical plots and HLine - (`#5203 `__) -- Added support for Ibis > 2.0 - (`#5204 `__) -- Allow lower dimensional views on arbitrary dimensioned elements - (`#5208 `__) -- Fix escaping of HTML on Div element - (`#5209 `__) -- Miscellaneous fixes to unit tests, including cudf test fixes as well - as addition of Python 3.9 and 3.10 to the test matrix - (`#5166 `__, - `#5199 `__, - `#5201 `__, - `#5206 `__) - -Version 1.14.7 -************** - -**December 16, 2021** - -This release contains a small number of important bug fixes. Many thanks -to @douglas-raillard-arm, @jenssss, @w31t1, @Hoxbro, @martinfleis, @maximlt, -@jlstevens, @jbednar, and @philippjfr. - -## Bug fixes: - -- Support xyzservices.TileProvider as hv.Tiles input - (`#5062 `__) -- Allow reversed layout/overlay binary operators for ``+`` and ``*`` to be used with custom objects - (`#5073 `__) -- Fix internal numpy.round usage - (`#5095 `__) -- Remove dependency on recent Panel release by importing bokeh version from util module - (`#5103 `__) -- Add missing bounds for the cache_size Parameter - (`#5105 `__) -- Add current_key property to DynamicMap - (`#5106 `__) -- Pin freetype on Windows to avoid matplotlib error - (`#5109 `__) -- Handle the empty string as a group name - (`#5131 `__) -- Do not merge partially overlapping Stream callbacks - (`#5133 `__) -- Fix Violin matplotlib rendering with non-finite values - (`#5135 `__) -- Fix matplotlib colorbar labeling for dim expressions - (`#5137 `__) -- Fix datetime clipping on RangeXY stream - (`#5138 `__) -- Ensure FreehandDraw renders when styles are set - (`#5139 `__) -- Validate dimensionality of xarray interface data - (`#5140 `__) -- Preserve cols when overlaying on layout - (`#5141 `__) -- Fix Bars legend error when overlaid with annotation - (`#5142 `__) -- Fix plotly Bar plots containing NaNs - (`#5143 `__) -- Do not raise deprecated .opts warning for empty groups - (`#5144 `__) -- Handle unsigned integer dtype in datashader aggregate operation - (`#5149 `__) -- Delay projection comparison to optimize geoviews - (`#5152 `__) -- Utility to convert datetime64 to int64 and test suite maintenance - (`#5157 `__) -- Fix for Contours consistent of empty and nonempty paths - (`#5162 __`) -- Fixed docs: - * Fix `fig_bounds` description in Plotting_with_Matplotlib.ipynb - (`#4983 `__) - * Fix broken link in Gridded user guide - (`#5098 `__) -- Improved docs: - * Switch to the Pydata Sphinx theme - (`#5163 `__) - - -Version 1.14.6 -************** - -**September 16, 2021** - -This is a hotfix release with a number of important bug fixes. Most -importantly, this version supports the recent bokeh 2.4.0 release. -Many thanks to @geronimos, @peterroelants, @douglas-raillard-arm, -@philippjfr and @jlstevens for contributing the fixes in this release. - -Bug fixes: - -- Compatibility for bokeh 2.4 and fixes to processing of falsey - properties and visible style property - (`#5059 `__, - `#5063 `__) -- Stricter validation of data.interface before calling subclass - (`#5050 `__) -- Fix to prevent options being ignored in some cases - (`#5016 `__) -- Improvements to linked selections including support for linked - selection lasso for cudf and improved warnings - (`#5044 `__, - `#5051 `__) -- Respect apply_ranges at range computation level - (`#5081 `__) -- Keep ordering of kdim when stacking Areas - (`#4971 `__) -- Apply hover postprocessor on updates - (`#5039 `__) - - -Version 1.14.5 -************** -**July 16, 2021** - -This is a hotfix release with a number of important bug fixes. Most -importantly, this version supports for the recent pandas 1.3.0 release. -Many thanks to @kgullikson88, @philippjfr and @jlstevens for -contributing the fixes in this release. - -Bug fixes: - -- Support for pandas>=1.3 - (`#5013 `__) -- Various bug fixes relating to dim transforms including the use of - parameters in slices and the use of getattribute - (`#4993 `__, - `#5001 `__, - `#5005 `__) - - -Version 1.14.4 -************** -**May 18, 2021** - -This release primarily focuses on a number of bug fixes. Many thanks to -@Hoxbro, @nitrocalcite, @brl0, @hyamanieu, @rafiyr, @jbednar, @jlstevens -and @philippjfr for contributing. - -Enhancements: - -- Re-enable ``SaveTool`` for plots with ``Tiles`` - (`#4922 `_) -- Enable dask ``TriMesh`` rasterization using datashader - (`#4935 `_) -- Use dataframe index for ``TriMesh`` node indices - (`#4936 `_) - -Bug fixes: - -- Fix hover for stacked ``Bars`` - (`#4892 `_) -- Check before dereferencing Bokeh colormappers - (`#4902 `_) -- Fix multiple parameterized inputs to ``dim`` - (`#4903 `_) -- Fix floating point error when generating bokeh Palettes - (`#4911 `_) -- Fix bug using dimensions with label on ``Bars`` - (`#4929 `_) -- Do not reverse colormaps with '_r' suffix a second time - (`#4931 `_) -- Fix remapping of ``Params`` stream parameter names - (`#4932 `_) -- Ensure ``Area.stack`` keeps labels - (`#4937 `_) - -Documentation: - -- Updated Dashboards user guide to show ``pn.bind`` first - (`#4907 `_) -- Updated docs to correctly declare Scatter kdims - (`#4914 `_) - -Compatibility: - -Unfortunately a number of tile sources are no longer publicly available. -Attempting to use these tile sources will now issue warnings unless -``hv.config.raise_deprecated_tilesource_exception`` is set to ``True`` -in which case exceptions will be raised instead. - -- The ``Wikipedia`` tile source is no longer available as it is no - longer being served outside the wikimedia domain. As one of the most - frequently used tile sources, HoloViews now issues a warning and - switches to the OpenStreetMap (OSM) tile source instead. -- The ``CartoMidnight`` and ``CartoEco`` tile sources are no longer - publicly available. Attempting to use these tile sources will result - in a deprecation warning. - - -Version 1.14.3 -************** -**April 8, 2021** - -This release contains a small number of bug fixes, enhancements and -compatibility for the latest release of matplotlib. Many thanks to -@stonebig, @Hoxbro, @jlstevens, @jbednar and @philippjfr. - -Enhancements: - -- Allow applying linked selections to chained ``DynamicMap`` - (`#4870 `__) -- Issuing improved error message when ``__radd__`` called with an - integer (`#4868 `__) -- Implement ``MultiInterface.assign`` - (`#4880 `__) -- Handle tuple unit on xarray attribute - (`#4881 `__) -- Support selection masks and expressions on gridded data - (`#4882 `__) - -Bug fixes: - -- Handle empty renderers when merging ``HoverTool.renderers`` - (`#4856 `__) - -Compatibility: - -- Support matplotlib versions >=3.4 - (`#4878 `__) - - -Version 1.14.2 -************** - -**March 2, 2021** - - -This release adds support for Bokeh 2.3, introduces a number of minor -enhancements, miscellaneous documentation improvements and a good number -of bug fixes. - -Many thanks to the many contributors to this release, whether directly -by submitting PRs or by reporting issues and making suggestions. -Specifically, we would like to thank @philippjfr for the Bokeh 2.3 -compatibility updates, @kcpevey, @timgates42, and @scottstanie for -documentation improvements as well as @Hoxbro and @LunarLanding for -various bug fixes. In addition, thanks to the maintainers @jbednar, -@jlstevens and @philippjfr for contributing to this release. - -Enhancements: - -- Bokeh 2.3 compatibility - (`#4805 `__, - `#4809 `__) -- Supporting dictionary streams parameter in DynamicMaps and operations - (`#4787 `__, - `#4818 `__, - `#4822 `__) -- Support spatialpandas DaskGeoDataFrame - (`#4792 `__) -- Disable zoom on axis for geographic plots - (`#4812 `__ -- Add support for non-aligned data in Area stack classmethod - (`#4836 `__) -- Handle arrays and datetime ticks - (`#4831 `__) -- Support single-value numpy array as input to HLine and VLine - (`#4798 `__) - -Bug fixes: - -- Ensure link_inputs parameter on operations is passed to apply - (`#4795 `__) -- Fix for muted option on overlaid Bokeh plots - (`#4830 `__) -- Check for nested dim dependencies - (`#4785 `__) -- Fixed np.nanmax call when computing ranges - (`#4847 `__) -- Fix for Dimension pickling - (`#4843 `__) -- Fixes for dask backed elements in plotting - (`#4813 `__) -- Handle isfinite for NumPy and Pandas masked arrays - (`#4817 `__) -- Fix plotting Graph on top of Tiles/Annotation - (`#4828 `__) -- Miscellaneous fixes for the Bokeh plotting extension - (`#4814 `__, - `#4839 `__) -- Miscellaneous fixes for index based linked selections - (`#4776 `__) - -Documentation: - -- Expanded on Tap Stream example in Reference Gallery - `#4782 `__ -- Miscellaneous typo and broken link fixes - (`#4783 `__, - `#4827 `__, - `#4844 `__, - `#4811 `__) - -Version 1.14.1 -************** - -**December 28, 2020** - -This release contains a small number of bug fixes addressing -regressions. Many thanks to the contributors to this release including -@csachs, @GilShoshan94 and the maintainers @jlstevens, @jbednar and -@philippjfr. - -Bug fixes: - -- Fix issues with linked selections on tables - (`#4758 `__) -- Fix Heatmap alpha dimension transform - (`#4757 `__) -- Do not drop tools in linked selections - (`#4756 `__) -- Fixed access to possibly non-existent key - (`#4742 `__) - -Documentation: - -- Warn about disabled interactive features on website - (`#4762 `__) - - -Version 1.14.0 -************** - -**December 1, 2020** - - -This release brings a number of major features including a new -IbisInterface, new Plotly Dash support and greatly improved Plotly -support, and greatly improved interaction and integration with -Datashader. Many thanks to the many contributors to this release, -whether directly by submitting PRs or by reporting issues and making -suggestions. Specifically, we would like to thank @philippjfr, -@jonmmease, and @tonyfast for their work on the IbisInterface and -@jonmmease for improving Plotly support, as well as @kcpevey, @Hoxbro, -@marckassay, @mcepl, and @ceball for various other enhancements, -improvements to documentation and testing infrastructure. In addition, -thanks to the maintainers @jbednar, @jlstevens and @philippjfr for -contributing to this release. This version includes a large number of -new features, enhancements, and bug fixes. - -It is important to note that version 1.14 will be the last HoloViews -release supporting Python 2. - -Major features: - -- New Plotly Dash support - (`#4605 `__) -- New Plotly support for Tiles element - (`#4686 `__) -- New IbisInterface - (`#4517 `__) -- Greatly improved Datashader ``rasterize()`` - (`#4567 `__). - Previously, many of the features of Datashader were available only - through ``datashade``, which rendered data all the way to RGB pixels - and thus prevented many client-side Bokeh features like hover, - colorbars, dynamic colormaps, etc. ``rasterize`` now supports all - these Bokeh features along with nearly all the Datashader features - previously only available through ``datashade``, including (now - client-side) histogram equalization with ``cnorm='eq_hist'`` and easy - control of transparency via a new ``Dimension.nodata`` parameter. - See the `Large Data User Guide - `__ for more - information. - -Enhancements: - -- Implemented datashader aggregation of Rectangles - (`#4701 `__) -- New support for robust color limits (``clim_percentile``) - (`#4712 `__) -- Support for dynamic overlays in link_selections - (`#4683 `__) -- Allow clashing Param stream contents - (`#4677 `__) -- Ensured pandas does not convert times to UTC - (`#4711 `__) -- Removed all use of cyordereddict - (`#4620 `__) -- Testing infrastructure moved to GH Actions - (`#4592 `__) - -Bug fixes: - -- Ensure RangeXY returns x/y ranges in correct order (#4665) - (`#4665 `__) -- Fix datashader instability with Plotly by disabling padding for RGB - elements (`#4705 `__) -- Various Dask and cuDF histogram fixes - (`#4691 `__) -- Fix handling of custom matplotlib and bokeh colormaps - (`#4693 `__) -- Fix cuDF values implementation - (`#4687 `__) -- Fixed range calculation on HexTiles - (`#4689 `__) -- Use PIL for RGB.load_image - (`#4639 `__) - -Documentation: - -- Clarified data types accepted by Points - (`#4430 `__) -- Updated Introduction notebook - (`#4682 `__) -- Fixed releases urls - (`#4672 `__) - -Compatibility: - -- Warning when there are multiple kdims on Chart elements - (`#4710 `__) -- Set histogram ``normed`` option to False by default - (`#4258 `__) -- The default colormap in holoviews is now ‘kbc_r’ instead of ‘fire’; - see issue - `#3500 `__ for - details. This change was made mainly because the highest value of the - fire colormap is white, which meant data was often not visible - against a white background. To restore the old behavior you can set - ``hv.config.default_cmap='fire'``, which you can do via the extension - e.g. ``hv.extension('bokeh', config=dict(default_cmap='fire'))``. - There is also ``hv.config.default_gridded_cmap`` which you can set to - ‘fire’ if you wish to use the old colormap for the ``Raster``, - ``Image`` and ``QuadMesh`` element types. The default ``HeatMap`` - colormap has also been set to ‘kbc_r’ for consistency and can be set - back to the old value of ‘RdYlBu_r’ via - ``hv.config.default_heatmap_cmap``. - - -Version 1.13 -~~~~~~~~~~~~ - -Version 1.13.5 -************** - -**October 23, 2020** - - -This version contains numerous bug fixes and a number of enhancements. -Many thanks for contribution by @bryevdv, @jbednar, @jlstevens, -@jonmmease, @kcpevey and @philippjfr. - -Bug fixes: - -- Improvements to iteration over Series in CuDF data backend - (`#4624 `_) -- Added .values_host calls needed for iteration in CuDF backend - (`#4646 `_) -- Fixed bug resetting ranges - (`#4654 `_) -- Fix bug matching elements to subplots in ``DynamicMap`` - (`#4649 `_) -- Ensure consistent split ``Violin`` color assignment - (`#4650 `_) -- Ensure ``PolyDrawCallback`` always has vdim data - (`#4644 `_) -- Set default align in bokeh correctly - (`#4637 `_) -- Fixed deserialization of polygon/multi_line CDS data in bokeh backend - (`#4631 `_) - -Enhancements: - -- Refactor of link selections streams - (`#4572 `_) -- Add ability to listen to dataset linked_selection - (`#4547 `_) -- Added ``selected`` parameter to Bokeh PathPlot - (`#4641 `_) - -Documentation: - -- Improved ``Bars`` reference example, demonstrating the dataframe constructor - (`#4656 `_) -- Various documentation fixes - (`#4628 `_) - -Version 1.13.4 -************** - -**September 8, 2020** - - -This version fixes a large number of bugs particularly relating to -linked selections. Additionally it introduces some enhancements laying -the groundwork for future functionality. Many thanks for contribution -by @ruoyu0088, @hamogu, @Dr-Irv, @jonmmease, @justinbois, @ahuang11, -and the core maintainer @philippjfr. - -Bug fixes: - -- Fix the ``.info`` property to return the info - (`#4513 `_) -- Set ``toolbar=True`` the default in ``save()`` - (`#4518 `_) -- Fix bug when the default value is 0 - (`#4537 `_) -- Ensure operations do not recursively accumulate pipelines - (`#4544 `_) -- Fixed whiskers for ``BoxWhisker`` so that they never point inwards - (`#4548 `_) -- Fix issues with boomeranging events when aspect is set - (`#4569 `_) -- Fix aspect if width/height has been constrained - (`#4579 `_) -- Fixed categorical handling in Geom plot types - (`#4575 `_) -- Do not attempt linking axes on annotations - (`#4584 `_) -- Reset ``RangeXY`` when ``framewise`` is set - (`#4585 `_) -- Add automatic collate for ``Overlay`` of ``AdjointLayout`` s - (`#4586 `_) -- Fixed color-ranging after box select on side histogram - (`#4587 `_) -- Use HTTPS throughout on homepage - (`#4588 `_) - -Compatibility: - -- Compatibility with bokeh 2.2 for CDSCallback - (`#4568 `_) -- Handle ``rcParam`` deprecations in matplotlib 3.3 - (`#4583 `_) - -Enhancements: - - -- Allow toggling the ``selection_mode`` on ``link_selections`` from the - context menu in the bokeh toolbar - (`#4604 `_) -- Optimize options machinery - (`#4545 `_) -- Add new ``Derived`` stream class - (`#4532 `_) -- Set Panel state to busy during callbacks - (`#4546 `_) -- Support positional stream args in ``DynamicMap`` callback - (`#4534 `_) -- ``legend_opts`` implemented - (`#4558 `_) -- Add ``History`` stream - (`#4554 `_) -- Updated spreading operation to support aggregate arrays - (`#4562 `_) -- Add ability to supply ``dim`` transforms for all dimensions - (`#4578 `_) -- Add 'vline' and 'hline' Hover mode - (`#4527 `_) -- Allow rendering to pgf in matplotlib - (`#4577 `_) - -Version 1.13.3 -************** - -**June 23, 2020** - - -This version introduces a number of enhancements of existing -functionality, particularly for features introduced in 1.13.0, -e.g. cuDF support and linked selections. In addition it introduces a -number of important bug fixes. Many thanks for contribution by -@kebowen730, @maximlt, @pretros1999, @alexbraditsas, @lelatbones, -@flothesof, @ruoyu0088, @cool-PR and the core maintainers @jbednar and -@philippjfr. - -Enhancements: - -* Expose ``center`` as an output rendering option - (`#4365 `_) -* Configurable throttling schemes for linked streams on the server - (`#4372 `_) -* Add support for lasso tool in linked selections - (`#4362 `_) -* Add support for NdOverlay in linked selections - (`#4481 `_) -* Add support for unwatching on ``Params`` stream - (`#4417 `_) -* Optimizations for the cuDF interface - (`#4436 `_) -* Add support for ``by`` aggregator in datashader operations - (`#4438 `_) -* Add support for cupy and dask histogram and box-whisker calculations - (`#4447 `_) -* Allow rendering HoloViews output as an ipywidget - (`#4404 `_) -* Allow ``DynamicMap`` callback to accept key dimension values as - variable kwargs - (`#4462 `_) -* Delete toolbar by default when rendering bokeh plot to PNG - (`#4422 `_) -* Ensure ``Bounds`` and ``Lasso`` events only trigger on mouseup - (`#4478 `_) -* Fix issues with ranges bouncing when PlotSize stream is attached - (`#4480 `_) -* Fix bug with hv.extension(inline=False) - (`#4491 `_) -* Handle missing categories on split Violin plot - (`#4482 `_) - -Bug fixes: - - -* Eliminate circular references to allow immediate garbage collection - (`#4368 `_, - `#4377 `_) -* Allow bytes as categories - (`#4392 `_) -* Fix handling of zero as log colormapper lower bound - (`#4383 `_) -* Do not compute data ranges if Dimension.values is supplied - (`#4416 `_) -* Fix RangeXY updates when zooming on only one axis - (`#4413 `_) -* Ensure that ranges do not bounce when data_aspect is set - (`#4431 `_) -* Fix bug specifying a rotation for Box element - (`#4460 `_) -* Fix handling of datetimes in bokeh RectanglesPlot - (`#4461 `_) -* Fix bug normalizing ranges across multiple plots when framewise=True - (`#4450 `_) -* Fix bug coloring adjoined histograms - (`#4458 `_) - - -Version 1.13.2 -************** - -**April 2, 2020** - -This is a minor patch release fixing a number of regressions -introduced as part of the 1.13.x releases. Many thanks to the -contributors including @eddienko, @poplarShift, @wuyuani135, @maximlt -and the maintainer @philippjfr. - -Enhancements: - -- Add PressUp and PanEnd streams (`#4334 - `_) - -Bug fixes: - -- Fix regression in single node Sankey computation - (`#4337 `_) -- Fix color and alpha option on bokeh Arrow plot - (`#4338 `_) -- Fix undefined JS variables in various bokeh links - (`#4341 `_) -- Fix matplotlib >=3.2.1 deprecation warnings - (`#4335 `_) -- Fix handling of document in server mode - (`#4355 `_) - -Version 1.13.1 -************** - -**March 25, 2020** - -This is a minor patch release to fix issues compatibility with the -about to be released Bokeh 2.0.1 release. Additionally this release -makes Pandas a hard dependency, which was already implicitly the case -in 1.13.0 but not declared. Lastly this release contains a small number -of enhancements and bug fixes. - -Enhancements: - -* Add option to set Plotly plots to responsive - (`#4319 `_) -* Unified datetime formatting in bokeh hover info - (`#4318 `_) -* Add explicit ``.df`` and ``.xr`` namespaces to ``dim`` expressions to - allow using dataframe and xarray APIs - (`#4320 `_) -* Allow using dim expressions as accessors - (`#4311 `_) -* Allow defining clim which defines only upper or lower bound and not - both (`#4314 `_) -* Improved exceptions when selected plotting extension is not loaded - (`#4325 `_) - -Bug fixes: - -* Fix regression in Overlay.relabel that occurred in 1.12.3 resulting - in relabeling of contained elements by default - (`#4246 `_) -* Fix bug when updating bokeh Arrow elements - (`#4313 `_) -* Fix bug where Layout/Overlay constructors would drop items - (`#4323 `_) - -Compatibility: - -* Fix compatibility with Bokeh 2.0.1 - (`#4308 `_) - -Documentation: - -* Update API reference manual - (`#4316 `_) - -Version 1.13.0 -************** - -**March 20, 2020** - -This release is packed full of features and includes a general -refactoring of how HoloViews renders widgets now built on top of the -Panel library. Many thanks to the many contributors to this release -either directly by submitting PRs or by reporting issues and making -suggestions. Specifically we would like to thank @poplarShift, -@jonmease, @flothesof, @julioasotodv, @ltalirz, @DancingQuanta, @ahuang, -@kcpevey, @Jacob-Barkhak, @nluetts, @harmbuisman, @ceball, @mgsnuno, -@srp3003, @jsignell as well as the maintainers @jbednar, @jlstevens and -@philippjfr for contributing to this release. This version includes the -addition of a large number of features, enhancements and bug fixes: - -`Read more about version 1.13 here -`__ (June 23, 2020) - - -Major features: - -- Add ``link_selection`` to make custom linked brushing simple - (`#3951 `__) -- ``link_selection`` builds on new support for much more powerful - data-transform pipelines: new ``Dataset.transform`` method - (`#237 `__, - `#3932 `__), ``dim`` - expressions in ``Dataset.select`` - (`#3920 `__), - arbitrary method calls on ``dim`` expressions - (`#4080 `__), and - ``Dataset.pipeline`` and ``Dataset.dataset`` properties to track - provenance of data -- Add Annotators to allow easily drawing, editing, and annotating - visual elements - (`#1185 `__) -- Completely replaced custom Javascript widgets with Panel-based - widgets allowing for customizable layout - (`#84 `__, - `#805 `__) -- Add ``HSpan``, ``VSpan``, ``Slope``, ``Segments`` and ``Rectangles`` - elements (`#3510 `__, - `#3532 `__, - `#4000 `__) -- Add support for cuDF GPU dataframes, cuPy backed xarrays, and GPU - datashading - (`#3982 `__) - -Other features - -- Add spatialpandas support and redesigned geometry interfaces for - consistent roundtripping - (`#4120 `__) -- Support GIF rendering with Bokeh and Plotly backends - (`#2956 `__, - `#4017 `__) -- Support for Plotly ``Bars``, ``Bounds``, ``Box``, ``Ellipse``, - ``HLine``, ``Histogram``, ``RGB``, ``VLine`` and ``VSpan`` plots -- Add ``UniformNdMapping.collapse`` to collapse nested datastructures - (`#4250 `__) -- Add ``CurveEdit`` and ``SelectionXY`` streams - (`#4119 `__, - `#4167 `__) -- Add ``apply_when`` helper to conditionally apply operations - (`#4289 `__) -- Display Javascript callback errors in the notebook - (`#4119 `__) -- Add support for linked streams in Plotly backend to enable rich - interactivity - (`#3880 `__, - `#3912 `__) - -Enhancements: - -- Support for packed values dimensions, e.g. 3D ``RGB``/``HSV`` arrays - (`#550 `__, - `#3983 `__) -- Allow selecting/slicing datetimes with strings - (`#886 `__) -- Support for datashading ``Area``, ``Spikes``, ``Segments`` and - ``Polygons`` - (`#4120 `__) -- ``HeatMap`` now supports mixed categorical/numeric axes - (`#2128 `__) -- Use ``__signature__`` to generate .opts tab completions - (`#4193 `__) -- Allow passing element-specific keywords through ``datashade`` and - ``rasterize`` - (`#4077 `__) - (`#3967 `__) -- Add ``per_element`` flag to ``.apply`` accessor - (`#4119 `__) -- Add ``selected`` plot option to control selected glyphs in bokeh - (`#4281 `__) -- Improve default ``Sankey`` ``node_padding`` heuristic - (`#4253 `__) -- Add ``hooks`` plot option for Plotly backend - (`#4157 `__) -- Support for split ``Violin`` plots in bokeh - (`#4112 `__) - -Bug fixes: - -- Fixed radial ``HeatMap`` sizing issues - (`#4162 `__) -- Switched to Panel for rendering machinery fixing various export - issues (`#3683 `__) -- Handle updating of user supplied ``HoverTool`` in bokeh - (`#4266 `__) -- Fix issues with single value datashaded plots - (`#3673 `__) -- Fix legend layout issues - (`#3786 `__) -- Fix linked axes issues with mixed date, categorical and numeric axes - in bokeh (`#3845 `__) -- Fixed handling of repeated dimensions in ``PandasInterface`` - (`#4139 `__) -- Fixed various issues related to widgets - (`#3868 `__, - `#2885 `__, - `#1677 `__, - `#3212 `__, - `#1059 `__, - `#3027 `__, - `#3777 `__) - -Library compatibility: - -- Better support for Pandas 1.0 - (`#4254 `__) -- Compatibility with Bokeh 2.0 - (`#4226 `__) - -Migration notes: - -- Geometry ``.iloc`` now indexes by geometry instead of by datapoint. - Convert to dataframe or dictionary before using ``.iloc`` to access - individual datapoints - (`#4104 `__) -- Padding around plot elements is now enabled by default, to revert set - ``hv.config.node_padding = 0`` - (`#1090 `__) -- Removed Bars ``group_index`` and ``stack_index`` options, which are - now controlled using the ``stacked`` option - (`#3985 `__) -- ``.table`` is deprecated; use ``.collapse`` method instead and cast - to ``Table`` - (`#3985 `__) -- ``HoloMap.split_overlays`` is deprecated and is now a private method - (`#3985 `__) -- ``Histogram.edges`` and ``Histogram.values`` properties are - deprecated; use ``dimension_values`` - (`#3985 `__) -- ``Element.collapse_data`` is deprecated; use the container’s - ``.collapse`` method instead - (`#3985 `__) -- ``hv.output`` ``filename`` argument is deprecated; use ``hv.save`` - instead (`#3985 `__) - - -Version 1.12 -~~~~~~~~~~~~ - - -Version 1.12.7 -************** - -**November 22, 2019** - - -This a very minor hotfix release fixing an important bug related to -axiswise normalization between plots. Many thanks to @srp3003 and -@philippjfr for contributing to this release. - -Enhancements: - -* Add styles attribute to PointDraw stream for consistency with other - drawing streams - (`#3819 `_) - -Bug fixes: - -* Fixed shared_axes/axiswise regression - (`#4097 `_) - - -Version 1.12.6 -************** - -**October 8, 2019** - -This is a minor release containing a large number of bug fixes thanks -to the contributions from @joelostblom, @ahuang11, @chbrandt, -@randomstuff, @jbednar and @philippjfr. It also contains a number of -enhancements. This is the last planned release in the 1.12.x series. - -Enhancements: - -* Ensured that shared_axes option on layout plots is respected across backends - (`#3410 `_) -* Allow plotting partially irregular (curvilinear) mesh - (`#3952 `_) -* Add support for dependent functions in dynamic operations - (`#3975 `_, - `#3980 `_) -* Add support for fast QuadMesh rasterization with datashader >= 0.8 - (`#4020 `_) -* Allow passing Panel widgets as operation parameter - (`#4028 `_) - -Bug fixes: - -* Fixed issue rounding datetimes in Curve step interpolation - (`#3958 `_) -* Fix resampling of categorical colorcet colormaps - (`#3977 `_) -* Ensure that changing the Stream source deletes the old source - (`#3978 `_) -* Ensure missing hover tool does not break plot - (`#3981 `_) -* Ensure .apply work correctly on HoloMaps - (`#3989 `_, - `#4025 `_) -* Ensure Grid axes are always aligned in bokeh - (`#3916 `_) -* Fix hover tool on Image and Raster plots with inverted axis - (`#4010 `_) -* Ensure that DynamicMaps are still linked to streams after groupby - (`#4012 `_) -* Using hv.renderer no longer switches backends - (`#4013 `_) -* Ensure that Points/Scatter categorizes data correctly when axes are inverted - (`#4014 `_) -* Fixed error creating legend for matplotlib Image artists - (`#4031 `_) -* Ensure that unqualified Options objects are supported - (`#4032 `_) -* Fix bounds check when constructing Image with ImageInterface - (`#4035 `_) -* Ensure elements cannot be constructed with wrong number of columns - (`#4040 `_) -* Ensure streaming data works on bokeh server - (`#4041 `_) - -Compatibility: - -* Ensure HoloViews is fully compatible with xarray 0.13.0 - (`#3973 `_) -* Ensure that deprecated matplotlib 3.1 rcparams do not warn - (`#4042 `_) -* Ensure compatibility with new legend options in bokeh 1.4.0 - (`#4036 `_) - - -Version 1.12.5 -************** - -**August 14, 2019** - -This is a very minor bug fix release ensuring compatibility with recent -releases of dask. - -Compatibility: - -* Ensure that HoloViews can be imported when dask is installed but - dask.dataframe is not. - (`#3900 `_) -* Fix for rendering Scatter3D with matplotlib 3.1 - (`#3898 `_) - -Version 1.12.4 -************** - -**August 4, 2019** - -This is a minor release with a number of bug and compatibility fixes -as well as a number of enhancements. - -Many thanks to recent @henriqueribeiro, @poplarShift, @hojo590, -@stuarteberg, @justinbois, @schumann-tim, @ZuluPro and @jonmmease for -their contributions and the many users filing issues. - -Enhancements: - -* Add numpy log to dim transforms - (`#3731 `_) -* Make Buffer stream following behavior togglable - (`#3823 `_) -* Added internal methods to access dask arrays and made histogram - operation operate on dask arrays - (`#3854 `_) -* Optimized range finding if Dimension.range is set - (`#3860 `_) -* Add ability to use functions annotated with param.depends as - DynamicMap callbacks - (`#3744 `_) - -Bug fixes: - -* Fixed handling datetimes on Spikes elements - (`#3736 `_) -* Fix graph plotting for unsigned integer node indices - (`#3773 `_) -* Fix sort=False on GridSpace and GridMatrix - (`#3769 `_) -* Fix extent scaling on VLine/HLine annotations - (`#3761 `_) -* Fix BoxWhisker to match convention - (`#3755 `_) -* Improved handling of custom array types - (`#3792 `_) -* Allow setting cmap on HexTiles in matplotlib - (`#3803 `_) -* Fixed handling of data_aspect in bokeh backend - (`#3848 `_, - `#3872 `_) -* Fixed legends on bokeh Path plots - (`#3809 `_) -* Ensure Bars respect xlim and ylim - (`#3853 `_) -* Allow setting Chord edge colors using explicit colormapping - (`#3734 `_) - -Compatibility: - -* Improve compatibility with deprecated matplotlib rcparams - (`#3745 `_, - `#3804 `_) - -Backwards incompatible changes: - -* Unfortunately due to a major mixup the data_aspect option added in - 1.12.0 was not correctly implemented and fixing it changed its - behavior significantly (inverting it entirely in some cases). -* A mixup in the convention used to compute the whisker of a - box-whisker plots was fixed resulting in different results going - forward. - -Version 1.12.3 -************** - -**May 20, 2019** - -This is a minor release primarily focused on a number of important bug -fixes. Thanks to our users for reporting issues, and special thanks to -the internal developers @philippjfr and @jlstevens and external -developers including @poplarShift, @fedario and @odoublewen for their -contributions. - -Bug fixes: - -- Fixed regression causing unhashable data to cause errors in streams - (`#3681 `_) -- Ensure that hv.help handles non-HoloViews objects - (`#3689 `_) -- Ensure that DataLink handles data containing NaNs - (`#3694 `_) -- Ensure that bokeh backend handles Cycle of markers - (`#3706 `_) -- Fix for using opts method on DynamicMap - (`#3691 `_) -- Ensure that bokeh backend handles DynamicMaps with variable length - NdOverlay (`#3696 `_) -- Fix default width/height setting for HeatMap - (`#3703 `_) -- Ensure that dask imports handle modularity - (`#3685 `_) -- Fixed regression in xarray data interface - (`#3724 `_) -- Ensure that RGB hover displays the integer RGB value - (`#3727 `_) -- Ensure that param streams handle subobjects - (`#3728 `_) - -Version 1.12.2 -************** - -**May 1, 2019** - -This is a minor release with a number of important bug fixes and a -small number of enhancements. Many thanks to our users for reporting -these issues, and special thanks to our internal developers -@philippjfr, @jlstevens and @jonmease and external contributors -including @ahuang11 and @arabidopsis for their contributions to the -code and the documentation. - -Enhancements: - -- Add styles argument to draw tool streams to allow cycling colors when - drawing glyphs - (`#3612 `__) -- Add ability to define alpha on (data)shade operation - (`#3611 `__) -- Ensure that categorical plots respect Dimension.values order - (`#3675 `__) - -Compatibility: - -- Compatibility with Plotly 3.8 - (`#3644 `__) - -Bug fixes: - -- Ensure that bokeh server plot updates have the exclusive Document - lock (`#3621 `__) -- Ensure that Dimensioned streams are inherited on ``__mul__`` - (`#3658 `__) -- Ensure that bokeh hover tooltips are updated when dimensions change - (`#3609 `__) -- Fix DynamicMap.event method for empty streams - (`#3564 `__) -- Fixed handling of datetimes on Path plots - (`#3464 `__, - `#3662 `__) -- Ensure that resampling operations do not cause event loops - (`#3614 `__) - -Backward compatibility: - -- Added color cycles on Violin and BoxWhisker elements due to earlier - regression (`#3592 `__) - - -Version 1.12.1 -************** - -**April 10, 2019** - -This is a minor release that pins to the newly released Bokeh 1.1 and -adds support for parameter instances as streams: - -Enhancements: - -- Add support for passing in parameter instances as streams - (`#3616 `__) - - -Version 1.12.0 -************** - -**April 2, 2019** - -This release provides a number of exciting new features as well as a set -of important bug fixes. Many thanks to our users for reporting these -issues, and special thanks to @ahuang11, @jonmmease, @poplarShift, -@reckoner, @scottclowe and @syhooper for their contributions to the code -and the documentation. - -Features: - -- New plot options for controlling layouts including a responsive mode - as well as improved control over aspect using the newly updated bokeh - layout engine - (`#3450 `__, - `#3575 `__) -- Added a succinct and powerful way of creating DynamicMaps from - functions and methods via the new ``.apply`` method - (`#3554 `__, - `#3474 `__) - -Enhancements: - -- Added a number of new plot options including a clabel param for - colorbars - (`#3517 `__), exposed - Sankey font size - (`#3535 `__) and added - a radius for bokeh nodes - (`#3556 `__) -- Switched notebook output to use an HTML mime bundle instead of - separate HTML and JS components - (`#3574 `__) -- Improved support for style mapping constant values via - ``dim.categorize`` - (`#3578 `__) - -Bug fixes: - -- Fixes for colorscales and colorbars - (`#3572 `__, - `#3590 `__) -- Other miscellaneous fixes - (`#3530 `__, - `#3536 `__, - `#3546 `__, - `#3560 `__, - `#3571 `__, - `#3580 `__, - `#3584 `__, - `#3585 `__, - `#3594 `__) - - -Version 1.11 -~~~~~~~~~~~~ - -Version 1.11.3 -************** - -**February 25, 2019** - -This is the last micro-release in the 1.11 series providing a number -of important fixes. Many thanks to our users for reporting these -issues and @poplarShift and @henriqueribeiro for contributing a number -of crucial fixes. - -Bug fixes: - -* All unused Options objects are now garbage collected fixing the last - memory leak (`#3438 `_) -* Ensured updating of size on matplotlib charts does not error - (`#3442 `_) -* Fix casting of datetimes on dask dataframes - (`#3460 `_) -* Ensure that calling redim does not break streams and links - (`#3478 `_) -* Ensure that matplotlib polygon plots close the edge path - (`#3477 `_) -* Fixed bokeh ArrowPlot error handling colorbars - (`#3476 `_) -* Fixed bug in angle conversion on the VectorField if invert_axes - (`#3488 `_) -* Ensure that all non-Annotation elements support empty constructors - (`#3511 `_) -* Fixed bug handling out-of-bounds errors when using tap events on - datetime axis - (`#3519 `_) - -Enhancements: - - -* Apply Labels element offset using a bokeh transform allowing Labels - element to share data with original data - (`#3445 `_) -* Allow using datetimes in xlim/ylim/zlim - (`#3491 `_) -* Optimized rendering of TriMesh wireframes - (`#3495 `_) -* Add support for datetime formatting when hovering on Image/Raster - (`#3520 `_) -* Added Tiles element from GeoViews - (`#3515 `_) - - -Version 1.11.2 -************** - -**January 28, 2019** - -This is a minor bug fix release with a number of minor but important -bug fixes. Special thanks to @darynwhite for his contributions. - -Bug fixes: - -* Fixed persisting options during clone on Overlay - (`#3435 `_) -* Ensure cftime datetimes are displayed as a slider - (`#3413 `_) -* Fixed timestamp selections on streams - (`#3427 `_) -* Compatibility with pandas 0.24.0 release - (`#3433 `_) - -Enhancements: - -* Allow defining hook on backend load - (`#3429 `_) -* Improvements for handling graph attributes in ``Graph.from_networkx`` - (`#3432 `_) - - -Version 1.11.1 -************** - -**January 17, 2019** - -This is a minor bug fix release with a number of important bug fixes, -enhancements and updates to the documentation. Special thanks to -Andrew Huang (@ahuang11), @garibarba and @Safrone for their -contributions. - -Bug fixes: - -* Fixed bug plotting adjoined histograms in matplotlib - (`#3377 `_) -* Fixed bug updating bokeh RGB alpha value - (`#3371 `_) -* Handled issue when colorbar limits were equal in bokeh - (`#3382 `_) -* Fixed bugs plotting empty Violin and BoxWhisker elements - (`#3397 `_, - `#3405 `_) -* Fixed handling of characters that have no uppercase on Layout and - Overlay objects - ((`#3403 `_) -* Fixed bug updating Polygon plots in bokeh - (`#3409 `_) - -Enhancements: - -* Provide control over gridlines ticker and mirrored axis ticker by - default (`#3398 `_) -* Enabled colorbars on CompositePlot classes such as Graphs, Chords - etc. (`#3396 `_) -* Ensure that xarray backend retains dimension metadata when casting - element (`#3401 `_) -* Consistently support clim options - (`#3382 `_) - -Documentation: - -* Completed updates from .options to .opts API in the documentation - (`#3364 <(https://github.com/pyviz/holoviews/pull/3364>`_, - `#3367 <(https://github.com/pyviz/holoviews/pull/3367>`_) - -Version 1.11.0 -************** - -**December 24, 2018** - -This is a major release containing a large number of features and API -improvements. Specifically this release was devoted to improving the -general usability and accessibility of the HoloViews API and -deprecating parts of the API in anticipation for the 2.0 release. -To enable deprecation warnings for these deprecations set: - -.. code-block:: - - hv.config.future_deprecations = True - - -The largest updates to the API relate to the options system which is now -more consistent, has better validation and better supports notebook -users without requiring IPython magics. The new ``dim`` transform -generalizes the mapping from data dimensions to visual dimensions, -greatly increasing the expressive power of the options system. Please -consult the updated user guides for more information. - -Special thanks for the contributions by Andrew Huang (@ahuang11), -Julia Signell (@jsignell), Jon Mease (@jonmmease), and Zachary Barry -(@zbarry). - -Features: - -* Generalized support for style mapping using ``dim`` transforms - (`2152 `_) -* Added alternative to opts magic with tab-completion - (`#3173 `_) -* Added support for Polygons with holes and improved contours - operation (`#3092 `_) -* Added support for Links to express complex interactivity in JS - (`#2832 `_) -* Plotly improvements including support for plotly 3.0 - (`#3194 `_), improved - support for containers - (`#3255 `_) and support - for more elements - (`#3256 `_) -* Support for automatically padding plots using new ``padding`` option - (`#2293 `_) -* Added ``xlim``\ /\ ``ylim`` plot options to simplify setting axis ranges - (`#2293 `_) -* Added ``xlabel``\ /\ ``ylabel`` plot options to simplify overriding axis - labels (`#2833 `_) -* Added ``xformatter``\ /\ ``yformatter`` plot options to easily override tick - formatter (`#3042 `_) -* Added ``active_tools`` options to allow defining tools to activate on - bokeh plot initialization - (`#3251 `_) -* Added ``FreehandDraw`` stream to allow freehand drawing on bokeh plots - (`#2937 `_) -* Added support for ``cftime`` types for dates which are not supported - by standard datetimes and calendars - (`#2728 `_) -* Added top-level ``save`` and ``render`` functions to simplify exporting - plots (`#3134 `_) -* Added support for updating Bokeh bokeh legends - (`#3139 `_) -* Added support for indicating directed graphs with arrows - (`#2521 `_) - -Enhancements: - -* Improved import times - (`#3055 `_) -* Adopted Google style docstring and documented most core methods and - classes (`#3128 `_ - -Bug fixes: - -* GIF rendering fixed under Windows - (`#3151 `_) -* Fixes for hover on Path elements in bokeh - (`#2472 `_, - `#2872 `_) -* Fixes for handling TriMesh value dimensions on rasterization - (`#3050 `_) - -Deprecations: - -* ``finalize_hooks`` renamed to ``hooks`` - (`#3134 `_) -* All ``*_index`` and related options are now deprecated including - ``color_index``, ``size_index``, ``scaling_method``, ``scaling_factor``, - ``size_fn`` (`#2152 `_) -* Bars ``group_index``, ``category_index`` and ``stack_index`` are deprecated in - favor of stacked option - (`#2828 `_) -* iris interface was moved to GeoViews - (`#3054 `_) -* Top-level namespace was cleaned up - (`#2224 `_) -* ``ElementOpration``, ``Layout.display`` and ``mdims`` argument to ``.to`` - now fully removed - (`#3128 `_) -* ``Element.mapping``, ``ItemTable.values``, ``Element.table``, - ``HoloMap.split_overlays``, ``ViewableTree.from_values``, - ``ViewableTree.regroup`` and ``Element.collapse_data`` methods now - marked for deprecation - (`#3128 `_) - - -Version 1.10 -~~~~~~~~~~~~ - -Version 1.10.8 -************** - -**October 29, 2018** - -This a likely the last hotfix release in the 1.10.x series containing -fixes for compatibility with bokeh 1.0 and matplotlib 3.0. It also -contains a wide array of fixes contributed and reported by users: - -Special thanks for the contributions by Andrew Huang (@ahuang11), -Julia Signell (@jsignell), and Zachary Barry (@zbarry). - -Enhancements: - -- Add support for labels, choord, hextiles and area in .to interface - (`#2924 `_) -- Allow defining default bokeh themes as strings on Renderer - (`#2972 `_) -- Allow specifying fontsize for categorical axis ticks in bokeh - (`#3047 `_) -- Allow hiding toolbar without disabling tools - (`#3074 `_) -- Allow specifying explicit colormapping on non-categorical data - (`#3071 `_) -- Support for displaying xarray without explicit coordinates - (`#2968 `_) - -Fixes: - -- Allow dictionary data to reference values which are not dimensions - (`#2855 `_, - `#2859 `_) -- Fixes for zero and non-finite ranges in datashader operation - (`#2860 `_, - `#2863 `_, - `#2869 `_) -- Fixes for CDSStream and drawing tools on bokeh server - (`#2915 `_) -- Fixed issues with nans, datetimes and streaming on Area and Spread - elements (`#2951 `_, - `c55b044 `_) -- General fixes for datetime handling - (`#3005 `_, - `#3045 `_, - `#3075 `_) -- Fixed handling of curvilinear and datetime coordinates on QuadMesh - (`#3017 `_, - `#3081 `_) -- Fixed issue when inverting a shared axis in bokeh - (`#3083 `_) -- Fixed formatting of values in HoloMap widgets - (`#2954 `_) -- Fixed setting fontsize for z-axis label - (`#2967 `_) - -Compatibility: - -- Suppress warnings about rcParams in matplotlib 3.0 - (`#3013 `_, - `#3058 `_, - `#3104 `_) -- Fixed incompatibility with Python <=3.5 - (`#3073 `_) -- Fixed incompatibility with bokeh >=1.0 - (`#3051 `_) - -Documentation: - -- Completely overhauled the FAQ - (`#2928 `_, - `#2941 `_, - `#2959 `_, - `#3025 `_) - - -Version 1.10.7 -************** - -**July 8, 2018** - -This a very minor hotfix release mostly containing fixes for datashader -aggregation of empty datasets: - -Fixes: - -- Fix datashader aggregation of empty and zero-range data - (`#2860 `_, - `#2863 `_) -- Disable validation for additional, non-referenced keys in the - DictInterface (`#2860 `_) -- Fixed frame lookup for non-overlapping dimensions - (`#2861 `_) -- Fixed ticks on log Colorbar if low value <= 0 - (`#2865 `_) - -Version 1.10.6 -************** - -**June 29, 2018** - -This another minor bug fix release in the 1.10 series and likely the -last one before the upcoming 1.11 release. In addition to some important -fixes relating to datashading and the handling of dask data, this -release includes a number of enhancements and fixes. - -Enhancements: - -- Added the ability to specify color intervals using the color_levels - plot options (`#2797 `_) -- Allow defining port and multiple websocket origins on BokehRenderer.app - (`#2801 `_) -- Support for datetimes in Curve step interpolation - (`#2757 `_) -- Add ability to mute legend by default - (`#2831 `_) -- Implemented ability to collapse and concatenate gridded data - (`#2762 `_) -- Add support for cumulative histogram and explicit bins - (`#2812 `_) - -Fixes: - -- Dataset discovers multi-indexes on dask dataframes - (`#2789 `_) -- Fixes for datashading NdOverlays with datetime axis and data with - zero range (`#2829 `_, - `#2842 `_) - -Version 1.10.5 -************** - -**June 5, 2018** - -This is a minor bug fix release containing a mixture of small -enhancements, a number of important fixes and improved compatibility -with pandas 0.23. - -Enhancements: - -- Graph.from_networkx now extracts node and edge attributes from - networkx graphs - (`#2714 `_) -- Added throttling support to scrubber widget - (`#2748 `_) -- histogram operation now works on datetimes - (`#2719 `_) -- Legends on NdOverlay containing overlays now supported - (`#2755 `_) -- Dataframe indexes may now be referenced in ``.to`` conversion - (`#2739 `_) -- Reindexing a gridded Dataset without arguments now behaves - consistently with NdMapping types and drops scalar dimensions making - it simpler to drop dimensions after selecting - (`#2746 `_) - -Fixes: - -- Various fixes for QuadMesh support including support for contours, - nan coordinates and inverted coordinates - (`#2691 `_, - `#2702 `_, - `#2771 `_) -- Fixed bugs laying out complex layouts in bokeh - (`#2740 `_) -- Fix for adding value dimensions to an xarray dataset - (`#2761 `_) - -Version 1.10.4 -************** - -**May 14, 2018** - -This is a minor bug fix release including a number of crucial fixes -for issues reported by our users. - -Enhancement: - -- Allow setting alpha on Image/RGB/HSV and Raster types in bokeh - (`#2680 `_) - -Fixes: - -- Fixed bug running display multiple times in one cell - (`#2677 `_) -- Avoid sending hover data unless explicitly requested - (`#2681 `_) -- Fixed bug slicing xarray with tuples - (`#2674 `_) - - -Version 1.10.3 -************** - -**May 8, 2018** - -This is a minor bug fix release including a number of crucial fixes for -issues reported by our users. - -Enhancement: - -- The dimensions of elements may now be changed allowing updates to - axis labels and table column headers - (`#2666 `__) - -Fixes: - -- Fix for ``labelled`` plot option - (`#2643 `__) -- Optimized initialization of dynamic plots specifying a large - parameter space - (`#2646 `__) -- Fixed unicode and reversed axis slicing issues in XArrayInterface - (`#2658 `__, - `#2653 `__) -- Fixed widget sorting issues when applying dynamic groupby - (`#2641 `__) - -API: - -- The PlotReset reset parameter was renamed to resetting to avoid clash - with a method - (`#2665 `__) -- PolyDraw tool data parameter now always indexed with 'xs' and 'ys' - keys for consistency - (`#2650 `__) - -Version 1.10.2 -************** - -**April 30, 2018** - -This is a minor bug fix release with a number of small fixes for -features and regressions introduced in 1.10: - -Enhancement: - -- Exposed Image hover functionality for upcoming bokeh 0.12.16 release - (`#2625 `__) - -Fixes: - -- Minor fixes for newly introduced elements and plots including Chord - (`#2581 `__) and - RadialHeatMap - (`#2610 `__ -- Fixes for .options method including resolving style and plot option - clashes (`#2411 `__) - and calling it without arguments - (`#2630 `__) -- Fixes for IPython display function - (`#2587 `__) and - display\_formats - (`#2592 `__) - -Deprecations: - -- BoxWhisker and Bars ``width`` bokeh style options and Arrow - matplotlib ``fontsize`` option are deprecated - (`#2411 `__) - -Version 1.10.1 -************** - -**April 20, 2018** - -This is a minor bug fix release with a number of fixes for regressions -and minor bugs introduced in the 1.10.0 release: - -Fixes: - -- Fixed static HTML export of notebooks - (`#2574 `__) -- Ensured Chord element allows recurrent edges - (`#2583 `__) -- Restored behavior for inferring key dimensions order from XArray - Dataset (`#2579 `__) -- Fixed Selection1D stream on bokeh server after changes in bokeh - 0.12.15 (`#2586 `__) - -Version 1.10.0 -************** - -**April 17, 2018** - -This is a major release with a large number of new features and bug -fixes, as well as a small number of API changes. Many thanks to the -numerous users who filed bug reports, tested development versions, and -contributed a number of new features and bug fixes, including special -thanks to @mansenfranzen, @ea42gh, @drs251 and @jakirkham. - -`Read more about version 1.10 here -`__ (April 11, 2018) - - -JupyterLab support: - -- Full compatibility with JupyterLab when installing the - jupyterlab\_holoviews extension - (`#687 `__) - -New components: - -- Added |Sankey|_ element to plot directed flow graphs - (`#1123 `__) -- Added |TriMesh|_ element - and datashading operation to plot small and large irregular meshes - (`#2143 `__) -- Added a |Chord|_ element - to draw flow graphs between different nodes - (`#2137 `__, - `#2143 `__) -- Added |HexTiles|_ element - to plot data binned into a hexagonal grid - (`#1141 `__) -- Added |Labels|_ element - to plot a large number of text labels at once (as data rather than as - annotations) - (`#1837 `__) -- Added |Div|_ element - to add arbitrary HTML elements to a Bokeh layout - (`#2221 `__) -- Added |PointDraw|_, |PolyDraw|_, |BoxEdit|_ and |PolyEdit|_ - streams to allow drawing, editing, and annotating glyphs on a Bokeh - plot, and syncing the resulting data to Python - (`#2268 `__) - -Features: - -- Added |radial HeatMap|_ option to allow plotting heatmaps with a cyclic x-axis - (`#2139 `__) -- All elements now support declaring bin edges as well as centers - allowing ``Histogram`` and ``QuadMesh`` to become first class - ``Dataset`` types - (`#547 `__) -- When using widgets, their initial or default value can now be set via - the ``Dimension.default`` parameter - (`#704 `__) -- n-dimensional Dask arrays are now supported directly via the gridded - dictionary data interface - (`#2305 `__) -- Added new `Styling - Plots `__ and - `Colormaps `__ user - guides, including new functionality for working with colormaps. - -Enhancements: - -- Improvements to exceptions - (`#1127 `__) -- Toolbar position and merging (via a new ``merge_toolbar`` option) can - now be controlled for Layout and Grid plots - (`#1977 `__) -- Bokeh themes can now be applied at the renderer level - (`#1861 `__) -- Dataframe and Series index can now be referenced by name when - constructing an element - (`#2000 `__) -- Option-setting methods such as ``.opts``, ``.options`` and - ``hv.opts`` now allow specifying the backend instead of defaulting to - the current backend - (`#1801 `__) -- Handled API changes in streamz 0.3.0 in Buffer stream - (`#2409 `__) -- Supported GIF output on windows using new Matplotlib pillow animation - support (`#385 `__) -- Provided simplified interface to ``rasterize`` most element types - using datashader - (`#2465 `__) -- ``Bivariate`` element now support ``levels`` as a plot option - (`#2099 `__) -- ``NdLayout`` and ``GridSpace`` now consistently support ``*`` overlay - operation (`#2075 `__) -- The Bokeh backend no longer has a hard dependency on Matplotlib - (`#829 `__) -- ``DynamicMap`` may now return (``Nd``)\ ``Overlay`` with varying - number of elements - (`#1388 `__) -- In the notebook, deleting or re-executing a cell will now delete the - plot and clean up any attached streams - (`#2141 `__) -- Added ``color_levels`` plot option to set discrete number of levels - during colormapping - (`#2483 `__) -- Expanded the `Large - Data `__ user guide - to show examples of all Element and Container types supported for - datashading and give performance guidelines. - -Fixes: - -- ``Layout`` and ``Overlay`` objects no longer create lower-case nodes - on attribute access - (`#2331 `__) -- ``Dimension.step`` now correctly respects both integer and float - steps (`#1707 `__) -- Fixed timezone issues when using linked streams on datetime axes - (`#2459 `__) - -Changes affecting backwards compatibility: - -- Image elements now expect and validate regular sampling - (`#1869 `__); for - genuinely irregularly sampled data QuadMesh should be used. -- Tabular elements will no longer default to use ``ArrayInterface``, - instead preferring pandas and dictionary data formats - (`#1236 `__) -- ``Cycle``/``Palette`` values are no longer zipped together; instead - they now cycle independently - (`#2333 `__) -- The default color ``Cycle`` was expanded to provide more unique - colors (`#2483 `__) -- Categorical colormapping was made consistent across backends, - changing the behavior of categorical Matplotlib colormaps - (`#2483 `__) -- Disabled auto-indexable property of the Dataset baseclass, i.e. if a - single column is supplied no integer index column is added - automatically - (`#2522 `__) - - -Version 1.9 -~~~~~~~~~~~ - - -Version 1.9.5 -************* - -**March 2, 2018** - -This release includes a very small number of minor bugfixes and a new -feature to simplify setting options in python: - -Enhancements: - -- Added .options method for simplified options setting. - (`#2306 `__) - -Fixes: - -- Allow plotting bytes datausing the Bokeh backend in python3 - (`#2357 `__) -- Allow .range to work on data with heterogeneous types in Python 3 - (`#2345 `__) -- Fixed bug streaming data containing datetimes using bokeh>-0.12.14 - (`#2383 `__) - -Version 1.9.4 -************* - -**February 16, 2018** - -This release contains a small number of important bug fixes: - -- Compatibility with recent versions of Dask and pandas - (`#2329 `__) -- Fixed bug referencing columns containing non-alphanumeric characters - in Bokeh Tables - (`#2336 `__) -- Fixed issue in regrid operation - (`2337 `__) -- Fixed issue when using datetimes with datashader when processing - ranges (`#2344 `__) - -Version 1.9.3 -************* - -**February 11, 2018** - -This release contains a number of important bug fixes and minor -enhancements. - -Particular thanks to @jbampton, @ea42gh, @laleph, and @drs251 for a -number of fixes and improvements to the documentation. - -Enhancements: - -- Optimized rendering of stream based OverlayPlots - (`#2253 `__) -- Added ``merge_toolbars`` and ``toolbar`` options to control toolbars - on ``Layout`` and Grid plots - (`#2289 `__) -- Optimized rendering of ``VectorField`` - (`#2314 `__) -- Improvements to documentation - (`#2198 `__, - `#2220 `__, - `#2233 `__, - `#2235 `__, - `#2316 `__) -- Improved Bokeh ``Table`` formatting - (`#2267 `__) -- Added support for handling datetime.date types - (`#2267 `__) -- Add support for pre- and post-process hooks on operations - (`#2246 `__, - `#2334 `__) - -Fixes: - -- Fix for Bokeh server widgets - (`#2218 `__) -- Fix using event based streams on Bokeh server - (`#2239 `__, - `#2256 `__) -- Switched to drawing ``Distribution``, ``Area`` and ``Spread`` using - patch glyphs in Bokeh fixing legends - (`#2225 `__) -- Fixed categorical coloring of ``Polygons``/``Path`` elements in - Matplotlib (`#2259 `__) -- Fixed bug computing categorical datashader aggregates - (`#2295 `__) -- Allow using ``Empty`` object in ``AdjointLayout`` - (`#2275 `__) - -API Changes: - -- Renamed ``Trisurface`` to ``TriSurface`` for future consistency - (`#2219 `__) - -Version 1.9.2 -************* - -**December 11, 2017** - -This release is a minor bug fix release patching various issues which -were found in the 1.9.1 release. - -Enhancements: - -- Improved the Graph element, optimizing the constructor and adding - support for defining a ``edge_color_index`` - (`#2145 `__) -- Added support for adding jitter to Bokeh Scatter and Points plots - (`e56208 `__) - -Fixes: - -- Ensure dimensions, group and label are inherited when casting Image - to QuadMesh (`#2144 `__) -- Handle compatibility for Bokeh version >- 0.12.11 - (`#2159 `__) -- Fixed broken Bokeh ArrowPlot - (`#2172 `__) -- Fixed Pointer based streams on datetime axes - (`#2179 `__) -- Allow constructing and plotting of empty Distribution and Bivariate - elements (`#2190 `__) -- Added support for hover info on Bokeh BoxWhisker plots - (`#2187 `__) -- Fixed bug attaching streams to (Nd)Overlay types - (`#2194 `__) - -Version 1.9.1 -************* - -**November 13, 2017** - -This release is a minor bug fix release patching various issues which -were found in the 1.9.0 release. - -Enhancements: - -- Exposed min\_alpha parameter on datashader shade and datashade - operations (`#2109 `__) - -Fixes: - -- Fixed broken Bokeh server linked stream throttling - (`#2112 `__) -- Fixed bug in Bokeh callbacks preventing linked streams using Bokeh's - on\_event callbacks from working - (`#2112 `__) -- Fixed insufficient validation issue for Image and bugs when applying - regrid operation to xarray based Images - (`#2117 `__) -- Fixed handling of dimensions and empty elements in univariate\_kde - and bivariate\_kde operations - (`#2103 `__) - -Version 1.9.0 -************* - -**November 3, 2017** - -This release includes a large number of long awaited features, -improvements and bug fixes, including streaming and graph support, -binary transfer of Bokeh data, fast Image/RGB regridding, first-class -statistics elements and a complete overhaul of the geometry elements. - -Particular thanks to all users and contributors who have reported issues -and submitted pull requests. - -Features: - -- The kdim and vdim keyword arguments are now positional making the - declaration of elements less verbose (e.g. Scatter(data, 'x', 'y')) - (`#1946 `__) -- Added Graph, Nodes, and EdgePaths elements adding support for - plotting network graphs - (`#1829 `__) -- Added datashader based regrid operation for fast Image and RGB - regridding (`#1773 `__) -- Added support for binary transport when plotting with Bokeh, - providing huge speedups for dynamic plots - (`#1894 `__, - `#1896 `__) -- Added Pipe and Buffer streams for streaming data support - (`#2011 `__) -- Add support for datetime axes on Image, RGB and when applying - datashading and regridding operations - (`#2023 `__) -- Added Distribution and Bivariate as first class elements which can be - plotted with Matplotlib and Bokeh without depending on seaborn - (`#1985 `__) -- Completely overhauled support for plotting geometries with Path, - Contours and Polygons elements including support for coloring - individual segments and paths by value - (`#1991 `__) - -Enhancements: - -- Add support for adjoining all elements on Matplotlib plots - (`#1033 `__) -- Improved exception handling for data interfaces - (`#2041 `__) -- Add groupby argument to histogram operation - (`#1725 `__) -- Add support for reverse sort on Dataset elements - (`#1843 `__) -- Added support for invert\_x/yaxis on all elements - (`#1872 `__, - `#1919 `__) - -Fixes: - -- Fixed a bug in Matplotlib causing the first frame in gif and mp4 - getting stuck - (`#1922 `__) -- Fixed various issues with support for new nested categorical axes in - Bokeh (`#1933 `__) -- A large range of other bug fixes too long to list here. - -Changes affecting backwards compatibility: - -- The contours operation no longer overlays the contours on top of the - supplied Image by default and returns a single Contours/Polygons - rather than an NdOverlay of them - (`#1991 `__) -- The values of the Distribution element should now be defined as a key - dimension (`#1985 `__) -- The seaborn interface was removed in its entirety being replaced by - first class support for statistics elements such as Distribution and - Bivariate (`#1985 `__) -- Since kdims and vdims can now be passed as positional arguments the - bounds argument on Image is no longer positional - (`#1946 `__). -- The datashade and shade cmap was reverted back to blue due to issues - with the fire cmap against a white background. - (`#2078 `__) -- Dropped all support for Bokeh versions older than 0.12.10 -- histogram operation now returns Histogram elements with less generic - value dimension and customizable label - (`#1836 `__) - -Version 1.8 -~~~~~~~~~~~ - -Version 1.8.4 -************* - -**September 13, 2017** - -This bugfix release includes a number of critical fixes for compatibility -with Bokeh 0.12.9 along with various other bug fixes. Many thanks to our -users for various detailed bug reports, feedback and contributions. - -Fixes: - -- Fixes to register BoundsXY stream. - (`#1826 `__) -- Fix for Bounds streams on Bokeh server. - (`#1883 `__) -- Compatibility with Matplotlib 2.1 - (`#1842 `__) -- Fixed bug in scrubber widget and support for scrubbing discrete - DynamicMaps (`#1832 `__) -- Various fixes for compatibility with Bokeh 0.12.9 - (`#1849 `__, - `#1866 `__) -- Fixes for setting QuadMesh ranges. - (`#1876 `__) -- Fixes for inverting Image/RGB/Raster axes in Bokeh. - (`#1872 `__) - -Version 1.8.3 -************* - -**August 21, 2017** - -This bugfix release fixes a number of minor issues identified since the -last release: - -Features: - -- Add support for setting the Bokeh sizing\_mode as a plot option - (`#1813 `__) - -Fixes: - -- Handle StopIteration on DynamicMap correctly. - (`#1792 `__) -- Fix bug with linked streams on empty source element - (`#1725 `__) -- Compatibility with latest datashader 0.6.0 release - (`#1773 `__) -- Fixed missing HTML closing tag in extension - (`#1797 `__, - `#1809 `__) -- Various fixes and improvements for documentation - (`#1664 `__, - `#1796 `__) - -Version 1.8.2 -************* - -**August 4, 2017** - -This bugfix release addresses a number of minor issues identified since -the 1.8.1 release: - -Feature: - -- Added support for groupby to histogram operation. - (`#1725 `__) - -Fixes: - -- Fixed problem with HTML export due to new extension logos. - (`#1778 `__) -- Replaced deprecated ``__call__`` usage with opts method throughout - codebase. (`#1759 `__, - `#1763 `__, - `#1779 `__) -- Fixed pip installation. - (`#1782 `__) -- Fixed miscellaneous bugs - (`#1724 `__, - `#1739 `__, - `#1711 `__) - -Version 1.8.1 -************* - -**July 7, 2017** - -This bugfix release addresses a number of minor issues identified since -the 1.8 release: - -Feature: - -- All enabled plotting extension logos now shown - (`#1694 `__) - -Fixes: - -- Updated search ordering when looking for holoviews.rc - (`#1700 `__) -- Fixed lower bound inclusivity bug when no upper bound supplied - (`#1686 `__) -- Raise SkipRendering error when plotting nested layouts - (`#1687 `__) -- Added safety margin for grid axis constraint issue - (`#1695 `__) -- Fixed bug when using +framewise - (`#1685 `__) -- Fixed handling of Spacer models in sparse grid - (`#1682 `__) -- Renamed Bounds to BoundsXY for consistency - (`#1672 `__) -- Fixed Bokeh log axes with axis lower bound <-0 - (`#1691 `__) -- Set default datashader cmap to fire - (`#1697 `__) -- Set SpikesPlot color index to None by default - (`#1671 `__) -- Documentation fixes - (`#1662 `__, - `#1665 `__, - `#1690 `__, - `#1692 `__, - `#1658 `__) - -Version 1.8.0 -************* - -**June 29, 2017** - -This release includes a complete and long awaited overhaul of the -HoloViews documentation and website, with a new gallery, getting-started -section, and logo. In the process, we have also improved and made small -fixes to all of the major new functionality that appeared in 1.7.0 but -was not properly documented until now. We want to thank all our old and -new contributors for providing feedback, bug reports, and pull requests. - -Major features: - -- Completely overhauled the documentation and website - (`#1384 `__, - `#1473 `__, - `#1476 `__, - `#1473 `__, - `#1537 `__, - `#1585 `__, - `#1628 `__, - `#1636 `__) -- Replaced dependency on bkcharts with new Bokeh bar plot - (`#1416 `__) and Bokeh - BoxWhisker plot - (`#1604 `__) -- Added support for drawing the ``Arrow`` annotation in Bokeh - (`#1608 `__) -- Added periodic method DynamicMap to schedule recurring events - (`#1429 `__) -- Cleaned up the API for deploying to Bokeh server - (`#1444 `__, - `#1469 `__, - `#1486 `__) -- Validation of invalid backend specific options - (`#1465 `__) -- Added utilities and entry points to convert notebooks to scripts - including magics - (`#1491 `__) -- Added support for rendering to png in Bokeh backend - (`#1493 `__) -- Made Matplotlib and Bokeh styling more consistent and dropped custom - Matplotlib rc file - (`#1518 `__) -- Added ``iloc`` and ``ndloc`` method to allow integer based indexing - on tabular and gridded datasets - (`#1435 `__) -- Added option to restore case sensitive completion order by setting - ``hv.extension.case_sensitive_completion-True`` in python or via - holoviews.rc file - (`#1613 `__) - -Other new features and improvements: - -- Optimized datashading of ``NdOverlay`` - (`#1430 `__) -- Expose last ``DynamicMap`` args and kwargs on Callable - (`#1453 `__) -- Allow colormapping ``Contours`` Element - (`#1499 `__) -- Add support for fixed ticks with labels in Bokeh backend - (`#1503 `__) -- Added a ``clim`` parameter to datashade controlling the color range - (`#1508 `__) -- Add support for wrapping xarray DataArrays containing Dask arrays - (`#1512 `__) -- Added support for aggregating to target ``Image`` dimensions in - datashader ``aggregate`` operation - (`#1513 `__) -- Added top-level hv.extension and ``hv.renderer`` utilities - (`#1517 `__) -- Added support for ``Splines`` defining multiple cubic splines in - Bokeh (`#1529 `__) -- Add support for redim.label to quickly define dimension labels - (`#1541 `__) -- Add ``BoundsX`` and ``BoundsY`` streams - (`#1554 `__) -- Added support for adjoining empty plots - (`#1561 `__) -- Handle zero-values correctly when using ``logz`` colormapping option - in Matplotlib - (`#1576 `__) -- Define a number of ``Cycle`` and ``Palette`` defaults across backends - (`#1605 `__) -- Many other small improvements and fixes - (`#1399 `__, - `#1400 `__, - `#1405 `__, - `#1412 `__, - `#1413 `__, - `#1418 `__, - `#1439 `__, - `#1442 `__, - `#1443 `__, - `#1467 `__, - `#1485 `__, - `#1505 `__, - `#1493 `__, - `#1509 `__, - `#1524 `__, - `#1543 `__, - `#1547 `__, - `#1560 `__, - `#1603 `__) - -Changes affecting backwards compatibility: - -- Renamed ``ElementOperation`` to ``Operation`` - (`#1421 `__) -- Removed ``stack_area`` operation in favor of ``Area.stack`` - classmethod (`#1515 `__) -- Removed all mpld3 support - (`#1516 `__) -- Added ``opts`` method on all types, replacing the now-deprecated - ``__call__`` syntax to set options - (`#1589 `__) -- Styling changes for both Matplotlib and Bokeh, which can be reverted - for a notebook with the ``config`` option of ``hv.extension``. For - instance, ``hv.extension('bokeh', config-dict(style_17-True))`` - (`#1518 `__) - -Version 1.7 -~~~~~~~~~~~ - -Version 1.7.0 -************* - -**April 25, 2017** - -This version is a major new release incorporating seven months of work -involving several hundred PRs and over 1700 commits. Highlights include -extensive new support for easily building highly interactive -`Bokeh `__ plots, support for using -`datashader `__-based plots for -working with large datasets, support for rendering images interactively -but outside of the notebook, better error handling, and support for -Matplotlib 2.0 and Bokeh 0.12.5. The PRs linked below serve as initial -documentation for these features, and full documentation will be added -in the run-up to HoloViews 2.0. - -Major features and improvements: - -- Interactive Streams API (PR - `#832 `__, - `#838 `__, - `#842 `__, - `#844 `__, - `#845 `__, - `#846 `__, - `#858 `__, - `#860 `__, - `#889 `__, - `#904 `__, - `#913 `__, - `#933 `__, - `#962 `__, - `#964 `__, - `#1094 `__, - `#1256 `__, - `#1274 `__, - `#1297 `__, - `#1301 `__, - `#1303 `__). -- Dynamic Callable API (PR - `#951 `__, - `#1103 `__, - `#1029 `__, - `#968 `__, - `#935 `__, - `#1063 `__, - `#1260 `__). -- Simpler and more powerful DynamicMap (PR - `#1238 `__, - `#1240 `__, - `#1243 `__, - `#1257 `__, - `#1267 `__, - `#1302 `__, - `#1304 `__, - `#1305 `__). -- Fully general support for Bokeh events (PR - `#892 `__, - `#1148 `__, - `#1235 `__). -- Datashader operations (PR - `#894 `__, - `#907 `__, - `#963 `__, - `#1125 `__, - `#1281 `__, - `#1306 `__). -- Support for Bokeh apps and Bokeh Server (PR - `#959 `__, - `#1283 `__). -- Working with renderers interactively outside the notebook (PR - `#1214 `__). -- Support for Matplotlib 2.0 (PR - `#867 `__, - `#868 `__, - `#1131 `__, - `#1264 `__, - `#1266 `__). -- Support for Bokeh 0.12.2, 0.12.3, 0.12.4, and 0.12.5 (PR - `#899 `__, - `#900 `__, - `#1007 `__, - `#1036 `__, - `#1116 `__). -- Many new features for the Bokeh backend: widgets editable (PR - `#1247 `__), selection - colors and interactive legends (PR - `#1220 `__), GridSpace - axes (PR `#1150 `__), - categorical axes and colormapping (PR - `#1089 `__, - `#1137 `__), computing - plot size (PR - `#1140 `__), GridSpaces - inside Layouts (PR - `#1104 `__), Layout/Grid - titles (PR `#1017 `__), - histogram with live colormapping (PR - `#928 `__), colorbars (PR - `#861 `__), - finalize\_hooks (PR - `#1040 `__), labelled - and show\_frame options (PR - `#863 `__, - `#1013 `__), styling - hover glyphs (PR - `#1286 `__), hiding - legends on BarPlot (PR - `#837 `__), VectorField - plot (PR `#1196 `__), - Histograms now have same color cycle as mpl - (`#1008 `__). -- Implemented convenience redim methods to easily set dimension ranges, - values etc. (PR - `#1302 `__) -- Made methods on and operations applied to DynamicMap lazy - (`#422 `__, - `#588 `__, - `#1188 `__, - `#1240 `__, - `#1227 `__) -- Improved documentation (PR - `#936 `__, - `#1070 `__, - `#1242 `__, - `#1273 `__, - `#1280 `__). -- Improved error handling (PR - `#906 `__, - `#932 `__, - `#939 `__, - `#949 `__, - `#1011 `__, - `#1290 `__, - `#1262 `__, - `#1295 `__), including - re-enabling option system keyword validation (PR - `#1277 `__). -- Improved testing (PR - `#834 `__, - `#871 `__, - `#881 `__, - `#941 `__, - `#1117 `__, - `#1153 `__, - `#1171 `__, - `#1207 `__, - `#1246 `__, - `#1259 `__, - `#1287 `__). - -Other new features and improvements: - -- Operations for timeseries (PR - `#1172 `__), - downsample\_columns (PR - `#903 `__), - interpolate\_curve (PR - `#1097 `__), and stacked - area (PR `#1193 `__). -- Dataset types can be declared as empty by passing an empty list (PR - `#1355 `__) -- Plot or style options for Curve interpolation (PR - `#1097 `__), transposing - layouts (PR `#1100 `__), - multiple paths (PR - `#997 `__), and norm for - ColorbarPlot (PR - `#957 `__). -- Improved options inheritance for more intuitive behavior (PR - `#1275 `__). -- Image interface providing similar functionality for Image and - non-Image types (making GridImage obsolete) (PR - `#994 `__). -- Dask data interface (PR - `#974 `__, - `#991 `__). -- xarray aggregate/reduce (PR - `#1192 `__). -- Indicate color clipping and control clipping colors (PR - `#686 `__). -- Better datetime handling (PR - `#1098 `__). -- Gridmatrix diagonal types (PR - `#1194 `__, - `#1027 `__). -- log option for histogram operation (PR - `#929 `__). -- Perceptually uniform fire colormap (PR - `#943 `__). -- Support for adjoining overlays (PR - `#1213 `__). -- coloring weighted average in SideHistogram (PR - `#1087 `__). -- HeatMap allows displaying multiple values on hover (PR - `#849 `__). -- Allow casting Image to QuadMesh (PR - `#1282 `__). -- Unused columns are now preserved in gridded groupby (PR - `#1154 `__). -- Optimizations and fixes for constructing Layout/Overlay types (PR - `#952 `__). -- DynamicMap fixes (PR - `#848 `__, - `#883 `__, - `#911 `__, - `#922 `__, - `#923 `__, - `#927 `__, - `#944 `__, - `#1170 `__, - `#1227 `__, - `#1270 `__). -- Bokeh-backend fixes including handling of empty frames - (`#835 `__), faster - updates (`#905 `__), - hover tool fixes - (`#1004 `__, - `#1178 `__, - `#1092 `__, - `#1250 `__) and many - more (PR `#537 `__, - `#851 `__, - `#852 `__, - `#854 `__, - `#880 `__, - `#896 `__, - `#898 `__, - `#921 `__, - `#934 `__, - `#1004 `__, - `#1010 `__, - `#1014 `__, - `#1030 `__, - `#1069 `__, - `#1072 `__, - `#1085 `__, - `#1157 `__, - `#1086 `__, - `#1169 `__, - `#1195 `__, - `#1263 `__). -- Matplotlib-backend fixes and improvements (PR - `#864 `__, - `#873 `__, - `#954 `__, - `#1037 `__, - `#1068 `__, - `#1128 `__, - `#1132 `__, - `#1143 `__, - `#1163 `__, - `#1209 `__, - `#1211 `__, - `#1225 `__, - `#1269 `__, - `#1300 `__). -- Many other small improvements and fixes (PR - `#830 `__, - `#840 `__, - `#841 `__, - `#850 `__, - `#855 `__, - `#856 `__, - `#859 `__, - `#865 `__, - `#893 `__, - `#897 `__, - `#902 `__, - `#912 `__, - `#916 `__, - `#925 `__, - `#938 `__, - `#940 `__, - `#948 `__, - `#950 `__, - `#955 `__, - `#956 `__, - `#967 `__, - `#970 `__, - `#972 `__, - `#973 `__, - `#981 `__, - `#992 `__, - `#998 `__, - `#1009 `__, - `#1012 `__, - `#1016 `__, - `#1023 `__, - `#1034 `__, - `#1043 `__, - `#1045 `__, - `#1046 `__, - `#1048 `__, - `#1050 `__, - `#1051 `__, - `#1054 `__, - `#1060 `__, - `#1062 `__, - `#1074 `__, - `#1082 `__, - `#1084 `__, - `#1088 `__, - `#1093 `__, - `#1099 `__, - `#1115 `__, - `#1119 `__, - `#1121 `__, - `#1130 `__, - `#1133 `__, - `#1151 `__, - `#1152 `__, - `#1155 `__, - `#1156 `__, - `#1158 `__, - `#1162 `__, - `#1164 `__, - `#1174 `__, - `#1175 `__, - `#1180 `__, - `#1187 `__, - `#1197 `__, - `#1202 `__, - `#1205 `__, - `#1206 `__, - `#1210 `__, - `#1217 `__, - `#1219 `__, - `#1228 `__, - `#1232 `__, - `#1241 `__, - `#1244 `__, - `#1245 `__, - `#1249 `__, - `#1254 `__, - `#1255 `__, - `#1271 `__, - `#1276 `__, - `#1278 `__, - `#1285 `__, - `#1288 `__, - `#1289 `__). - -Changes affecting backwards compatibility: - -- Automatic coloring and sizing on Points now disabled (PR - `#748 `__). -- Deprecated max\_branches output magic option (PR - `#1293 `__). -- Deprecated GridImage (PR - `#1292 `__, - `#1223 `__). -- Deprecated NdElement (PR - `#1191 `__). -- Deprecated DFrame conversion methods (PR - `#1065 `__). -- Banner text removed from notebook\_extension() (PR - `#1231 `__, - `#1291 `__). -- Bokeh's Matplotlib compatibility module removed (PR - `#1239 `__). -- ls as Matplotlib linestyle alias dropped (PR - `#1203 `__). -- mdims argument of conversion interface renamed to groupby (PR - `#1066 `__). -- Replaced global alias state with Dimension.label - (`#1083 `__). -- DynamicMap only update ranges when set to framewise -- Deprecated DynamicMap sampled, bounded, open and generator modes - (`#969 `__, - `#1305 `__) -- Layout.display method is now deprecated - (`#1026 `__) -- Layout fix for Matplotlib figures with non-square aspects introduced - in 1.6.2 (PR `#826 `__), - now enabled by default. - -Version 1.6 -~~~~~~~~~~~ - -Version 1.6.2 -************* - -**August 23, 2016** - -Bug fix release with various fixes for gridded data backends and -optimizations for Bokeh. - -- Optimized Bokeh event messaging, reducing the average json payload by - 30-50% (PR `#807 `__). -- Fixes for correctly handling NdOverlay types returned by DynamicMaps - (PR `#814 `__). -- Added support for datetime64 handling in Matplotlib and support for - datetime formatters on Dimension.type\_formatters (PR - `#816 `__). -- Fixed handling of constant dimensions when slicing xarray datasets - (PR `#817 `__). -- Fixed support for passing custom dimensions to iris Datasets (PR - `#818 `__). -- Fixed support for add\_dimension on xarray interface (PR - `#820 `__). -- Improved extents computation on Matplotlib SpreadPlot (PR - `#821 `__). -- Bokeh backend avoids sending data for static frames and empty events - (PR `#822 `__). -- Added major layout fix for figures with non-square aspects, reducing - the amount of unnecessary whitespace (PR - `#826 `__). Disabled by - default until 1.7 release but can be enabled with: - -.. code:: python - - from holoviews.plotting.mpl import LayoutPlot - LayoutPlot.v17_layout_format - True - LayoutPlot.vspace - 0.3 - -Version 1.6.1 -************* - -**July 27, 2016** - -Bug fix release following the 1.6 major release with major bug fixes for -the grid data interfaces and improvements to the options system. - -- Ensured that style options incompatible with active backend are - ignored (PR `#802 `__). -- Added support for placing legends outside the plot area in Bokeh (PR - `#801 `__). -- Fix to ensure Bokeh backend does not depend on pandas (PR - `#792 `__). -- Fixed option system to ensure correct inheritance when redefining - options (PR `#796 `__). -- Major refactor and fixes for the grid based data backends (iris, - xarray and arrays with coordinates) ensuring the data is oriented and - transposed correctly (PR - `#794 `__). - -Version 1.6.0 -************* - -**July 14, 2016** - -A major release with an optional new data interface based on xarray, -support for batching Bokeh plots for huge increases in performance, -support for Bokeh 0.12 and various other fixes and improvements. - -Features and improvements: - -- Made VectorFieldPlot more general with support for independent - coloring and scaling (PR - `#701 `__). -- Iris interface now allows tuple and dict formats in the constructor - (PR `#709 `__. -- Added support for dynamic groupby on all data interfaces (PR - `#711 `__). -- Added an xarray data interface (PR - `#713 `__). -- Added the redim method to all Dimensioned objects making it easy to - quickly change dimension names and attributes on nested objects - `#715 `__). -- Added support for batching plots (PR - `#715 `__). -- Support for Bokeh 0.12 release (PR - `#725 `__). -- Added support for logz option on Bokeh Raster plots (PR - `#729 `__). -- Bokeh plots now support custom tick formatters specified via - Dimension value\_format (PR - `#728 `__). - -Version 1.5 -~~~~~~~~~~~ - -Version 1.5.0 -************* - -**May 12, 2016** - -A major release with a large number of new features including new data -interfaces for grid based data, major improvements for DynamicMaps and a -large number of bug fixes. - -Features and improvements: - -- Added a grid based data interface to explore n-dimensional gridded - data easily (PR - `#562 `__). -- Added data interface based on `iris - Cubes `__ (PR - `#624 `__). -- Added support for dynamic operations and overlaying of DynamicMaps - (PR `#588 `__). -- Added support for applying groupby operations to DynamicMaps (PR - `#667 `__). -- Added dimension value formatting in widgets (PR - `#562 `__). -- Added support for indexing and slicing with a function (PR - `#619 `__). -- Improved throttling behavior on widgets (PR - `#596 `__). -- Major refactor of Matplotlib plotting classes to simplify - implementing new Element plots (PR - `#438 `__). -- Added Renderer.last\_plot attribute to allow easily debugging or - modifying the last displayed plot (PR - `#538 `__). -- Added Bokeh QuadMeshPlot (PR - `#661 `__). - -Bug fixes: - -- Fixed overlaying of 3D Element types (PR - `#504 `__). -- Fix for Bokeh hovertools with dimensions with special characters (PR - `#524 `__). -- Fixed bugs in seaborn Distribution Element (PR - `#630 `__). -- Fix for inverted Raster.reduce method (PR - `#672 `__). -- Fixed Store.add\_style\_opts method (PR - `#587 `__). -- Fixed bug preventing simultaneous logx and logy plot options (PR - `#554 `__). - -Backwards compatibility: - -- Renamed ``Columns`` type to ``Dataset`` (PR - `#620 `__). - -Version 1.4 -~~~~~~~~~~~ - -Version 1.4.3 -************* - -**February 11, 2016** - -A minor bugfix release to patch a number of small but important issues. - -Fixes and improvements: - -- Added a `DynamicMap - Tutorial `__ to - explain how to explore very large or continuous parameter spaces in - HoloViews (`PR - #470 `__). -- Various fixes and improvements for DynamicMaps including slicing (`PR - #488 `__) and - validation (`PR - #483 `__) and - serialization (`PR - #483 `__) -- Widgets containing Matplotlib plots now display the first frame from - cache providing at least the initial frame when exporting DynamicMaps - (`PR #486 `__) -- Fixed plotting Bokeh plots using widgets in live mode, after changes - introduced in latest Bokeh version (commit - `1b87c91e9 `__). -- Fixed issue in coloring Point/Scatter objects by values (`Issue - #467 `__). - -Backwards compatibility: - -- The behavior of the ``scaling_factor`` on Point and Scatter plots has - changed now simply multiplying ``area`` or ``width`` (as defined by - the ``scaling_method``). To disable scaling points by a dimension set - ``size_index-None``. -- Removed hooks to display 3D Elements using the ``BokehMPLRawWrapper`` - in Bokeh (`PR #477 `__) -- Renamed the DynamicMap mode ``closed`` to ``bounded`` (`PR - #477 `__) - -Version 1.4.2 -************* - -**February 7, 2016** - -Over the past month since the 1.4.1 release, we have improved our -infrastructure for building documentation, updated the main website and -made several additional usability improvements. - -Documentation changes: - -- Major overhaul of website and notebook building making it much easier - to test user contributions (`Issue - #180 `__, `PR - #429 `__) -- Major rewrite of the documentation (`PR - #401 `__, `PR - #411 `__) -- Added Columnar Data Tutorial and removed most of Pandas Conversions - as it is now supported by the core. - -Fixes and improvements: - -- Major improvement for grid based layouts with varying aspects (`PR - #457 `__) -- Fix for interleaving %matplotline inline and holoviews plots (`Issue - #179 `__) -- Matplotlib legend z-orders and updating fixed (`Issue - #304 `__, `Issue - #305 `__) -- ``color_index`` and ``size_index`` plot options support specifying - dimension by name (`Issue - #391 `__) -- Added ``Area`` Element type for drawing area under or between Curves. - (`PR #427 `__) -- Fixed issues where slicing would remove styles applied to an Element. - (`Issue #423 `__, `PR - #439 `__) -- Updated the ``title_format`` plot option to support a - ``{dimensions}`` formatter (`PR - #436 `__) -- Improvements to Renderer API to allow JS and CSS requirements for - exporting standalone widgets (`PR - #426 `__) -- Compatibility with the latest Bokeh 0.11 release (`PR - #393 `__) - -Version 1.4.1 -************* - -**December 22, 2015** - -Over the past two weeks since the 1.4 release, we have implemented -several important bug fixes and have made several usability -improvements. - -New features: - -- Improved help system. It is now possible to recursively list all the - applicable documentation for a composite object. In addition, the - documentation may now be filtered using a regular expression pattern. - (`PR #370 `__) -- HoloViews now supports multiple active display hooks making it easier - to use nbconvert. For instance, PNG data will be embedded in the - notebook if the argument display\_formats-['html','png'] is supplied - to the notebook\_extension. (`PR - #355 `__) -- Improvements to the display of DynamicMaps as well as many new - improvements to the Bokeh backend including better VLines/HLines and - support for the Bars element. (`PR - #367 `__ , `PR - #362 `__, `PR - #339 `__). -- New Spikes and BoxWhisker elements suitable for representing - distributions as a sequence of lines or as a box-and-whisker plot. - (`PR #346 `__, `PR - #339 `__) -- Improvements to the notebook\_extension. For instance, executing - hv.notebook\_extension('bokeh') will now load BokehJS and - automatically activate the Bokeh backend (if available). -- Significant performance improvements when using the groupby operation - on HoloMaps and when working with highly nested datastructures. (`PR - #349 `__, `PR - #359 `__) - -Notable bug fixes: - -- DynamicMaps are now properly integrated into the style system and can - be customized in the same way as HoloMaps. (`PR - #368 `__) -- Widgets now work correctly when unicode is used in the dimension - labels and values (`PR - #376 `__). - -Version 1.4.0 -************* - -**December 4, 2015** - -Over the past few months we have added several major new features and -with the help of our users have been able to address a number of bugs -and inconsistencies. We have closed 57 issues and added over 1100 new -commits. - -Major new features: - -- Data API: The new data API brings an extensible system of to add new - data interfaces to column based Element types. These interfaces allow - applying powerful operations on the data independently of the data - format. The currently supported datatypes include NumPy, pandas - dataframes and a simple dictionary format. (`PR - #284 `__) -- Backend API: In this release we completely refactored the rendering, - plotting and IPython display system to make it easy to add new - plotting backends. Data may be styled and pickled for each backend - independently and renderers now support exporting all plotting data - including widgets as standalone HTML files or with separate JSON - data. -- Bokeh backend: The first new plotting backend added via the new - backend API. Bokeh plots allow for much faster plotting and greater - interactivity. Supports most Element types and layouts and provides - facilities for sharing axes across plots and linked brushing across - plots. (`PR #250 `__) -- DynamicMap: The new DynamicMap class allows HoloMap data to be - generated on-the-fly while running a Jupyter IPython notebook kernel. - Allows visualization of unbounded data streams and smooth exploration - of large continuous parameter spaces. (`PR - #278 `__) - -Other features: - -- Easy definition of custom aliases for group, label and Dimension - names, allowing easier use of LaTeX. -- New Trisurface and QuadMesh elements. -- Widgets now allow expressing hierarchical relationships between - dimensions. -- Added GridMatrix container for heterogeneous Elements and gridmatrix - operation to generate scatter matrix showing relationship between - dimensions. -- Filled contour regions can now be generated using the contours - operation. -- Consistent indexing semantics for all Elements and support for - boolean indexing for Columns and NdMapping types. -- New hv.notebook\_extension function offers a more flexible - alternative to %load\_ext, e.g. for loading other extensions - hv.notebook\_extension(bokeh-True). - -Experimental features: - -- Bokeh callbacks allow adding interactivity by communicating between - BokehJS tools and the IPython kernel, e.g. allowing downsampling - based on the zoom level. - -Notable bug fixes: - -- Major speedup rendering large HoloMaps (~ 2-3 times faster). -- Colorbars now consistent for all plot configurations. -- Style pickling now works correctly. - -API Changes: - -- Dimension formatter parameter now deprecated in favor of - value\_format. -- Types of Chart and Table Element data now dependent on selected - interface. -- DFrame conversion interface deprecated in favor of Columns pandas - interface. - -Version 1.3 -~~~~~~~~~~~ - -Version 1.3.2 -************* - -**July 6, 2015** - -Minor bugfix release to address a small number of issues: - -Features: - -- Added support for colorbars on Surface Element (1cd5281). -- Added linewidth style option to SurfacePlot (9b6ccc5). - -Bug fixes: - -- Fixed inversion inversion of y-range during sampling (6ff81bb). -- Fixed overlaying of 3D elements (787d511). -- Ensuring that underscore.js is loaded in widgets (f2f6378). -- Fixed Python3 issue in Overlay.get (8ceabe3). - -Version 1.3.1 -************* - -**July 1, 2015** - -Minor bugfix release to address a number of issues that weren't caught -in time for the 1.3.0 release with the addition of a small number of -features: - -Features: - -- Introduced new ``Spread`` element to plot errors and confidence - intervals (30d3184). -- ``ErrorBars`` and ``Spread`` elements now allow most Chart - constructor types (f013deb). - -Bug fixes: - -- Fixed unicode handling for dimension labels (061e9af). -- Handling of invalid dimension label characters in widgets (a101b9e). -- Fixed setting of fps option for MPLRenderer video output (c61b9df). -- Fix for multiple and animated colorbars (5e1e4b5). -- Fix to Chart slices starting or ending at zero (edd0039). - -Version 1.3.0 -************* - -**June 27, 2015** - -Since the last release we closed over 34 issues and have made 380 -commits mostly focused on fixing bugs, cleaning up the API and working -extensively on the plotting and rendering system to ensure HoloViews is -fully backend independent. - -We'd again like to thank our growing user base for all their input, -which has helped us in making the API more understandable and fixing a -number of important bugs. - -Highlights/Features: - -- Allowed display of data structures which do not match the recommended - nesting hierarchy (67b28f3, fbd89c3). -- Dimensions now sanitized for ``.select``, ``.sample`` and ``.reduce`` - calls (6685633, 00b5a66). -- Added ``holoviews.ipython.display`` function to render (and display) - any HoloViews object, useful for IPython interact widgets (0fa49cd). -- Table column widths now adapt to cell contents (be90a54). -- Defaulting to Matplotlib ticking behavior (62e1e58). -- Allowed specifying fixed figure sizes to Matplotlib via - ``fig_inches`` tuples using (width, None) and (None, height) formats - (632facd). -- Constructors of ``Chart``, ``Path`` and ``Histogram`` classes now - support additional data formats (2297375). -- ``ScrubberWidget`` now supports all figure formats (c317db4). -- Allowed customizing legend positions on ``Bars`` Elements (5a12882). -- Support for multiple colorbars on one axis (aac7b92). -- ``.reindex`` on ``NdElement`` types now support converting between - key and value dimensions allowing more powerful conversions. - (03ac3ce) -- Improved support for casting between ``Element`` types (cdaab4e, - b2ad91b, ce7fe2d, 865b4d5). -- The ``%%opts`` cell magic may now be used multiple times in the same - cell (2a77fd0) -- Matplotlib rcParams can now be set correctly per figure (751210f). -- Improved ``OptionTree`` repr which now works with eval (2f824c1). -- Refactor of rendering system and IPython extension to allow easy - swapping of plotting backend (#141) -- Large plotting optimization by computing tight ``bbox_inches`` once - (e34e339). -- Widgets now cache frames in the DOM, avoiding flickering in some - browsers and make use of jinja2 template inheritance. (fc7dd2b) -- Calling a HoloViews object without arguments now clears any - associated custom styles. (9e8c343) - -API Changes - -- Renamed key\_dimensions and value\_dimensions to kdims and vdims - respectively, while providing backward compatibility for passing and - accessing the long names (8feb7d2). -- Combined x/y/zticker plot options into x/y/zticks parameters which - now accept an explicit number of ticks, an explicit list of tick - positions (and labels), and a Matplotlib tick locator. -- Changed backend options in %output magic, ``nbagg`` and ``d3`` are - now modes of the Matplotlib backend and can be selected with - ``backend-'matplotlib:nbagg'`` and ``backend-'matplotlib:mpld3'`` - respectively. The 'd3' and 'nbagg' options remain supported but will - be deprecated in future. -- Customizations should no longer be applied directly to - ``Store.options``; the ``Store.options(backend-'matplotlib')`` object - should be customized instead. There is no longer a need to call the - deprecated ``Store.register_plots`` method. - -Version 1.2 -~~~~~~~~~~~ - -Version 1.2.0 -************* - -**May 27, 2015** - -Since the last release we closed over 20 issues and have made 334 -commits, adding a ton of functionality and fixing a large range of bugs -in the process. - -In this release we received some excellent feedback from our users, -which has been greatly appreciated and has helped us address a wide -range of problems. - -Highlights/Features: - -- Added new ``ErrorBars`` Element (f2b276b) -- Added ``Empty`` pseudo-Element to define empty placeholders in - Layouts (35bac9f1d) -- Added support for changing font sizes easily (0f54bea) -- Support for holoviews.rc file (79076c8) -- Many major speed optimizations for working with and plotting - HoloViews data structures (fe87b4c, 7578c51, 5876fe6, 8863333) -- Support for ``GridSpace`` with inner axes (93295c8) -- New ``aspect_weight`` and ``tight`` Layout plot options for more - customizability of Layout arrangements (4b1f03d, e6a76b7) -- Added ``bgcolor`` plot option to easily set axis background color - (92eb95c) -- Improved widget layout (f51af02) -- New ``OutputMagic`` css option to style html output (9d42dc2) -- Experimental support for PDF output (1e8a59b) -- Added support for 3D interactivity with nbagg (781bc25) -- Added ability to support deprecated plot options in %%opts magic. -- Added ``DrawPlot`` simplifying the implementation of custom plots - (38e9d44) - -API changes: - -- ``Path`` and ``Histogram`` support new constructors (7138ef4, - 03b5d38) -- New depth argument on the relabel method (f89b89f) -- Interface to Pandas improved (1a7cd3d) -- Removed ``xlim``, ``ylim`` and ``zlim`` to eliminate redundancy. -- Renaming of various plot and style options including: - - - ``figure_*`` to ``fig_*`` - - ``vertical_spacing`` and ``horizontal_spacing`` to ``vspace`` and - ``hspace`` respectively - - \* Deprecation of confusing ``origin`` style option on RasterPlot -- ``Overlay.__getitem__`` no longer supports integer indexing (use - ``get`` method instead) - -Important bug fixes: - -- Important fixes to inheritance in the options system (d34a931, - 71c1f3a7) -- Fixes to the select method (df839bea5) -- Fixes to normalization system (c3ef40b) -- Fixes to ``Raster`` and ``Image`` extents, ``__getitem__`` and - sampling. -- Fixed bug with disappearing adjoined plots (2360972) -- Fixed plot ordering of overlaid elements across a ``HoloMap`` - (c4f1685) - -Version 1.1 -~~~~~~~~~~~ - -Version 1.1.0 -************* - -**April 15, 2015** - -Highlights: - -- Support for nbagg as a backend (09eab4f1) -- New .hvz file format for saving HoloViews objects (bfd5f7af) -- New ``Polygon`` element type (d1ec8ec8) -- Greatly improved Unicode support throughout, including support for - unicode characters in Python 3 attribute names (609a8454) -- Regular SelectionWidget now supports live rendering (eb5bf8b6) -- Supports a list of objects in Layout and Overlay constructors - (5ba1866e) -- Polar projections now supported (3801b76e) - -API changes (not backward compatible): - -- ``xlim``, ``ylim``, ``zlim``, ``xlabel``, ``ylabel`` and ``zlabel`` - have been deprecated (081d4123) -- Plotting options ``show_xaxis`` and ``show_yaxis`` renamed to - ``xaxis`` and ``yaxis``, respectively (13393f2a). -- Deprecated IPySelectionWidget (f59c34c0) - -In addition to the above improvements, many miscellaneous bug fixes were -made. - -Version 1.0 -~~~~~~~~~~~ - -Version 1.0.1 -************* - -**March 26, 2015** - -Minor release addressing bugs and issues with 1.0.0. - -Highlights: - -- New separate Pandas Tutorial (8455abc3) -- Silenced warnings when loading the IPython extension in IPython 3 - (aaa6861b) -- Added more useful installation options via ``setup.py`` (72ece4db) -- Improvements and bug-fixes for the ``%%opts`` magic tab-completion - (e0ad7108) -- ``DFrame`` now supports standard constructor for pandas dataframes - (983825c5) -- ``Tables`` are now correctly formatted using the appropriate - ``Dimension`` formatter (588bc2a3) -- Support for unlimited alphabetical subfigure labelling (e039d00b) -- Miscellaneous bug fixes, including Python 3 compatibility - improvements. - -Version 1.0.0 -************* - -**March 16, 2015** - -First public release available on GitHub and PyPI. - -.. Backticks and links don't play nicely together in RST - -.. |Sankey| replace:: ``Sankey`` -.. _Sankey: http://holoviews.org/reference/elements/bokeh/Sankey.html - -.. |TriMesh| replace:: ``TriMesh`` -.. _TriMesh: http://holoviews.org/reference/elements/bokeh/TriMesh.html - -.. |Chord| replace:: ``Chord`` -.. _Chord: http://holoviews.org/reference/elements/bokeh/Chord.html - -.. |HexTiles| replace:: ``HexTiles`` -.. _HexTiles: http://holoviews.org/reference/elements/bokeh/HexTiles.html - -.. |Labels| replace:: ``Labels`` -.. _Labels: http://holoviews.org/reference/elements/bokeh/Labels.html - -.. |Div| replace:: ``Div`` -.. _Div: http://holoviews.org/reference/elements/bokeh/Div.html - -.. |PointDraw| replace:: ``PointDraw`` -.. _PointDraw: http://holoviews.org/reference/streams/bokeh/PointDraw.html - -.. |PolyDraw| replace:: ``PolyDraw`` -.. _PolyDraw: http://holoviews.org/reference/streams/bokeh/PolyDraw.html - -.. |BoxEdit| replace:: ``BoxEdit`` -.. _BoxEdit: http://holoviews.org/reference/streams/bokeh/BoxEdit.html - -.. |PolyEdit| replace:: ``PolyEdit`` -.. _PolyEdit: http://holoviews.org/reference/streams/bokeh/PolyEdit.html - -.. |radial HeatMap| replace:: radial ``HeatMap`` -.. _radial HeatMap: http://holoviews.org/reference/elements/bokeh/RadialHeatMap.html From e70ed3274a6f4345e51d5ee9e070e47b1d7730e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Tue, 7 May 2024 21:29:26 +0200 Subject: [PATCH 24/43] Add `test-gpu` task (#6217) --- holoviews/core/data/cudf.py | 6 ++- holoviews/element/selection.py | 51 ++++++++++++++----- holoviews/tests/conftest.py | 29 +++++++---- .../tests/core/data/test_cudfinterface.py | 33 +++++++----- holoviews/tests/element/test_selection.py | 18 +++++++ holoviews/tests/operation/test_datashader.py | 16 +++--- holoviews/tests/operation/test_operation.py | 36 +++++++++++++ pixi.toml | 19 ++++++- 8 files changed, 160 insertions(+), 48 deletions(-) diff --git a/holoviews/core/data/cudf.py b/holoviews/core/data/cudf.py index 0c88d4ec2a..72fafab305 100644 --- a/holoviews/core/data/cudf.py +++ b/holoviews/core/data/cudf.py @@ -241,7 +241,11 @@ def _select_mask_neighbor(cls, dataset, selection): select_mask_neighbor = [False, True, True, True, True, False] """ - raise NotImplementedError + mask = cls.select_mask(dataset, selection).to_cupy() + extra = (mask[1:] ^ mask[:-1]) + mask[1:] |= extra + mask[:-1] |= extra + return mask @classmethod def select(cls, dataset, selection_mask=None, **selection): diff --git a/holoviews/element/selection.py b/holoviews/element/selection.py index 25d448e127..a6c8ff43ac 100644 --- a/holoviews/element/selection.py +++ b/holoviews/element/selection.py @@ -80,26 +80,53 @@ def spatial_select_gridded(xvals, yvals, geometry): sel_mask = spatial_select_columnar(xvals.flatten(), yvals.flatten(), geometry) return sel_mask.reshape(xvals.shape) +def _cuspatial_old(xvals, yvals, geometry): + import cudf + import cuspatial + + result = cuspatial.point_in_polygon( + xvals, + yvals, + cudf.Series([0], index=["selection"]), + [0], + geometry[:, 0], + geometry[:, 1], + ) + return result.values + + +def _cuspatial_new(xvals, yvals, geometry): + import cudf + import cuspatial + import geopandas + from shapely.geometry import Polygon + + df = cudf.DataFrame({'x':xvals, 'y':yvals}) + points = cuspatial.GeoSeries.from_points_xy( + df.interleave_columns().astype('float') + ) + polygons = cuspatial.GeoSeries( + geopandas.GeoSeries(Polygon(geometry)), index=["selection"] + ) + result = cuspatial.point_in_polygon(points,polygons) + return result.values.ravel() + + def spatial_select_columnar(xvals, yvals, geometry, geom_method=None): if 'cudf' in sys.modules: import cudf + import cupy as cp if isinstance(xvals, cudf.Series): xvals = xvals.values.astype('float') yvals = yvals.values.astype('float') try: - import cuspatial - result = cuspatial.point_in_polygon( - xvals, - yvals, - cudf.Series([0], index=["selection"]), - [0], - geometry[:, 0], - geometry[:, 1], - ) - return result.values + try: + return _cuspatial_old(xvals, yvals, geometry) + except TypeError: + return _cuspatial_new(xvals, yvals, geometry) except ImportError: - xvals = np.asarray(xvals) - yvals = np.asarray(yvals) + xvals = cp.asnumpy(xvals) + yvals = cp.asnumpy(yvals) if 'dask' in sys.modules: import dask.dataframe as dd if isinstance(xvals, dd.Series): diff --git a/holoviews/tests/conftest.py b/holoviews/tests/conftest.py index 6ab08c282e..8b6219afe1 100644 --- a/holoviews/tests/conftest.py +++ b/holoviews/tests/conftest.py @@ -4,24 +4,35 @@ import panel as pn import pytest -from panel.tests.conftest import ( # noqa: F401 - optional_markers, - port, - pytest_addoption, - pytest_configure, - server_cleanup, -) +from panel.tests.conftest import port, server_cleanup # noqa: F401 from panel.tests.util import serve_and_wait import holoviews as hv +CUSTOM_MARKS = ("ui", "gpu") + + +def pytest_addoption(parser): + for marker in CUSTOM_MARKS: + parser.addoption( + f"--{marker}", + action="store_true", + default=False, + help=f"Run {marker} related tests", + ) + + +def pytest_configure(config): + for marker in CUSTOM_MARKS: + config.addinivalue_line("markers", f"{marker}: {marker} test marker") + def pytest_collection_modifyitems(config, items): skipped, selected = [], [] - markers = [m for m in optional_markers if config.getoption(f"--{m}")] + markers = [m for m in CUSTOM_MARKS if config.getoption(f"--{m}")] empty = not markers for item in items: - if empty and any(m in item.keywords for m in optional_markers): + if empty and any(m in item.keywords for m in CUSTOM_MARKS): skipped.append(item) elif empty: selected.append(item) diff --git a/holoviews/tests/core/data/test_cudfinterface.py b/holoviews/tests/core/data/test_cudfinterface.py index 87cb469190..30d7314639 100644 --- a/holoviews/tests/core/data/test_cudfinterface.py +++ b/holoviews/tests/core/data/test_cudfinterface.py @@ -1,18 +1,15 @@ import logging -from unittest import SkipTest import numpy as np - -try: - import cudf -except ImportError: - raise SkipTest("Could not import cuDF, skipping cuDFInterface tests.") +import pytest from holoviews.core.data import Dataset from holoviews.core.spaces import HoloMap from .base import HeterogeneousColumnTests, InterfaceTests +pytestmark = pytest.mark.gpu + class cuDFInterfaceTests(HeterogeneousColumnTests, InterfaceTests): """ @@ -20,16 +17,21 @@ class cuDFInterfaceTests(HeterogeneousColumnTests, InterfaceTests): """ datatype = 'cuDF' - data_type = cudf.DataFrame __test__ = True + @property + def data_type(self): + import cudf + return cudf.DataFrame + def setUp(self): super().setUp() logging.getLogger('numba.cuda.cudadrv.driver').setLevel(30) + @pytest.mark.xfail(reason="cuDF does not support variance aggregation") def test_dataset_2D_aggregate_spread_fn_with_duplicates(self): - raise SkipTest("cuDF does not support variance aggregation") + super().test_dataset_2D_aggregate_spread_fn_with_duplicates() def test_dataset_mixed_type_range(self): ds = Dataset((['A', 'B', 'C', None],), 'A') @@ -102,12 +104,15 @@ def test_dataset_groupby_second_dim(self): kdims=['Age']) self.assertEqual(self.table.groupby(['Age']).apply('sort'), grouped) + @pytest.mark.xfail(reason="cuDF does not support variance aggregation") def test_dataset_aggregate_string_types_size(self): - raise SkipTest("cuDF does not support variance aggregation") + super().test_dataset_aggregate_string_types_size() def test_select_with_neighbor(self): - try: - # Not currently supported by CuDF - super().test_select_with_neighbor() - except NotImplementedError: - raise SkipTest("Not supported") + import cupy as cp + + select = self.table.interface.select_mask(self.table.dataset, {"Weight": 18}) + select_neighbor = self.table.interface._select_mask_neighbor(self.table.dataset, dict(Weight=18)) + + np.testing.assert_almost_equal(cp.asnumpy(select), [False, True, False]) + np.testing.assert_almost_equal(cp.asnumpy(select_neighbor), [True, True, True]) diff --git a/holoviews/tests/element/test_selection.py b/holoviews/tests/element/test_selection.py index 4586371262..517653e8e4 100644 --- a/holoviews/tests/element/test_selection.py +++ b/holoviews/tests/element/test_selection.py @@ -682,6 +682,24 @@ def test_numpy(self, geometry, pt_mask, pandas_df, _method): mask = spatial_select_columnar(pandas_df.x.to_numpy(copy=True), pandas_df.y.to_numpy(copy=True), geometry, _method) assert np.array_equal(mask, pt_mask) + @pytest.mark.gpu + def test_cudf(self, geometry, pt_mask, pandas_df, _method, unimport): + import cudf + import cupy as cp + unimport('cuspatial') + + df = cudf.from_pandas(pandas_df) + mask = spatial_select_columnar(df.x, df.y, geometry, _method) + assert np.array_equal(cp.asnumpy(mask), pt_mask) + + @pytest.mark.gpu + def test_cuspatial(self, geometry, pt_mask, pandas_df, _method): + import cudf + import cupy as cp + + df = cudf.from_pandas(pandas_df) + mask = spatial_select_columnar(df.x, df.y, geometry, _method) + assert np.array_equal(cp.asnumpy(mask), pt_mask) @pytest.mark.parametrize("geometry", [geometry_encl, geometry_noencl]) class TestSpatialSelectColumnarDaskMeta: diff --git a/holoviews/tests/operation/test_datashader.py b/holoviews/tests/operation/test_datashader.py index 531d869a09..1573e35499 100644 --- a/holoviews/tests/operation/test_datashader.py +++ b/holoviews/tests/operation/test_datashader.py @@ -60,19 +60,12 @@ except ImportError: raise SkipTest('Datashader not available') -try: - import cudf - import cupy -except ImportError: - cudf = None - try: import spatialpandas except ImportError: spatialpandas = None spatialpandas_skip = skipIf(spatialpandas is None, "SpatialPandas not available") -cudf_skip = skipIf(cudf is None, "cuDF not available") import logging @@ -135,15 +128,18 @@ def test_aggregate_points_count_column(self): vdims=[Dimension('z Count', nodata=0)]) self.assertEqual(img, expected) - @cudf_skip + @pytest.mark.gpu def test_aggregate_points_cudf(self): + import cudf + import cupy + points = Points([(0.2, 0.3), (0.4, 0.7), (0, 0.99)], datatype=['cuDF']) - self.assertIsInstance(points.data, cudf.DataFrame) + assert isinstance(points.data, cudf.DataFrame) img = aggregate(points, dynamic=False, x_range=(0, 1), y_range=(0, 1), width=2, height=2) expected = Image(([0.25, 0.75], [0.25, 0.75], [[1, 0], [2, 0]]), vdims=[Dimension('Count', nodata=0)]) - self.assertIsInstance(img.data.Count.data, cupy.ndarray) + assert isinstance(img.data.Count.data, cupy.ndarray) self.assertEqual(img, expected) def test_aggregate_zero_range_points(self): diff --git a/holoviews/tests/operation/test_operation.py b/holoviews/tests/operation/test_operation.py index 942ea6de1b..01fd131f6c 100644 --- a/holoviews/tests/operation/test_operation.py +++ b/holoviews/tests/operation/test_operation.py @@ -16,6 +16,10 @@ except ImportError: ibis = None +try: + import cudf +except ImportError: + cudf = None from holoviews import ( Area, @@ -459,6 +463,38 @@ def test_dataset_histogram_explicit_bins_ibis(self): vdims=('x_count', 'Count')) self.assertEqual(op_hist, hist) + @pytest.mark.gpu + def test_dataset_histogram_cudf(self): + df = pd.DataFrame(dict(x=np.arange(10))) + t = cudf.from_pandas(df) + ds = Dataset(t, vdims='x') + op_hist = histogram(ds, dimension='x', num_bins=3, normed=True) + + hist = Histogram(([0, 3, 6, 9], [0.1, 0.1, 0.133333]), + vdims=('x_frequency', 'Frequency')) + self.assertEqual(op_hist, hist) + + @pytest.mark.gpu + def test_dataset_cumulative_histogram_cudf(self): + df = pd.DataFrame(dict(x=np.arange(10))) + t = cudf.from_pandas(df) + ds = Dataset(t, vdims='x') + op_hist = histogram(ds, num_bins=3, cumulative=True, normed=True) + + hist = Histogram(([0, 3, 6, 9], [0.3, 0.6, 1]), + vdims=('x_frequency', 'Frequency')) + self.assertEqual(op_hist, hist) + + @pytest.mark.gpu + def test_dataset_histogram_explicit_bins_cudf(self): + df = pd.DataFrame(dict(x=np.arange(10))) + t = cudf.from_pandas(df) + ds = Dataset(t, vdims='x') + op_hist = histogram(ds, bins=[0, 1, 3], normed=False) + + hist = Histogram(([0, 1, 3], [1, 3]), + vdims=('x_count', 'Count')) + self.assertEqual(op_hist, hist) def test_points_histogram_bin_range(self): points = Points([float(i) for i in range(10)]) diff --git a/pixi.toml b/pixi.toml index 0b42ef1724..9df38ecb40 100644 --- a/pixi.toml +++ b/pixi.toml @@ -15,6 +15,7 @@ test-311 = ["py311", "test-core", "test", "example", "test-example", "test-unit- test-312 = ["py312", "test-core", "test", "example", "test-example", "test-unit-task"] test-ui = ["py312", "test-core", "test", "test-ui"] test-core = ["py312", "test-core", "test-unit-task"] +test-gpu = ["py311", "test-core", "test", "test-gpu"] docs = ["py311", "example", "doc"] build = ["py311", "build"] lint = ["py311", "lint"] @@ -26,7 +27,7 @@ packaging = "*" pandas = ">=0.20.0" panel = ">=1.0" param = ">=1.12.0,<3.0" -pip= "*" +pip = "*" pyviz_comms = ">=2.1" # Recommended bokeh = ">=3.1" @@ -95,7 +96,7 @@ xarray = ">=0.10.4" xyzservices = "*" [feature.test.target.unix.dependencies] -tsdownsample = "*" # currently not available on Windows +tsdownsample = "*" # currently not available on Windows [feature.test-example.tasks] test-example = 'pytest -n logical --dist loadscope --nbval-lax examples' @@ -119,6 +120,20 @@ _install-ui = 'playwright install chromium' cmd = 'pytest holoviews/tests/ui --ui --browser chromium' depends_on = ["_install-ui"] +[feature.test-gpu] +channels = ["pyviz/label/dev", "rapidsai", "conda-forge"] +platforms = ["linux-64"] + +[feature.test-gpu.dependencies] +cuda-version = "12.2.*" +cudf = "24.04.*" +cupy = "*" +cuspatial = "*" +rmm = { version = "*", channel = "rapidsai" } + +[feature.test-gpu.tasks] +test-gpu = { cmd = "pytest holoviews/tests --gpu", env = { NUMBA_CUDA_LOW_OCCUPANCY_WARNINGS = '0' } } + # ============================================= # =================== DOCS ==================== # ============================================= From c488d776e6bcebf9f7a46ab9bb3954dd63256448 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Thu, 9 May 2024 13:40:44 +0200 Subject: [PATCH 25/43] Update pre-commit prettier repo (#6219) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b122f3f2cd..5f0832e8fb 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -45,8 +45,8 @@ repos: rev: v0.10.0.1 hooks: - id: shellcheck - - repo: https://github.com/pre-commit/mirrors-prettier - rev: v3.1.0 + - repo: https://github.com/hoxbro/prettier-pre-commit + rev: v3.2.5 hooks: - id: prettier exclude: conda.recipe/meta.yaml From d33891545e70cb6b9e779fa5941f23b333b3246d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Fri, 10 May 2024 13:32:49 +0200 Subject: [PATCH 26/43] Add toml formatter (#6227) --- .pre-commit-config.yaml | 13 +++++++ pyproject.toml | 82 ++++++++++++++++++++--------------------- 2 files changed, 53 insertions(+), 42 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5f0832e8fb..c2d28bfbc4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -53,3 +53,16 @@ repos: types_or: - markdown - yaml + - repo: https://github.com/hoxbro/taplo-pre-commit + rev: v0.7.0 + hooks: + - id: taplo + args: + [ + --option, + align_comments=false, + --option, + column_width=100, + --option, + "indent_string= ", + ] diff --git a/pyproject.toml b/pyproject.toml index 9fa06e809f..d3874f838e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,9 +13,7 @@ authors = [ { name = "Jean-Luc Stevens", email = "developers@holoviz.org" }, { name = "Philipp Rudiger", email = "developers@holoviz.org" }, ] -maintainers = [ - { name = "HoloViz developers", email = "developers@holoviz.org" }, -] +maintainers = [{ name = "HoloViz developers", email = "developers@holoviz.org" }] classifiers = [ "License :: OSI Approved :: BSD License", "Development Status :: 5 - Production/Stable", @@ -85,12 +83,12 @@ filterwarnings = [ # 2022-12: Warnings which should be fixed in Panel "ignore:make_current is deprecated; start the event loop first:DeprecationWarning:panel.io.server", # 2023-01: Numpy 1.24 warnings - "ignore:`.+?` is a deprecated alias for `.+?`.:DeprecationWarning:bokeh", # https://github.com/bokeh/bokeh/pull/12690 - "ignore:`.+?` is a deprecated alias for `.+?`.:DeprecationWarning:cupy", # https://github.com/cupy/cupy/pull/7245 - "ignore:`.+?` is a deprecated alias for `.+?`.:DeprecationWarning:plotly.express.imshow_utils", # https://github.com/plotly/plotly.py/pull/3997 - "ignore:`.+?` is a deprecated alias for `.+?`.:DeprecationWarning:skimage.util.dtype", # https://github.com/scikit-image/scikit-image/pull/6637 + "ignore:`.+?` is a deprecated alias for `.+?`.:DeprecationWarning:bokeh", # https://github.com/bokeh/bokeh/pull/12690 + "ignore:`.+?` is a deprecated alias for `.+?`.:DeprecationWarning:cupy", # https://github.com/cupy/cupy/pull/7245 + "ignore:`.+?` is a deprecated alias for `.+?`.:DeprecationWarning:plotly.express.imshow_utils", # https://github.com/plotly/plotly.py/pull/3997 + "ignore:`.+?` is a deprecated alias for `.+?`.:DeprecationWarning:skimage.util.dtype", # https://github.com/scikit-image/scikit-image/pull/6637 # 2023-01: Sqlalchemy 2.0 warning: - "ignore: Deprecated API features detected:DeprecationWarning:ibis.backends.base.sql.alchemy", # https://github.com/ibis-project/ibis/issues/5048 + "ignore: Deprecated API features detected:DeprecationWarning:ibis.backends.base.sql.alchemy", # https://github.com/ibis-project/ibis/issues/5048 # 2023-03: Already handling the nested sequence "ignore:Creating an ndarray from ragged nested sequences:numpy.VisibleDeprecationWarning:holoviews.core.data.spatialpandas", # 2023-09: Dash needs to update their code to use the comm module and pkg_resources @@ -101,30 +99,28 @@ filterwarnings = [ # 2023-09: Not a relevant warning for HoloViews "ignore:Jupyter is migrating its paths to use standard platformdirs:DeprecationWarning", # 2023-09: `pkg_resources` is deprecated - "ignore:Deprecated call to `pkg_resources.+?'mpl_toolkits:DeprecationWarning", # https://github.com/matplotlib/matplotlib/issues/25244 - "ignore:Deprecated call to `pkg_resources.+?'sphinxcontrib:DeprecationWarning", # https://github.com/mgaitan/sphinxcontrib-mermaid/issues/119 + "ignore:Deprecated call to `pkg_resources.+?'mpl_toolkits:DeprecationWarning", # https://github.com/matplotlib/matplotlib/issues/25244 + "ignore:Deprecated call to `pkg_resources.+?'sphinxcontrib:DeprecationWarning", # https://github.com/mgaitan/sphinxcontrib-mermaid/issues/119 "ignore: pkg_resources is deprecated as an API:DeprecationWarning:streamz.plugins", # https://github.com/python-streamz/streamz/issues/460 # 2023-10: Datetime's utctimestamp() and utcnow() is deprecated in Python 3.12 - "ignore:datetime.datetime.utcfromtimestamp():DeprecationWarning:dateutil.tz.tz", # https://github.com/dateutil/dateutil/pull/1285 - "ignore:datetime.datetime.utcfromtimestamp():DeprecationWarning:bokeh", # https://github.com/bokeh/bokeh/issues/13125 - "ignore:datetime.datetime.utcnow():DeprecationWarning:bokeh", # https://github.com/bokeh/bokeh/issues/13125 + "ignore:datetime.datetime.utcfromtimestamp():DeprecationWarning:dateutil.tz.tz", # https://github.com/dateutil/dateutil/pull/1285 + "ignore:datetime.datetime.utcfromtimestamp():DeprecationWarning:bokeh", # https://github.com/bokeh/bokeh/issues/13125 + "ignore:datetime.datetime.utcnow():DeprecationWarning:bokeh", # https://github.com/bokeh/bokeh/issues/13125 # 2024-01: Pandas 2.2 - "ignore:When grouping with a length-1 list::dask.dataframe.groupby", # https://github.com/dask/dask/issues/10572 - "ignore:\\s*Pyarrow will become a required dependency of pandas:DeprecationWarning", # Will go away by itself in Pandas 3.0 - "ignore:Passing a (SingleBlockManager|BlockManager) to (Series|GeoSeries|DataFrame|GeoDataFrame) is deprecated:DeprecationWarning", # https://github.com/holoviz/spatialpandas/issues/137 + "ignore:When grouping with a length-1 list::dask.dataframe.groupby", # https://github.com/dask/dask/issues/10572 + "ignore:\\s*Pyarrow will become a required dependency of pandas:DeprecationWarning", # Will go away by itself in Pandas 3.0 + "ignore:Passing a (SingleBlockManager|BlockManager) to (Series|GeoSeries|DataFrame|GeoDataFrame) is deprecated:DeprecationWarning", # https://github.com/holoviz/spatialpandas/issues/137 # 2024-02 - "ignore:The current Dask DataFrame implementation is deprecated:DeprecationWarning", # https://github.com/dask/dask/issues/10917 + "ignore:The current Dask DataFrame implementation is deprecated:DeprecationWarning", # https://github.com/dask/dask/issues/10917 # 2024-04 - "ignore:No data was collected:coverage.exceptions.CoverageWarning", # https://github.com/pytest-dev/pytest-cov/issues/627 + "ignore:No data was collected:coverage.exceptions.CoverageWarning", # https://github.com/pytest-dev/pytest-cov/issues/627 + # 2024-05 + "ignore:backend2gui is deprecated since IPython 8.24:DeprecationWarning", # https://github.com/holoviz/holoviews/pull/6227#issuecomment-2104401396 ] [tool.coverage] omit = ["holoviews/__version.py"] -exclude_also = [ - "if __name__ == .__main__.:", - "if TYPE_CHECKING:", - "if ._pyodide. in sys.modules:", -] +exclude_also = ["if __name__ == .__main__.:", "if TYPE_CHECKING:", "if ._pyodide. in sys.modules:"] [tool.ruff] fix = true @@ -149,32 +145,34 @@ select = [ ] ignore = [ - "E402", # Module level import not at top of file - "E501", # Line too long - "E701", # Multiple statements on one line - "E712", # Comparison to true should be is - "E731", # Do not assign a lambda expression, use a def - "E741", # Ambiguous variable name - "F405", # From star imports - "PLE0604", # Invalid object in `__all__`, must contain only strings - "PLE0605", # Invalid format for `__all__` - "PLR091", # Too many arguments/branches/statements - "PLR2004", # Magic value used in comparison - "PLW2901", # `for` loop variable is overwritten - "RUF005", # Consider {expr} instead of concatenation - "RUF012", # Mutable class attributes should use `typing.ClassVar` + "E402", # Module level import not at top of file + "E501", # Line too long + "E701", # Multiple statements on one line + "E712", # Comparison to true should be is + "E731", # Do not assign a lambda expression, use a def + "E741", # Ambiguous variable name + "F405", # From star imports + "PLE0604", # Invalid object in `__all__`, must contain only strings + "PLE0605", # Invalid format for `__all__` + "PLR091", # Too many arguments/branches/statements + "PLR2004", # Magic value used in comparison + "PLW2901", # `for` loop variable is overwritten + "RUF005", # Consider {expr} instead of concatenation + "RUF012", # Mutable class attributes should use `typing.ClassVar` ] unfixable = [ - "F401", # Unused imports - "F841", # Unused variables + "F401", # Unused imports + "F841", # Unused variables ] [tool.ruff.lint.per-file-ignores] "__init__.py" = ["F403"] "holoviews/tests/*" = [ - "RUF001", "RUF002", "RUF003", # Ambiguous unicode character - "NPY002", # Replace legacy `np.random.rand` call with Generator - "B904", # Within an `except` clause, raise exceptions with from err or None + "RUF001", # Ambiguous unicode character + "RUF002", # Ambiguous unicode character + "RUF003", # Ambiguous unicode character + "NPY002", # Replace legacy `np.random.rand` call with Generator + "B904", # Within an `except` clause, raise exceptions with from err or None ] [tool.ruff.lint.isort] From cbd8e0ceda4bad8132fee37af67ddf75854a6319 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Tue, 14 May 2024 11:35:59 +0200 Subject: [PATCH 27/43] Update CSS for documentation (#6228) --- .pre-commit-config.yaml | 1 + doc/_static/css/custom.css | 30 +++++++++++++++++++++--------- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c2d28bfbc4..f2b179339a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -53,6 +53,7 @@ repos: types_or: - markdown - yaml + - css - repo: https://github.com/hoxbro/taplo-pre-commit rev: v0.7.0 hooks: diff --git a/doc/_static/css/custom.css b/doc/_static/css/custom.css index f14b920b16..a99250e447 100644 --- a/doc/_static/css/custom.css +++ b/doc/_static/css/custom.css @@ -1,14 +1,26 @@ +:root[data-theme="light"], +:root[data-theme="dark"] { + --pst-color-inline-code: var(--holoviz-main-color); + --pst-color-link-hover: var(--holoviz-main-color); + --pst-color-link: var(--holoviz-main-color); + --pst-color-primary: var(--holoviz-main-color); + --pst-color-secondary-highlight: var(--holoviz-main-color); + --pst-color-secondary: var(--holoviz-main-color); + --sd-color-card-border-hover: var(--holoviz-main-color); +} + :root[data-theme="light"] { - --pst-color-primary: rgb(47,47,47); - --pst-color-link: rgb(163,26,38); - --pst-color-link-hover: rgb(254,203,56); - --pst-color-surface: rgb(250, 250, 250); + --holoviz-main-color: #a31a26; +} + +:root[data-theme="dark"] { + --holoviz-main-color: #f37323; } #binder-link { - display: inline-block; - font-size: 0.9rem; - padding-left: 1.5rem; - padding-top: 1rem; - padding-bottom: 1rem; + display: inline-block; + font-size: 0.9rem; + padding-left: 1.5rem; + padding-top: 1rem; + padding-bottom: 1rem; } From 6f042b33c236c680c7fffe99ac1182dc6fc22020 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Tue, 14 May 2024 13:42:34 +0200 Subject: [PATCH 28/43] Bump minimum version of param to 2.0 and add stream transform (#6230) --- holoviews/core/accessors.py | 11 ++++++----- holoviews/core/spaces.py | 8 ++++---- holoviews/core/util.py | 14 ++++---------- holoviews/streams.py | 30 +++++++++++++----------------- holoviews/util/__init__.py | 12 ++++++------ holoviews/util/transform.py | 16 +++++----------- pixi.toml | 2 +- pyproject.toml | 2 +- 8 files changed, 40 insertions(+), 55 deletions(-) diff --git a/holoviews/core/accessors.py b/holoviews/core/accessors.py index b5b2e15680..00ead6da61 100644 --- a/holoviews/core/accessors.py +++ b/holoviews/core/accessors.py @@ -2,7 +2,6 @@ Module for accessor objects for viewable HoloViews objects. """ import copy -import sys from functools import wraps from types import FunctionType @@ -133,6 +132,8 @@ def __call__(self, apply_function, streams=None, link_inputs=True, A new object where the function was applied to all contained (Nd)Overlay or Element objects. """ + from panel.widgets.base import Widget + from ..util import Dynamic from .data import Dataset from .dimension import ViewableElement @@ -168,10 +169,10 @@ def apply_function(object, **kwargs): 'method exists on the object.') return method(*args, **kwargs) - if 'panel' in sys.modules: - from panel.widgets.base import Widget - kwargs = {k: v.param.value if isinstance(v, Widget) else v - for k, v in kwargs.items()} + kwargs = { + k: v.param.value if isinstance(v, Widget) else v + for k, v in kwargs.items() + } spec = Element if per_element else ViewableElement applies = isinstance(self._obj, spec) diff --git a/holoviews/core/spaces.py b/holoviews/core/spaces.py index 8eddb146f0..2bce983830 100644 --- a/holoviews/core/spaces.py +++ b/holoviews/core/spaces.py @@ -458,8 +458,8 @@ class Callable(param.Parameterized): information see the DynamicMap tutorial at holoviews.org. """ - callable = param.Callable(default=None, constant=True, doc=""" - The callable function being wrapped.""", **util.disallow_refs) + callable = param.Callable(default=None, constant=True, allow_refs=False, doc=""" + The callable function being wrapped.""") inputs = param.List(default=[], constant=True, doc=""" The list of inputs the callable function is wrapping. Used @@ -546,7 +546,7 @@ def __call__(self, *args, **kwargs): # Nothing to do for callbacks that accept no arguments kwarg_hash = kwargs.pop('_memoization_hash_', ()) (self.args, self.kwargs) = (args, kwargs) - if util.param_version >= util.Version('2.0.0') and isinstance(self.callable, param.rx): + if isinstance(self.callable, param.rx): return self.callable.rx.value elif not args and not kwargs and not any(kwarg_hash): return self.callable() @@ -774,7 +774,7 @@ def __init__(self, callback, initial_items=None, streams=None, **params): streams = streams_list_from_dict(streams) # If callback is a parameterized method and watch is disabled add as stream - if util.param_version > util.Version('2.0.0rc1') and param.parameterized.resolve_ref(callback): + if param.parameterized.resolve_ref(callback): streams.append(callback) elif (params.get('watch', True) and (util.is_param_method(callback, has_deps=True) or (isinstance(callback, FunctionType) and hasattr(callback, '_dinfo')))): diff --git a/holoviews/core/util.py b/holoviews/core/util.py index 3f325ddc8d..c2ff670d50 100644 --- a/holoviews/core/util.py +++ b/holoviews/core/util.py @@ -42,8 +42,6 @@ anonymous_dimension_label = '_' -disallow_refs = {'allow_refs': False} if param_version > Version('2.0.0rc1') else {} - # Argspec was removed in Python 3.11 ArgSpec = namedtuple('ArgSpec', 'args varargs keywords defaults') @@ -1613,6 +1611,8 @@ def resolve_dependent_value(value): A new value where any parameter dependencies have been resolved. """ + from panel.widgets import RangeSlider + range_widget = False if isinstance(value, list): value = [resolve_dependent_value(v) for v in value] @@ -1629,14 +1629,8 @@ def resolve_dependent_value(value): resolve_dependent_value(value.step), ) - if 'panel' in sys.modules: - from panel.depends import param_value_if_widget - from panel.widgets import RangeSlider - range_widget = isinstance(value, RangeSlider) - if param_version > Version('2.0.0rc1'): - value = param.parameterized.resolve_value(value) - else: - value = param_value_if_widget(value) + range_widget = isinstance(value, RangeSlider) + value = param.parameterized.resolve_value(value) if is_param_method(value, has_deps=True): value = value() diff --git a/holoviews/streams.py b/holoviews/streams.py index e004aaaccc..c8c37656d3 100644 --- a/holoviews/streams.py +++ b/holoviews/streams.py @@ -4,7 +4,6 @@ server-side or in Javascript in the Jupyter notebook (client-side). """ -import sys import weakref from collections import defaultdict from contextlib import contextmanager @@ -16,7 +15,6 @@ import numpy as np import pandas as pd import param -from packaging.version import Version from .core import util from .core.ndmapping import UniformNdMapping @@ -49,12 +47,7 @@ def streams_list_from_dict(streams): "Converts a streams dictionary into a streams list" params = {} for k, v in streams.items(): - if 'panel' in sys.modules: - if util.param_version > util.Version('2.0.0rc1'): - v = param.parameterized.transform_reference(v) - else: - from panel.depends import param_value_if_widget - v = param_value_if_widget(v) + v = param.parameterized.transform_reference(v) if isinstance(v, param.Parameter) and v.owner is not None: params[k] = v else: @@ -225,10 +218,7 @@ def _process_streams(cls, streams): rename = {(p.owner, p.name): k for k, p in deps.get('kw', {}).items()} s = Params(parameters=dep_params, rename=rename) else: - if util.param_version > util.Version('2.0.0rc1'): - deps = param.parameterized.resolve_ref(s) - else: - deps = None + deps = param.parameterized.resolve_ref(s) if deps: s = Params(parameters=deps) else: @@ -689,16 +679,13 @@ class Params(Stream): parameterized = param.ClassSelector(class_=(param.Parameterized, param.parameterized.ParameterizedMetaclass), - constant=True, allow_None=True, doc=""" - Parameterized instance to watch for parameter changes.""", **util.disallow_refs) + constant=True, allow_None=True, allow_refs=False, doc=""" + Parameterized instance to watch for parameter changes.""") parameters = param.List(default=[], constant=True, doc=""" Parameters on the parameterized to watch.""") def __init__(self, parameterized=None, parameters=None, watch=True, watch_only=False, **params): - if util.param_version < Version('1.8.0') and watch: - raise RuntimeError('Params stream requires param version >= 1.8.0, ' - 'to support watching parameters.') if parameters is None: parameters = [parameterized.param[p] for p in parameterized.param if p != 'name'] else: @@ -1907,3 +1894,12 @@ def __init__(self, vertex_style=None, shared=True, **params): vertex_style = {} self.shared = shared super().__init__(vertex_style=vertex_style, **params) + + +def _streams_transform(obj): + if isinstance(obj, Pipe): + return obj.param.data + return obj + + +param.reactive.register_reference_transform(_streams_transform) diff --git a/holoviews/util/__init__.py b/holoviews/util/__init__.py index 9ea64c5a19..f99e3f7a38 100644 --- a/holoviews/util/__init__.py +++ b/holoviews/util/__init__.py @@ -890,8 +890,8 @@ class Dynamic(param.ParameterizedFunction): shared_data = param.Boolean(default=False, doc=""" Whether the cloned DynamicMap will share the same cache.""") - streams = param.ClassSelector(default=[], class_=(list, dict), doc=""" - List of streams to attach to the returned DynamicMap""", **util.disallow_refs) + streams = param.ClassSelector(default=[], class_=(list, dict), allow_refs=False, doc=""" + List of streams to attach to the returned DynamicMap""") def __call__(self, map_obj, **params): watch = params.pop('watch', True) @@ -921,6 +921,8 @@ def _get_streams(self, map_obj, watch=True): of supplied stream classes and instances are processed and added to the list. """ + from panel.widgets.base import Widget + if isinstance(self.p.streams, dict): streams = defaultdict(dict) stream_specs, params = [], {} @@ -961,10 +963,8 @@ def _get_streams(self, map_obj, watch=True): params = {} for k, v in self.p.kwargs.items(): - if 'panel' in sys.modules: - from panel.widgets.base import Widget - if isinstance(v, Widget): - v = v.param.value + if isinstance(v, Widget): + v = v.param.value if isinstance(v, param.Parameter) and isinstance(v.owner, param.Parameterized): params[k] = v streams += Params.from_params(params) diff --git a/holoviews/util/transform.py b/holoviews/util/transform.py index 63444f67a2..69965af1dd 100644 --- a/holoviews/util/transform.py +++ b/holoviews/util/transform.py @@ -1,5 +1,4 @@ import operator -import sys from types import BuiltinFunctionType, BuiltinMethodType, FunctionType, MethodType import numpy as np @@ -360,11 +359,7 @@ def register(cls, key, function): @property def params(self): - if 'panel' in sys.modules: - from panel.widgets.base import Widget - else: - Widget = None - + from panel.widgets.base import Widget params = {} for op in self.ops: op_args = list(op['args'])+list(op['kwargs'].values()) @@ -373,18 +368,17 @@ def params(self): op_args += [op['fn'].index] op_args = flatten(op_args) for op_arg in op_args: - if Widget and isinstance(op_arg, Widget): + if isinstance(op_arg, Widget): op_arg = op_arg.param.value if isinstance(op_arg, dim): params.update(op_arg.params) elif isinstance(op_arg, slice): (start, stop, step) = (op_arg.start, op_arg.stop, op_arg.step) - - if Widget and isinstance(start, Widget): + if isinstance(start, Widget): start = start.param.value - if Widget and isinstance(stop, Widget): + if isinstance(stop, Widget): stop = stop.param.value - if Widget and isinstance(step, Widget): + if isinstance(step, Widget): step = step.param.value if isinstance(start, param.Parameter): diff --git a/pixi.toml b/pixi.toml index 9df38ecb40..7e6a08d23f 100644 --- a/pixi.toml +++ b/pixi.toml @@ -26,7 +26,7 @@ numpy = ">=1.0" packaging = "*" pandas = ">=0.20.0" panel = ">=1.0" -param = ">=1.12.0,<3.0" +param = ">=2.0,<3.0" pip = "*" pyviz_comms = ">=2.1" # Recommended diff --git a/pyproject.toml b/pyproject.toml index d3874f838e..ed94c5e2d8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,7 +30,7 @@ classifiers = [ "Topic :: Software Development :: Libraries", ] dependencies = [ - "param >=1.12.0,<3.0", + "param >=2.0,<3.0", "numpy >=1.0", "pyviz_comms >=2.1", "panel >=1.0", From a566b7803710c1917582038af2ecbc51d689db80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Thu, 16 May 2024 15:09:33 +0200 Subject: [PATCH 29/43] General maintenance (#6235) --- .github/workflows/nightly_lock.yaml | 2 +- .pre-commit-config.yaml | 2 +- doc/index.md | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/nightly_lock.yaml b/.github/workflows/nightly_lock.yaml index efae867a46..c6fda817af 100644 --- a/.github/workflows/nightly_lock.yaml +++ b/.github/workflows/nightly_lock.yaml @@ -18,5 +18,5 @@ jobs: AWS_DEFAULT_REGION: "eu-west-1" PACKAGE: "holoviews" run: | - zip $(date +%Y-%m-%d).zip pixi.lock + zip $(date +%Y-%m-%d).zip pixi.lock pixi.toml aws s3 cp ./$(date +%Y-%m-%d).zip s3://assets.holoviz.org/lock/$PACKAGE/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f2b179339a..31b4e80a13 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,7 +20,7 @@ repos: - id: check-json - id: detect-private-key - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.4.3 + rev: v0.4.4 hooks: - id: ruff files: holoviews/|scripts/ diff --git a/doc/index.md b/doc/index.md index 16f412bd05..f4aa322172 100644 --- a/doc/index.md +++ b/doc/index.md @@ -1,11 +1,11 @@ -

+

**Stop plotting your data - annotate your data and let it visualize itself.**
-
- +
+
HoloViews is an [open-source](https://github.com/holoviz/holoviews/blob/main/LICENSE.txt) Python library designed to make data analysis and visualization seamless @@ -48,7 +48,7 @@ on [Holoviz Discord](https://discord.gg/AXRHnJU6sP). - +
From ee20d2357c567e3043b7255fa14c64a0f9e6f206 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Fri, 17 May 2024 13:10:04 +0200 Subject: [PATCH 30/43] Allow Bars to be plotted on continuous axes (#6145) --- examples/reference/elements/bokeh/Bars.ipynb | 38 ++++- .../reference/elements/matplotlib/Bars.ipynb | 37 ++++- examples/reference/elements/plotly/Bars.ipynb | 35 +++++ holoviews/plotting/bokeh/chart.py | 33 ++-- holoviews/plotting/bokeh/plot.py | 1 - holoviews/plotting/mixins.py | 8 +- holoviews/plotting/mpl/chart.py | 57 +++++-- holoviews/plotting/plotly/chart.py | 5 +- holoviews/plotting/plotly/element.py | 1 - .../tests/plotting/bokeh/test_barplot.py | 65 ++++++++ .../tests/plotting/matplotlib/test_barplot.py | 145 ++++++++++++++++++ .../tests/plotting/plotly/test_barplot.py | 68 +++++++- 12 files changed, 460 insertions(+), 33 deletions(-) create mode 100644 holoviews/tests/plotting/matplotlib/test_barplot.py diff --git a/examples/reference/elements/bokeh/Bars.ipynb b/examples/reference/elements/bokeh/Bars.ipynb index c0ff456f40..b07e94eb0b 100644 --- a/examples/reference/elements/bokeh/Bars.ipynb +++ b/examples/reference/elements/bokeh/Bars.ipynb @@ -59,7 +59,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "A ``Bars`` element can be sliced and selecting on like any other element:" + "A `Bars` element can be sliced and selected on like any other element:" ] }, { @@ -88,7 +88,41 @@ "\n", "# or using .redim.values(**{'Car Occupants': ['three', 'two', 'four', 'one', 'five', 'six']})\n", "\n", - "hv.Bars(data, occupants, 'Count') " + "hv.Bars(data, occupants, 'Count')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "`Bars` also supports continuous data and x-axis." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "data = pd.DataFrame({\"x\": [0, 1, 5], \"y\": [0, 2, 10]})\n", + "hv.Bars(data, [\"x\"], [\"y\"])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "And datetime data and x-axis." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "data = pd.DataFrame({\"x\": pd.date_range(\"2017-01-01\", \"2017-01-03\"), \"y\": [0, 2, -1]})\n", + "hv.Bars(data, [\"x\"], [\"y\"])" ] }, { diff --git a/examples/reference/elements/matplotlib/Bars.ipynb b/examples/reference/elements/matplotlib/Bars.ipynb index 0f4dcaba0d..1f4320583e 100644 --- a/examples/reference/elements/matplotlib/Bars.ipynb +++ b/examples/reference/elements/matplotlib/Bars.ipynb @@ -17,6 +17,7 @@ "metadata": {}, "outputs": [], "source": [ + "import pandas as pd\n", "import numpy as np\n", "import holoviews as hv\n", "hv.extension('matplotlib')" @@ -80,6 +81,40 @@ "hv.Bars(data, occupants, 'Count') " ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "`Bars` also supports continuous data and x-axis." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "data = pd.DataFrame({\"x\": [0, 1, 5], \"y\": [0, 2, 10]})\n", + "hv.Bars(data, [\"x\"], [\"y\"])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "And datetime data and x-axis." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "data = pd.DataFrame({\"x\": pd.date_range(\"2017-01-01\", \"2017-01-03\"), \"y\": [0, 2, -1]})\n", + "hv.Bars(data, [\"x\"], [\"y\"])" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -169,5 +204,5 @@ } }, "nbformat": 4, - "nbformat_minor": 2 + "nbformat_minor": 4 } diff --git a/examples/reference/elements/plotly/Bars.ipynb b/examples/reference/elements/plotly/Bars.ipynb index 48ca5d1f7b..5b9ef0dec3 100644 --- a/examples/reference/elements/plotly/Bars.ipynb +++ b/examples/reference/elements/plotly/Bars.ipynb @@ -17,6 +17,7 @@ "metadata": {}, "outputs": [], "source": [ + "import pandas as pd\n", "import numpy as np\n", "import holoviews as hv\n", "hv.extension('plotly')" @@ -80,6 +81,40 @@ "hv.Bars(data, occupants, 'Count')" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "`Bars` also support continuous data and x-axis." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "data = pd.DataFrame({\"x\": [0, 1, 5], \"y\": [0, 2, 10]})\n", + "hv.Bars(data, [\"x\"], [\"y\"])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "And datetime data and x-axis." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "data = pd.DataFrame({\"x\": pd.date_range(\"2017-01-01\", \"2017-01-03\"), \"y\": [0, 2, -1]})\n", + "hv.Bars(data, [\"x\"], [\"y\"])" + ] + }, { "cell_type": "markdown", "metadata": {}, diff --git a/holoviews/plotting/bokeh/chart.py b/holoviews/plotting/bokeh/chart.py index 40fbc7bf18..fdd7cd97a7 100644 --- a/holoviews/plotting/bokeh/chart.py +++ b/holoviews/plotting/bokeh/chart.py @@ -2,13 +2,13 @@ import numpy as np import param -from bokeh.models import CategoricalColorMapper, CustomJS, FactorRange, Range1d, Whisker +from bokeh.models import CategoricalColorMapper, CustomJS, Whisker from bokeh.models.tools import BoxSelectTool from bokeh.transform import jitter from ...core.data import Dataset from ...core.dimension import dimension_name -from ...core.util import dimension_sanitizer, isfinite +from ...core.util import dimension_sanitizer, isdatetime, isfinite from ...operation import interpolate_curve from ...util.transform import dim from ..mixins import AreaMixin, BarsMixin, SpikesMixin @@ -780,10 +780,6 @@ class BarPlot(BarsMixin, ColorbarPlot, LegendPlot): _nonvectorized_styles = base_properties + ['bar_width', 'cmap'] _plot_methods = dict(single=('vbar', 'hbar')) - # Declare that y-range should auto-range if not bounded - _x_range_type = FactorRange - _y_range_type = Range1d - def _axis_properties(self, axis, key, plot, dimension=None, ax_mapping=None): if ax_mapping is None: @@ -865,7 +861,7 @@ def _add_color_data(self, ds, ranges, style, cdim, data, mapping, factors, color for k, cd in cdata.items(): if isinstance(cmapper, CategoricalColorMapper) and cd.dtype.kind in 'uif': cd = categorize_array(cd, cdim) - if k not in data or len(data[k]) != next(len(data[key]) for key in data if key != k): + if k not in data or (len(data[k]) != next(len(data[key]) for key in data if key != k)): data[k].append(cd) else: data[k][-1] = cd @@ -889,6 +885,7 @@ def get_data(self, element, ranges, style): grouping = 'grouped' group_dim = element.get_dimension(1) + data = defaultdict(list) xdim = element.get_dimension(0) ydim = element.vdims[0] no_cidx = self.color_index is None @@ -906,25 +903,38 @@ def get_data(self, element, ranges, style): hover = 'hover' in self.handles # Group by stack or group dim if necessary + xdiff = None + xvals = element.dimension_values(xdim) if group_dim is None: grouped = {0: element} + is_dt = isdatetime(xvals) + if is_dt or xvals.dtype.kind not in 'OU': + xdiff = np.abs(np.diff(xvals)) + if len(np.unique(xdiff)) == 1 and xdiff[0] == 0: + xdiff = 1 + if is_dt: + width *= xdiff.astype('timedelta64[ms]').astype(np.int64) + else: + width /= xdiff + width = np.min(width) else: grouped = element.groupby(group_dim, group_type=Dataset, container_type=dict, datatype=['dataframe', 'dictionary']) + width = abs(width) y0, y1 = ranges.get(ydim.name, {'combined': (None, None)})['combined'] if self.logy: bottom = (ydim.range[0] or (0.01 if y1 > 0.01 else 10**(np.log10(y1)-2))) else: bottom = 0 + # Map attributes to data if grouping == 'stacked': mapping = {'x': xdim.name, 'top': 'top', 'bottom': 'bottom', 'width': width} elif grouping == 'grouped': - mapping = {'x': 'xoffsets', 'top': ydim.name, 'bottom': bottom, - 'width': width} + mapping = {'x': 'xoffsets', 'top': ydim.name, 'bottom': bottom, 'width': width} else: mapping = {'x': xdim.name, 'top': ydim.name, 'bottom': bottom, 'width': width} @@ -955,7 +965,6 @@ def get_data(self, element, ranges, style): factors, colors = None, None # Iterate over stacks and groups and accumulate data - data = defaultdict(list) baselines = defaultdict(lambda: {'positive': bottom, 'negative': 0}) for k, ds in grouped.items(): k = k[0] if isinstance(k, tuple) else k @@ -994,7 +1003,7 @@ def get_data(self, element, ranges, style): ds = ds.add_dimension(group_dim, ds.ndims, gval) data[group_dim.name].append(ds.dimension_values(group_dim)) else: - data[xdim.name].append(ds.dimension_values(xdim)) + data[xdim.name].append(xvals) data[ydim.name].append(ds.dimension_values(ydim)) if hover and grouping != 'stacked': @@ -1026,7 +1035,7 @@ def get_data(self, element, ranges, style): # Ensure x-values are categorical xname = dimension_sanitizer(xdim.name) - if xname in sanitized_data: + if xname in sanitized_data and isinstance(sanitized_data[xname], np.ndarray) and sanitized_data[xname].dtype.kind not in 'uifM' and not isdatetime(sanitized_data[xname]): sanitized_data[xname] = categorize_array(sanitized_data[xname], xdim) # If axes inverted change mapping to match hbar signature diff --git a/holoviews/plotting/bokeh/plot.py b/holoviews/plotting/bokeh/plot.py index 2f74b52359..77658f43db 100644 --- a/holoviews/plotting/bokeh/plot.py +++ b/holoviews/plotting/bokeh/plot.py @@ -154,7 +154,6 @@ def _postprocess_data(self, data): new_data = {} for k, values in data.items(): values = decode_bytes(values) # Bytes need decoding to strings - # Certain datetime types need to be converted if len(values) and isinstance(values[0], cftime_types): if any(v.calendar not in _STANDARD_CALENDARS for v in values): diff --git a/holoviews/plotting/mixins.py b/holoviews/plotting/mixins.py index 2c940b9543..db3d2d75aa 100644 --- a/holoviews/plotting/mixins.py +++ b/holoviews/plotting/mixins.py @@ -160,8 +160,9 @@ def get_extents(self, element, ranges, range_type='combined', **kwargs): s0 = min(s0, 0) if util.isfinite(s0) else 0 s1 = max(s1, 0) if util.isfinite(s1) else 0 ranges[vdim]['soft'] = (s0, s1) + l, b, r, t = super().get_extents(element, ranges, range_type, ydim=element.vdims[0]) if range_type not in ('combined', 'data'): - return super().get_extents(element, ranges, range_type, ydim=element.vdims[0]) + return l, b, r, t # Compute stack heights xdim = element.kdims[0] @@ -173,14 +174,15 @@ def get_extents(self, element, ranges, range_type='combined', **kwargs): else: y0, y1 = ranges[vdim]['combined'] + x0, x1 = (l, r) if util.isnumeric(l) and len(element.kdims) == 1 else ('', '') if range_type == 'data': - return ('', y0, '', y1) + return (x0, y0, x1, y1) padding = 0 if self.overlaid else self.padding _, ypad, _ = get_axis_padding(padding) y0, y1 = util.dimension_range(y0, y1, ranges[vdim]['hard'], ranges[vdim]['soft'], ypad, self.logy) y0, y1 = util.dimension_range(y0, y1, self.ylim, (None, None)) - return ('', y0, '', y1) + return (x0, y0, x1, y1) def _get_coords(self, element, ranges, as_string=True): """ diff --git a/holoviews/plotting/mpl/chart.py b/holoviews/plotting/mpl/chart.py index 5508200f14..05dba48efd 100644 --- a/holoviews/plotting/mpl/chart.py +++ b/holoviews/plotting/mpl/chart.py @@ -926,6 +926,8 @@ def _finalize_ticks(self, axis, element, xticks, yticks, zticks): def _create_bars(self, axis, element, ranges, style): # Get values dimensions, and style information (gdim, cdim, sdim), values = self._get_values(element, ranges) + + cats = None style_dim = None if sdim: cats = values['stack'] @@ -941,7 +943,23 @@ def _create_bars(self, axis, element, ranges, style): style_map = {None: {}} # Compute widths - width = (1-(2.*self.bar_padding)) / len(values.get('category', [None])) + xvals = element.dimension_values(0) + is_dt = isdatetime(xvals) + continuous = True + if is_dt or xvals.dtype.kind not in 'OU' and not (cdim or len(element.kdims) > 1): + xdiff_vals = date2num(xvals) if is_dt else xvals + xdiff = np.abs(np.diff(xdiff_vals)) + if len(np.unique(xdiff)) == 1: + # if all are same + xdiff = 1 + else: + xdiff = np.min(xdiff) + width = (1 - self.bar_padding) * xdiff + else: + xdiff = len(values.get('category', [None])) + width = (1 - self.bar_padding) / xdiff + continuous = False + if self.invert_axes: plot_fn = 'barh' x, y, w, bottom = 'y', 'width', 'height', 'left' @@ -952,6 +970,8 @@ def _create_bars(self, axis, element, ranges, style): # Iterate over group, category and stack dimension values # computing xticks and drawing bars and applying styles xticks, labels, bar_data = [], [], {} + categories = values.get('category', [None]) + num_categories = len(categories) for gidx, grp in enumerate(values.get('group', [None])): sel_key = {} label = None @@ -959,14 +979,21 @@ def _create_bars(self, axis, element, ranges, style): grp_label = gdim.pprint_value(grp) sel_key[gdim.name] = [grp] yalign = -0.04 if cdim and self.multi_level else 0 - xticks.append((gidx+0.5, grp_label, yalign)) - for cidx, cat in enumerate(values.get('category', [None])): - xpos = gidx+self.bar_padding+(cidx*width) + goffset = width * (num_categories / 2 - 0.5) + if num_categories > 1: + # mini offset needed or else combines with non-continuous + goffset += width / 1000 + + xpos = gidx+goffset if not continuous else xvals[gidx] + if not continuous: + xticks.append(((xpos), grp_label, yalign)) + for cidx, cat in enumerate(categories): + xpos = gidx+(cidx*width) if not continuous else xvals[gidx] if cat is not None: label = cdim.pprint_value(cat) sel_key[cdim.name] = [cat] - if self.multi_level: - xticks.append((xpos+width/2., label, 0)) + if self.multi_level and not continuous: + xticks.append((xpos, label, 0)) prev = 0 for stk in values.get('stack', [None]): if stk is not None: @@ -975,7 +1002,8 @@ def _create_bars(self, axis, element, ranges, style): el = element.select(**sel_key) vals = el.dimension_values(element.vdims[0].name) val = float(vals[0]) if len(vals) else np.nan - xval = xpos+width/2. + xval = xpos + if label in bar_data: group = bar_data[label] group[x].append(xval) @@ -1014,8 +1042,19 @@ def _create_bars(self, axis, element, ranges, style): legend_opts.update(**leg_spec) axis.legend(title=title, **legend_opts) - return bars, xticks, ax_dims - + x_range = ranges[gdim.name]["data"] + if continuous and not is_dt: + if style.get('align', 'center') == 'center': + left_multiplier = 0.5 + right_multiplier = 0.5 + else: + left_multiplier = 0 + right_multiplier = 1 + ranges[gdim.name]["data"] = ( + x_range[0] - width * left_multiplier, + x_range[1] + width * right_multiplier + ) + return bars, xticks if not continuous else None, ax_dims class SpikesPlot(SpikesMixin, PathPlot, ColorbarPlot): diff --git a/holoviews/plotting/plotly/chart.py b/holoviews/plotting/plotly/chart.py index dbf47863a9..b44b109ded 100644 --- a/holoviews/plotting/plotly/chart.py +++ b/holoviews/plotting/plotly/chart.py @@ -255,7 +255,7 @@ def get_data(self, element, ranges, style, **kwargs): values.append(sel.iloc[0, 1] if len(sel) else 0) bars.append({ 'orientation': orientation, 'showlegend': False, - x: [xdim.pprint_value(v) for v in xvals], + x: xvals, y: np.nan_to_num(values)}) elif stack_dim or not self.multi_level: group_dim = stack_dim or group_dim @@ -270,7 +270,7 @@ def get_data(self, element, ranges, style, **kwargs): values.append(sel.iloc[0, 1] if len(sel) else 0) bars.append({ 'orientation': orientation, 'name': group_dim.pprint_value(k), - x: [xdim.pprint_value(v) for v in xvals], + x: xvals, y: np.nan_to_num(values)}) else: values = element.dimension_values(vdim) @@ -279,6 +279,7 @@ def get_data(self, element, ranges, style, **kwargs): x: [[d.pprint_value(v) for v in element.dimension_values(d)] for d in (xdim, group_dim)], y: np.nan_to_num(values)}) + return bars def init_layout(self, key, element, ranges, **kwargs): diff --git a/holoviews/plotting/plotly/element.py b/holoviews/plotting/plotly/element.py index 564f67139f..2ffcd46361 100644 --- a/holoviews/plotting/plotly/element.py +++ b/holoviews/plotting/plotly/element.py @@ -269,7 +269,6 @@ def graph_options(self, element, ranges, style, is_geo=False, **kwargs): else: opts.update({STYLE_ALIASES.get(k, k): v for k, v in style.items() if k != 'cmap'}) - return opts def init_graph(self, datum, options, index=0, **kwargs): diff --git a/holoviews/tests/plotting/bokeh/test_barplot.py b/holoviews/tests/plotting/bokeh/test_barplot.py index 032618bc48..fd16dbd7bb 100644 --- a/holoviews/tests/plotting/bokeh/test_barplot.py +++ b/holoviews/tests/plotting/bokeh/test_barplot.py @@ -1,4 +1,5 @@ import numpy as np +import pandas as pd from bokeh.models import CategoricalColorMapper, LinearAxis, LinearColorMapper from holoviews.core.overlay import NdOverlay, Overlay @@ -276,3 +277,67 @@ def test_bars_color_index_color_clash(self): "mapping for 'color' option and declare a color_index; ignoring the color_index.\n" ) self.assertEqual(log_msg, warning) + + def test_bars_continuous_data_list_same_interval(self): + bars = Bars(([0, 1, 2], [10, 20, 30])) + plot = bokeh_renderer.get_plot(bars) + np.testing.assert_almost_equal(plot.handles["glyph"].width, 0.8) + + def test_bars_continuous_data_list_same_interval_custom_width(self): + bars = Bars(([0, 1, 2], [10, 20, 30])).opts(bar_width=0.5) + plot = bokeh_renderer.get_plot(bars) + assert plot.handles["glyph"].width == 0.5 + + def test_bars_continuous_data_list_diff_interval(self): + bars = Bars(([0, 3, 10], [10, 20, 30])) + plot = bokeh_renderer.get_plot(bars) + np.testing.assert_almost_equal(plot.handles["glyph"].width, 0.11428571) + + def test_bars_continuous_datetime(self): + bars = Bars((pd.date_range("1/1/2000", periods=10), np.random.rand(10))) + plot = bokeh_renderer.get_plot(bars) + np.testing.assert_almost_equal(plot.handles["glyph"].width, 69120000.0) + + def test_bars_not_continuous_data_list(self): + bars = Bars([("A", 1), ("B", 2), ("C", 3)]) + plot = bokeh_renderer.get_plot(bars) + assert plot.handles["glyph"].width == 0.8 + + def test_bars_not_continuous_data_list_custom_width(self): + bars = Bars([("A", 1), ("B", 2), ("C", 3)]).opts(bar_width=1) + plot = bokeh_renderer.get_plot(bars) + assert plot.handles["glyph"].width == 1 + + def test_bars_group(self): + samples = 100 + + pets = ["Cat", "Dog", "Hamster", "Rabbit"] + genders = ["Female", "Male", "N/A"] + + np.random.seed(100) + pets_sample = np.random.choice(pets, samples) + gender_sample = np.random.choice(genders, samples) + + bars = Bars( + (pets_sample, gender_sample, np.ones(samples)), ["Pets", "Gender"] + ).aggregate(function=np.sum) + plot = bokeh_renderer.get_plot(bars) + assert plot.handles["glyph"].width == 0.8 + + def test_bar_group_stacked(self): + samples = 100 + + pets = ["Cat", "Dog", "Hamster", "Rabbit"] + genders = ["Female", "Male", "N/A"] + + np.random.seed(100) + pets_sample = np.random.choice(pets, samples) + gender_sample = np.random.choice(genders, samples) + + bars = ( + Bars((pets_sample, gender_sample, np.ones(samples)), ["Pets", "Gender"]) + .aggregate(function=np.sum) + .opts(stacked=True) + ) + plot = bokeh_renderer.get_plot(bars) + assert plot.handles["glyph"].width == 0.8 diff --git a/holoviews/tests/plotting/matplotlib/test_barplot.py b/holoviews/tests/plotting/matplotlib/test_barplot.py new file mode 100644 index 0000000000..5e4f85cd1a --- /dev/null +++ b/holoviews/tests/plotting/matplotlib/test_barplot.py @@ -0,0 +1,145 @@ +import matplotlib.dates as mdates +import numpy as np +import pandas as pd +from matplotlib.text import Text + +from holoviews.element import Bars + +from ...utils import LoggingComparisonTestCase +from .test_plot import TestMPLPlot, mpl_renderer + + +class TestBarPlot(LoggingComparisonTestCase, TestMPLPlot): + + def test_bars_continuous_data_list_same_interval(self): + bars = Bars(([0, 1, 2], [10, 20, 30])) + plot = mpl_renderer.get_plot(bars) + ax = plot.handles["axis"] + np.testing.assert_almost_equal(ax.get_xlim(), (-0.4, 2.4)) + assert ax.patches[0].get_width() == 0.8 + + def test_bars_continuous_data_list_diff_interval(self): + bars = Bars(([0, 3, 10], [10, 20, 30])) + plot = mpl_renderer.get_plot(bars) + ax = plot.handles["axis"] + np.testing.assert_almost_equal(ax.get_xlim(), (-1.2, 11.2)) + np.testing.assert_almost_equal(ax.patches[0].get_width(), 2.4) + assert len(ax.get_xticks()) > 3 + + def test_bars_continuous_datetime(self): + bars = Bars((pd.date_range("1/1/2000", periods=10), np.random.rand(10))) + plot = mpl_renderer.get_plot(bars) + ax = plot.handles["axis"] + assert ax.get_xticklabels()[0].get_text() == "2000-01-01" + assert ax.get_xticklabels()[-1].get_text() == "2000-01-10" + assert ax.patches[0].get_width() == 0.8 + assert len(ax.get_xticks()) == 10 + + bars.opts(xformatter=mdates.DateFormatter("%d")) + plot = mpl_renderer.get_plot(bars) + ax = plot.handles["axis"] + assert ax.get_xticklabels()[0].get_text() == "01" + assert ax.get_xticklabels()[-1].get_text() == "10" + + def test_bars_not_continuous_data_list(self): + bars = Bars([("A", 1), ("B", 2), ("C", 3)]) + plot = mpl_renderer.get_plot(bars) + ax = plot.handles["axis"] + np.testing.assert_almost_equal(ax.get_xlim(), (-0.54, 2.54)) + assert ax.patches[0].get_width() == 0.8 + np.testing.assert_equal(ax.get_xticks(), [0, 1, 2]) + np.testing.assert_equal( + [xticklabel.get_text() for xticklabel in ax.get_xticklabels()], + ["A", "B", "C"], + ) + + def test_bars_group(self): + samples = 100 + + pets = ["Cat", "Dog", "Hamster", "Rabbit"] + genders = ["Female", "Male", "N/A"] + + np.random.seed(100) + pets_sample = np.random.choice(pets, samples) + gender_sample = np.random.choice(genders, samples) + + bars = Bars( + (pets_sample, gender_sample, np.ones(samples)), ["Pets", "Gender"] + ).aggregate(function=np.sum) + plot = mpl_renderer.get_plot(bars) + ax = plot.handles["axis"] + + np.testing.assert_almost_equal(ax.get_xlim(), (-0.3233333, 3.8566667)) + assert ax.patches[0].get_width() == 0.26666666666666666 + ticklabels = ax.get_xticklabels() + expected = [ + Text(0.0, 0, "Female"), + Text(0.26666666666666666, 0, "N/A"), + Text(0.26693333333333336, -0.04, "Cat"), + Text(0.5333333333333333, 0, "Male"), + Text(1.0, 0, "Female"), + Text(1.2666666666666666, 0, "N/A"), + Text(1.2669333333333332, -0.04, "Rabbit"), + Text(1.5333333333333332, 0, "Male"), + Text(2.0, 0, "Female"), + Text(2.2666666666666666, 0, "N/A"), + Text(2.2669333333333332, -0.04, "Hamster"), + Text(2.533333333333333, 0, "Male"), + Text(3.0, 0, "Female"), + Text(3.2666666666666666, 0, "N/A"), + Text(3.2669333333333332, -0.04, "Dog"), + Text(3.533333333333333, 0, "Male"), + ] + + for i, ticklabel in enumerate(ticklabels): + assert ticklabel.get_text() == expected[i].get_text() + assert ticklabel.get_position() == expected[i].get_position() + + def test_bar_group_stacked(self): + samples = 100 + + pets = ["Cat", "Dog", "Hamster", "Rabbit"] + genders = ["Female", "Male", "N/A"] + + np.random.seed(100) + pets_sample = np.random.choice(pets, samples) + gender_sample = np.random.choice(genders, samples) + + bars = ( + Bars((pets_sample, gender_sample, np.ones(samples)), ["Pets", "Gender"]) + .aggregate(function=np.sum) + .opts(stacked=True) + ) + plot = mpl_renderer.get_plot(bars) + ax = plot.handles["axis"] + + np.testing.assert_almost_equal(ax.get_xlim(), (-0.59, 3.59)) + assert ax.patches[0].get_width() == 0.8 + ticklabels = ax.get_xticklabels() + expected = [ + Text(0.0, 0, "Cat"), + Text(1.0, 0, "Rabbit"), + Text(2.0, 0, "Hamster"), + Text(3.0, 0, "Dog"), + ] + + for i, ticklabel in enumerate(ticklabels): + assert ticklabel.get_text() == expected[i].get_text() + assert ticklabel.get_position() == expected[i].get_position() + + def test_group_dim(self): + bars = Bars( + ([3, 10, 1] * 10, ["A", "B"] * 15, np.random.randn(30)), + ["Group", "Category"], + "Value", + ).aggregate(function=np.mean) + plot = mpl_renderer.get_plot(bars) + ax = plot.handles["axis"] + + np.testing.assert_almost_equal(ax.get_xlim(), (-0.34, 2.74)) + assert ax.patches[0].get_width() == 0.4 + assert len(ax.get_xticks()) > 3 + + xticklabels = ['A', '1', 'B', 'A', '3', 'B', 'A', '10', 'B'] + for i, tick in enumerate(ax.get_xticklabels()): + assert tick.get_text() == xticklabels[i] diff --git a/holoviews/tests/plotting/plotly/test_barplot.py b/holoviews/tests/plotting/plotly/test_barplot.py index 6c24744cfd..bf0a7ea717 100644 --- a/holoviews/tests/plotting/plotly/test_barplot.py +++ b/holoviews/tests/plotting/plotly/test_barplot.py @@ -1,4 +1,5 @@ import numpy as np +import pandas as pd from holoviews.element import Bars @@ -10,7 +11,7 @@ class TestBarsPlot(TestPlotlyPlot): def test_bars_plot(self): bars = Bars([3, 2, 1]) state = self._get_plot_state(bars) - self.assertEqual(state['data'][0]['x'], ['0', '1', '2']) + self.assertEqual(state['data'][0]['x'], [0, 1, 2]) self.assertEqual(state['data'][0]['y'], np.array([3, 2, 1])) self.assertEqual(state['data'][0]['type'], 'bar') self.assertEqual(state['layout']['xaxis']['range'], [None, None]) @@ -21,7 +22,7 @@ def test_bars_plot(self): def test_bars_plot_inverted(self): bars = Bars([3, 2, 1]).opts(invert_axes=True) state = self._get_plot_state(bars) - self.assertEqual(state['data'][0]['y'], ['0', '1', '2']) + self.assertEqual(state['data'][0]['y'], [0, 1, 2]) self.assertEqual(state['data'][0]['x'], np.array([3, 2, 1])) self.assertEqual(state['data'][0]['type'], 'bar') self.assertEqual(state['layout']['xaxis']['range'], [0, 3.2]) @@ -91,3 +92,66 @@ def test_visible(self): element = Bars([3, 2, 1]).opts(visible=False) state = self._get_plot_state(element) self.assertEqual(state['data'][0]['visible'], False) + + def test_bars_continuous_data_list_same_interval(self): + bars = Bars(([0, 1, 2], [10, 20, 30])) + plot = self._get_plot_state(bars) + np.testing.assert_equal(plot['data'][0]['x'], [0, 1, 2]) + np.testing.assert_equal(plot['data'][0]['y'], [10, 20, 30]) + + def test_bars_continuous_data_list_diff_interval(self): + bars = Bars(([0, 3, 10], [10, 20, 30])) + plot = self._get_plot_state(bars) + np.testing.assert_equal(plot['data'][0]['x'], [0, 3, 10]) + np.testing.assert_equal(plot['data'][0]['y'], [10, 20, 30]) + + def test_bars_continuous_datetime(self): + y = np.random.rand(10) + bars = Bars((pd.date_range("1/1/2000", periods=10), y)) + plot = self._get_plot_state(bars) + np.testing.assert_equal(plot['data'][0]['x'], pd.date_range("1/1/2000", periods=10).values.astype(float)) + np.testing.assert_equal(plot['data'][0]['y'], y) + + def test_bars_not_continuous_data_list(self): + bars = Bars([("A", 1), ("B", 2), ("C", 3)]) + plot = self._get_plot_state(bars) + np.testing.assert_equal(plot['data'][0]['x'], ["A", "B", "C"]) + np.testing.assert_equal(plot['data'][0]['y'], [1, 2, 3]) + + def test_bars_group(self): + samples = 100 + + pets = ["Cat", "Dog", "Hamster", "Rabbit"] + genders = ["Female", "Male", "N/A"] + + np.random.seed(100) + pets_sample = np.random.choice(pets, samples) + gender_sample = np.random.choice(genders, samples) + + bars = Bars( + (pets_sample, gender_sample, np.ones(samples)), ["Pets", "Gender"] + ).aggregate(function=np.sum) + plot = self._get_plot_state(bars) + np.testing.assert_equal(set(plot['data'][0]['x'][0]), set(pets)) + np.testing.assert_equal( + plot['data'][0]['y'], np.array([6., 10., 10., 10., 7., 10., 6., 10., 9., 7., 8., 7.]) + ) + + def test_bar_group_stacked(self): + samples = 100 + + pets = ["Cat", "Dog", "Hamster", "Rabbit"] + genders = ["Female", "Male", "N/A"] + + np.random.seed(100) + pets_sample = np.random.choice(pets, samples) + gender_sample = np.random.choice(genders, samples) + + bars = ( + Bars((pets_sample, gender_sample, np.ones(samples)), ["Pets", "Gender"]) + .aggregate(function=np.sum) + .opts(stacked=True) + ) + plot = self._get_plot_state(bars) + np.testing.assert_equal(set(plot['data'][0]['x']), set(pets)) + np.testing.assert_equal(plot['data'][0]['y'], np.array([8, 7, 6, 7])) From 4a526c09ba4705027b98bd602e1460b162e3938c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Fri, 17 May 2024 13:11:24 +0200 Subject: [PATCH 31/43] Add min_interval and max_interval to the RangeToolLink (#6134) --- examples/gallery/demos/bokeh/eeg_viewer.ipynb | 6 +-- holoviews/plotting/bokeh/links.py | 44 +++++++++++---- holoviews/plotting/links.py | 6 +++ holoviews/tests/conftest.py | 29 +++++----- holoviews/tests/ui/bokeh/test_links.py | 53 +++++++++++++++++++ 5 files changed, 109 insertions(+), 29 deletions(-) create mode 100644 holoviews/tests/ui/bokeh/test_links.py diff --git a/examples/gallery/demos/bokeh/eeg_viewer.ipynb b/examples/gallery/demos/bokeh/eeg_viewer.ipynb index 01f3cb98c1..52f9ed23f3 100644 --- a/examples/gallery/demos/bokeh/eeg_viewer.ipynb +++ b/examples/gallery/demos/bokeh/eeg_viewer.ipynb @@ -46,7 +46,6 @@ "metadata": {}, "outputs": [], "source": [ - "\n", "N_CHANNELS = 10\n", "N_SECONDS = 5\n", "SAMPLING_RATE = 200\n", @@ -141,7 +140,7 @@ "source": [ "## Building the dashboard\n", "\n", - "Finally, we use [`RangeToolLink`](../../../user_guide/Linking_Plots.ipynb) to connect the minimap `Image` and the EEG `Overlay`, setting bounds for the initial viewable area. Once the plots are linked and assembled into a unified dashboard, you can interact with it. Experiment by dragging the selection box on the minimap or resizing it by clicking and dragging its edges." + "Finally, we use [`RangeToolLink`](../../../user_guide/Linking_Plots.ipynb) to connect the minimap `Image` and the EEG `Overlay`, setting bounds for the initially viewable area with `boundsx` and `boundsy`, and finally a max range of 2 seconds with `intervalsx`. Once the plots are linked and assembled into a unified dashboard, you can interact with it. Experiment by dragging the selection box on the minimap or resizing it by clicking and dragging its edges." ] }, { @@ -153,7 +152,8 @@ "source": [ "RangeToolLink(\n", " minimap, eeg, axes=[\"x\", \"y\"],\n", - " boundsx=(None, 2), boundsy=(None, 6.5)\n", + " boundsx=(None, 2), boundsy=(None, 6.5),\n", + " intervalsx=(None, 2),\n", ")\n", "\n", "dashboard = (eeg + minimap).opts(merge_tools=False).cols(1)\n", diff --git a/holoviews/plotting/bokeh/links.py b/holoviews/plotting/bokeh/links.py index 1145b98ea9..9aaea35fc5 100644 --- a/holoviews/plotting/bokeh/links.py +++ b/holoviews/plotting/bokeh/links.py @@ -12,6 +12,7 @@ VertexTableLink, ) from ..plot import GenericElementPlot, GenericOverlayPlot +from .util import bokeh34 class LinkCallback: @@ -141,23 +142,46 @@ def __init__(self, root_model, link, source_plot, target_plot): continue axes[f'{axis}_range'] = target_plot.handles[f'{axis}_range'] - bounds = getattr(link, f'bounds{axis}', None) - if bounds is None: - continue + interval = getattr(link, f'intervals{axis}', None) + if interval is not None and bokeh34: + min, max = interval + if min is not None: + axes[f'{axis}_range'].min_interval = min + if max is not None: + axes[f'{axis}_range'].max_interval = max + self._set_range_for_interval(axes[f'{axis}_range'], max) - start, end = bounds - if start is not None: - axes[f'{axis}_range'].start = start - axes[f'{axis}_range'].reset_start = start - if end is not None: - axes[f'{axis}_range'].end = end - axes[f'{axis}_range'].reset_end = end + bounds = getattr(link, f'bounds{axis}', None) + if bounds is not None: + start, end = bounds + if start is not None: + axes[f'{axis}_range'].start = start + axes[f'{axis}_range'].reset_start = start + if end is not None: + axes[f'{axis}_range'].end = end + axes[f'{axis}_range'].reset_end = end tool = RangeTool(**axes) source_plot.state.add_tools(tool) if toolbars: toolbars[0].tools.append(tool) + def _set_range_for_interval(self, axis, max): + # Changes the existing Range1d axis range to be in the interval + for n in ("", "reset_"): + start = getattr(axis, f"{n}start") + try: + end = start + max + except Exception as e: + # Handle combinations of datetime axis and timedelta interval + # Likely a better way to do this + try: + import pandas as pd + end = (pd.array([start]) + pd.array([max]))[0] + except Exception: + raise e from None + setattr(axis, f"{n}end", end) + class DataLinkCallback(LinkCallback): """ diff --git a/holoviews/plotting/links.py b/holoviews/plotting/links.py index dbe4d6c000..0d528c4a2e 100644 --- a/holoviews/plotting/links.py +++ b/holoviews/plotting/links.py @@ -109,6 +109,12 @@ class RangeToolLink(Link): boundsy = param.Tuple(default=None, length=2, doc=""" (start, end) bounds for the y-axis""") + intervalsx = param.Tuple(default=None, length=2, doc=""" + (min, max) intervals for the x-axis""") + + intervalsy = param.Tuple(default=None, length=2, doc=""" + (min, max) intervals for the y-axis""") + _requires_target = True diff --git a/holoviews/tests/conftest.py b/holoviews/tests/conftest.py index 8b6219afe1..c0b202f587 100644 --- a/holoviews/tests/conftest.py +++ b/holoviews/tests/conftest.py @@ -74,33 +74,30 @@ def ibis_sqlite_backend(): ibis.set_backend(None) -@pytest.fixture -def bokeh_backend(): - hv.renderer("bokeh") +def _plotting_backend(backend): + pytest.importorskip(backend) + if not hv.extension._loaded: + hv.extension(backend) + hv.renderer(backend) prev_backend = hv.Store.current_backend - hv.Store.current_backend = "bokeh" + hv.Store.current_backend = backend yield hv.Store.current_backend = prev_backend +@pytest.fixture +def bokeh_backend(): + yield from _plotting_backend("bokeh") + + @pytest.fixture def mpl_backend(): - pytest.importorskip("matplotlib") - hv.renderer("matplotlib") - prev_backend = hv.Store.current_backend - hv.Store.current_backend = "matplotlib" - yield - hv.Store.current_backend = prev_backend + yield from _plotting_backend("matplotlib") @pytest.fixture def plotly_backend(): - pytest.importorskip("plotly") - hv.renderer("plotly") - prev_backend = hv.Store.current_backend - hv.Store.current_backend = "plotly" - yield - hv.Store.current_backend = prev_backend + yield from _plotting_backend("plotly") @pytest.fixture diff --git a/holoviews/tests/ui/bokeh/test_links.py b/holoviews/tests/ui/bokeh/test_links.py new file mode 100644 index 0000000000..2ed4431de0 --- /dev/null +++ b/holoviews/tests/ui/bokeh/test_links.py @@ -0,0 +1,53 @@ +from datetime import timedelta + +import numpy as np +import pandas as pd +import pytest +from bokeh.sampledata.stocks import AAPL + +import holoviews as hv +from holoviews.plotting.links import RangeToolLink + +from .. import expect + +pytestmark = pytest.mark.ui + + +@pytest.mark.usefixtures("bokeh_backend") +@pytest.mark.parametrize( + ["index", "intervalsx", "x_range_src", "x_range_tgt"], + [ + (range(len(AAPL["date"])), (100, 365), (0, 365), (0, 3269)), + ( + pd.to_datetime(AAPL["date"]), + (timedelta(days=100), timedelta(days=365)), + ( + np.array(["2000-03-01"], dtype="datetime64[ns]")[0], + pd.Timestamp("2001-03-01"), + ), + np.array(["2000-03-01", "2013-03-01"], dtype="datetime64[ns]"), + ), + ], + ids=["int", "datetime"], +) +def test_rangetool_link_interval(serve_hv, index, intervalsx, x_range_src, x_range_tgt): + df = pd.DataFrame(AAPL["close"], columns=["close"], index=index) + df.index.name = "Date" + + aapl_curve = hv.Curve(df, "Date", ("close", "Price ($)")) + tgt = aapl_curve.relabel("AAPL close price").opts(width=800, labelled=["y"]) + src = aapl_curve.opts(width=800, height=100, yaxis=None) + + RangeToolLink(src, tgt, axes=["x", "y"], intervalsx=intervalsx) + layout = (tgt + src).cols(1) + layout.opts(hv.opts.Layout(shared_axes=False)) + + page = serve_hv(layout) + hv_plot = page.locator(".bk-events") + expect(hv_plot).to_have_count(2) + + bk_model = hv.render(layout) + bk_src = bk_model.children[0][0] + np.testing.assert_equal((bk_src.x_range.start, bk_src.x_range.end), x_range_src) + bk_tgt = bk_model.children[1][0] + np.testing.assert_equal((bk_tgt.x_range.start, bk_tgt.x_range.end), x_range_tgt) From f001b12cbcec953b4f44a99387f5a8ffeb8237ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Fri, 17 May 2024 13:16:22 +0200 Subject: [PATCH 32/43] Fix violin plot in Plotly (#6237) --- holoviews/plotting/plotly/stats.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/holoviews/plotting/plotly/stats.py b/holoviews/plotting/plotly/stats.py index 2907d7653d..5414e9d70b 100644 --- a/holoviews/plotting/plotly/stats.py +++ b/holoviews/plotting/plotly/stats.py @@ -87,6 +87,8 @@ def get_data(self, element, ranges, style, **kwargs): axis = 'x' if self.invert_axes else 'y' for key, group in groups: if element.kdims: + if isinstance(key, str): + key = (key,) label = ','.join([d.pprint_value(v) for d, v in zip(element.kdims, key)]) else: label = key From 023ad7165c4a0b3c11f4cf3297ea5b2b1a9e28df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Fri, 17 May 2024 13:17:44 +0200 Subject: [PATCH 33/43] Don't error on TypeError in DaskInterface when sorting (#6221) --- holoviews/core/data/dask.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/holoviews/core/data/dask.py b/holoviews/core/data/dask.py index ebffe3519d..9c0476b067 100644 --- a/holoviews/core/data/dask.py +++ b/holoviews/core/data/dask.py @@ -82,8 +82,11 @@ def range(cls, dataset, dimension): dimension = dataset.get_dimension(dimension, strict=True) column = dataset.data[dimension.name] if column.dtype.kind == 'O': - column = np.sort(column[column.notnull()].compute()) - return (column[0], column[-1]) if len(column) else (None, None) + try: + column = np.sort(column[column.notnull()].compute()) + return (column[0], column[-1]) if len(column) else (None, None) + except TypeError: + return (None, None) else: if dimension.nodata is not None: column = cls.replace_value(column, dimension.nodata) From 802edf77c89449c9392b4e58a72909c611e5a81d Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Fri, 17 May 2024 13:52:19 +0200 Subject: [PATCH 34/43] Add support for rasterizing geopandas dataframes directly (#5958) --- holoviews/operation/datashader.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/holoviews/operation/datashader.py b/holoviews/operation/datashader.py index 869bb16933..870708fdf0 100644 --- a/holoviews/operation/datashader.py +++ b/holoviews/operation/datashader.py @@ -71,6 +71,7 @@ ds_version = Version(ds.__version__) ds15 = ds_version >= Version('0.15.1') +ds16 = ds_version >= Version('0.16.0') class AggregationOperation(ResampleOperation2D): @@ -1388,7 +1389,8 @@ def _process(self, element, key=None): if element._plot_id in self._precomputed: data, col = self._precomputed[element._plot_id] else: - if 'spatialpandas' not in element.interface.datatype: + if (('spatialpandas' not in element.interface.datatype) and + (not (ds16 and 'geodataframe' in element.interface.datatype))): element = element.clone(datatype=['spatialpandas']) data = element.data col = element.interface.geo_column(data) From 8b63dc2ef8314ef1a5b573d79e56d73d6cd24112 Mon Sep 17 00:00:00 2001 From: Douglas Raillard Date: Fri, 17 May 2024 13:04:22 +0100 Subject: [PATCH 35/43] Allow options for non-enabled backends (#6196) --- holoviews/core/dimension.py | 3 ++- holoviews/util/__init__.py | 16 ++++++++++------ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/holoviews/core/dimension.py b/holoviews/core/dimension.py index 99dd590562..3da9ac4898 100644 --- a/holoviews/core/dimension.py +++ b/holoviews/core/dimension.py @@ -1271,7 +1271,8 @@ def options(self, *args, clone=True, **kwargs): obj = self for backend, expanded in expanded_backends: - obj = obj.opts._dispatch_opts(expanded, backend=backend, clone=clone) + if expanded is not None: + obj = obj.opts._dispatch_opts(expanded, backend=backend, clone=clone) return obj def _repr_mimebundle_(self, include=None, exclude=None): diff --git a/holoviews/util/__init__.py b/holoviews/util/__init__.py index f99e3f7a38..30cadf8b5f 100644 --- a/holoviews/util/__init__.py +++ b/holoviews/util/__init__.py @@ -282,7 +282,9 @@ def defaults(cls, *options, **kwargs): if kwargs and len(kwargs) != 1 and next(iter(kwargs.keys())) != 'backend': raise Exception('opts.defaults only accepts "backend" keyword argument') - cls._linemagic(cls._expand_options(merge_options_to_dict(options)), backend=kwargs.get('backend')) + expanded = cls._expand_options(merge_options_to_dict(options)) + expanded = expanded or {} + cls._linemagic(expanded, backend=kwargs.get('backend')) @classmethod def _expand_by_backend(cls, options, backend): @@ -315,7 +317,7 @@ def _expand_options(cls, options, backend=None): """ Validates and expands a dictionaries of options indexed by type[.group][.label] keys into separate style, plot, norm and - output options. + output options. If the backend is not loaded, ``None`` is returned. opts._expand_options({'Image': dict(cmap='viridis', show_title=False)}) @@ -339,8 +341,9 @@ def _expand_options(cls, options, backend=None): try: backend_options = Store.options(backend=backend or current_backend) - except KeyError as e: - raise Exception(f'The {e} backend is not loaded. Please load the backend using hv.extension.') from None + except KeyError: + return None + expanded = {} if isinstance(options, list): options = merge_options_to_dict(options) @@ -451,8 +454,9 @@ def builder(cls, spec=None, **kws): backend = kws.get('backend', None) keys = set(kws.keys()) if backend: - allowed_kws = cls._element_keywords(backend, - elements=[element])[element] + keywords = cls._element_keywords(backend, + elements=[element]) + allowed_kws = keywords.get(element, keys) invalid = keys - set(allowed_kws) else: mismatched = {} From 850c521063a11dc208f9f0a1cc4237064f9b6649 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Fri, 17 May 2024 14:46:16 +0200 Subject: [PATCH 36/43] Support ImageStack in dynspread (#6024) --- holoviews/operation/datashader.py | 2 ++ holoviews/tests/operation/test_datashader.py | 8 ++++++++ 2 files changed, 10 insertions(+) diff --git a/holoviews/operation/datashader.py b/holoviews/operation/datashader.py index 870708fdf0..66013cd98f 100644 --- a/holoviews/operation/datashader.py +++ b/holoviews/operation/datashader.py @@ -1644,6 +1644,8 @@ def _process(self, element, key=None): if isinstance(element, RGB): rgb = element.rgb data = self._preprocess_rgb(rgb) + elif isinstance(element, ImageStack): + data = element.data elif isinstance(element, Image): data = element.clone(datatype=['xarray']).data[element.vdims[0].name] else: diff --git a/holoviews/tests/operation/test_datashader.py b/holoviews/tests/operation/test_datashader.py index 1573e35499..5cf1dceae0 100644 --- a/holoviews/tests/operation/test_datashader.py +++ b/holoviews/tests/operation/test_datashader.py @@ -48,6 +48,7 @@ datashade, directly_connect_edges, ds_version, + dynspread, inspect, inspect_points, inspect_polygons, @@ -1563,3 +1564,10 @@ def test_imagestack_datashade_count_cat(): df = pd.DataFrame({"x": range(3), "y": range(3), "c": range(3)}) op = datashade(Points(df), aggregator=ds.count_cat("c")) render(op) # should not error out + + +def test_imagestack_dynspread(): + df = pd.DataFrame({'x':[-16.8, 7.3], 'y': [-0.42, 13.6], 'language':['Marathi', 'Luganda']}) + points = Points(df, ['x','y'], ['language']) + op = dynspread(rasterize(points, aggregator=ds.by('language', ds.count()))) + render(op) # should not error out From 945e1798fa7c7b97a1135e8db78eac3b994541c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Fri, 17 May 2024 16:00:08 +0200 Subject: [PATCH 37/43] Remove sampledata from UI test --- holoviews/tests/ui/bokeh/test_links.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/holoviews/tests/ui/bokeh/test_links.py b/holoviews/tests/ui/bokeh/test_links.py index 2ed4431de0..bb6eeda403 100644 --- a/holoviews/tests/ui/bokeh/test_links.py +++ b/holoviews/tests/ui/bokeh/test_links.py @@ -3,7 +3,6 @@ import numpy as np import pandas as pd import pytest -from bokeh.sampledata.stocks import AAPL import holoviews as hv from holoviews.plotting.links import RangeToolLink @@ -17,21 +16,26 @@ @pytest.mark.parametrize( ["index", "intervalsx", "x_range_src", "x_range_tgt"], [ - (range(len(AAPL["date"])), (100, 365), (0, 365), (0, 3269)), ( - pd.to_datetime(AAPL["date"]), + range(3000), + (100, 365), + (0, 365), + (0, 3000 - 1), + ), + ( + pd.date_range("2000-03-01", periods=3000), (timedelta(days=100), timedelta(days=365)), ( np.array(["2000-03-01"], dtype="datetime64[ns]")[0], pd.Timestamp("2001-03-01"), ), - np.array(["2000-03-01", "2013-03-01"], dtype="datetime64[ns]"), + np.array(["2000-03-01", "2008-05-17"], dtype="datetime64[ns]"), ), ], ids=["int", "datetime"], ) def test_rangetool_link_interval(serve_hv, index, intervalsx, x_range_src, x_range_tgt): - df = pd.DataFrame(AAPL["close"], columns=["close"], index=index) + df = pd.DataFrame(range(3000), columns=["close"], index=index) df.index.name = "Date" aapl_curve = hv.Curve(df, "Date", ("close", "Price ($)")) From 46ffe7c995acd029e1e138f98d7ce3f35bf0d96d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Sun, 19 May 2024 10:52:02 +0200 Subject: [PATCH 38/43] No nighly lock for forks (#6239) --- .github/workflows/nightly_lock.yaml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/nightly_lock.yaml b/.github/workflows/nightly_lock.yaml index c6fda817af..7731dceb06 100644 --- a/.github/workflows/nightly_lock.yaml +++ b/.github/workflows/nightly_lock.yaml @@ -4,6 +4,9 @@ on: schedule: - cron: "0 0 * * *" +env: + PACKAGE: "holoviews" + jobs: pixi_lock: name: Pixi lock @@ -12,11 +15,11 @@ jobs: steps: - uses: holoviz-dev/holoviz_tasks/pixi_lock@pixi - name: Upload lock-file to S3 + if: github.repository == 'holoviz/${{ env.PACKAGE }}' env: AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} AWS_DEFAULT_REGION: "eu-west-1" - PACKAGE: "holoviews" run: | zip $(date +%Y-%m-%d).zip pixi.lock pixi.toml aws s3 cp ./$(date +%Y-%m-%d).zip s3://assets.holoviz.org/lock/$PACKAGE/ From 2ba3d78f6c30340a607c3e63def5dfadc2c02f58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Mon, 20 May 2024 10:11:48 +0200 Subject: [PATCH 39/43] No nighly uploads from forks --- .github/workflows/nightly_lock.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/nightly_lock.yaml b/.github/workflows/nightly_lock.yaml index 7731dceb06..9b93b85a21 100644 --- a/.github/workflows/nightly_lock.yaml +++ b/.github/workflows/nightly_lock.yaml @@ -15,7 +15,7 @@ jobs: steps: - uses: holoviz-dev/holoviz_tasks/pixi_lock@pixi - name: Upload lock-file to S3 - if: github.repository == 'holoviz/${{ env.PACKAGE }}' + if: "!github.event.pull_request.head.repo.fork" env: AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} From 732876b22e2b25aab6346dd13216930884d3acfa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Thu, 23 May 2024 12:00:16 +0200 Subject: [PATCH 40/43] Fix links (#6246) --- examples/README.md | 1 - examples/user_guide/Deploying_Bokeh_Apps.ipynb | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/examples/README.md b/examples/README.md index c8a747e9d0..7e958bc6b5 100644 --- a/examples/README.md +++ b/examples/README.md @@ -9,7 +9,6 @@ This directory contains all the notebooks built as part of the - `gallery`: Examples shown on the [gallery page](https://holoviews.org/gallery/index.html). - `getting_started`: Notebooks used in the [getting started](https://holoviews.org/getting_started/index.html) guide. - `reference`: Notebooks shown in the website [reference gallery](https://holoviews.org/reference/index.html) -- `topics`: Notebooks shown in the [showcase](https://holoviews.org/reference/showcase/index.html) - `user_guide`: Notebooks used in the [user guide](https://holoviews.org/user_guide/index.html). ## Contributing to examples diff --git a/examples/user_guide/Deploying_Bokeh_Apps.ipynb b/examples/user_guide/Deploying_Bokeh_Apps.ipynb index 139df45923..6b8bdfd6cb 100644 --- a/examples/user_guide/Deploying_Bokeh_Apps.ipynb +++ b/examples/user_guide/Deploying_Bokeh_Apps.ipynb @@ -789,7 +789,7 @@ "metadata": {}, "source": [ "If instead we want to deploy this we could add `.servable` as discussed before or use `pn.serve`. Note however that when using `pn.serve` all sessions will share the same state therefore it is best to \n", - "wrap the creation of the app in a function which we can then provide to `pn.serve`. For more detail on deploying Panel applications also see the [Panel server deployment guide](https://panel.holoviz.org/user_guide/Server_Deployment.html).\n", + "wrap the creation of the app in a function which we can then provide to `pn.serve`. For more detail on deploying Panel applications also see the [Panel server deployment guide](https://panel.holoviz.org/how_to/server/index.html).\n", "\n", "Now we can reimplement the same example using Bokeh allowing us to compare and contrast the approaches:" ] From 6b0121d5a3685989fca58a1687961523a5fd575c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Thu, 23 May 2024 17:03:34 +0200 Subject: [PATCH 41/43] Numpy 2.0 compability (#6238) --- .github/workflows/test.yaml | 10 ++++-- examples/conftest.py | 17 ++++++++++ holoviews/core/sheetcoords.py | 2 +- holoviews/core/util.py | 3 ++ holoviews/tests/core/test_dimensions.py | 6 +++- .../tests/element/test_comparisondimension.py | 7 +++- holoviews/tests/test_streams.py | 13 ++++--- pixi.toml | 34 +++++++++++++++++++ pyproject.toml | 2 +- 9 files changed, 83 insertions(+), 11 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index b16390f645..1a8340ad1d 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -85,7 +85,10 @@ jobs: run: | MATRIX=$(jq -nsc '{ "os": ["ubuntu-latest", "macos-latest", "windows-latest"], - "environment": ["test-39", "test-312"] + "environment": ["test-39", "test-312"], + "include": [ + { "os": "ubuntu-latest", "environment": "test-numpy" } + ] }') echo "MATRIX=$MATRIX" >> $GITHUB_ENV - name: Set test matrix with 'full' option @@ -93,7 +96,10 @@ jobs: run: | MATRIX=$(jq -nsc '{ "os": ["ubuntu-latest", "macos-latest", "windows-latest"], - "environment": ["test-39", "test-310", "test-311", "test-312"] + "environment": ["test-39", "test-310", "test-311", "test-312"], + "include": [ + { "os": "ubuntu-latest", "environment": "test-numpy" } + ] }') echo "MATRIX=$MATRIX" >> $GITHUB_ENV - name: Set test matrix with 'downstream' option diff --git a/examples/conftest.py b/examples/conftest.py index 22dced7136..588cabb548 100644 --- a/examples/conftest.py +++ b/examples/conftest.py @@ -1,6 +1,7 @@ import os import platform import sys +from importlib.util import find_spec import bokeh import pandas as pd @@ -62,6 +63,22 @@ "user_guide/Plotting_with_Matplotlib.ipynb", ] +# 2024-05: Numpy 2.0 +if find_spec("datashader") is None: + collect_ignore_glob += [ + "reference/elements/matplotlib/ImageStack.ipynb", + "reference/elements/plotly/ImageStack.ipynb", + "user_guide/15-Large_Data.ipynb", + "user_guide/16-Streaming_Data.ipynb", + "user_guide/Linked_Brushing.ipynb", + "user_guide/Network_Graphs.ipynb", + ] + +if find_spec("scikit-image"): + collect_ignore_glob += [ + "user_guide/Network_Graphs.ipynb", + ] + def pytest_runtest_makereport(item, call): """ diff --git a/holoviews/core/sheetcoords.py b/holoviews/core/sheetcoords.py index a6e8d163cc..690a55d709 100644 --- a/holoviews/core/sheetcoords.py +++ b/holoviews/core/sheetcoords.py @@ -365,7 +365,7 @@ def __new__(cls, bounds, sheet_coordinate_system, force_odd=False, else: slicespec=Slice._boundsspec2slicespec(bounds.lbrt(),sheet_coordinate_system) # Using numpy.int32 for legacy reasons - a = np.array(slicespec, dtype=np.int32, copy=False).view(cls) + a = np.asarray(slicespec, dtype=np.int32).view(cls) return a diff --git a/holoviews/core/util.py b/holoviews/core/util.py index c2ff670d50..2e2732bfb0 100644 --- a/holoviews/core/util.py +++ b/holoviews/core/util.py @@ -50,6 +50,9 @@ _PANDAS_ROWS_LARGE = 1_000_000 _PANDAS_SAMPLE_SIZE = 1_000_000 +numpy_version = Version(Version(np.__version__).base_version) +NUMPY_GE_200 = numpy_version >= Version("2") + pandas_version = Version(pd.__version__) try: if pandas_version >= Version('1.3.0'): diff --git a/holoviews/tests/core/test_dimensions.py b/holoviews/tests/core/test_dimensions.py index 06f77066b9..7b79e1bcea 100644 --- a/holoviews/tests/core/test_dimensions.py +++ b/holoviews/tests/core/test_dimensions.py @@ -5,6 +5,7 @@ import pandas as pd from holoviews.core import Dimension, Dimensioned +from holoviews.core.util import NUMPY_GE_200 from holoviews.element.comparison import ComparisonTestCase from ..utils import LoggingComparisonTestCase @@ -243,7 +244,10 @@ def test_tuple_clone(self): class DimensionDefaultTest(ComparisonTestCase): def test_validate_default_against_values(self): - msg = r"Dimension\('A'\) default 1\.1 not found in declared values: \[0, 1\]" + if NUMPY_GE_200: + msg = r"Dimension\('A'\) default 1\.1 not found in declared values: \[np\.int64\(0\), np\.int64\(1\)\]" + else: + msg = r"Dimension\('A'\) default 1\.1 not found in declared values: \[0, 1\]" with self.assertRaisesRegex(ValueError, msg): Dimension('A', values=[0, 1], default=1.1) diff --git a/holoviews/tests/element/test_comparisondimension.py b/holoviews/tests/element/test_comparisondimension.py index 1234ce27c8..3bdfb04c4e 100644 --- a/holoviews/tests/element/test_comparisondimension.py +++ b/holoviews/tests/element/test_comparisondimension.py @@ -2,6 +2,7 @@ Test cases for Dimension and Dimensioned object comparison. """ from holoviews.core import Dimension, Dimensioned +from holoviews.core.util import NUMPY_GE_200 from holoviews.element.comparison import ComparisonTestCase @@ -74,7 +75,11 @@ def test_dimension_comparison_values_unequal(self): try: self.assertEqual(self.dimension4, self.dimension8) except AssertionError as e: - self.assertEqual(str(e), "Dimension parameter 'values' mismatched: [] != ['a', 'b']") + if NUMPY_GE_200: + msg = "Dimension parameter 'values' mismatched: [] != [np.str_('a'), np.str_('b')]" + else: + msg = "Dimension parameter 'values' mismatched: [] != ['a', 'b']" + self.assertEqual(str(e), msg) def test_dimension_comparison_types_unequal(self): try: diff --git a/holoviews/tests/test_streams.py b/holoviews/tests/test_streams.py index 17cb93bc30..2bc957f679 100644 --- a/holoviews/tests/test_streams.py +++ b/holoviews/tests/test_streams.py @@ -11,7 +11,7 @@ import holoviews as hv from holoviews.core.spaces import DynamicMap -from holoviews.core.util import Version +from holoviews.core.util import NUMPY_GE_200, Version from holoviews.element import Curve, Histogram, Points, Polygons, Scatter from holoviews.element.comparison import ComparisonTestCase from holoviews.streams import * # noqa (Test all available streams) @@ -1421,6 +1421,7 @@ def test_selection_expr_stream_hist_invert_xaxis_yaxis(self): def test_selection_expr_stream_polygon_index_cols(self): + # TODO: Should test both spatialpandas and shapely # Create SelectionExpr on element try: import shapely # noqa except ImportError: @@ -1444,10 +1445,12 @@ def test_selection_expr_stream_polygon_index_cols(self): self.assertIsNone(expr_stream.bbox) self.assertIsNone(expr_stream.selection_expr) + fmt = lambda x: list(map(np.str_, x)) if NUMPY_GE_200 else x + expr_stream.input_streams[2].event(index=[0, 1]) self.assertEqual( repr(expr_stream.selection_expr), - repr(dim('cat').isin(['a', 'b'])) + repr(dim('cat').isin(fmt(['a', 'b']))) ) self.assertEqual(expr_stream.bbox, None) self.assertEqual(len(events), 1) @@ -1456,7 +1459,7 @@ def test_selection_expr_stream_polygon_index_cols(self): expr_stream.input_streams[0].event(bounds=(0, 0, 4, 1)) self.assertEqual( repr(expr_stream.selection_expr), - repr(dim('cat').isin(['a', 'b'])) + repr(dim('cat').isin(fmt(['a', 'b']))) ) self.assertEqual(len(events), 1) @@ -1464,7 +1467,7 @@ def test_selection_expr_stream_polygon_index_cols(self): expr_stream.input_streams[1].event(geometry=np.array([(0, 0), (4, 0), (4, 2), (0, 2)])) self.assertEqual( repr(expr_stream.selection_expr), - repr(dim('cat').isin(['a', 'b', 'c'])) + repr(dim('cat').isin(fmt(['a', 'b', 'c']))) ) self.assertEqual(len(events), 2) @@ -1472,7 +1475,7 @@ def test_selection_expr_stream_polygon_index_cols(self): expr_stream.input_streams[2].event(index=[1, 2]) self.assertEqual( repr(expr_stream.selection_expr), - repr(dim('cat').isin(['b', 'c'])) + repr(dim('cat').isin(fmt(['b', 'c']))) ) self.assertEqual(expr_stream.bbox, None) self.assertEqual(len(events), 3) diff --git a/pixi.toml b/pixi.toml index 7e6a08d23f..bf8e0bd0c3 100644 --- a/pixi.toml +++ b/pixi.toml @@ -15,6 +15,7 @@ test-311 = ["py311", "test-core", "test", "example", "test-example", "test-unit- test-312 = ["py312", "test-core", "test", "example", "test-example", "test-unit-task"] test-ui = ["py312", "test-core", "test", "test-ui"] test-core = ["py312", "test-core", "test-unit-task"] +test-numpy = ["py312", "test-core", "test-unit-task", "numpy2", "test-example"] test-gpu = ["py311", "test-core", "test", "test-gpu"] docs = ["py311", "example", "doc"] build = ["py311", "build"] @@ -134,6 +135,39 @@ rmm = { version = "*", channel = "rapidsai" } [feature.test-gpu.tasks] test-gpu = { cmd = "pytest holoviews/tests --gpu", env = { NUMBA_CUDA_LOW_OCCUPANCY_WARNINGS = '0' } } +[feature.numpy2] +channels = ["pyviz/label/dev", "conda-forge/label/numpy_rc", "numba/label/dev", "conda-forge"] + +[feature.numpy2.dependencies] +numpy = "2.*" +numba = { version = "0.60.*", channel = "numba/label/dev" } +llvmlite = { version = "*", channel = "numba/label/dev" } + +# test dependencies +cftime = "*" +contourpy = "*" +dask-core = "*" +datashader = ">=0.11.1" +ffmpeg = "*" +ibis-sqlite = "*" +nbconvert = "*" +networkx = "*" +pillow = "*" +scipy = ">=1.10" # Python 3.9 + Windows downloads 1.9 +selenium = "*" +shapely = "*" +# spatialpandas = "*" +xarray = ">=0.10.4" +xyzservices = "*" + +# Examples (removed duplicates) +# netcdf4 = "*" +notebook = "*" +pooch = "*" +# pyarrow = "*" +# scikit-image = "*" +streamz = ">=0.5.0" + # ============================================= # =================== DOCS ==================== # ============================================= diff --git a/pyproject.toml b/pyproject.toml index ed94c5e2d8..fb9b916db0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -90,7 +90,7 @@ filterwarnings = [ # 2023-01: Sqlalchemy 2.0 warning: "ignore: Deprecated API features detected:DeprecationWarning:ibis.backends.base.sql.alchemy", # https://github.com/ibis-project/ibis/issues/5048 # 2023-03: Already handling the nested sequence - "ignore:Creating an ndarray from ragged nested sequences:numpy.VisibleDeprecationWarning:holoviews.core.data.spatialpandas", + "ignore:Creating an ndarray from ragged nested sequences::holoviews.core.data.spatialpandas", # 2023-09: Dash needs to update their code to use the comm module and pkg_resources "ignore:The `.+?` class has been deprecated:DeprecationWarning:dash._jupyter", "ignore:pkg_resources is deprecated as an API:DeprecationWarning:dash.dash", From 142fa1ac812c0fefaaf0377f2166f58f7bbeb009 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Thu, 23 May 2024 18:25:00 +0200 Subject: [PATCH 42/43] Fix conftest.py --- examples/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/conftest.py b/examples/conftest.py index 588cabb548..a6ba9f708d 100644 --- a/examples/conftest.py +++ b/examples/conftest.py @@ -74,7 +74,7 @@ "user_guide/Network_Graphs.ipynb", ] -if find_spec("scikit-image"): +if find_spec("scikit-image") is None: collect_ignore_glob += [ "user_guide/Network_Graphs.ipynb", ] From d458f604ecf07bd264bef0dab788c742d30e88c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Mon, 27 May 2024 10:22:46 +0200 Subject: [PATCH 43/43] Update pre-commit (#6249) --- .pre-commit-config.yaml | 4 ++-- examples/reference/elements/bokeh/HexTiles.ipynb | 2 +- examples/reference/elements/matplotlib/HexTiles.ipynb | 2 +- pyproject.toml | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 31b4e80a13..9bb53aa002 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,7 +20,7 @@ repos: - id: check-json - id: detect-private-key - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.4.4 + rev: v0.4.5 hooks: - id: ruff files: holoviews/|scripts/ @@ -30,7 +30,7 @@ repos: - id: clean-notebook args: [--strip-trailing-newlines] - repo: https://github.com/codespell-project/codespell - rev: v2.2.6 + rev: v2.3.0 hooks: - id: codespell additional_dependencies: diff --git a/examples/reference/elements/bokeh/HexTiles.ipynb b/examples/reference/elements/bokeh/HexTiles.ipynb index 749db1569c..de78a1f1af 100644 --- a/examples/reference/elements/bokeh/HexTiles.ipynb +++ b/examples/reference/elements/bokeh/HexTiles.ipynb @@ -28,7 +28,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "``HexTiles`` provide a representation of the data as a bivariate histogram useful for visualizing the structure in larger datasets. The ``HexTiles`` are computed by tesselating the x-, y-ranges of the data, storing the number of points falling in each hexagonal bin. By default ``HexTiles`` simply counts the data in each bin but it also supports weighted aggregations. Currently only linearly spaced bins are supported when using the bokeh backend.\n", + "``HexTiles`` provide a representation of the data as a bivariate histogram useful for visualizing the structure in larger datasets. The ``HexTiles`` are computed by tessellating the x-, y-ranges of the data, storing the number of points falling in each hexagonal bin. By default ``HexTiles`` simply counts the data in each bin but it also supports weighted aggregations. Currently only linearly spaced bins are supported when using the bokeh backend.\n", "\n", "As a simple example we will generate 100,000 normally distributed points and plot them using ``HexTiles``:" ] diff --git a/examples/reference/elements/matplotlib/HexTiles.ipynb b/examples/reference/elements/matplotlib/HexTiles.ipynb index 98a82a92d3..49117ee729 100644 --- a/examples/reference/elements/matplotlib/HexTiles.ipynb +++ b/examples/reference/elements/matplotlib/HexTiles.ipynb @@ -29,7 +29,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "``HexTiles`` provide a representation of the data as a bivariate histogram useful for visualizing the structure in larger datasets. The ``HexTiles`` are computed by tesselating the x-, y-ranges of the data, storing the number of points falling in each hexagonal bin. By default ``HexTiles`` simply counts the data in each bin but it also supports weighted aggregations. Currently only linearly spaced bins are supported when using the bokeh backend.\n", + "``HexTiles`` provide a representation of the data as a bivariate histogram useful for visualizing the structure in larger datasets. The ``HexTiles`` are computed by tessellating the x-, y-ranges of the data, storing the number of points falling in each hexagonal bin. By default ``HexTiles`` simply counts the data in each bin but it also supports weighted aggregations. Currently only linearly spaced bins are supported when using the bokeh backend.\n", "\n", "As a simple example we will generate 100,000 normally distributed points and plot them using ``HexTiles``:" ] diff --git a/pyproject.toml b/pyproject.toml index fb9b916db0..a1d48ea35a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -180,6 +180,6 @@ known-first-party = ["holoviews"] combine-as-imports = true [tool.codespell] -ignore-words-list = "lod,nd,ndoes,reenabled,spreaded,whn,ser" +ignore-words-list = "lod,nd,ndoes,reenabled,spreaded,whn,ser,assertIn" skip = "doc/generate_modules.py" write-changes = true