1515import matplotlib .ticker
1616import numpy as np
1717import pandas as pd
18- import scanpy as sc
1918import spatialdata as sd
2019import xarray as xr
21- from anndata import AnnData
2220from matplotlib import patheffects
2321from matplotlib .cm import ScalarMappable
2422from matplotlib .colors import Colormap , ListedColormap , Normalize
2523from scanpy ._settings import settings as sc_settings
2624from scanpy .plotting ._tools .scatterplots import _add_categorical_legend
2725from spatialdata import get_extent , get_values
28- from spatialdata ._core .query .relational_query import match_table_to_element
2926from spatialdata .models import PointsModel , ShapesModel , get_table_keys
3027from spatialdata .transformations import set_transformation
3128from spatialdata .transformations .transformations import Identity
3835 _get_colors_for_categorical_obs ,
3936 _get_linear_colormap ,
4037 _map_color_seg ,
41- _maybe_set_colors ,
4238 _prepare_cmap_norm ,
4339 _resolve_continuous_norm ,
4440 resolve_color ,
4844 _ax_show_and_transform ,
4945 _build_ds_colorbar ,
5046 _circle_buffer_quad_segs ,
47+ _color_vector_is_uniform ,
5148 _datashader_canvas_from_dataframe ,
5249 _get_extent_and_range_for_datashader_canvas ,
5350 _hex_no_alpha ,
@@ -333,7 +330,6 @@ def _add_legend_and_colorbar(
333330 ax : matplotlib .axes .SubplotBase ,
334331 cax : ScalarMappable | None ,
335332 fig_params : FigParams ,
336- adata : AnnData | None ,
337333 col_for_color : str | None ,
338334 color_spec : ColorSpec ,
339335 palette : ListedColormap | list [str ] | None ,
@@ -387,7 +383,6 @@ def _add_legend_and_colorbar(
387383 ax = ax ,
388384 cax = cax ,
389385 fig_params = fig_params ,
390- adata = adata ,
391386 value_to_plot = col_for_color ,
392387 color_source_vector = color_source_vector ,
393388 color_vector = color_vector ,
@@ -755,7 +750,6 @@ def _draw_centroids(xy: np.ndarray, radius: float | None = None) -> None:
755750 color_spec = color_spec ,
756751 norm = norm ,
757752 na_color = render_params .cmap_params .na_color ,
758- adata = table ,
759753 col_for_color = col_for_color ,
760754 palette = palette ,
761755 fig_params = fig_params ,
@@ -1025,7 +1019,6 @@ def _draw_centroids(xy: np.ndarray, radius: float | None = None) -> None:
10251019 ax = ax ,
10261020 cax = cax ,
10271021 fig_params = fig_params ,
1028- adata = table ,
10291022 col_for_color = col_for_color ,
10301023 color_spec = color_spec ,
10311024 palette = palette ,
@@ -1059,18 +1052,26 @@ def _scatter_points(
10591052 Shared scatter primitive for points and the centroid "fast mode" of shapes/labels;
10601053 ``color_vector`` is per-point hex strings or numeric values mapped through ``cmap``/``norm``.
10611054 """
1055+ # When every marker is the same resolved hex colour (no-color / single colour / collapsed grey), pass a
1056+ # scalar ``color=`` instead of a per-point ``c=`` array: matplotlib then skips its per-point colour
1057+ # machinery — the dominant cost at scale (10M points: ~9s -> ~3.7s) — for a visually identical result.
1058+ # Numeric vectors keep the ``c=``/``cmap``/``norm`` path (they need the colormap).
1059+ cv = np .asarray (color_vector )
1060+ color_kwargs : dict [str , Any ]
1061+ if cv .ndim == 1 and cv .dtype .kind in "US" and _color_vector_is_uniform (cv ):
1062+ color_kwargs = {"color" : str (cv [0 ])}
1063+ else :
1064+ color_kwargs = {"c" : color_vector , "cmap" : cmap , "norm" : norm }
10621065 return ax .scatter (
10631066 x ,
10641067 y ,
10651068 s = size ,
1066- c = color_vector ,
10671069 rasterized = sc_settings ._vector_friendly ,
1068- cmap = cmap ,
1069- norm = norm ,
10701070 alpha = alpha ,
10711071 transform = trans_data ,
10721072 zorder = zorder ,
10731073 plotnonfinite = True , # nan points should be rendered as well
1074+ ** color_kwargs ,
10741075 )
10751076
10761077
@@ -1110,7 +1111,6 @@ def _render_centroids_as_points(
11101111 color_spec : ColorSpec ,
11111112 norm : Normalize | None ,
11121113 na_color : Any ,
1113- adata : AnnData | None ,
11141114 col_for_color : str | None ,
11151115 palette : Any ,
11161116 fig_params : FigParams ,
@@ -1170,7 +1170,6 @@ def _render_centroids_as_points(
11701170 ax = ax ,
11711171 cax = cax ,
11721172 fig_params = fig_params ,
1173- adata = adata ,
11741173 col_for_color = col_for_color ,
11751174 color_spec = color_spec ,
11761175 palette = palette ,
@@ -1382,50 +1381,15 @@ def _render_points(
13821381 else points_pd_with_color
13831382 )
13841383
1385- # we construct an anndata to hack the plotting functions
1386- if table_name is None :
1387- adata = AnnData (
1388- X = points [["x" , "y" ]].values ,
1389- obs = points [coords ],
1390- dtype = points [["x" , "y" ]].values .dtype ,
1391- )
1392- else :
1393- matched_table = match_table_to_element (sdata = sdata , element_name = element , table_name = table_name )
1394- adata_obs = matched_table .obs .copy ()
1395- # if the points are colored by values in X (or a different layer), add the values to obs
1396- if col_for_color in matched_table .var_names :
1397- if table_layer is None :
1398- adata_obs [col_for_color ] = matched_table [:, col_for_color ].X .flatten ()
1399- else :
1400- adata_obs [col_for_color ] = matched_table [:, col_for_color ].layers [table_layer ].flatten ()
1401- adata = AnnData (
1402- X = points [["x" , "y" ]].values ,
1403- obs = adata_obs ,
1404- dtype = points [["x" , "y" ]].values .dtype ,
1405- uns = matched_table .uns ,
1406- )
1407- sdata_filt [table_name ] = adata
1408-
1409- # we can modify the sdata because of dealing with a copy
1384+ # Color (from a points column, table obs, or table var/X) is already materialized on `points` via
1385+ # the get_values merge above; coordinates and the legend come straight from `points`/`color_spec`,
1386+ # so no AnnData round-trip is needed. resolve_color reads the original table (kept in sdata_filt)
1387+ # for any user-defined uns palette.
14101388
14111389 # Convert back to dask dataframe to modify sdata
14121390 transformation_in_cs = sdata_filt .points [element ].attrs ["transform" ][coordinate_system ]
14131391 _reparse_points (sdata_filt , element , points_for_model , transformation_in_cs , coordinate_system , col_for_color )
14141392
1415- if col_for_color is not None :
1416- assert isinstance (col_for_color , str )
1417- cols = sc .get .obs_df (adata , [col_for_color ])
1418- # maybe set color based on type
1419- if isinstance (cols [col_for_color ].dtype , pd .CategoricalDtype ):
1420- uns_color_key = f"{ col_for_color } _colors"
1421- if uns_color_key in adata .uns :
1422- _maybe_set_colors (
1423- source = adata ,
1424- target = adata ,
1425- key = col_for_color ,
1426- palette = palette ,
1427- )
1428-
14291393 # when user specified a single color, we emulate the form of `na_color` and use it
14301394 default_color = (
14311395 render_params .color if col_for_color is None and color is not None else render_params .cmap_params .na_color
@@ -1478,9 +1442,8 @@ def _render_points(
14781442 color_spec = color_spec .filter (keep )
14791443 if int (keep .sum ()) == 0 :
14801444 return
1481- # filter the materialized points, adata, and re-register in sdata_filt
1445+ # filter the materialized points and re-register in sdata_filt
14821446 points = points [keep ].reset_index (drop = True )
1483- adata = adata [keep ]
14841447 _reparse_points (sdata_filt , element , points , transformation_in_cs , coordinate_system , col_for_color )
14851448
14861449 color_spec = color_spec .apply_transfunc (render_params .transfunc )
@@ -1550,8 +1513,8 @@ def _render_points(
15501513 update_parameters = not _mpl_ax_contains_elements (ax )
15511514 cax = _scatter_points (
15521515 ax ,
1553- adata [:, 0 ]. X . flatten (),
1554- adata [:, 1 ]. X . flatten (),
1516+ points [ "x" ]. to_numpy (),
1517+ points [ "y" ]. to_numpy (),
15551518 color_spec .color_vector ,
15561519 size = render_params .size ,
15571520 cmap = render_params .cmap_params .cmap ,
@@ -1570,7 +1533,6 @@ def _render_points(
15701533 ax = ax ,
15711534 cax = cax ,
15721535 fig_params = fig_params ,
1573- adata = adata ,
15741536 col_for_color = col_for_color ,
15751537 color_spec = color_spec ,
15761538 palette = None ,
@@ -2409,7 +2371,6 @@ def _render_labels(
24092371 ),
24102372 norm = render_params .cmap_params .fresh_norm (), # ax.scatter autoscales in place; don't mutate the shared norm
24112373 na_color = na_color ,
2412- adata = table if table_name is not None else None ,
24132374 col_for_color = col_for_color ,
24142375 palette = palette ,
24152376 fig_params = fig_params ,
@@ -2510,7 +2471,6 @@ def _draw_labels(
25102471 ax = ax ,
25112472 cax = cax ,
25122473 fig_params = fig_params ,
2513- adata = table ,
25142474 col_for_color = col_for_color ,
25152475 color_spec = color_spec ,
25162476 palette = palette ,
0 commit comments