@@ -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
8391def 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
250275def _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" ,
0 commit comments