diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py index e46be71504..48138bfa64 100644 --- a/holoviews/plotting/bokeh/element.py +++ b/holoviews/plotting/bokeh/element.py @@ -76,6 +76,7 @@ get_axis_class, get_scale, get_tab_title, + get_ticker_axis_props, glyph_order, hold_policy, match_ax_type, @@ -246,6 +247,16 @@ class ElementPlot(BokehPlot, GenericElementPlot): hover_mode = param.ObjectSelector(default='mouse', objects=['mouse', 'vline', 'hline'], doc=""" The hover mode determines how the hover tool is activated.""") + xticks = param.ClassSelector(class_=(int, list, tuple, np.ndarray, Ticker), default=None, doc=""" + Ticks along x-axis specified as an integer, explicit list of + tick locations, or bokeh Ticker object. If set to None default + bokeh ticking behavior is applied.""") + + yticks = param.ClassSelector(class_=(int, list, tuple, np.ndarray, Ticker), default=None, doc=""" + Ticks along y-axis specified as an integer, explicit list of + tick locations, or bokeh Ticker object. If set to None default + bokeh ticking behavior is applied.""") + toolbar = param.ObjectSelector(default='right', objects=["above", "below", "left", "right", "disable", None], @@ -1051,29 +1062,9 @@ def _axis_properties(self, axis, key, plot, dimension=None, if rotation: axis_props['major_label_orientation'] = np.radians(rotation) ticker = self.xticks if axis == 'x' else self.yticks - if isinstance(ticker, np.ndarray): - ticker = list(ticker) - if isinstance(ticker, Ticker): - axis_props['ticker'] = ticker - elif isinstance(ticker, int): - axis_props['ticker'] = BasicTicker(desired_num_ticks=ticker) - elif isinstance(ticker, (tuple, list)): - if all(isinstance(t, tuple) for t in ticker): - ticks, labels = zip(*ticker) - # Ensure floats which are integers are serialized as ints - # because in JS the lookup fails otherwise - ticks = [int(t) if isinstance(t, float) and t.is_integer() else t - for t in ticks] - labels = [l if isinstance(l, str) else str(l) - for l in labels] - else: - ticks, labels = ticker, None - if ticks and util.isdatetime(ticks[0]): - ticks = [util.dt_to_int(tick, 'ms') for tick in ticks] - axis_props['ticker'] = FixedTicker(ticks=ticks) - if labels is not None: - axis_props['major_label_overrides'] = dict(zip(ticks, labels)) - elif self._subcoord_overlaid and axis == 'y': + if not (self._subcoord_overlaid and axis == 'y'): + axis_props.update(get_ticker_axis_props(ticker)) + else: ticks, labels = [], [] idx = 0 for el, sp in zip(self.current_frame, self.subplots.values()): @@ -2470,6 +2461,11 @@ class ColorbarPlot(ElementPlot): #FFFFFFFF or a length 3 or length 4 tuple specifying values in the range 0-1 or a named HTML color.""") + cticks = param.ClassSelector(class_=(int, list, tuple, np.ndarray, Ticker), default=None, doc=""" + Ticks along colorbar-axis specified as an integer, explicit list of + tick locations, or bokeh Ticker object. If set to None default + bokeh ticking behavior is applied.""") + logz = param.Boolean(default=False, doc=""" Whether to apply log scaling to the z-axis.""") @@ -2513,6 +2509,9 @@ def _draw_colorbar(self, plot, color_mapper, prefix=''): if self.cformatter is not None: self.colorbar_opts.update({'formatter': wrap_formatter(self.cformatter, 'c')}) + if self.cticks is not None: + self.colorbar_opts.update(get_ticker_axis_props(self.cticks)) + for tk in ['cticks', 'ticks']: ticksize = self._fontsize(tk, common=False).get('fontsize') if ticksize is not None: diff --git a/holoviews/plotting/bokeh/util.py b/holoviews/plotting/bokeh/util.py index 9b5a662b90..5d25b2349e 100644 --- a/holoviews/plotting/bokeh/util.py +++ b/holoviews/plotting/bokeh/util.py @@ -39,12 +39,14 @@ ) from bokeh.models.formatters import PrintfTickFormatter, TickFormatter from bokeh.models.scales import CategoricalScale, LinearScale, LogScale +from bokeh.models.tickers import BasicTicker, FixedTicker, Ticker from bokeh.models.widgets import DataTable, Div from bokeh.plotting import figure from bokeh.themes import built_in_themes from bokeh.themes.theme import Theme from packaging.version import Version +from ...core import util from ...core.layout import Layout from ...core.ndmapping import NdMapping from ...core.overlay import NdOverlay, Overlay @@ -1167,3 +1169,30 @@ def dtype_fix_hook(plot, element): for k, v in data.items(): if hasattr(v, "dtype") and v.dtype.kind == "U": data[k] = v.tolist() + + +def get_ticker_axis_props(ticker): + axis_props = {} + if isinstance(ticker, np.ndarray): + ticker = list(ticker) + if isinstance(ticker, Ticker): + axis_props['ticker'] = ticker + elif isinstance(ticker, int): + axis_props['ticker'] = BasicTicker(desired_num_ticks=ticker) + elif isinstance(ticker, (tuple, list)): + if all(isinstance(t, tuple) for t in ticker): + ticks, labels = zip(*ticker) + # Ensure floats which are integers are serialized as ints + # because in JS the lookup fails otherwise + ticks = [int(t) if isinstance(t, float) and t.is_integer() else t + for t in ticks] + labels = [l if isinstance(l, str) else str(l) + for l in labels] + else: + ticks, labels = ticker, None + if ticks and util.isdatetime(ticks[0]): + ticks = [util.dt_to_int(tick, 'ms') for tick in ticks] + axis_props['ticker'] = FixedTicker(ticks=ticks) + if labels is not None: + axis_props['major_label_overrides'] = dict(zip(ticks, labels)) + return axis_props diff --git a/holoviews/plotting/plot.py b/holoviews/plotting/plot.py index 4bd69f6756..cc9bdbc489 100644 --- a/holoviews/plotting/plot.py +++ b/holoviews/plotting/plot.py @@ -1132,15 +1132,13 @@ class GenericElementPlot(DimensionedPlot): yrotation = param.Integer(default=None, bounds=(0, 360), doc=""" Rotation angle of the yticks.""") - xticks = param.Parameter(default=None, doc=""" + xticks = param.ClassSelector(class_=(int, list, tuple, np.ndarray), default=None, doc=""" Ticks along x-axis specified as an integer, explicit list of - tick locations, or bokeh Ticker object. If set to None default - bokeh ticking behavior is applied.""") + tick locations. If set to None default ticking behavior is applied.""") - yticks = param.Parameter(default=None, doc=""" + yticks = param.ClassSelector(class_=(int, list, tuple, np.ndarray), default=None, doc=""" Ticks along y-axis specified as an integer, explicit list of - tick locations, or bokeh Ticker object. If set to None - default bokeh ticking behavior is applied.""") + tick locations. If set to None default ticking behavior is applied.""") # A dictionary mapping of the plot methods used to draw the # glyphs corresponding to the ElementPlot, can support two diff --git a/holoviews/tests/plotting/bokeh/test_elementplot.py b/holoviews/tests/plotting/bokeh/test_elementplot.py index 9cef2f3b6c..af8e52b243 100644 --- a/holoviews/tests/plotting/bokeh/test_elementplot.py +++ b/holoviews/tests/plotting/bokeh/test_elementplot.py @@ -885,6 +885,51 @@ def test_explicit_categorical_cmap_on_integer_data(self): self.assertEqual(cmapper.factors, ['0', '1', '2', '3']) self.assertEqual(cmapper.palette, ['blue', 'red', 'green', 'purple']) + def test_cticks_int(self): + img = Image(np.array([[0, 1], [2, 3]])).opts(cticks=3, colorbar=True) + plot = bokeh_renderer.get_plot(img) + colorbar = plot.handles["colorbar"] + ticker = colorbar.ticker + assert ticker.desired_num_ticks == 3 + + def test_cticks_list(self): + img = Image(np.array([[0, 1], [2, 3]])).opts(cticks=[1, 2], colorbar=True) + plot = bokeh_renderer.get_plot(img) + colorbar = plot.handles["colorbar"] + ticker = colorbar.ticker + assert ticker.ticks == [1, 2] + + def test_cticks_tuple(self): + img = Image(np.array([[0, 1], [2, 3]])).opts(cticks=(1, 2), colorbar=True) + plot = bokeh_renderer.get_plot(img) + colorbar = plot.handles["colorbar"] + ticker = colorbar.ticker + assert ticker.ticks == (1, 2) + + def test_cticks_np_array(self): + img = Image(np.array([[0, 1], [2, 3]])).opts(cticks=np.array([1, 2]), colorbar=True) + plot = bokeh_renderer.get_plot(img) + colorbar = plot.handles["colorbar"] + ticker = colorbar.ticker + assert ticker.ticks == [1, 2] + + def test_cticks_labels(self): + img = Image(np.array([[0, 1], [2, 3]])).opts(cticks=[(1, "A"), (2, "B")], colorbar=True) + plot = bokeh_renderer.get_plot(img) + colorbar = plot.handles["colorbar"] + assert colorbar.major_label_overrides == {1: "A", 2: "B"} + ticker = colorbar.ticker + assert ticker.ticks == [1, 2] + + def test_cticks_ticker(self): + img = Image(np.array([[0, 1], [2, 3]])).opts( + cticks=FixedTicker(ticks=[0, 1]), colorbar=True + ) + plot = bokeh_renderer.get_plot(img) + colorbar = plot.handles["colorbar"] + ticker = colorbar.ticker + assert ticker.ticks == [0, 1] + class TestOverlayPlot(TestBokehPlot):