Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 98 additions & 0 deletions examples/reference/features/bokeh/Sizebar.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "df229fb9-0a56-47b4-a8d2-72cab8782624",
"metadata": {},
"source": [
"#### **Title**: Sizebar\n",
"\n",
"**Dependencies**: Bokeh >= 3.8.0\n",
"\n",
"**Backends**: [Bokeh](./Sizebar.ipynb)\n",
"\n",
"The `sizebar` feature makes a sizebar on the element to help gauge the size of circles on a plot. For this to work it need to have `radius` option set."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "1cee420f-3ad1-4b0c-ae66-b26d726d7a0a",
"metadata": {},
"outputs": [],
"source": [
"import numpy as np\n",
"\n",
"import holoviews as hv\n",
"from holoviews.plotting.util import rgb2hex\n",
"\n",
"rng = np.random.default_rng(1)\n",
"N = 100\n",
"x = rng.random(size=N) * 100\n",
"y = rng.random(size=N) * 100\n",
"radii = rng.random(size=N) * 10\n",
"colors = np.array(\n",
" [(r, g, 150) for r, g in zip(50 + 2 * x, 30 + 2 * y)], dtype=np.uint8\n",
")\n",
"colors = list(map(rgb2hex, colors / 256))\n",
"\n",
"hv.extension(\"bokeh\")\n",
"\n",
"points = hv.Points((x, y, radii, colors), vdims=[\"radii\", \"colors\"])\n",
"points.opts(\n",
" color=\"colors\", radius=\"radii\", alpha=0.6, height=500, width=500, sizebar=True\n",
")"
]
},
{
"cell_type": "markdown",
"id": "d58cb23b-206b-4368-aed7-b083a7766320",
"metadata": {},
"source": [
"Zoom in and out to see the sizebar dynamically adjust."
]
},
{
"cell_type": "markdown",
"id": "39af797f-4ea2-449b-96c7-eb7d0da8394e",
"metadata": {},
"source": [
"### Customization\n",
"\n",
"In the plot above, you can see that we applied the sizebar. Below are further customization options for the sizebar:\n",
"\n",
"- The `sizebar_location` parameter defines the positioning anchor for the sizebar. Options include `\"above\"`, `\"below\"`, `\"left\"`, `\"right\"`, and `\"center\"`.\n",
"- The `sizebar_orientation` parameter controls whether the sizebar is displayed `\"horizontal\"` (default) or `\"vertical\"`.\n",
"- The `sizebar_color` parameter specifies the glyph color in the sizebar (default: `\"black\"`).\n",
"- The `sizebar_alpha` parameter sets the transparency of the sizebar glyph, between `0` and `1` (default: `0.6`).\n",
"- The `sizebar_bounds` parameter can be used to manually set the bounds of the sizebar as a tuple `(min, max)`. By default, `None` lets the bounds be determined automatically from the data.\n",
"- The `sizebar_opts` parameter enables specific styling options for the sizebar. See the [Bokeh SizeBar documentation](https://docs.bokeh.org/en/latest/docs/reference/models/annotations.html#bokeh.models.SizeBar) for available options.\n",
"\n",
"All these parameters are only utilized if `sizebar=True` is set in `.opts()`."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "0ab063b3-d1cb-40b4-9fa7-a5860932c16d",
"metadata": {},
"outputs": [],
"source": [
"points.clone().opts(\n",
" sizebar_location=\"right\",\n",
" sizebar_orientation=\"vertical\",\n",
" sizebar_color=\"red\",\n",
" sizebar_alpha=1,\n",
")"
]
}
],
"metadata": {
"language_info": {
"name": "python",
"pygments_lexer": "ipython3"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
87 changes: 85 additions & 2 deletions holoviews/plotting/bokeh/chart.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from ...core.util import dimension_sanitizer, isdatetime, isfinite
from ...operation import interpolate_curve
from ...util.transform import dim
from ...util.warnings import warn
from ..mixins import AreaMixin, BarsMixin, SpikesMixin
from ..util import compute_sizes, get_min_distance
from .element import ColorbarPlot, ElementPlot, LegendPlot, OverlayPlot
Expand All @@ -24,10 +25,92 @@
mpl_to_bokeh,
rgb2hex,
)
from .util import categorize_array
from .util import BOKEH_GE_3_8_0, categorize_array


class PointPlot(LegendPlot, ColorbarPlot):
class SizebarMixin(LegendPlot):

sizebar = param.Boolean(default=False, doc="""
Whether to display a sizebar.""")

sizebar_location = param.Selector(
default="below",
objects=["above", "below", "left", "right", "center"],
doc="""
Location anchor for positioning scale bar, default to 'below'.

The sizebar_location is only used if sizebar is True.""")

sizebar_orientation = param.Selector(default="horizontal", objects=["horizontal", "vertical"], doc="""
Orientation of the sizebar, default to 'horizontal'.

The sizebar_orientation is only used if sizebar is True.
""")

sizebar_color = param.String(default="black", doc="""
Color of the glyph in the sizebar, default to 'black'.

The sizebar_color is only used if sizebar is True.""")

sizebar_alpha = param.Number(default=0.6, bounds=(0, 1), doc="""
Alpha value of the glyph in the sizebar, default to 0.6.

The sizebar_alpha is only used if sizebar is True.""")

sizebar_bounds = param.NumericTuple(default=None, length=2, doc="""
Bounds of the sizebar, default to None which will automatically
determine the bounds based on the data.

The sizebar_bounds is only used if sizebar is True.""")

sizebar_opts = param.Dict(
default={}, doc="""
Allows setting specific styling options for the sizebar.
See https://docs.bokeh.org/en/latest/docs/reference/models/annotations.html#bokeh.models.SizeBar
for more information.

The sizebar_opts is only used if sizebar is True.""")

def _init_glyph(self, plot, mapping, properties):
renderer, glyph = super()._init_glyph(plot, mapping, properties)
if self.sizebar:
self._draw_sizebar(plot, renderer, glyph)
return renderer, glyph

def _draw_sizebar(self, plot, renderer, glyph):
if not BOKEH_GE_3_8_0:
raise RuntimeError("Sizebar requires Bokeh >= 3.8.0")

from bokeh.models import SizeBar
from bokeh.models.glyph import RadialGlyph

if not isinstance(glyph, RadialGlyph):
if isinstance(self, PointPlot):
# PointPlot have both Scatter and Circle plot methods
msg = "For sizebar to work you need to have radius set"
warn(msg, category=RuntimeWarning)
return

sizebar_kwargs = dict(
self.sizebar_opts,
renderer=renderer,
orientation=self.sizebar_orientation,
glyph_fill_color=self.sizebar_color,
glyph_fill_alpha=self.sizebar_alpha,
bounds=self.sizebar_bounds or "auto",
)

if "width" not in sizebar_kwargs: # Width is the primary axis
match (self.sizebar_location, self.sizebar_orientation):
case (("above" | "below"), "horizontal") | (("left" | "right"), "vertical"):
sizebar_kwargs["width"] = "max"

sizebar = SizeBar(**sizebar_kwargs)
plot.add_layout(sizebar, self.sizebar_location)
self.handles['sizebar'] = sizebar


class PointPlot(SizebarMixin, ColorbarPlot):

jitter = param.Number(default=None, bounds=(0, None), doc="""
The amount of jitter to apply to offset the points along the x-axis.""")
Expand Down
2 changes: 1 addition & 1 deletion holoviews/plotting/bokeh/element.py
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ class ElementPlot(BokehPlot, GenericElementPlot):
scalebar = param.Boolean(default=False, doc="""
Whether to display a scalebar.""")

scalebar_range =param.Selector(default="x", objects=["x", "y"], doc="""
scalebar_range = param.Selector(default="x", objects=["x", "y"], doc="""
Whether to have the scalebar on the x or y axis.""")

scalebar_unit = param.ClassSelector(default=None, class_=(str, tuple), doc="""
Expand Down
93 changes: 92 additions & 1 deletion holoviews/tests/plotting/bokeh/test_pointplot.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import numpy as np
import pandas as pd
import pytest
from bokeh.models import (
CategoricalColorMapper,
Circle,
Expand All @@ -14,7 +15,8 @@
from holoviews.core import NdOverlay
from holoviews.core.options import Cycle
from holoviews.element import Points
from holoviews.plotting.bokeh.util import property_to_dict
from holoviews.plotting.bokeh.chart import SizebarMixin
from holoviews.plotting.bokeh.util import BOKEH_GE_3_8_0, property_to_dict
from holoviews.streams import Stream

from ..utils import ParamLogStream
Expand Down Expand Up @@ -581,3 +583,92 @@ def test_point_radius_then_size_then_radius(self):
handles = bokeh_renderer.get_plot(plot).handles
glyph = handles["glyph"]
assert isinstance(glyph, Circle)


@pytest.mark.skipif(not BOKEH_GE_3_8_0, reason="Needs Bokeh 3.8")
class TestSizeBar:

def setup_method(self):
np.random.seed(1)
N = 100
x = np.random.random(size=N) * 100
y = np.random.random(size=N) * 100
radii = np.random.random(size=N) * 10
self.plot = hv.Points((x, y, radii), vdims=["radii"]).opts(radius="radii")

def get_handles(self):
return bokeh_renderer.get_plot(self.plot).handles

def get_sizebar(self):
return self.get_handles().get("sizebar")

def test_init(self):
from bokeh.models import SizeBar

assert self.get_sizebar() is None

self.plot.opts(sizebar=True)
assert isinstance(self.get_sizebar(), SizeBar)

@pytest.mark.parametrize("location", [SizebarMixin.param.sizebar_location.default])
def test_location(self, location):
self.plot.opts(sizebar=True, sizebar_location=location)
handles = self.get_handles()
assert handles["sizebar"] in getattr(handles["plot"], location)

@pytest.mark.parametrize("orientation", [SizebarMixin.param.sizebar_orientation.default])
def test_orientation(self, orientation):
self.plot.opts(sizebar=True, sizebar_orientation=orientation)
assert self.get_sizebar().orientation == orientation

def test_style(self):
self.plot.opts(sizebar=True, sizebar_color="red", sizebar_alpha = 0.1)
sizebar = self.get_sizebar()
assert sizebar.glyph_fill_alpha == 0.1
assert sizebar.glyph_fill_color == "red"

@pytest.mark.parametrize("bounds", [(0, 10), (0, float("inf"))])
def test_bounds(self, bounds):
self.plot.opts(sizebar=True, sizebar_bounds=bounds)
assert self.get_sizebar().bounds == bounds

@pytest.mark.parametrize("location", [SizebarMixin.param.sizebar_location.default])
@pytest.mark.parametrize("orientation", [SizebarMixin.param.sizebar_orientation.default])
@pytest.mark.parametrize("set_width", [True, False])
def test_max_size(self, location, orientation, set_width):
self.plot.opts(sizebar=True, sizebar_location=location, sizebar_orientation=orientation)
if set_width:
self.plot.opts(sizebar_opts={"width": 216}) # Using 216 as it will never be a default

width = self.get_sizebar().width
match (location, orientation, set_width):
case ("above" | "below", "horizontal", False):
assert width == "max"
case ("left" | "right", "vertical", False):
assert width == "max"
case _:
if set_width:
assert width == 216
else:
assert width != "max"

def test_overlay(self):
# Mainly just to check it does not raise an exception
p1 = self.plot.opts(sizebar=True)
p2 = hv.Curve([1, 2, 3])
combined = p1 * p2

bk_element = hv.render(combined)
assert len(bk_element.renderers) == 2 # the two plots
assert len(bk_element.below) == 2 # axis and sizebar

def test_layout(self):
# Mainly just to check it does not raise an exception
p1 = self.plot.opts(sizebar=True)
p2 = hv.Curve([1, 2, 3])
combined = p1 + p2

bk_element = hv.render(combined)
assert len(bk_element.children) == 2
assert len(bk_element.children[0][0].below) == 2
assert len(bk_element.children[1][0].below) == 1
Loading