|
34 | 34 | from spatialdata_plot._logging import _log_context, logger |
35 | 35 | from spatialdata_plot.pl._color import ( |
36 | 36 | ColorSpec, |
| 37 | + ColorType, |
37 | 38 | _get_colors_for_categorical_obs, |
38 | 39 | _get_linear_colormap, |
39 | 40 | _map_color_seg, |
@@ -968,9 +969,11 @@ def _render_shapes( |
968 | 969 | path.vertices = trans.transform(path.vertices) |
969 | 970 |
|
970 | 971 | if color_spec.is_continuous: |
971 | | - # Colorbar range from the same resolved norm the fill pixels use. |
| 972 | + # Colorbar uses the same resolved norm the fill pixels use, including its subclass |
| 973 | + # (LogNorm/PowerNorm) — set_norm, not set_clim, which would leave the collection's |
| 974 | + # default linear Normalize in place and mis-scale the bar for non-linear norms. |
972 | 975 | used_norm = _resolve_continuous_norm(color_spec.color_vector, render_params.cmap_params) |
973 | | - _cax.set_clim(vmin=used_norm.vmin, vmax=used_norm.vmax) |
| 976 | + _cax.set_norm(used_norm) |
974 | 977 |
|
975 | 978 | _add_legend_and_colorbar( |
976 | 979 | ax=ax, |
@@ -2297,25 +2300,29 @@ def _render_labels( |
2297 | 2300 | # (`_map_color_seg` Case C) instead of collapsing every dot to a single na_color. |
2298 | 2301 | point_color_vector = np.random.default_rng(42).random((len(point_ids), 3)) |
2299 | 2302 | point_color_source_vector = None |
| 2303 | + point_colortype: ColorType = "none" # colour is not data-driven |
2300 | 2304 | allow_datashader = False |
2301 | 2305 | elif len(color_spec.color_vector) == len(instance_id): |
2302 | | - # data-driven colour is per-instance |
| 2306 | + # data-driven colour is per-instance; carry the upstream classification (invariant under mask) |
2303 | 2307 | point_color_vector = np.asarray(color_spec.color_vector)[keep] |
2304 | 2308 | point_color_source_vector = None if color_spec.source_vector is None else color_spec.source_vector[keep] |
| 2309 | + point_colortype = color_spec.colortype |
2305 | 2310 | else: |
2306 | 2311 | # literal colour / user-set na_color -> one colour per centroid |
2307 | 2312 | point_color_vector = np.full(len(point_ids), na_color.get_hex_with_alpha()) |
2308 | 2313 | point_color_source_vector = None |
| 2314 | + point_colortype = "none" # colour is not data-driven |
2309 | 2315 | # transform rendered-raster intrinsic centroids to coordinate-system coords |
2310 | 2316 | xy = trans.transform(np.column_stack([centroids["x"].to_numpy(), centroids["y"].to_numpy()])) |
2311 | 2317 | _render_centroids_as_points( |
2312 | 2318 | ax, |
2313 | 2319 | render_params, |
2314 | 2320 | x=xy[:, 0], |
2315 | 2321 | y=xy[:, 1], |
2316 | | - # point colours are derived fresh; classify by source so the spec stays self-consistent |
| 2322 | + # point colours are derived fresh; carry the resolved colortype so the spec invariant |
| 2323 | + # (categorical => pd.Categorical source) holds and `none` is not mislabelled categorical |
2317 | 2324 | color_spec=ColorSpec( |
2318 | | - "categorical" if point_color_source_vector is not None else "continuous", |
| 2325 | + point_colortype, |
2319 | 2326 | point_color_source_vector, |
2320 | 2327 | point_color_vector, |
2321 | 2328 | ), |
@@ -2352,20 +2359,17 @@ def _draw_labels( |
2352 | 2359 | outline_color_source_vector=outline_color_source_vector if seg_boundaries else None, |
2353 | 2360 | ) |
2354 | 2361 |
|
2355 | | - # labels is pre-baked RGB; cmap/norm only drive the colorbar, so feed the same resolved norm. |
2356 | | - cax = ax.imshow( |
2357 | | - labels, |
2358 | | - rasterized=True, |
2359 | | - cmap=None if color_spec.is_categorical else render_params.cmap_params.cmap, |
2360 | | - norm=None |
2361 | | - if color_spec.is_categorical |
2362 | | - else _resolve_continuous_norm(color_spec.color_vector, render_params.cmap_params), |
2363 | | - alpha=alpha, |
2364 | | - origin="lower", |
2365 | | - zorder=render_params.zorder, |
2366 | | - ) |
2367 | | - cax.set_transform(trans_data) |
2368 | | - return cax |
| 2362 | + # labels is pre-baked RGB, so imshow ignores cmap/norm for display. Passing the resolved |
| 2363 | + # norm to imshow would make it try to normalize the RGBA array — which raises for a |
| 2364 | + # non-linear norm (LogNorm/PowerNorm). Display the RGB without a norm and build the |
| 2365 | + # continuous colorbar mappable separately from the resolved norm (mirrors the outline path), |
| 2366 | + # so the colorbar reflects the real norm subclass. |
| 2367 | + img = ax.imshow(labels, rasterized=True, alpha=alpha, origin="lower", zorder=render_params.zorder) |
| 2368 | + img.set_transform(trans_data) |
| 2369 | + if color_spec.is_categorical: |
| 2370 | + return img |
| 2371 | + used_norm = _resolve_continuous_norm(color_spec.color_vector, render_params.cmap_params) |
| 2372 | + return ScalarMappable(norm=used_norm, cmap=render_params.cmap_params.cmap) |
2369 | 2373 |
|
2370 | 2374 | # When color is a literal (col_for_color is None) and no explicit outline_color, |
2371 | 2375 | # use the literal color for outlines so they are visible (e.g., color='white' on |
|
0 commit comments