Skip to content

Conversation

Sonja-Stockhaus
Copy link
Collaborator

For outsourcing the plotting from squidpy to spatialdata-plot: refactor the outlines of the shapes to be more similar to squidpy. This includes:

  1. outlines always lie behind the shapes (both are rendered separately with the shapes on top of the outlines). Note: since the usual behavior of mpl and ds is to draw the line so that half of it lies "within" and the other half outside the shape, the outlines will shine through for instance if alpha_fill < 1.0!
  2. enabling a double outline. Now, the arguments outline_width and outline_color take either a single argument as before, or a tuple of two values referring to the outer and inner outline. If one of the two arguments indicates that 2 outlines should be drawn but the other is not used (or only contains of 1 value which would be interpreted as referring to the outer outline), the missing value(s) are set to the defaults which are for the outer outline: 1.5 and "black" and for the inner outline: 0.5 and "white". If the linewidth for the inner outline is wider than the one of the outer outline, it will hide the outer outline (if alpha = 1.0).

@codecov-commenter
Copy link

codecov-commenter commented Jun 17, 2025

Codecov Report

❌ Patch coverage is 78.94737% with 48 lines in your changes missing coverage. Please review.
✅ Project coverage is 83.80%. Comparing base (fd11c33) to head (5942e13).

Files with missing lines Patch % Lines
src/spatialdata_plot/pl/utils.py 75.43% 28 Missing ⚠️
src/spatialdata_plot/pl/render_params.py 76.25% 19 Missing ⚠️
src/spatialdata_plot/pl/render.py 96.87% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #472      +/-   ##
==========================================
- Coverage   83.89%   83.80%   -0.09%     
==========================================
  Files           8        8              
  Lines        1819     1976     +157     
==========================================
+ Hits         1526     1656     +130     
- Misses        293      320      +27     
Files with missing lines Coverage Δ
src/spatialdata_plot/pl/basic.py 89.32% <100.00%> (ø)
src/spatialdata_plot/pl/render.py 92.51% <96.87%> (+1.00%) ⬆️
src/spatialdata_plot/pl/render_params.py 89.14% <76.25%> (-10.86%) ⬇️
src/spatialdata_plot/pl/utils.py 78.31% <75.43%> (+0.37%) ⬆️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@Sonja-Stockhaus
Copy link
Collaborator Author

Note: when alpha_fill = 1.0, you only see half of the outline, since the "inner part" lies behind the actual shape. So the outline will appear narrower than you might expect from the outline_width parameter

@Sonja-Stockhaus
Copy link
Collaborator Author

Regarding the "outline lies partly behind shape and will shine through if fill_alpha != 1" issue:

  • a nicer solution might be to upscale the shapes that are used for plotting the outline(s) so that the outline perfectly lies around the shape
  • for circles, that's easy: in the _process_point() sub-method in utils._get_collection_shape(), one can compute added_size_for_outline = 0.5 * linewidth and add that value to the radius of the mpatches.Circle objects
  • however, for polygons it gets more complicated, since the points whould have to be shifted away from the centroid by a value that does not only depend on the linewidth but also the angle. I did not reach a satisfying solution there and it would get even more complex with concave shapes or multipolygons
  • there might be an easier solution using shapely, but so far we think it's too much effort/perfectionism for too little improvement. So for now, we stick to the default matplotlib/datashader behavior.

@Sonja-Stockhaus Sonja-Stockhaus marked this pull request as ready for review June 18, 2025 10:49
@Sonja-Stockhaus Sonja-Stockhaus requested a review from timtreis June 18, 2025 10:49
@Sonja-Stockhaus Sonja-Stockhaus marked this pull request as draft August 1, 2025 11:30
@Sonja-Stockhaus
Copy link
Collaborator Author

Updates: we introduced a Color class that stores colors together with an alpha value.
Colors can be passed by the user as color name (e.g. "red"), an RGB(A) array (e.g. [0, 0, 0]) or a hex string ("#000000") in the format "#RRGGBB" or "#RRGGBBAA".
When alpha is set manually via a parameter, this value takes precedence (without being multiplied by potentially implicated alpha values from the color or a colormap). Else, if the hex string/RBGA array implies an alpha, this alters the opacity of the object. This behavior mirrors matplotlib. This is also true for outlines of shapes. When there are two outlines, they can therefore also have different opacity.
Change of shape outline plotting behavior: it would be sufficient to specify an outline color and/or width without setting the outline_alpha parameter in order for outlines to be drawn with alpha=1.0 (or whatever is specified by color)

@Sonja-Stockhaus Sonja-Stockhaus marked this pull request as ready for review September 16, 2025 18:12
Copy link
Member

@timtreis timtreis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens if a user specifies just the outline alpha but nothing else?

outline_color = render_params.outline_params.inner_outline_color.get_hex()
ds_inner_outlines = ds.tf.shade(
agg_inner_outlines,
cmap=outline_color,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're just passing a hex to something that expects a cmap, what are the implications of this?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the original typehint is "list of colors or matplotlib.colors.Colormap". So it's called cmap, but doesn't necessarily expect a colormap object. Colors can be specified by name, hex code or RGB arrays. If you only give one color, it doesn't have to be a list. So it shouldn't have implications for us

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So it automagically converts single colors to a list under-the-hood here? Or should we pass in either a one-element list or a list with the color duplicated?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it doesn't directly convert the single color to a list of the color. But the case is explicitly handled:
grafik
The rgb() function can work with color names and hex strings. So I'd say what we do here is absolutely fine and intended behavior

@timtreis timtreis added enhancement New feature or request shapes 🫧 Anything related to Shapes release-added labels Sep 26, 2025
@Sonja-Stockhaus Sonja-Stockhaus merged commit 0975891 into main Sep 26, 2025
4 checks passed
@Sonja-Stockhaus Sonja-Stockhaus deleted the feature/outline_refactoring branch September 26, 2025 13:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request release-added shapes 🫧 Anything related to Shapes
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants