diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 552c05c208..ffadee37a0 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -51,10 +51,16 @@ jobs: bash ./scripts/build_conda.sh - name: conda dev upload if: (github.event_name == 'push' && (contains(steps.vars.outputs.tag, 'a') || contains(steps.vars.outputs.tag, 'b') || contains(steps.vars.outputs.tag, 'rc'))) - run: doit package_upload --token=$CONDA_UPLOAD_TOKEN --label=dev + run: | + VERSION="$(echo "$(ls dist/*.whl)" | cut -d- -f2)" + FILE="$CONDA_PREFIX/conda-bld/noarch/holoviews-$VERSION-py_0.tar.bz2" + anaconda --token $CONDA_UPLOAD_TOKEN upload --user pyviz --label=dev $FILE - name: conda main upload if: (github.event_name == 'push' && !(contains(steps.vars.outputs.tag, 'a') || contains(steps.vars.outputs.tag, 'b') || contains(steps.vars.outputs.tag, 'rc'))) - run: doit package_upload --token=$CONDA_UPLOAD_TOKEN --label=dev --label=main + run: | + VERSION="$(echo "$(ls dist/*.whl)" | cut -d- -f2)" + FILE="$CONDA_PREFIX/conda-bld/noarch/holoviews-$VERSION-py_0.tar.bz2" + anaconda --token $CONDA_UPLOAD_TOKEN upload --user pyviz --label=dev --label=main $FILE pip_build: name: Build PyPI Packages runs-on: 'ubuntu-latest' diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 553bc72640..7275289b90 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -63,7 +63,7 @@ jobs: python-version: ${{ matrix.python-version }} channel-priority: strict channels: pyviz/label/dev,conda-forge,nodefaults - envs: "-o flakes -o tests -o examples_tests" + envs: "-o flakes -o tests -o examples_tests -o test_ci" cache: true conda-update: true id: install @@ -109,7 +109,7 @@ jobs: name: ui_test_suite python-version: ${{ matrix.python-version }} channels: pyviz/label/dev,bokeh,conda-forge,nodefaults - envs: "-o recommended -o tests -o build" + envs: "-o recommended -o tests -o build -o test_ci" cache: true playwright: true id: install @@ -150,7 +150,7 @@ jobs: python-version: ${{ matrix.python-version }} channel-priority: strict channels: pyviz/label/dev,conda-forge,nodefaults - envs: "-o tests_core" + envs: "-o tests_core -o test_ci" cache: true conda-update: true id: install diff --git a/examples/conftest.py b/examples/conftest.py index f3d197dc0a..c0d792b079 100644 --- a/examples/conftest.py +++ b/examples/conftest.py @@ -60,12 +60,17 @@ def pytest_runtest_makereport(item, call): """ from _pytest.runner import pytest_runtest_makereport + tr = pytest_runtest_makereport(item, call) if call.excinfo is not None: - msg = "Kernel died before replying to kernel_info" - if call.excinfo.type == RuntimeError and call.excinfo.value.args[0] == msg: - tr.outcome = 'skipped' - tr.wasxfail = f"reason: {msg}" + msgs = [ + "Kernel died before replying to kernel_info", + "Kernel didn't respond in 60 seconds", + ] + for msg in msgs: + if call.excinfo.type == RuntimeError and call.excinfo.value.args[0] in msg: + tr.outcome = "skipped" + tr.wasxfail = f"reason: {msg}" return tr diff --git a/holoviews/core/data/dictionary.py b/holoviews/core/data/dictionary.py index 5d0171f9c3..07c54073a1 100644 --- a/holoviews/core/data/dictionary.py +++ b/holoviews/core/data/dictionary.py @@ -1,4 +1,4 @@ -from collections import defaultdict +from collections import OrderedDict, defaultdict import numpy as np @@ -18,7 +18,7 @@ class DictInterface(Interface): are collections representing the values in that column. """ - types = (dict,) + types = (dict, OrderedDict) datatype = 'dictionary' @@ -109,10 +109,11 @@ def init(cls, eltype, data, kdims, vdims): if not cls.expanded([vs for d, vs in unpacked if d in dimensions and not isscalar(vs)]): raise ValueError('DictInterface expects data to be of uniform shape.') - if isinstance(data, dict): + # OrderedDict can't be replaced with dict: https://github.com/holoviz/holoviews/pull/5925 + if isinstance(data, OrderedDict): data.update(unpacked) else: - data = dict(unpacked) + data = OrderedDict(unpacked) return data, {'kdims':kdims, 'vdims':vdims}, {} diff --git a/holoviews/core/data/ibis.py b/holoviews/core/data/ibis.py index 94066d6708..16e17f7976 100644 --- a/holoviews/core/data/ibis.py +++ b/holoviews/core/data/ibis.py @@ -23,6 +23,11 @@ def ibis4(): return ibis_version() >= Version("4.0") +@lru_cache +def ibis5(): + return ibis_version() >= Version("5.0") + + class IbisInterface(Interface): types = () @@ -163,8 +168,8 @@ def histogram(cls, expr, bins, density=True, weights=None): else: # sort_by will be removed in Ibis 5.0 hist_bins = binned.value_counts().sort_by('bucket').execute() - - for b, v in zip(hist_bins['bucket'], hist_bins['count']): + metric_name = 'bucket_count' if ibis5() else 'count' + for b, v in zip(hist_bins['bucket'], hist_bins[metric_name]): if np.isnan(b): continue hist[int(b)] = v @@ -172,7 +177,7 @@ def histogram(cls, expr, bins, density=True, weights=None): raise NotImplementedError("Weighted histograms currently " "not implemented for IbisInterface.") if density: - hist = hist/expr.count().execute() + hist = hist/expr.count().execute()/np.diff(bins) return hist, bins @classmethod diff --git a/holoviews/operation/element.py b/holoviews/operation/element.py index 58cde6da18..dcade0b2ee 100644 --- a/holoviews/operation/element.py +++ b/holoviews/operation/element.py @@ -737,10 +737,16 @@ def _process(self, element, key=None): # Mask data if is_ibis_expr(data): + from ..core.data.ibis import ibis5 + mask = data.notnull() if self.p.nonzero: mask = mask & (data != 0) - data = data.to_projection() + if ibis5(): + data = data.as_table() + else: + # to_projection removed in ibis 5.0.0 + data = data.to_projection() data = data[mask] no_data = not len(data.head(1).execute()) data = data[dim.name] diff --git a/holoviews/plotting/bokeh/callbacks.py b/holoviews/plotting/bokeh/callbacks.py index f70a4f9051..62251b5aac 100644 --- a/holoviews/plotting/bokeh/callbacks.py +++ b/holoviews/plotting/bokeh/callbacks.py @@ -25,7 +25,7 @@ BoxEdit, PointDraw, PolyDraw, PolyEdit, CDSStream, FreehandDraw, CurveEdit, SelectionXY, Lasso, SelectMode ) -from .util import bokeh3, bokeh33, convert_timestamp +from .util import bokeh33, convert_timestamp from ...util.warnings import warn @@ -618,23 +618,6 @@ class RangeXYCallback(Callback): 'y1': 'cb_obj.y1', } - _js_on_event = """ - if (this._updating) - return - const plot = this.origin - const plots = plot.x_range.plots.concat(plot.y_range.plots) - for (const p of plots) { - const event = new this.constructor(p.x_range.start, p.x_range.end, p.y_range.start, p.y_range.end) - event._updating = true - p.trigger_event(event) - } - """ - - def set_callback(self, handle): - super().set_callback(handle) - if not bokeh3: - handle.js_on_event('rangesupdate', CustomJS(code=self._js_on_event)) - def _process_msg(self, msg): if self.plot.state.x_range is not self.plot.handles['x_range']: x_range = self.plot.handles['x_range'] diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py index 6cbe7f2ad2..c5ee44675c 100644 --- a/holoviews/plotting/bokeh/element.py +++ b/holoviews/plotting/bokeh/element.py @@ -49,7 +49,7 @@ ) from .tabular import TablePlot from .util import ( - TOOL_TYPES, bokeh_version, bokeh3, bokeh32, date_to_integer, + TOOL_TYPES, bokeh_version, bokeh32, date_to_integer, decode_bytes, get_tab_title, glyph_order, py2js_tickformatter, recursive_model_update, theme_attr_json, cds_column_replace, hold_policy, match_dim_specs, compute_layout_properties, @@ -58,12 +58,8 @@ get_scale, get_axis_class ) -if bokeh3: - from bokeh.models.formatters import CustomJSTickFormatter - from bokeh.models.layouts import TabPanel -else: - from bokeh.models.formatters import FuncTickFormatter as CustomJSTickFormatter - from bokeh.models.layouts import Panel as TabPanel +from bokeh.models.formatters import CustomJSTickFormatter +from bokeh.models.layouts import TabPanel try: TOOLS_MAP = Tool._known_aliases @@ -685,10 +681,7 @@ def _init_plot(self, key, element, plots, ranges=None): properties.update(**self._plot_properties(key, element)) - if bokeh3: - figure = bokeh.plotting.figure - else: - figure = bokeh.plotting.Figure + figure = bokeh.plotting.figure with warnings.catch_warnings(): # Bokeh raises warnings about duplicate tools but these @@ -1721,18 +1714,13 @@ def _update_glyph(self, renderer, properties, mapping, glyph, source, data): server = self.renderer.mode == 'server' with hold_policy(self.document, 'collect', server=server): empty_data = {c: [] for c in columns} - if bokeh3: - event = ModelChangedEvent( - document=self.document, - model=source, - attr='data', - new=empty_data, - setter='empty' - ) - else: - event = ModelChangedEvent( - self.document, source, 'data', source.data, empty_data, empty_data, setter='empty' - ) + event = ModelChangedEvent( + document=self.document, + model=source, + attr='data', + new=empty_data, + setter='empty' + ) self.document.callbacks._held_events.append(event) if legend is not None: @@ -2565,7 +2553,7 @@ def _process_legend(self, plot=None): or not self.show_legend): legend.items[:] = [] else: - if bokeh3 and self.legend_cols: + if self.legend_cols: plot.legend.nrows = self.legend_cols else: plot.legend.orientation = 'horizontal' if self.legend_cols else 'vertical' @@ -2668,8 +2656,6 @@ def _process_legend(self, overlay): options[k] = v pos = self.legend_position - if not bokeh3: - options['orientation'] = 'horizontal' if self.legend_cols else 'vertical' if pos in ['top', 'bottom'] and not self.legend_cols: options['orientation'] = 'horizontal' @@ -2679,7 +2665,7 @@ def _process_legend(self, overlay): options.update(self._fontsize('legend', 'label_text_font_size')) options.update(self._fontsize('legend_title', 'title_text_font_size')) - if bokeh3 and self.legend_cols: + if self.legend_cols: options.update({"ncols": self.legend_cols}) legend.update(**options) diff --git a/holoviews/plotting/bokeh/graphs.py b/holoviews/plotting/bokeh/graphs.py index 4f6924b07e..7f591ef8e1 100644 --- a/holoviews/plotting/bokeh/graphs.py +++ b/holoviews/plotting/bokeh/graphs.py @@ -19,7 +19,6 @@ base_properties, line_properties, fill_properties, text_properties, rgba_tuple ) -from .util import bokeh3 class GraphPlot(GraphMixin, CompositeElementPlot, ColorbarPlot, LegendPlot): @@ -185,8 +184,6 @@ def get_data(self, element, ranges, style): index = nodes.astype(np.int32) layout = {k: (y, x) if self.invert_axes else (x, y) for k, (x, y) in zip(index, node_positions)} - if not bokeh3: - layout = {str(k): v for k, v in layout.items()} point_data = {'index': index} diff --git a/holoviews/plotting/bokeh/links.py b/holoviews/plotting/bokeh/links.py index 55850eeb08..154cfb3974 100644 --- a/holoviews/plotting/bokeh/links.py +++ b/holoviews/plotting/bokeh/links.py @@ -2,8 +2,8 @@ from bokeh.models import CustomJS from bokeh.models.tools import RangeTool +from bokeh.models import Toolbar -from .util import bokeh3 from ...core.util import isscalar from ..links import ( Link, RectanglesTableLink, DataLink, RangeToolLink, @@ -11,11 +11,6 @@ ) from ..plot import GenericElementPlot, GenericOverlayPlot -if bokeh3: - from bokeh.models import Toolbar -else: - from bokeh.models import ToolbarBox as Toolbar # Not completely correct - class LinkCallback: @@ -158,11 +153,8 @@ def __init__(self, root_model, link, source_plot, target_plot): tool = RangeTool(**axes) source_plot.state.add_tools(tool) - if bokeh3 and toolbars: + if toolbars: toolbars[0].tools.append(tool) - elif toolbars: - toolbar = toolbars[0].toolbar - toolbar.tools.append(tool) class DataLinkCallback(LinkCallback): @@ -206,8 +198,6 @@ def __init__(self, root_model, link, source_plot, target_plot): renderer.update(data_source=src_cds) else: renderer.update(source=src_cds) - if not bokeh3 and hasattr(renderer, 'view'): - renderer.view.update(source=src_cds) target_plot.handles['source'] = src_cds target_plot.handles['cds'] = src_cds for callback in target_plot.callbacks: diff --git a/holoviews/plotting/bokeh/plot.py b/holoviews/plotting/bokeh/plot.py index 20d780d695..ccc01aa4f5 100644 --- a/holoviews/plotting/bokeh/plot.py +++ b/holoviews/plotting/bokeh/plot.py @@ -30,14 +30,11 @@ from ..util import attach_streams, displayable, collate from .links import LinkCallback from .util import ( - bokeh3, filter_toolboxes, make_axis, sync_legends, update_shared_sources, empty_plot, + filter_toolboxes, make_axis, sync_legends, update_shared_sources, empty_plot, decode_bytes, theme_attr_json, cds_column_replace, get_default, merge_tools, select_legends ) -if bokeh3: - from bokeh.models.layouts import TabPanel -else: - from bokeh.models.layouts import Panel as TabPanel +from bokeh.models.layouts import TabPanel class BokehPlot(DimensionedPlot, CallbackPlot): @@ -271,10 +268,9 @@ def _get_title_div(self, key, default_fontsize='15pt', width=450): if 'title' in self.handles: title_div = self.handles['title'] - elif bokeh3: - title_div = Div(width=width, styles={"white-space": "nowrap"}) # so it won't wrap long titles easily else: - title_div = Div(width=width, style={"white-space": "nowrap"}) # so it won't wrap long titles easily + # so it won't wrap long titles easily + title_div = Div(width=width, styles={"white-space": "nowrap"}) title_div.text = title_tags return title_div @@ -310,8 +306,6 @@ def sync_sources(self): renderer.update(data_source=new_source) else: renderer.update(source=new_source) - if not bokeh3 and hasattr(renderer, 'view'): - renderer.view.update(source=new_source) plot.handles['source'] = plot.handles['cds'] = new_source plots.append(plot) shared_sources.append(new_source) @@ -592,7 +586,7 @@ def initialize_plot(self, ranges=None, plots=[]): if self.sync_legends: sync_legends(plot) plot = self._make_axes(plot) - if bokeh3 and hasattr(plot, "toolbar") and self.merge_tools: + if hasattr(plot, "toolbar") and self.merge_tools: plot.toolbar = merge_tools(plots) title = self._get_title_div(self.keys[-1]) @@ -645,7 +639,7 @@ def _make_axes(self, plot): x_axis.margin = (0, 0, 0, 50) r1, r2 = r1[::-1], r2[::-1] plot = gridplot([r1, r2], merge_tools=False) - if bokeh3 and self.merge_tools: + if self.merge_tools: plot.toolbar = merge_tools([r1, r2]) elif y_axis: models = [y_axis, plot] @@ -952,7 +946,7 @@ def initialize_plot(self, plots=None, ranges=None): merge_tools=False, toolbar_location=self.toolbar, sizing_mode=sizing_mode) - if bokeh3 and self.merge_tools: + if self.merge_tools: grid.toolbar = merge_tools(children) tab_plots.append((title, grid)) continue @@ -994,7 +988,7 @@ def initialize_plot(self, plots=None, ranges=None): ) if self.sync_legends: sync_legends(layout_plot) - if bokeh3 and self.merge_tools: + if self.merge_tools: layout_plot.toolbar = merge_tools(plot_grid) title = self._get_title_div(self.keys[-1]) diff --git a/holoviews/plotting/bokeh/raster.py b/holoviews/plotting/bokeh/raster.py index d6d1d66f8a..6eda70d400 100644 --- a/holoviews/plotting/bokeh/raster.py +++ b/holoviews/plotting/bokeh/raster.py @@ -12,7 +12,7 @@ from .element import ColorbarPlot, LegendPlot from .selection import BokehOverlaySelectionDisplay from .styles import base_properties, fill_properties, line_properties, mpl_to_bokeh -from .util import bokeh3, colormesh +from .util import colormesh class RasterPlot(ColorbarPlot): @@ -102,10 +102,6 @@ def get_data(self, element, ranges, style): l, b, r, t = b, l, t, r dh, dw = t-b, r-l - if self.invert_xaxis and not bokeh3: - l, r = r, l - if self.invert_yaxis and not bokeh3: - b, t = t, b data = dict(x=[l], y=[b], dw=[dw], dh=[dh]) for i, vdim in enumerate(element.vdims, 2): @@ -118,10 +114,6 @@ def get_data(self, element, ranges, style): img = np.array([[np.NaN]]) if self.invert_axes ^ (type(element) is Raster): img = img.T - if self.invert_xaxis and not bokeh3: - img = img[:, ::-1] - if self.invert_yaxis and not bokeh3: - img = img[::-1] key = 'image' if i == 2 else dimension_sanitizer(vdim.name) data[key] = [img] @@ -212,12 +204,6 @@ def get_data(self, element, ranges, style): l, b, r, t = b, l, t, r dh, dw = t-b, r-l - if self.invert_xaxis and not bokeh3: - l, r = r, l - img = img[:, ::-1] - if self.invert_yaxis and not bokeh3: - img = img[::-1] - b, t = t, b if 0 in img.shape: img = np.zeros((1, 1), dtype=np.uint32) diff --git a/holoviews/plotting/bokeh/util.py b/holoviews/plotting/bokeh/util.py index 3d30153b23..dde5e46197 100644 --- a/holoviews/plotting/bokeh/util.py +++ b/holoviews/plotting/bokeh/util.py @@ -30,6 +30,10 @@ from bokeh.models.widgets import DataTable, Div from bokeh.themes.theme import Theme from bokeh.themes import built_in_themes +from bokeh.layouts import group_tools +from bokeh.models.formatters import CustomJSTickFormatter +from bokeh.models import Toolbar, Tabs, GridPlot, SaveTool, CopyTool, ExamineTool, FullscreenTool, LayoutDOM +from bokeh.plotting import figure from packaging.version import Version from ...core.layout import Layout @@ -44,26 +48,11 @@ from ..util import dim_axis_label from ...util.warnings import deprecated + bokeh_version = Version(bokeh.__version__) -bokeh3 = bokeh_version >= Version("3.0") bokeh32 = bokeh_version >= Version("3.2") bokeh33 = bokeh_version >= Version("3.3") -if bokeh3: - from bokeh.layouts import group_tools - from bokeh.models.formatters import CustomJSTickFormatter - from bokeh.models import Toolbar, Tabs, GridPlot, SaveTool, CopyTool, ExamineTool, FullscreenTool, LayoutDOM - from bokeh.plotting import figure - class WidgetBox: pass # Does not exist in Bokeh 3 - -else: - from bokeh.layouts import WidgetBox - from bokeh.models.formatters import FuncTickFormatter as CustomJSTickFormatter - from bokeh.models.widgets import Tabs - from bokeh.models import ToolbarBox as Toolbar # Not completely correct - from bokeh.plotting import Figure as figure - class GridPlot: pass # Does not exist in Bokeh 2 - TOOL_TYPES = { 'pan': tools.PanTool, 'xpan': tools.PanTool, @@ -176,7 +165,7 @@ def compute_plot_size(plot): elif isinstance(plot, (Div, Toolbar)): # Cannot compute size for Div or Toolbar return 0, 0 - elif isinstance(plot, (Row, Column, Tabs, WidgetBox)): + elif isinstance(plot, (Row, Column, Tabs)): if not plot.children: return 0, 0 if isinstance(plot, Row) or (isinstance(plot, Toolbar) and plot.toolbar_location not in ['right', 'left']): w_agg, h_agg = (np.sum, np.max) @@ -420,14 +409,12 @@ def merge(tool, group): def sync_legends(bokeh_layout): """This syncs the legends of all plots in a grid based on their name. - Only works for Bokeh 3 and above. - Parameters ---------- bokeh_layout : bokeh.models.{GridPlot, Row, Column} Gridplot to sync legends of. """ - if not bokeh3 or len(bokeh_layout.children) < 2: + if len(bokeh_layout.children) < 2: return # Collect all glyph with names @@ -665,7 +652,7 @@ def pad_width(model, table_padding=0.85, tabs_padding=1.2): elif isinstance(model, DataTable): width = model.width model.width = int(table_padding*width) - elif isinstance(model, (WidgetBox, Div)): + elif isinstance(model, Div): width = model.width elif model: width = model.width @@ -688,8 +675,7 @@ def pad_plots(plots): row_widths.append(width) widths.append(row_widths) - layout = Column if bokeh3 else WidgetBox - plots = [[layout(p, width=w) if isinstance(p, (DataTable, Tabs)) else p + plots = [[Column(p, width=w) if isinstance(p, (DataTable, Tabs)) else p for p, w in zip(row, ws)] for row, ws in zip(plots, widths)] return plots @@ -1171,8 +1157,6 @@ def wrap_formatter(formatter, axis): def property_to_dict(x): """ Convert Bokeh's property Field and Value to a dictionary - - Was added in bokeh 3.0 """ try: @@ -1192,8 +1176,6 @@ def dtype_fix_hook(plot, element): # https://github.com/holoviz/holoviews/issues/5726 # Should be fixed in Bokeh 3.2 - if not bokeh3: - return try: renderers = plot.handles["plot"].renderers for renderer in renderers: diff --git a/holoviews/tests/conftest.py b/holoviews/tests/conftest.py index ea50664a3f..91b706a8cc 100644 --- a/holoviews/tests/conftest.py +++ b/holoviews/tests/conftest.py @@ -1,3 +1,5 @@ +import pytest + from panel.tests.conftest import server_cleanup, port, pytest_addoption, pytest_configure, optional_markers # noqa @@ -26,3 +28,15 @@ def pytest_collection_modifyitems(config, items): dask.config.set({"dataframe.convert-string": False}) except Exception: pass + + +@pytest.fixture +def ibis_sqlite_backend(): + try: + import ibis + except ImportError: + yield None + else: + ibis.set_backend('sqlite') + yield + ibis.set_backend(None) diff --git a/holoviews/tests/operation/test_operation.py b/holoviews/tests/operation/test_operation.py index 87a6dd8032..feee387e9f 100644 --- a/holoviews/tests/operation/test_operation.py +++ b/holoviews/tests/operation/test_operation.py @@ -10,6 +10,11 @@ except ImportError: da = None +try: + import ibis +except ImportError: + ibis = None + from holoviews import (HoloMap, NdOverlay, NdLayout, GridSpace, Image, Contours, Polygons, Points, Histogram, Curve, Area, QuadMesh, Dataset, renderer) @@ -20,6 +25,7 @@ interpolate_curve, decimate) da_skip = skipIf(da is None, "dask.array is not available") +ibis_skip = skipIf(ibis is None, "ibis is not available") class OperationTests(ComparisonTestCase): @@ -179,6 +185,43 @@ def test_dataset_weighted_histogram_dask(self): self.assertIsInstance(op_hist.data['y'], da.Array) self.assertEqual(op_hist, hist) + @ibis_skip + @pytest.mark.usefixtures('ibis_sqlite_backend') + def test_dataset_histogram_ibis(self): + df = pd.DataFrame(dict(x=np.arange(10))) + t = ibis.memtable(df, name='t') + 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) + + @ibis_skip + @pytest.mark.usefixtures('ibis_sqlite_backend') + def test_dataset_cumulative_histogram_ibis(self): + df = pd.DataFrame(dict(x=np.arange(10))) + t = ibis.memtable(df, name='t') + 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) + + @ibis_skip + @pytest.mark.usefixtures('ibis_sqlite_backend') + def test_dataset_histogram_explicit_bins_ibis(self): + df = pd.DataFrame(dict(x=np.arange(10))) + t = ibis.memtable(df, name='t') + 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)]) op_hist = histogram(points, num_bins=3, bin_range=(0, 3), normed=True) diff --git a/holoviews/tests/plotting/bokeh/test_elementplot.py b/holoviews/tests/plotting/bokeh/test_elementplot.py index 4ca0e0c835..4577a99c72 100644 --- a/holoviews/tests/plotting/bokeh/test_elementplot.py +++ b/holoviews/tests/plotting/bokeh/test_elementplot.py @@ -10,7 +10,6 @@ from holoviews.element import Curve, Image, Scatter, Labels, HeatMap from holoviews.streams import Stream, PointDraw from holoviews.plotting.util import process_cmap -from holoviews.plotting.bokeh.util import bokeh3 from holoviews.util import render from .test_plot import TestBokehPlot, bokeh_renderer @@ -24,10 +23,7 @@ NumeralTickFormatter, LogTicker, LinearColorMapper, LogColorMapper, EqHistColorMapper) -if bokeh3: - from bokeh.models.formatters import CustomJSTickFormatter -else: - from bokeh.models.formatters import FuncTickFormatter as CustomJSTickFormatter +from bokeh.models.formatters import CustomJSTickFormatter class TestElementPlot(LoggingComparisonTestCase, TestBokehPlot): diff --git a/holoviews/tests/plotting/bokeh/test_graphplot.py b/holoviews/tests/plotting/bokeh/test_graphplot.py index ec540936bf..f0a47df345 100644 --- a/holoviews/tests/plotting/bokeh/test_graphplot.py +++ b/holoviews/tests/plotting/bokeh/test_graphplot.py @@ -3,7 +3,7 @@ from holoviews.core.data import Dataset from holoviews.element import Graph, Nodes, TriMesh, Chord, VLine, circular_layout from holoviews.util.transform import dim -from holoviews.plotting.bokeh.util import property_to_dict, bokeh3 +from holoviews.plotting.bokeh.util import property_to_dict from bokeh.models import (NodesAndLinkedEdges, EdgesAndLinkedNodes, NodesOnly, Patches) from bokeh.models.mappers import CategoricalColorMapper, LinearColorMapper @@ -36,10 +36,7 @@ def test_plot_simple_graph(self): self.assertEqual(node_source.data['index'], self.source) self.assertEqual(edge_source.data['start'], self.source) self.assertEqual(edge_source.data['end'], self.target) - if bokeh3: - layout = {z: (x, y) for x, y, z in self.graph.nodes.array()} - else: - layout = {str(z): (x, y) for x, y, z in self.graph.nodes.array()} + layout = {z: (x, y) for x, y, z in self.graph.nodes.array()} self.assertEqual(layout_source.graph_layout, layout) @@ -64,10 +61,7 @@ def test_plot_graph_with_paths(self): edges = graph.edgepaths.split() self.assertEqual(edge_source.data['xs'], [path.dimension_values(0) for path in edges]) self.assertEqual(edge_source.data['ys'], [path.dimension_values(1) for path in edges]) - if bokeh3: - layout = {z: (x, y) for x, y, z in self.graph.nodes.array()} - else: - layout = {str(z): (x, y) for x, y, z in self.graph.nodes.array()} + layout = {z: (x, y) for x, y, z in self.graph.nodes.array()} self.assertEqual(layout_source.graph_layout, layout) def test_graph_inspection_policy_nodes(self): @@ -322,10 +316,7 @@ def test_plot_simple_trimesh(self): self.assertEqual(node_source.data['index'], np.arange(4)) self.assertEqual(edge_source.data['start'], np.arange(2)) self.assertEqual(edge_source.data['end'], np.arange(1, 3)) - if bokeh3: - layout = {z: (x, y) for x, y, z in self.trimesh.nodes.array()} - else: - layout = {str(z): (x, y) for x, y, z in self.trimesh.nodes.array()} + layout = {z: (x, y) for x, y, z in self.trimesh.nodes.array()} self.assertEqual(layout_source.graph_layout, layout) def test_plot_simple_trimesh_filled(self): @@ -337,10 +328,7 @@ def test_plot_simple_trimesh_filled(self): self.assertEqual(node_source.data['index'], np.arange(4)) self.assertEqual(edge_source.data['start'], np.arange(2)) self.assertEqual(edge_source.data['end'], np.arange(1, 3)) - if bokeh3: - layout = {z: (x, y) for x, y, z in self.trimesh.nodes.array()} - else: - layout = {str(z): (x, y) for x, y, z in self.trimesh.nodes.array()} + layout = {z: (x, y) for x, y, z in self.trimesh.nodes.array()} self.assertEqual(layout_source.graph_layout, layout) def test_trimesh_edges_categorical_colormapped(self): diff --git a/holoviews/tests/plotting/bokeh/test_gridplot.py b/holoviews/tests/plotting/bokeh/test_gridplot.py index b8f44359b2..7b1f93451c 100644 --- a/holoviews/tests/plotting/bokeh/test_gridplot.py +++ b/holoviews/tests/plotting/bokeh/test_gridplot.py @@ -5,17 +5,13 @@ from holoviews.element import Curve, Image, Points from holoviews.operation import gridmatrix from holoviews.streams import Stream -from holoviews.plotting.bokeh.util import bokeh3 from .test_plot import TestBokehPlot, bokeh_renderer from bokeh.layouts import Column from bokeh.models import Div -if bokeh3: - from bokeh.models import Toolbar -else: - from bokeh.models import ToolbarBox as Toolbar # Not completely correct +from bokeh.models import Toolbar @@ -111,10 +107,7 @@ def test_grid_set_toolbar_location(self): grid = GridSpace({0: Curve([]), 1: Points([])}, 'X').opts(toolbar='left') plot = bokeh_renderer.get_plot(grid) self.assertIsInstance(plot.state, Column) - if bokeh3: - self.assertIsInstance(plot.state.children[0].toolbar, Toolbar) - else: - self.assertIsInstance(plot.state.children[0].children[0], Toolbar) + self.assertIsInstance(plot.state.children[0].toolbar, Toolbar) def test_grid_disable_toolbar(self): diff --git a/holoviews/tests/plotting/bokeh/test_layoutplot.py b/holoviews/tests/plotting/bokeh/test_layoutplot.py index 275c96417c..4f7df4979a 100644 --- a/holoviews/tests/plotting/bokeh/test_layoutplot.py +++ b/holoviews/tests/plotting/bokeh/test_layoutplot.py @@ -1,7 +1,6 @@ import datetime as dt import re -import pytest import numpy as np from holoviews.core import (HoloMap, GridSpace, Layout, Empty, Dataset, @@ -10,23 +9,16 @@ from holoviews.streams import Stream from holoviews.util import render, opts from holoviews.util.transform import dim -from holoviews.plotting.bokeh.util import bokeh3 -from bokeh.models import Div, GlyphRenderer, Tabs, Spacer, Title, Row, Column +from bokeh.models import Div, GlyphRenderer, Tabs, Spacer, Title from ...utils import LoggingComparisonTestCase from .test_plot import TestBokehPlot, bokeh_renderer -if bokeh3: - from bokeh.models.layouts import TabPanel - from bokeh.plotting import figure - from bokeh.models import GridPlot - from bokeh.models import Toolbar -else: - from bokeh.models.layouts import Panel as TabPanel - from bokeh.plotting import Figure as figure - from bokeh.models import ToolbarBox as Toolbar # Not completely correct - from bokeh.models import GridBox as GridPlot # Not completely correct +from bokeh.models.layouts import TabPanel +from bokeh.plotting import figure +from bokeh.models import GridPlot +from bokeh.models import Toolbar class TestLayoutPlot(LoggingComparisonTestCase, TestBokehPlot): @@ -172,8 +164,7 @@ def test_layout_title_update(self): 'font-weight:bold;font-size:12pt">Default: 1') self.assertEqual(title.text, text) - @pytest.mark.skipif(not bokeh3, reason="Only work for Bokeh 3") - def test_layout_gridspaces_bokeh3(self): + def test_layout_gridspaces(self): layout = (GridSpace({(i, j): Curve(range(i+j)) for i in range(1, 3) for j in range(2,4)}) + GridSpace({(i, j): Curve(range(i+j)) for i in range(1, 3) @@ -204,45 +195,6 @@ def test_layout_gridspaces_bokeh3(self): for gfig, *_ in grid.children: self.assertIsInstance(gfig, figure) - @pytest.mark.skipif(bokeh3, reason="Only work for Bokeh 2") - def test_layout_gridspaces_bokeh2(self): - layout = (GridSpace({(i, j): Curve(range(i+j)) for i in range(1, 3) - for j in range(2,4)}) + - GridSpace({(i, j): Curve(range(i+j)) for i in range(1, 3) - for j in range(2,4)}) + - Curve(range(10))).cols(2) - layout_plot = bokeh_renderer.get_plot(layout) - plot = layout_plot.state - - # Unpack until getting down to two rows - self.assertIsInstance(plot, Column) - self.assertEqual(len(plot.children), 2) - toolbar, grid = plot.children - self.assertIsInstance(toolbar, Toolbar) - self.assertIsInstance(grid, GridPlot) - self.assertEqual(len(grid.children), 3) - (col1, *_), (col2, *_), _ = grid.children - self.assertIsInstance(col1, Column) - self.assertIsInstance(col2, Column) - grid1 = col1.children[0] - grid2 = col2.children[0] - - # Check the row of GridSpaces - self.assertEqual(len(grid1.children), 3) - _, (col1, *_), _ = grid1.children - self.assertIsInstance(col1, Column) - inner_grid1 = col1.children[0] - - self.assertEqual(len(grid2.children), 3) - _, (col2, *_), _ = grid2.children - self.assertIsInstance(col2, Column) - inner_grid2 = col2.children[0] - for grid in [inner_grid1, inner_grid2]: - self.assertEqual(len(grid.children), 4) - for gfig, *_ in grid.children: - self.assertIsInstance(gfig, figure) - - def test_layout_instantiate_subplots(self): layout = (Curve(range(10)) + Curve(range(10)) + Image(np.random.rand(10,10)) + Curve(range(10)) + Curve(range(10))) @@ -262,10 +214,7 @@ def test_empty_adjoint_plot(self): plot = bokeh_renderer.get_plot(adjoint) adjoint_plot = plot.subplots[(0, 0)] self.assertEqual(len(adjoint_plot.subplots), 3) - if bokeh3: - grid = plot.state - else: - grid = plot.state.children[1] + grid = plot.state (f1, *_), (f2, *_), (s1, *_) = grid.children self.assertIsInstance(grid, GridPlot) self.assertIsInstance(s1, Spacer) @@ -285,11 +234,8 @@ def test_empty_adjoint_plot_with_renderer(self): def test_layout_plot_with_adjoints(self): layout = (Curve([]) + Curve([]).hist()).cols(1) plot = bokeh_renderer.get_plot(layout) - if bokeh3: - grid = plot.state - toolbar = grid.toolbar - else: - toolbar, grid = plot.state.children + grid = plot.state + toolbar = grid.toolbar self.assertIsInstance(toolbar, Toolbar) self.assertIsInstance(grid, GridPlot) for (fig, _, _) in grid.children: @@ -372,12 +318,8 @@ def test_layout_empty_subplots(self): def test_layout_set_toolbar_location(self): layout = (Curve([]) + Points([])).opts(toolbar='left') plot = bokeh_renderer.get_plot(layout) - if bokeh3: - self.assertIsInstance(plot.state, GridPlot) - self.assertIsInstance(plot.state.toolbar, Toolbar) - else: - self.assertIsInstance(plot.state, Row) - self.assertIsInstance(plot.state.children[0], Toolbar) + self.assertIsInstance(plot.state, GridPlot) + self.assertIsInstance(plot.state.toolbar, Toolbar) def test_layout_disable_toolbar(self): layout = (Curve([]) + Points([])).opts(toolbar=None) diff --git a/holoviews/tests/plotting/bokeh/test_plot.py b/holoviews/tests/plotting/bokeh/test_plot.py index aa0e04fa84..0b4a1d5b98 100644 --- a/holoviews/tests/plotting/bokeh/test_plot.py +++ b/holoviews/tests/plotting/bokeh/test_plot.py @@ -1,5 +1,4 @@ import numpy as np -import pytest import pyviz_comms as comms from param import concrete_descendents @@ -14,7 +13,6 @@ ) from holoviews.plotting.bokeh.callbacks import Callback from holoviews.plotting.bokeh.element import ElementPlot -from holoviews.plotting.bokeh.util import bokeh3 bokeh_renderer = Store.renderers['bokeh'] @@ -83,7 +81,6 @@ def _test_hover_info(self, element, tooltips, line_policy='nearest', formatters= self.assertTrue(any(renderer in h.renderers for h in hover)) -@pytest.mark.skipif(not bokeh3, reason="Bokeh>=3.0 required") def test_sync_two_plots(): curve = lambda i: Curve(np.arange(10) * i, label="ABC"[i]) plot1 = curve(0) * curve(1) @@ -103,7 +100,6 @@ def test_sync_two_plots(): assert v[0].code == "dst.muted = src.muted" -@pytest.mark.skipif(not bokeh3, reason="Bokeh>=3.0 required") def test_sync_three_plots(): curve = lambda i: Curve(np.arange(10) * i, label="ABC"[i]) plot1 = curve(0) * curve(1) diff --git a/holoviews/tests/plotting/bokeh/test_rasterplot.py b/holoviews/tests/plotting/bokeh/test_rasterplot.py index 041ea2ccd7..872a916838 100644 --- a/holoviews/tests/plotting/bokeh/test_rasterplot.py +++ b/holoviews/tests/plotting/bokeh/test_rasterplot.py @@ -3,7 +3,6 @@ import numpy as np from holoviews.element import Raster, Image, RGB, ImageStack -from holoviews.plotting.bokeh.util import bokeh3 from holoviews.plotting.bokeh.raster import ImageStackPlot from .test_plot import TestBokehPlot, bokeh_renderer @@ -59,18 +58,11 @@ def test_raster_invert_axes(self): plot = bokeh_renderer.get_plot(raster) source = plot.handles["source"] - if bokeh3: - np.testing.assert_equal(source.data["image"][0], arr.T) - assert source.data["x"][0] == 0 - assert source.data["y"][0] == 0 - assert source.data["dw"][0] == 2 - assert source.data["dh"][0] == 3 - else: - np.testing.assert_equal(source.data["image"][0], np.rot90(arr)) - assert source.data["x"][0] == 0 - assert source.data["y"][0] == 3 - assert source.data["dw"][0] == 2 - assert source.data["dh"][0] == 3 + np.testing.assert_equal(source.data["image"][0], arr.T) + assert source.data["x"][0] == 0 + assert source.data["y"][0] == 0 + assert source.data["dw"][0] == 2 + assert source.data["dh"][0] == 3 def test_image_invert_axes(self): arr = np.array([[0, 1, 2], [3, 4, 5]]) @@ -95,12 +87,8 @@ def test_image_invert_xaxis(self): assert cdata["dh"] == [1.0] assert cdata["dw"] == [1.0] - if bokeh3: - assert cdata["x"] == [-0.5] - np.testing.assert_equal(cdata["image"][0], arr[::-1]) - else: - assert cdata["x"] == [0.5] - np.testing.assert_equal(cdata["image"][0], arr[::-1, ::-1]) + assert cdata["x"] == [-0.5] + np.testing.assert_equal(cdata["image"][0], arr[::-1]) def test_image_invert_yaxis(self): arr = np.random.rand(10, 10) @@ -114,12 +102,8 @@ def test_image_invert_yaxis(self): assert cdata["dh"] == [1.0] assert cdata["dw"] == [1.0] - if bokeh3: - assert cdata["y"] == [-0.5] - np.testing.assert_equal(cdata["image"][0], arr[::-1]) - else: - assert cdata["y"] == [0.5] - np.testing.assert_equal(cdata["image"][0], arr) + assert cdata["y"] == [-0.5] + np.testing.assert_equal(cdata["image"][0], arr[::-1]) def test_rgb_invert_xaxis(self): rgb = RGB(np.random.rand(10, 10, 3)).opts(invert_xaxis=True) @@ -131,11 +115,7 @@ def test_rgb_invert_xaxis(self): assert cdata["y"] == [-0.5] assert cdata["dh"] == [1.0] assert cdata["dw"] == [1.0] - - if bokeh3: - assert cdata["x"] == [-0.5] - else: - assert cdata["x"] == [0.5] + assert cdata["x"] == [-0.5] def test_rgb_invert_yaxis(self): rgb = RGB(np.random.rand(10, 10, 3)).opts(invert_yaxis=True) @@ -147,11 +127,7 @@ def test_rgb_invert_yaxis(self): assert cdata["x"] == [-0.5] assert cdata["dh"] == [1.0] assert cdata["dw"] == [1.0] - - if bokeh3: - assert cdata["y"] == [-0.5] - else: - assert cdata["y"] == [0.5] + assert cdata["y"] == [-0.5] def test_image_stack_tuple(self): x = np.arange(0, 3) diff --git a/holoviews/tests/plotting/bokeh/test_renderer.py b/holoviews/tests/plotting/bokeh/test_renderer.py index ddd6bf3844..202b655e64 100644 --- a/holoviews/tests/plotting/bokeh/test_renderer.py +++ b/holoviews/tests/plotting/bokeh/test_renderer.py @@ -8,7 +8,6 @@ from holoviews.streams import Stream from holoviews.plotting import Renderer from holoviews.element.comparison import ComparisonTestCase -from holoviews.plotting.bokeh.util import bokeh3 from pyviz_comms import CommManager @@ -81,10 +80,7 @@ def test_get_size_tables_in_layout(self): self.assertEqual((w, h), (800, 300)) def test_theme_rendering(self): - if bokeh3: - attrs = {'figure': {'outline_line_color': '#444444'}} - else: - attrs = {'Figure': {'outline_line_color': '#444444'}} + attrs = {'figure': {'outline_line_color': '#444444'}} theme = Theme(json={'attrs' : attrs}) self.renderer.theme = theme plot = self.renderer.get_plot(Curve([])) diff --git a/setup.py b/setup.py index 5883b890a5..c4a72abb4c 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ "param >=1.12.0,<3.0", "numpy >=1.0", "pyviz_comms >=0.7.4", - "panel >=0.13.1", + "panel >=1.0", "colorcet", "packaging", "pandas >=0.20.0", @@ -35,11 +35,10 @@ 'flaky', 'matplotlib >=3, <3.8', # 3.8 breaks tests 'nbconvert', - 'bokeh', + 'bokeh >=3.1', 'pillow', 'plotly >=4.0', 'dash >=1.16', - 'codecov', 'ipython >=5.4.0', ] @@ -48,7 +47,7 @@ # of those. extras_require['tests'] = extras_require['tests_core'] + [ 'dask', - 'ibis-framework != 7.0.0', # Mapped to ibis-sqlite in setup.cfg for conda + 'ibis-framework', # Mapped to ibis-sqlite in setup.cfg for conda 'xarray >=0.10.4', 'networkx', 'shapely', @@ -60,6 +59,11 @@ 'datashader >=0.11.1', ] +extras_require['test_ci'] = [ + 'codecov', + "pytest-github-actions-annotate-failures", +] + extras_require['tests_gpu'] = extras_require['tests'] + [ 'cudf', ] @@ -73,7 +77,7 @@ # IPython Notebook + pandas + matplotlib + bokeh extras_require["recommended"] = extras_require["notebook"] + [ "matplotlib >=3", - "bokeh >=2.4.3", + "bokeh >=3.1", ] # Requirements to run all examples @@ -112,16 +116,13 @@ 'mpl_sample_data >=3.1.3', 'pscript', 'graphviz', - 'bokeh >2.2', + 'bokeh >=3.1', 'pooch', 'selenium', ] extras_require['all'] = sorted(set(sum(extras_require.values(), []))) -extras_require['bokeh2'] = ["panel ==0.14.4", "param ==1.13.0"] # Hard-pin to not pull in rc releases -extras_require['bokeh3'] = ["panel >=1.0.0"] - extras_require["build"] = [ "param >=1.7.0", "setuptools >=30.3.0",