Skip to content

Commit 95c89da

Browse files
authored
Allow customizing legend (#146)
* legend bottom right * param * Add more interactivity * update test * Apply suggestions from code review Co-authored-by: Andrew <15331990+ahuang11@users.noreply.github.com> * revert * fix? * fix tests
1 parent 4f308fe commit 95c89da

2 files changed

Lines changed: 108 additions & 6 deletions

File tree

src/hv_anndata/plotting/manifoldmap.py

Lines changed: 103 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,14 @@ class ManifoldMapConfig(TypedDict, total=False):
7878
ls: link_selections | None
7979
"""Operation and plot options for the labeller"""
8080
labeller_opts: dict[str, Any]
81+
legend_position: str
82+
"""Bokeh legend position (default: "bottom_right")"""
83+
legend_alpha: float
84+
"""Legend background opacity (default: 0.6)"""
85+
legend_font_size: str
86+
"""Legend label font size (default: "8pt")"""
87+
legend_ncols: int
88+
"""Number of legend columns (default: 1)"""
8189

8290

8391
def create_manifoldmap_plot( # noqa: C901, PLR0912, PLR0914, PLR0915
@@ -131,6 +139,10 @@ def create_manifoldmap_plot( # noqa: C901, PLR0912, PLR0914, PLR0915
131139
streams = config.get("streams", [])
132140
ls = config.get("ls")
133141
labeller_opts = config.get("labeller_opts")
142+
legend_position = config.get("legend_position", "bottom_right")
143+
legend_alpha = config.get("legend_alpha", 0.6)
144+
legend_font_size = config.get("legend_font_size", 8)
145+
legend_ncols = config.get("legend_ncols", 1)
134146

135147
color_data = adata[color_by]
136148

@@ -182,7 +194,7 @@ def create_manifoldmap_plot( # noqa: C901, PLR0912, PLR0914, PLR0915
182194
LassoSelectTool(persistent=True),
183195
],
184196
show_legend=show_legend,
185-
legend_position="right",
197+
legend_position=legend_position,
186198
)
187199

188200
# Apply different rendering based on configuration
@@ -193,7 +205,12 @@ def create_manifoldmap_plot( # noqa: C901, PLR0912, PLR0914, PLR0915
193205
# Apply datashading with different approaches for categorical vs continuous
194206
# TODO: change once https://github.com/holoviz/holoviews/issues/6799 is fixed
195207
elif categorical:
196-
plot = _apply_categorical_datashading(plot, color_by=color_by.name, cmap=cmap)
208+
plot = _apply_categorical_datashading(
209+
plot,
210+
color_by=color_by.name,
211+
cmap=cmap,
212+
legend_position=legend_position,
213+
)
197214
else:
198215
# For continuous data, take the mean
199216
aggregator = ds.mean(color_by.name)
@@ -244,11 +261,23 @@ def create_manifoldmap_plot( # noqa: C901, PLR0912, PLR0914, PLR0915
244261
final_kwargs["ylabel"] = yaxis_label
245262

246263
# Apply final options to the plot
247-
return plot.opts(title=title, show_legend=show_legend, **final_kwargs)
264+
final_opts = dict(title=title, show_legend=show_legend, **final_kwargs)
265+
if show_legend:
266+
final_opts["legend_cols"] = legend_ncols
267+
final_opts["legend_opts"] = {
268+
"background_fill_alpha": legend_alpha,
269+
"border_line_alpha": legend_alpha,
270+
"label_text_font_size": f"{legend_font_size}pt",
271+
}
272+
return plot.opts(**final_opts)
248273

249274

250275
def _apply_categorical_datashading(
251-
plot: hv.Element, *, color_by: str, cmap: Sequence[str]
276+
plot: hv.Element,
277+
*,
278+
color_by: str,
279+
cmap: Sequence[str],
280+
legend_position: str = "bottom_right",
252281
) -> hv.Element:
253282
"""Apply datashading to categorical data.
254283
@@ -260,6 +289,8 @@ def _apply_categorical_datashading(
260289
Name of the color variable
261290
cmap
262291
Colormap to use
292+
legend_position
293+
Position for the legend
263294
264295
Returns
265296
-------
@@ -288,7 +319,7 @@ def _apply_categorical_datashading(
288319
# Don't include the selector heading
289320
selector_in_hovertool=False,
290321
show_legend=True,
291-
legend_position="right",
322+
legend_position=legend_position,
292323
)
293324

294325

@@ -392,6 +423,39 @@ class ManifoldMap(pn.viewable.Viewer):
392423
plot_opts: dict = param.Dict( # type: ignore[assignment]
393424
default={}, doc="HoloViews plot options for the manifoldmap plot"
394425
)
426+
legend_position: str = param.Selector( # type: ignore[assignment]
427+
default="bottom_right",
428+
objects=[
429+
"top_left",
430+
"top_center",
431+
"top_right",
432+
"center_left",
433+
"center",
434+
"center_right",
435+
"bottom_left",
436+
"bottom_center",
437+
"bottom_right",
438+
"right",
439+
"left",
440+
],
441+
doc="Bokeh legend position",
442+
)
443+
legend_alpha: float = param.Number( # type: ignore[assignment]
444+
default=0.6,
445+
bounds=(0, 1),
446+
step=0.1,
447+
doc="Legend background opacity",
448+
)
449+
legend_font_size: int = param.Integer( # type: ignore[assignment]
450+
default=8,
451+
bounds=(5, 14),
452+
doc="Legend label font size in pt",
453+
)
454+
legend_ncols: int = param.Integer( # type: ignore[assignment]
455+
default=1,
456+
bounds=(1, 4),
457+
doc="Number of legend columns",
458+
)
395459
labeller_opts: dict = param.Dict( # type: ignore[assignment]
396460
default={}, doc="Operation and plot options for the labeller"
397461
)
@@ -584,6 +648,10 @@ def create_plot(
584648
streams=self.streams,
585649
ls=self.ls,
586650
labeller_opts=self.labeller_opts,
651+
legend_position=self.legend_position,
652+
legend_alpha=self.legend_alpha,
653+
legend_font_size=self.legend_font_size,
654+
legend_ncols=self.legend_ncols,
587655
)
588656

589657
self.plot = create_manifoldmap_plot(
@@ -613,6 +681,10 @@ def create_plot(
613681
"_replot",
614682
"plot_opts",
615683
"color_by",
684+
"legend_position",
685+
"legend_alpha",
686+
"legend_font_size",
687+
"legend_ncols",
616688
)
617689
def _plot_view(self) -> hv.Element:
618690
plot = self.create_plot(
@@ -694,6 +766,32 @@ def __panel__(self) -> pn.viewable.Viewable:
694766
sizing_mode="stretch_width",
695767
visible=self.param._categorical, # noqa: SLF001
696768
),
769+
pmui.Details(
770+
pmui.widgets.Select.from_param(
771+
self.param.legend_position,
772+
sizing_mode="stretch_width",
773+
),
774+
pn.widgets.FloatSlider.from_param(
775+
self.param.legend_alpha,
776+
name="Legend Opacity",
777+
sizing_mode="stretch_width",
778+
value_throttled=self.param.legend_alpha,
779+
),
780+
pmui.widgets.IntSlider.from_param(
781+
self.param.legend_font_size,
782+
name="Font Size (pt)",
783+
sizing_mode="stretch_width",
784+
value_throttled=self.param.legend_font_size,
785+
),
786+
pmui.widgets.IntSlider.from_param(
787+
self.param.legend_ncols,
788+
sizing_mode="stretch_width",
789+
value_throttled=self.param.legend_ncols,
790+
),
791+
title="Legend Options",
792+
sizing_mode="stretch_width",
793+
visible=self.param._categorical, # noqa: SLF001
794+
),
697795
visible=self.param.show_widgets,
698796
sx={"border": 1, "borderColor": "#e3e3e3", "borderRadius": 1},
699797
sizing_mode="stretch_width",

tests/plotting/test_manifold.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ def test_create_manifoldmap_plot_no_datashading(
7373
assert plot_opts["padding"] == 0
7474
assert len(plot_opts["tools"]) == 3
7575
assert "hover" in plot_opts["tools"]
76-
assert plot_opts["legend_position"] == "right"
76+
assert plot_opts["legend_position"] == "bottom_right"
7777
assert plot_opts["min_width"] == 300
7878
assert plot_opts["min_height"] == 300
7979
assert plot_opts["responsive"]
@@ -186,6 +186,10 @@ def test_manifoldmap_create_plot(mock_cmp: Mock, sadata: ad.AnnData) -> None:
186186
responsive=True,
187187
ls=None,
188188
labeller_opts={},
189+
legend_position="bottom_right",
190+
legend_alpha=0.6,
191+
legend_font_size=8,
192+
legend_ncols=1,
189193
)
190194

191195

0 commit comments

Comments
 (0)