Skip to content

Commit

Permalink
Add support for cticks (#6257)
Browse files Browse the repository at this point in the history
  • Loading branch information
ahuang11 authored Jun 4, 2024
1 parent 6e501e9 commit d74dc71
Show file tree
Hide file tree
Showing 4 changed files with 100 additions and 29 deletions.
45 changes: 22 additions & 23 deletions holoviews/plotting/bokeh/element.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@
get_axis_class,
get_scale,
get_tab_title,
get_ticker_axis_props,
glyph_order,
hold_policy,
match_ax_type,
Expand Down Expand Up @@ -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],
Expand Down Expand Up @@ -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()):
Expand Down Expand Up @@ -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.""")

Expand Down Expand Up @@ -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:
Expand Down
29 changes: 29 additions & 0 deletions holoviews/plotting/bokeh/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
10 changes: 4 additions & 6 deletions holoviews/plotting/plot.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
45 changes: 45 additions & 0 deletions holoviews/tests/plotting/bokeh/test_elementplot.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):

Expand Down

0 comments on commit d74dc71

Please sign in to comment.