@@ -458,6 +458,92 @@ def _extract_color_column(
458458 return values .reindex (element .index )
459459
460460
461+ def _resolve_color_origins (
462+ value_to_plot : str | None ,
463+ sdata : sd .SpatialData ,
464+ element_name : list [str ] | str | None ,
465+ table_name : str | None ,
466+ ) -> tuple [list [Any ], bool ]:
467+ """Locate the color column and resolve df-vs-table shadowing; raise if it lives in >1 place."""
468+ origins = _locate_value (value_key = value_to_plot , sdata = sdata , element_name = element_name , table_name = table_name )
469+ # An explicit `table_name=` disambiguates a column present in both the element df and the table.
470+ explicit_table_shadows_df = table_name is not None and any (o .origin == "df" for o in origins )
471+ if explicit_table_shadows_df :
472+ origins = [o for o in origins if o .origin != "df" ]
473+ if len (origins ) > 1 :
474+ raise ValueError (
475+ f"Color key '{ value_to_plot } ' for element '{ element_name } ' was found in multiple locations: { origins } . "
476+ "Please keep it in exactly one place (preferably on the points parquet for speed) to avoid ambiguity."
477+ )
478+ return origins , explicit_table_shadows_df
479+
480+
481+ def _fetch_color_source_vector (
482+ sdata : sd .SpatialData ,
483+ element : SpatialElement | None ,
484+ element_name : list [str ] | str | None ,
485+ value_to_plot : str | None ,
486+ table_name : str | None ,
487+ table_layer : str | None ,
488+ origins : list [Any ],
489+ explicit_table_shadows_df : bool ,
490+ preloaded_color_data : pd .Series | None ,
491+ ) -> ArrayLike | pd .Series :
492+ """Read the raw color column, preferring a direct aligned read over a whole-table join."""
493+ if preloaded_color_data is not None :
494+ return preloaded_color_data
495+ if (
496+ isinstance (element , GeoDataFrame )
497+ and isinstance (element_name , str )
498+ and table_name is not None
499+ and table_name in sdata .tables
500+ and origins [0 ].origin in ("obs" , "var" )
501+ ):
502+ # Fast path: read the single aligned column directly instead of joining/copying the
503+ # whole annotating table (the join's out-of-order sparse row-gather dominates large renders).
504+ return _extract_color_column (
505+ sdata [table_name ],
506+ value_to_plot ,
507+ origin = origins [0 ].origin ,
508+ element = element ,
509+ element_name = element_name ,
510+ table_layer = table_layer ,
511+ )
512+ if explicit_table_shadows_df :
513+ # Pass the table as `element` so upstream `get_values` skips the
514+ # element-column lookup and avoids the multi-origin error.
515+ return get_values (
516+ value_key = value_to_plot ,
517+ element = sdata [table_name ],
518+ element_name = element_name ,
519+ table_layer = table_layer ,
520+ )[value_to_plot ]
521+ return get_values (
522+ value_key = value_to_plot ,
523+ sdata = sdata ,
524+ element_name = element_name ,
525+ table_name = table_name ,
526+ table_layer = table_layer ,
527+ )[value_to_plot ]
528+
529+
530+ def _resolve_color_table (value_from_element : bool , table_name : str | None , sdata : sd .SpatialData ) -> str | None :
531+ """Pick which table supplies .uns colors: none if the value is element-local, else the named or sole table."""
532+ if value_from_element :
533+ return None
534+ if table_name is not None :
535+ if table_name in sdata .tables :
536+ return table_name
537+ logger .warning (f"Table '{ table_name } ' not found in `sdata.tables`. Falling back to default behavior." )
538+ return None
539+ table_keys = list (sdata .tables .keys ())
540+ if not table_keys :
541+ return None
542+ if len (table_keys ) > 1 :
543+ logger .warning (f"No table name provided, using '{ table_keys [0 ]} ' as fallback for color mapping." )
544+ return table_keys [0 ]
545+
546+
461547def _set_color_source_vec (
462548 sdata : sd .SpatialData ,
463549 element : SpatialElement | None ,
@@ -478,26 +564,7 @@ def _set_color_source_vec(
478564 color = np .full (len (element ), na_color .get_hex_with_alpha ())
479565 return color , color , False
480566
481- # Figure out where to get the color from
482- origins = _locate_value (
483- value_key = value_to_plot ,
484- sdata = sdata ,
485- element_name = element_name ,
486- table_name = table_name ,
487- )
488-
489- # When both the element's own dataframe and the chosen table contain a
490- # column with this name, an explicit `table_name=` resolves the ambiguity —
491- # keep only the table origin and skip the multi-origin error below.
492- explicit_table_shadows_df = table_name is not None and any (o .origin == "df" for o in origins )
493- if explicit_table_shadows_df :
494- origins = [o for o in origins if o .origin != "df" ]
495-
496- if len (origins ) > 1 :
497- raise ValueError (
498- f"Color key '{ value_to_plot } ' for element '{ element_name } ' was found in multiple locations: { origins } . "
499- "Please keep it in exactly one place (preferably on the points parquet for speed) to avoid ambiguity."
500- )
567+ origins , explicit_table_shadows_df = _resolve_color_origins (value_to_plot , sdata , element_name , table_name )
501568
502569 if len (origins ) == 1 and value_to_plot is not None :
503570 if table_name is not None :
@@ -507,42 +574,17 @@ def _set_color_source_vec(
507574 element_name = element_name ,
508575 table_name = table_name ,
509576 )
510- if preloaded_color_data is not None :
511- color_source_vector = preloaded_color_data
512- elif (
513- isinstance (element , GeoDataFrame )
514- and isinstance (element_name , str )
515- and table_name is not None
516- and table_name in sdata .tables
517- and origins [0 ].origin in ("obs" , "var" )
518- ):
519- # Fast path: read the single aligned column directly instead of joining/copying the
520- # whole annotating table (the join's out-of-order sparse row-gather dominates large renders).
521- color_source_vector = _extract_color_column (
522- sdata [table_name ],
523- value_to_plot ,
524- origin = origins [0 ].origin ,
525- element = element ,
526- element_name = element_name ,
527- table_layer = table_layer ,
528- )
529- elif explicit_table_shadows_df :
530- # Pass the table as `element` so upstream `get_values` skips the
531- # element-column lookup and avoids the multi-origin error.
532- color_source_vector = get_values (
533- value_key = value_to_plot ,
534- element = sdata [table_name ],
535- element_name = element_name ,
536- table_layer = table_layer ,
537- )[value_to_plot ]
538- else :
539- color_source_vector = get_values (
540- value_key = value_to_plot ,
541- sdata = sdata ,
542- element_name = element_name ,
543- table_name = table_name ,
544- table_layer = table_layer ,
545- )[value_to_plot ]
577+ color_source_vector = _fetch_color_source_vector (
578+ sdata = sdata ,
579+ element = element ,
580+ element_name = element_name ,
581+ value_to_plot = value_to_plot ,
582+ table_name = table_name ,
583+ table_layer = table_layer ,
584+ origins = origins ,
585+ explicit_table_shadows_df = explicit_table_shadows_df ,
586+ preloaded_color_data = preloaded_color_data ,
587+ )
546588
547589 color_series = (
548590 color_source_vector if isinstance (color_source_vector , pd .Series ) else pd .Series (color_source_vector )
@@ -588,29 +630,8 @@ def _set_color_source_vec(
588630 processed = processed .reorder_categories (sorted (processed .categories ))
589631 color_source_vector = processed # convert, e.g., `pd.Series`
590632
591- # When the value lives on the element's own DataFrame (origin="df"),
592- # there is no reason to look up a table for .uns colors.
593633 value_from_element = origins [0 ].origin == "df"
594-
595- # Use the provided table_name parameter, fall back to only one present
596- table_to_use : str | None
597- if value_from_element :
598- table_to_use = None
599- elif table_name is not None and table_name in sdata .tables :
600- table_to_use = table_name
601- elif table_name is not None and table_name not in sdata .tables :
602- logger .warning (f"Table '{ table_name } ' not found in `sdata.tables`. Falling back to default behavior." )
603- table_to_use = None
604- else :
605- table_keys = list (sdata .tables .keys ())
606- if len (table_keys ) == 1 :
607- table_to_use = table_keys [0 ]
608- elif len (table_keys ) > 1 :
609- table_to_use = table_keys [0 ]
610- logger .warning (f"No table name provided, using '{ table_to_use } ' as fallback for color mapping." )
611- else :
612- table_to_use = None
613-
634+ table_to_use = _resolve_color_table (value_from_element , table_name , sdata )
614635 adata_for_mapping = sdata [table_to_use ] if table_to_use is not None else None
615636
616637 # Check if custom colors exist in the resolved table's .uns slot
0 commit comments