@@ -518,10 +518,16 @@ def _update_on_color_by(self) -> None:
518518 if old_is_categorical != self ._categorical or not self .colormap :
519519 cmaps = CAT_CMAPS if self ._categorical else CONT_CMAPS
520520 self .param .colormap .objects = cmaps
521- if self .colormap is None or self .colormap not in cmaps :
522- self .colormap = next (iter (cmaps .values ()))
523- else :
524- self .colormap = cmaps [self .colormap ]
521+ # colormap holds a palette (list) once selected, or a name (str)
522+ # initially; membership must test values, not (unhashable) keys.
523+ current = (
524+ cmaps .get (self .colormap )
525+ if isinstance (self .colormap , str )
526+ else self .colormap
527+ )
528+ if current not in cmaps .values ():
529+ current = next (iter (cmaps .values ()))
530+ self .colormap = current
525531 self ._replot = True
526532
527533 @hold ()
@@ -540,8 +546,11 @@ def _update_axes(self) -> None:
540546 def _get_dim (self ) -> AdDim :
541547 if self .color_by_dim == "obs" :
542548 return A .obs [self .color_by ]
549+ # Color each observation by the gene's expression (a column of X, i.e. an
550+ # obs-axis vector) so it matches the obsm-based x/y coordinates.
551+ # Referencing the var axis here raises a DataError (obs vs var mismatch).
543552 if not self .var_reference :
544- return A .var . index
553+ return A .X [:, self . color_by ]
545554 var = self .adata .var .query (f'feature_name == "{ self .color_by } "' ).index
546555 if len (var ) > 1 :
547556 msg = (
@@ -621,6 +630,9 @@ def create_plot(
621630 """
622631 dr_label = self .get_reduction_label (dr_key )
623632
633+ if not color_by :
634+ return pmui .pane .Typography ("Please select a variable to color by." )
635+
624636 if x_value == y_value :
625637 return pmui .pane .Typography (
626638 "Please select different dimensions for X and Y axes."
@@ -696,6 +708,11 @@ def _plot_view(self) -> hv.Element:
696708 show_labels = self .show_labels ,
697709 cmap = self .colormap ,
698710 )
711+ # create_plot returns a Typography pane (not a HoloViews element) for
712+ # invalid axis selections, e.g. the transient x == y state while
713+ # switching reductions; only apply opts to real hv elements.
714+ if not isinstance (plot , hv .core .Dimensioned ):
715+ return plot
699716 return plot .opts (** self .plot_opts )
700717
701718 def __panel__ (self ) -> pn .viewable .Viewable :
@@ -737,21 +754,24 @@ def __panel__(self) -> pn.viewable.Viewable:
737754 stylesheets = [stylesheet ],
738755 sizing_mode = "stretch_width" ,
739756 )
740- # Create widget box
741- widgets = pmui .Column (
742- pmui .widgets .Select .from_param (
743- self .param .reduction ,
744- description = "" ,
745- sizing_mode = "stretch_width" ,
746- ),
747- pmui .widgets .Select .from_param (
748- self .param .x_axis ,
749- sizing_mode = "stretch_width" ,
750- ),
751- pmui .widgets .Select .from_param (
752- self .param .y_axis ,
753- sizing_mode = "stretch_width" ,
757+ # Create widget box in a Bokeh pn.Column so the Bokeh ColorMap isn't a
758+ # child of a React subtree (React tears out its DOM on mount). The
759+ # selectors use AutocompleteInput rather than Select: pmui's Select
760+ # renders its dropdown in an MUI portal that mis-anchors to the top-left
761+ # inside a Bokeh layout, whereas AutocompleteInput anchors correctly.
762+ select_kw = dict (
763+ min_characters = 0 ,
764+ search_strategy = "includes" ,
765+ case_sensitive = False ,
766+ description = "" ,
767+ sizing_mode = "stretch_width" ,
768+ )
769+ widgets = pn .Column (
770+ pmui .widgets .AutocompleteInput .from_param (
771+ self .param .reduction , ** select_kw
754772 ),
773+ pmui .widgets .AutocompleteInput .from_param (self .param .x_axis , ** select_kw ),
774+ pmui .widgets .AutocompleteInput .from_param (self .param .y_axis , ** select_kw ),
755775 color_by_dim ,
756776 color ,
757777 colormap ,
@@ -767,9 +787,8 @@ def __panel__(self) -> pn.viewable.Viewable:
767787 visible = self .param ._categorical , # noqa: SLF001
768788 ),
769789 pmui .Details (
770- pmui .widgets .Select .from_param (
771- self .param .legend_position ,
772- sizing_mode = "stretch_width" ,
790+ pmui .widgets .AutocompleteInput .from_param (
791+ self .param .legend_position , ** select_kw
773792 ),
774793 pn .widgets .FloatSlider .from_param (
775794 self .param .legend_alpha ,
@@ -793,7 +812,7 @@ def __panel__(self) -> pn.viewable.Viewable:
793812 visible = self .param ._categorical , # noqa: SLF001
794813 ),
795814 visible = self .param .show_widgets ,
796- sx = {"border" : 1 , "borderColor" : " #e3e3e3" , "borderRadius " : 1 },
815+ styles = {"border" : "1px solid #e3e3e3" , "border-radius " : "4px" },
797816 sizing_mode = "stretch_width" ,
798817 max_width = 280 ,
799818 min_height = 590 ,
0 commit comments