Skip to content

Commit 4154499

Browse files
committed
fix(as_points): size datashader canvas to the axes, not the figure
The datashader result is a data-coordinate image that scales with the axes, while matplotlib markers are fixed in display points. The canvas was sized from fig.get_size_inches()*dpi (the whole figure), but the axes are smaller (margins, colorbar), so the image - and every dot - was scaled down: labels (with colorbar) 0.81x matplotlib, shapes 0.88x. Size the as_markers canvas to the axes display box (ax.get_window_extent()) so 1 canvas px == 1 axes-display px and the sqrt(s)*dpi/144 spread radius matches the marker. Now mean 0.99 +/-5% across sizes/figs/dpi/elements; render_points untouched (byte-identical). Visual tests render at a non-overlapping size so the engines' overlap handling (stack vs aggregate) doesn't enter the pairs.
1 parent b7e2fd6 commit 4154499

3 files changed

Lines changed: 15 additions & 6 deletions

File tree

src/spatialdata_plot/pl/render.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,6 @@
6363
_build_shape_patches,
6464
_check_obs_var_shadow,
6565
_color_vector_to_rgba,
66-
_compute_datashader_canvas_params,
6766
_convert_shapes,
6867
_datashader_canvas_from_dataframe,
6968
_decorate_axs,
@@ -1258,10 +1257,17 @@ def _datashader_points(
12581257
px: int | None = None if density else int(np.round(np.sqrt(size) * (fig_params.fig.dpi / px_div)))
12591258

12601259
if as_markers and axes_extent is not None:
1261-
# rasterize over the same extent the axes will use, so 1 canvas px == 1 display px (as render_points)
1260+
# Rasterize over the element extent (the frame the axes use) at the AXES' display resolution, not
1261+
# the figure's. The datashader result is a data-coordinate image that scales with the axes, while
1262+
# matplotlib markers are fixed in display points; sizing the canvas to the figure (which is larger
1263+
# than the axes once margins/colorbar are accounted for) shrank the dots. With 1 canvas px == 1
1264+
# axes-display px, the spread radius (matplotlib's marker radius, sqrt(s)*dpi/144) matches.
12621265
x_ext = [float(axes_extent["x"][0]), float(axes_extent["x"][1])]
12631266
y_ext = [float(axes_extent["y"][0]), float(axes_extent["y"][1])]
1264-
plot_width, plot_height, x_ext, y_ext, factor = _compute_datashader_canvas_params(x_ext, y_ext, fig_params)
1267+
bb = ax.get_window_extent()
1268+
rx, ry = x_ext[1] - x_ext[0], y_ext[1] - y_ext[0]
1269+
factor = max(rx / bb.width, ry / bb.height)
1270+
plot_width, plot_height = int(round(rx / factor)), int(round(ry / factor))
12651271
else:
12661272
plot_width, plot_height, x_ext, y_ext, factor = _datashader_canvas_from_dataframe(df, fig_params)
12671273
cvs = ds.Canvas(plot_width=plot_width, plot_height=plot_height, x_range=x_ext, y_range=y_ext)

tests/pl/test_render_labels.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -437,8 +437,9 @@ def test_plot_can_color_labels_by_gene_symbols(self, sdata_blobs: SpatialData):
437437

438438
@staticmethod
439439
def _as_points(sdata_blobs: SpatialData, method: str, color: str = "instance_id"):
440-
# identical params for both backends, so the matplotlib and datashader baselines are comparable
441-
return sdata_blobs.pl.render_labels("blobs_labels", color=color, as_points=True, method=method, size=600)
440+
# identical params for both backends; non-overlapping size so the only place the engines differ
441+
# (overlap: matplotlib stacks markers, datashader aggregates) doesn't enter the comparison
442+
return sdata_blobs.pl.render_labels("blobs_labels", color=color, as_points=True, method=method, size=120)
442443

443444
@staticmethod
444445
def _add_categorical_color(sdata_blobs: SpatialData) -> str:

tests/pl/test_render_shapes.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1111,7 +1111,9 @@ def test_plot_can_color_shapes_by_gene_symbols(self, sdata_blobs: SpatialData):
11111111
@staticmethod
11121112
def _as_points(sdata_blobs: SpatialData, method: str):
11131113
# identical params for both backends, so the matplotlib and datashader baselines are comparable
1114-
return sdata_blobs.pl.render_shapes("blobs_circles", as_points=True, method=method, size=600)
1114+
# non-overlapping size: matplotlib (vector, stacks markers) and datashader (raster, aggregates)
1115+
# only diverge where dots overlap, so keep them apart to compare the backends fairly
1116+
return sdata_blobs.pl.render_shapes("blobs_circles", as_points=True, method=method, size=120)
11151117

11161118
def test_plot_shapes_as_points_matplotlib(self, sdata_blobs: SpatialData):
11171119
"""as_points draws one dot per shape at its centroid (matplotlib backend)."""

0 commit comments

Comments
 (0)