Skip to content

Commit 0ee9396

Browse files
committed
Merge branch 'main' into plot_labels
2 parents 1d4fd56 + 667f430 commit 0ee9396

10 files changed

Lines changed: 325 additions & 68 deletions

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ repos:
1313
hooks:
1414
- id: prettier
1515
- repo: https://github.com/astral-sh/ruff-pre-commit
16-
rev: v0.11.2
16+
rev: v0.11.6
1717
hooks:
1818
- id: ruff
1919
args: [--fix, --exit-non-zero-on-fix]

README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
[![Documentation][badge-docs]][link-docs]
77
[![Codecov][badge-codecov]][link-codecov]
88
[![Documentation][badge-pypi]][link-pypi]
9+
[![Anaconda-Server Badge](https://anaconda.org/conda-forge/spatialdata-plot/badges/version.svg)](https://anaconda.org/conda-forge/spatialdata-plot)
910
[![DOI](https://zenodo.org/badge/588223127.svg)](https://zenodo.org/badge/latestdoi/588223127)
1011

1112
[badge-tests]: https://img.shields.io/github/actions/workflow/status/scverse/spatialdata-plot/test.yaml?branch=main
@@ -62,11 +63,10 @@ Marconato, L., Palla, G., Yamauchi, K.A. et al. SpatialData: an open and univers
6263

6364
[scverse-discourse]: https://discourse.scverse.org/
6465
[issue-tracker]: https://github.com/scverse/spatialdata-plot/issues
65-
[changelog]: https://spatialdata-plot.readthedocs.io/latest/changelog.html
6666
[link-docs]: https://spatialdata-plot.readthedocs.io
67-
[link-api]: https://spatialdata.scverse.org/projects/plot/en/latest/api.html
68-
[link-design-doc]: https://spatialdata.scverse.org/en/latest/design_doc.html
69-
[link-notebooks]: https://spatialdata.scverse.org/en/latest/tutorials/notebooks/notebooks.html
67+
[link-api]: https://spatialdata.scverse.org/projects/plot/en/stable/api.html
68+
[link-design-doc]: https://spatialdata.scverse.org/en/stable/design_doc.html
69+
[link-notebooks]: https://spatialdata.scverse.org/en/stable/tutorials/notebooks/notebooks.html
7070
[//]: # "numfocus-fiscal-sponsor-attribution"
7171

7272
spatialdata-plot is part of the scverse® project ([website](https://scverse.org), [governance](https://scverse.org/about/roles)) and is fiscally sponsored by [NumFOCUS](https://numfocus.org/).
50.5 KB
Loading

docs/contributing.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
Please refer to the [contribution guide from the `spatialdata` repository](https://github.com/scverse/spatialdata/blob/main/docs/contributing.md).
44

5-
### Testing the correctness of the plots
5+
## Testing the correctness of the plots
66

77
Many tests will produce plots and check that they are correct by comparing them with a previously saved and serialized version of the same plots. The ground truth images are located in `tests/_images`. Different OS/versions may produce similar but not identical plots (for instance the ticks/padding could vary). To take into account for this please consider the following:
88

docs/index.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
99
api.md
1010
changelog.md
11-
template_usage.md
1211
contributing.md
1312
references.md
1413
```

src/spatialdata_plot/pl/render.py

Lines changed: 89 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
_get_extent_and_range_for_datashader_canvas,
4646
_get_linear_colormap,
4747
_get_transformation_matrix_for_datashader,
48+
_hex_no_alpha,
4849
_is_coercable_to_float,
4950
_map_color_seg,
5051
_maybe_set_colors,
@@ -185,17 +186,20 @@ def _render_shapes(
185186
sdata_filt.shapes[element].loc[is_point, "geometry"] = _geometry[is_point].buffer(scale.to_numpy())
186187

187188
# apply transformations to the individual points
188-
element_trans = get_transformation(sdata_filt.shapes[element])
189+
element_trans = get_transformation(sdata_filt.shapes[element], to_coordinate_system=coordinate_system)
189190
tm = _get_transformation_matrix_for_datashader(element_trans)
190191
transformed_element = sdata_filt.shapes[element].transform(
191192
lambda x: (np.hstack([x, np.ones((x.shape[0], 1))]) @ tm)[:, :2]
192193
)
193194
transformed_element = ShapesModel.parse(
194-
gpd.GeoDataFrame(data=sdata_filt.shapes[element].drop("geometry", axis=1), geometry=transformed_element)
195+
gpd.GeoDataFrame(
196+
data=sdata_filt.shapes[element].drop("geometry", axis=1),
197+
geometry=transformed_element,
198+
)
195199
)
196200

197201
plot_width, plot_height, x_ext, y_ext, factor = _get_extent_and_range_for_datashader_canvas(
198-
transformed_element, coordinate_system, ax, fig_params
202+
transformed_element, "global", ax, fig_params
199203
)
200204

201205
cvs = ds.Canvas(plot_width=plot_width, plot_height=plot_height, x_range=x_ext, y_range=y_ext)
@@ -208,15 +212,23 @@ def _render_shapes(
208212
aggregate_with_reduction = None
209213
if col_for_color is not None and (render_params.groups is None or len(render_params.groups) > 1):
210214
if color_by_categorical:
211-
agg = cvs.polygons(transformed_element, geometry="geometry", agg=ds.by(col_for_color, ds.count()))
215+
agg = cvs.polygons(
216+
transformed_element,
217+
geometry="geometry",
218+
agg=ds.by(col_for_color, ds.count()),
219+
)
212220
else:
213221
reduction_name = render_params.ds_reduction if render_params.ds_reduction is not None else "mean"
214222
logger.info(
215223
f'Using the datashader reduction "{reduction_name}". "max" will give an output very close '
216224
"to the matplotlib result."
217225
)
218226
agg = _datashader_aggregate_with_function(
219-
render_params.ds_reduction, cvs, transformed_element, col_for_color, "shapes"
227+
render_params.ds_reduction,
228+
cvs,
229+
transformed_element,
230+
col_for_color,
231+
"shapes",
220232
)
221233
# save min and max values for drawing the colorbar
222234
aggregate_with_reduction = (agg.min(), agg.max())
@@ -246,7 +258,7 @@ def _render_shapes(
246258
agg = agg.where((agg != norm.vmin) | (np.isnan(agg)), other=0.5)
247259

248260
color_key = (
249-
[x[:-2] for x in color_vector.categories.values]
261+
[_hex_no_alpha(x) for x in color_vector.categories.values]
250262
if (type(color_vector) is pd.core.arrays.categorical.Categorical)
251263
and (len(color_vector.categories.values) > 1)
252264
else None
@@ -257,7 +269,7 @@ def _render_shapes(
257269
if color_vector is not None:
258270
ds_cmap = color_vector[0]
259271
if isinstance(ds_cmap, str) and ds_cmap[0] == "#":
260-
ds_cmap = ds_cmap[:-2]
272+
ds_cmap = _hex_no_alpha(ds_cmap)
261273

262274
ds_result = _datashader_map_aggregate_to_color(
263275
agg,
@@ -272,7 +284,10 @@ def _render_shapes(
272284
# else: all elements would get alpha=0 and the color bar would have a weird range
273285
if aggregate_with_reduction[0] == aggregate_with_reduction[1]:
274286
ds_cmap = matplotlib.colors.to_hex(render_params.cmap_params.cmap(0.0), keep_alpha=False)
275-
aggregate_with_reduction = (aggregate_with_reduction[0], aggregate_with_reduction[0] + 1)
287+
aggregate_with_reduction = (
288+
aggregate_with_reduction[0],
289+
aggregate_with_reduction[0] + 1,
290+
)
276291

277292
ds_result = _datashader_map_aggregate_to_color(
278293
agg,
@@ -468,7 +483,9 @@ def _render_points(
468483
# we construct an anndata to hack the plotting functions
469484
if table_name is None:
470485
adata = AnnData(
471-
X=points[["x", "y"]].values, obs=points[coords].reset_index(), dtype=points[["x", "y"]].values.dtype
486+
X=points[["x", "y"]].values,
487+
obs=points[coords].reset_index(),
488+
dtype=points[["x", "y"]].values.dtype,
472489
)
473490
else:
474491
adata_obs = sdata_filt[table_name].obs
@@ -496,7 +513,9 @@ def _render_points(
496513
sdata_filt.points[element] = PointsModel.parse(points, coordinates={"x": "x", "y": "y"})
497514
# restore transformation in coordinate system of interest
498515
set_transformation(
499-
element=sdata_filt.points[element], transformation=transformation_in_cs, to_coordinate_system=coordinate_system
516+
element=sdata_filt.points[element],
517+
transformation=transformation_in_cs,
518+
to_coordinate_system=coordinate_system,
500519
)
501520

502521
if col_for_color is not None:
@@ -586,7 +605,11 @@ def _render_points(
586605
"to the matplotlib result."
587606
)
588607
agg = _datashader_aggregate_with_function(
589-
render_params.ds_reduction, cvs, transformed_element, col_for_color, "points"
608+
render_params.ds_reduction,
609+
cvs,
610+
transformed_element,
611+
col_for_color,
612+
"points",
590613
)
591614
# save min and max values for drawing the colorbar
592615
aggregate_with_reduction = (agg.min(), agg.max())
@@ -642,7 +665,10 @@ def _render_points(
642665
# else: all elements would get alpha=0 and the color bar would have a weird range
643666
if aggregate_with_reduction[0] == aggregate_with_reduction[1] and (ds_span is None or ds_span != [0, 1]):
644667
ds_cmap = matplotlib.colors.to_hex(render_params.cmap_params.cmap(0.0), keep_alpha=False)
645-
aggregate_with_reduction = (aggregate_with_reduction[0], aggregate_with_reduction[0] + 1)
668+
aggregate_with_reduction = (
669+
aggregate_with_reduction[0],
670+
aggregate_with_reduction[0] + 1,
671+
)
646672

647673
ds_result = _datashader_map_aggregate_to_color(
648674
agg,
@@ -805,7 +831,12 @@ def _render_images(
805831

806832
# norm needs to be passed directly to ax.imshow(). If we normalize before, that method would always clip.
807833
_ax_show_and_transform(
808-
layer, trans_data, ax, cmap=cmap, zorder=render_params.zorder, norm=render_params.cmap_params.norm
834+
layer,
835+
trans_data,
836+
ax,
837+
cmap=cmap,
838+
zorder=render_params.zorder,
839+
norm=render_params.cmap_params.norm,
809840
)
810841

811842
if legend_params.colorbar:
@@ -832,7 +863,11 @@ def _render_images(
832863
else: # -> use given cmap for each channel
833864
channel_cmaps = [render_params.cmap_params.cmap] * n_channels
834865
stacked = (
835-
np.stack([channel_cmaps[ind](layers[ch]) for ind, ch in enumerate(channels)], 0).sum(0) / n_channels
866+
np.stack(
867+
[channel_cmaps[ind](layers[ch]) for ind, ch in enumerate(channels)],
868+
0,
869+
).sum(0)
870+
/ n_channels
836871
)
837872
stacked = stacked[:, :, :3]
838873
logger.warning(
@@ -844,7 +879,13 @@ def _render_images(
844879
"Consider using 'palette' instead."
845880
)
846881

847-
_ax_show_and_transform(stacked, trans_data, ax, render_params.alpha, zorder=render_params.zorder)
882+
_ax_show_and_transform(
883+
stacked,
884+
trans_data,
885+
ax,
886+
render_params.alpha,
887+
zorder=render_params.zorder,
888+
)
848889

849890
# 2B) Image has n channels, no palette/cmap info -> sample n categorical colors
850891
elif palette is None and not got_multiple_cmaps:
@@ -858,7 +899,13 @@ def _render_images(
858899
colored = np.stack([channel_cmaps[ind](layers[ch]) for ind, ch in enumerate(channels)], 0).sum(0)
859900
colored = colored[:, :, :3]
860901

861-
_ax_show_and_transform(colored, trans_data, ax, render_params.alpha, zorder=render_params.zorder)
902+
_ax_show_and_transform(
903+
colored,
904+
trans_data,
905+
ax,
906+
render_params.alpha,
907+
zorder=render_params.zorder,
908+
)
862909

863910
# 2C) Image has n channels and palette info
864911
elif palette is not None and not got_multiple_cmaps:
@@ -869,16 +916,32 @@ def _render_images(
869916
colored = np.stack([channel_cmaps[i](layers[c]) for i, c in enumerate(channels)], 0).sum(0)
870917
colored = colored[:, :, :3]
871918

872-
_ax_show_and_transform(colored, trans_data, ax, render_params.alpha, zorder=render_params.zorder)
919+
_ax_show_and_transform(
920+
colored,
921+
trans_data,
922+
ax,
923+
render_params.alpha,
924+
zorder=render_params.zorder,
925+
)
873926

874927
elif palette is None and got_multiple_cmaps:
875928
channel_cmaps = [cp.cmap for cp in render_params.cmap_params] # type: ignore[union-attr]
876929
colored = (
877-
np.stack([channel_cmaps[ind](layers[ch]) for ind, ch in enumerate(channels)], 0).sum(0) / n_channels
930+
np.stack(
931+
[channel_cmaps[ind](layers[ch]) for ind, ch in enumerate(channels)],
932+
0,
933+
).sum(0)
934+
/ n_channels
878935
)
879936
colored = colored[:, :, :3]
880937

881-
_ax_show_and_transform(colored, trans_data, ax, render_params.alpha, zorder=render_params.zorder)
938+
_ax_show_and_transform(
939+
colored,
940+
trans_data,
941+
ax,
942+
render_params.alpha,
943+
zorder=render_params.zorder,
944+
)
882945

883946
elif palette is not None and got_multiple_cmaps:
884947
raise ValueError("If 'palette' is provided, 'cmap' must be None.")
@@ -1011,7 +1074,9 @@ def _draw_labels(seg_erosionpx: int | None, seg_boundaries: bool, alpha: float)
10111074
# outline-only case
10121075
elif render_params.fill_alpha == 0.0 and render_params.outline_alpha > 0.0:
10131076
cax = _draw_labels(
1014-
seg_erosionpx=render_params.contour_px, seg_boundaries=True, alpha=render_params.outline_alpha
1077+
seg_erosionpx=render_params.contour_px,
1078+
seg_boundaries=True,
1079+
alpha=render_params.outline_alpha,
10151080
)
10161081
alpha_to_decorate_ax = render_params.outline_alpha
10171082

@@ -1022,7 +1087,9 @@ def _draw_labels(seg_erosionpx: int | None, seg_boundaries: bool, alpha: float)
10221087

10231088
# ... then overlay the contour
10241089
cax_contour = _draw_labels(
1025-
seg_erosionpx=render_params.contour_px, seg_boundaries=True, alpha=render_params.outline_alpha
1090+
seg_erosionpx=render_params.contour_px,
1091+
seg_boundaries=True,
1092+
alpha=render_params.outline_alpha,
10261093
)
10271094

10281095
# pass the less-transparent _cax for the legend
@@ -1047,7 +1114,7 @@ def _draw_labels(seg_erosionpx: int | None, seg_boundaries: bool, alpha: float)
10471114
legend_fontweight=legend_params.legend_fontweight,
10481115
legend_loc=legend_params.legend_loc,
10491116
legend_fontoutline=legend_params.legend_fontoutline,
1050-
na_in_legend=legend_params.na_in_legend if groups is None else len(groups) == len(set(color_vector)),
1117+
na_in_legend=(legend_params.na_in_legend if groups is None else len(groups) == len(set(color_vector))),
10511118
colorbar=legend_params.colorbar,
10521119
scalebar_dx=scalebar_params.scalebar_dx,
10531120
scalebar_units=scalebar_params.scalebar_units,

0 commit comments

Comments
 (0)