Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
a55318c
fix
timtreis Sep 3, 2024
381b895
fixed case without info for column
timtreis Sep 3, 2024
487f6a5
fixed case for no-column, but modified na_color
timtreis Sep 3, 2024
a9c7609
added images from runner
timtreis Sep 3, 2024
c7e3260
bugfix for NA color
timtreis Sep 3, 2024
d0b7a1a
lowered testing threshold because mismatches are not being flagged
timtreis Sep 3, 2024
a9d5dbb
modified test to be visually easier to compare
timtreis Sep 3, 2024
fd762a9
further lowered testing threshold
timtreis Sep 3, 2024
729d465
Changed points behaviour and lowered test threshold
timtreis Sep 3, 2024
4421383
modified tests for better display
timtreis Sep 3, 2024
608bea0
fixed bug in typecheck
timtreis Sep 3, 2024
69bfc35
fixed test, added images from runner
timtreis Sep 3, 2024
7db0860
Updated CHANGELOG, added pic from runner
timtreis Sep 3, 2024
01a4f73
Removed dead code
timtreis Sep 3, 2024
eb2fc12
simplified test
timtreis Sep 3, 2024
4d40479
added images from runner
timtreis Sep 3, 2024
f8d6bac
fixed test
timtreis Sep 4, 2024
f6a2d16
fix
timtreis Sep 4, 2024
268a1b2
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Sep 4, 2024
81204ea
removed cbar
timtreis Sep 4, 2024
efb0f19
Merge branch 'bugfix/342-coloring-labels-by-a-continuous-variable-app…
timtreis Sep 4, 2024
ca0ff4e
merge
timtreis Sep 4, 2024
62d7cb9
added img from runner
timtreis Sep 4, 2024
6f4a634
Removed percentiles_for_norm parameter, delegating to cmap.norm
timtreis Sep 4, 2024
9377422
fixed shapes cbar logic
timtreis Sep 4, 2024
d4cfc2a
fixed test img generation
timtreis Sep 4, 2024
385dac3
fixed cmap limits for shapes
timtreis Sep 4, 2024
cc21303
modified test
timtreis Sep 4, 2024
2309404
updated CHANGELOG, added img from runner
timtreis Sep 4, 2024
a97eec1
Update CHANGELOG.md
timtreis Sep 4, 2024
667c689
Merge branch 'main' into 324-unable-to-set-vmin-vmax-when-plotting-ve…
timtreis Sep 4, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -18,11 +18,15 @@ and this project adheres to [Semantic Versioning][].

- Lowered RMSE-threshold for plot-based tests from 45 to 15 (#344)
- When subsetting to `groups`, `NA` isn't automatically added to legend (#344)
- When rendering a single image channel, a colorbar is now shown (#346)
- Removed `percentiles_for_norm` parameter (#346)
- Changed `norm` to no longer accept bools, only `mpl.colors.Normalise` or `None` (#346)

### Fixed

- Filtering with `groups` now preserves original cmap (#344)
- Non-selected `groups` are now not shown in `na_color` (#344)
- Several issues associated with `norm` and `colorbar` (#346)

## [0.2.5] - 2024-08-23

Expand Down
16 changes: 3 additions & 13 deletions src/spatialdata_plot/pl/basic.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -166,7 +166,7 @@ def render_shapes(
outline_color: str | list[float] = "#000000ff",
outline_alpha: float | int = 0.0,
cmap: Colormap | str | None = None,
norm: bool | Normalize = False,
norm: Normalize | None = None,
scale: float | int = 1.0,
method: str | None = None,
table_name: str | None = None,
Expand DownExpand Up@@ -301,7 +301,7 @@ def render_points(
palette: list[str] | str | None = None,
na_color: ColorLike | None = "default",
cmap: Colormap | str | None = None,
norm: None | Normalize = None,
norm: Normalize | None = None,
size: float | int = 1.0,
method: str | None = None,
table_name: str | None = None,
Expand DownExpand Up@@ -422,7 +422,6 @@ def render_images(
na_color: ColorLike | None = "default",
palette: list[str] | str | None = None,
alpha: float | int = 1.0,
percentiles_for_norm: tuple[float, float] | None = None,
scale: str | None = None,
**kwargs: Any,
) -> sd.SpatialData:
Expand DownExpand Up@@ -457,8 +456,6 @@ def render_images(
Palette to color images. The number of palettes should be equal to the number of channels.
alpha : float | int, default 1.0
Alpha value for the images. Must be a numeric between 0 and 1.
percentiles_for_norm : tuple[float, float] | None
Optional pair of floats (pmin < pmax, 0-100) which will be used for quantile normalization.
scale : str | None
Influences the resolution of the rendering. Possibilities include:
1) `None` (default): The image is rasterized to fit the canvas size. For
Expand DownExpand Up@@ -486,20 +483,14 @@ def render_images(
cmap=cmap,
norm=norm,
scale=scale,
percentiles_for_norm=percentiles_for_norm,
)

sdata = self._copy()
sdata = _verify_plotting_tree(sdata)
n_steps = len(sdata.plotting_tree.keys())

for element, param_values in params_dict.items():
# cmap_params = _prepare_cmap_norm(
# cmap=params_dict[element]["cmap"],
# norm=norm,
# na_color=params_dict[element]["na_color"], # type: ignore[arg-type]
# **kwargs,
# )

cmap_params: list[CmapParams] | CmapParams
if isinstance(cmap, list):
cmap_params = [
Expand All@@ -525,7 +516,6 @@ def render_images(
cmap_params=cmap_params,
palette=param_values["palette"],
alpha=param_values["alpha"],
percentiles_for_norm=param_values["percentiles_for_norm"],
scale=param_values["scale"],
zorder=n_steps,
)
Expand Down
30 changes: 13 additions & 17 deletions src/spatialdata_plot/pl/render.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,6 +9,7 @@
import datashader as ds
import geopandas as gpd
import matplotlib
import matplotlib.pyplot as plt
import matplotlib.transforms as mtransforms
import numpy as np
import pandas as pd
Expand DownExpand Up@@ -47,7 +48,6 @@
_maybe_set_colors,
_mpl_ax_contains_elements,
_multiscale_to_spatial_image,
_normalize,
_rasterize_if_necessary,
_set_color_source_vec,
to_hex,
Expand DownExpand Up@@ -128,6 +128,7 @@ def _render_shapes(
shapes = shapes.reset_index()
color_source_vector = color_source_vector[mask]
color_vector = color_vector[mask]

shapes = gpd.GeoDataFrame(shapes, geometry="geometry")

# Using dict.fromkeys here since set returns in arbitrary order
Expand DownExpand Up@@ -255,9 +256,13 @@ def _render_shapes(
for path in _cax.get_paths():
path.vertices = trans.transform(path.vertices)

# Sets the limits of the colorbar to the values instead of [0, 1]
if not norm and not values_are_categorical:
_cax.set_clim(min(color_vector), max(color_vector))
if not values_are_categorical:
# If the user passed a Normalize object with vmin/vmax we'll use those,
# # if not we'll use the min/max of the color_vector
_cax.set_clim(
vmin=render_params.cmap_params.norm.vmin or min(color_vector),
vmax=render_params.cmap_params.norm.vmax or max(color_vector),
)

if len(set(color_vector)) != 1 or list(set(color_vector))[0] != to_hex(render_params.cmap_params.na_color):
# necessary in case different shapes elements are annotated with one table
Expand DownExpand Up@@ -603,11 +608,6 @@ def _render_images(
if n_channels == 1 and not isinstance(render_params.cmap_params, list):
layer = img.sel(c=channels[0]).squeeze() if isinstance(channels[0], str) else img.isel(c=channels[0]).squeeze()

if render_params.percentiles_for_norm != (None, None):
layer = _normalize(
layer, pmin=render_params.percentiles_for_norm[0], pmax=render_params.percentiles_for_norm[1], clip=True
)

if render_params.cmap_params.norm: # type: ignore[attr-defined]
layer = render_params.cmap_params.norm(layer) # type: ignore[attr-defined]

Expand All@@ -623,20 +623,16 @@ def _render_images(

_ax_show_and_transform(layer, trans_data, ax, cmap=cmap, zorder=render_params.zorder)

if legend_params.colorbar:
sm = plt.cm.ScalarMappable(cmap=cmap, norm=render_params.cmap_params.norm)
fig_params.fig.colorbar(sm, ax=ax)

# 2) Image has any number of channels but 1
else:
layers = {}
for ch_index, c in enumerate(channels):
layers[c] = img.sel(c=c).copy(deep=True).squeeze()

if render_params.percentiles_for_norm != (None, None):
layers[c] = _normalize(
layers[c],
pmin=render_params.percentiles_for_norm[0],
pmax=render_params.percentiles_for_norm[1],
clip=True,
)

if not isinstance(render_params.cmap_params, list):
if render_params.cmap_params.norm is not None:
layers[c] = render_params.cmap_params.norm(layers[c])
Expand Down
29 changes: 1 addition & 28 deletions src/spatialdata_plot/pl/utils.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -489,7 +489,7 @@ def _get_scalebar(

def _prepare_cmap_norm(
cmap: Colormap | str | None = None,
norm: Normalize | bool = False,
norm: Normalize | None = None,
na_color: ColorLike | None = None,
vmin: float | None = None,
vmax: float | None = None,
Expand DownExpand Up@@ -1623,29 +1623,6 @@ def _type_check_params(param_dict: dict[str, Any], element_type: str) -> dict[st
if scale < 0:
raise ValueError("Parameter 'scale' must be a positive number.")

if (percentiles_for_norm := param_dict.get("percentiles_for_norm")) is None:
percentiles_for_norm = (None, None)
elif not (isinstance(percentiles_for_norm, (list, tuple)) or len(percentiles_for_norm) != 2):
raise TypeError("Parameter 'percentiles_for_norm' must be a list or tuple of exactly two floats or None.")
elif not all(
isinstance(p, (float, int, type(None)))
and isinstance(p, type(percentiles_for_norm[0]))
and (p is None or 0 <= p <= 100)
for p in percentiles_for_norm
):
raise TypeError(
"Each item in 'percentiles_for_norm' must be of the same dtype and must be a float or int within [0, 100], "
"or None"
)
elif (
percentiles_for_norm[0] is not None
and percentiles_for_norm[1] is not None
and percentiles_for_norm[0] > percentiles_for_norm[1]
):
raise ValueError("The first number in 'percentiles_for_norm' must not be smaller than the second.")
if "percentiles_for_norm" in param_dict:
param_dict["percentiles_for_norm"] = percentiles_for_norm

if size := param_dict.get("size"):
if not isinstance(size, (float, int)):
raise TypeError("Parameter 'size' must be numeric.")
Expand DownExpand Up@@ -1886,7 +1863,6 @@ def _validate_image_render_params(
cmap: list[Colormap | str] | Colormap | str | None,
norm: Normalize | None,
scale: str | None,
percentiles_for_norm: tuple[float | None, float | None] | None,
) -> dict[str, dict[str, Any]]:
param_dict: dict[str, Any] = {
"sdata": sdata,
Expand All@@ -1898,7 +1874,6 @@ def _validate_image_render_params(
"cmap": cmap,
"norm": norm,
"scale": scale,
"percentiles_for_norm": percentiles_for_norm,
}
param_dict = _type_check_params(param_dict, "images")

Expand DownExpand Up@@ -1945,8 +1920,6 @@ def _validate_image_render_params(
else:
element_params[el]["scale"] = scale

element_params[el]["percentiles_for_norm"] = param_dict["percentiles_for_norm"]

return element_params


Expand Down
Binary file modifiedtests/_images/Images_can_pass_cmap_to_single_channel.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modifiedtests/_images/Images_can_pass_color_to_each_channel.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modifiedtests/_images/Images_can_pass_color_to_single_channel.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modifiedtests/_images/Images_can_stack_render_images.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modifiedtests/_images/Shapes_colorbar_can_be_normalised.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modifiedtests/_images/Shapes_colorbar_respects_input_limits.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 3 additions & 1 deletion tests/pl/test_render.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -23,7 +23,9 @@ def test_render_images_can_plot_one_cyx_image(request):
def test_render_images_can_plot_multiple_cyx_images(share_coordinate_system: str, request):
fun = request.getfixturevalue("get_sdata_with_multiple_images")
sdata = fun(share_coordinate_system)
sdata.pl.render_images().pl.show()
sdata.pl.render_images().pl.show(
colorbar=False, # otherwise we'll get one cbar per image in the same cs
)
axs = plt.gcf().get_axes()

if share_coordinate_system == "all":
Expand Down
22 changes: 6 additions & 16 deletions tests/pl/test_render_images.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,7 +2,6 @@
import matplotlib
import numpy as np
import scanpy as sc
from matplotlib import pyplot as plt
from matplotlib.colors import Normalize
from spatial_image import to_spatial_image
from spatialdata import SpatialData
Expand DownExpand Up@@ -49,9 +48,6 @@ def test_plot_can_render_a_single_channel_from_image(self, sdata_blobs: SpatialD
def test_plot_can_render_a_single_channel_from_multiscale_image(self, sdata_blobs: SpatialData):
sdata_blobs.pl.render_images(element="blobs_multiscale_image", channel=0).pl.show()

def test_plot_can_render_a_single_channel_from_image_no_el(self, sdata_blobs: SpatialData):
sdata_blobs.pl.render_images(channel=0).pl.show()

def test_plot_can_render_a_single_channel_str_from_image(self, sdata_blobs_str: SpatialData):
sdata_blobs_str.pl.render_images(element="blobs_image", channel="c1").pl.show()

Expand All@@ -70,16 +66,13 @@ def test_plot_can_render_two_channels_str_from_image(self, sdata_blobs_str: Spat
def test_plot_can_render_two_channels_str_from_multiscale_image(self, sdata_blobs_str: SpatialData):
sdata_blobs_str.pl.render_images(element="blobs_multiscale_image", channel=["c1", "c2"]).pl.show()

def test_plot_can_pass_vmin_vmax(self, sdata_blobs: SpatialData):
fig, axs = plt.subplots(ncols=2, figsize=(6, 3))
sdata_blobs.pl.render_images(element="blobs_image", channel=1).pl.show(ax=axs[0])
sdata_blobs.pl.render_images(element="blobs_image", channel=1, vmin=0, vmax=0.4).pl.show(ax=axs[1])

def test_plot_can_pass_normalize(self, sdata_blobs: SpatialData):
fig, axs = plt.subplots(ncols=2, figsize=(6, 3))
def test_plot_can_pass_normalize_clip_True(self, sdata_blobs: SpatialData):
norm = Normalize(vmin=0, vmax=0.4, clip=True)
sdata_blobs.pl.render_images(element="blobs_image", channel=1).pl.show(ax=axs[0])
sdata_blobs.pl.render_images(element="blobs_image", channel=1, norm=norm).pl.show(ax=axs[1])
sdata_blobs.pl.render_images(element="blobs_image", channel=0, norm=norm).pl.show()

def test_plot_can_pass_normalize_clip_False(self, sdata_blobs: SpatialData):
norm = Normalize(vmin=0, vmax=0.4, clip=False)
sdata_blobs.pl.render_images(element="blobs_image", channel=0, norm=norm).pl.show()

def test_plot_can_pass_color_to_single_channel(self, sdata_blobs: SpatialData):
sdata_blobs.pl.render_images(element="blobs_image", channel=1, palette="red").pl.show()
Expand All@@ -97,9 +90,6 @@ def test_plot_can_pass_cmap_to_each_channel(self, sdata_blobs: SpatialData):
element="blobs_image", channel=[0, 1, 2], cmap=["Reds", "Greens", "Blues"]
).pl.show()

def test_plot_can_normalize_image(self, sdata_blobs: SpatialData):
sdata_blobs.pl.render_images(element="blobs_image", percentiles_for_norm=(5, 90)).pl.show()

def test_plot_can_render_multiscale_image(self, sdata_blobs: SpatialData):
sdata_blobs.pl.render_images("blobs_multiscale_image").pl.show()

Expand Down
4 changes: 3 additions & 1 deletion tests/pl/test_render_shapes.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,7 @@
import pandas as pd
import scanpy as sc
from anndata import AnnData
from matplotlib.colors import Normalize
from shapely.geometry import MultiPolygon, Point, Polygon
from spatialdata import SpatialData, deepcopy
from spatialdata.models import ShapesModel, TableModel
Expand DownExpand Up@@ -146,7 +147,8 @@ def test_plot_colorbar_can_be_normalised(self, sdata_blobs: SpatialData):
sdata_blobs["table"].obs["region"] = ["blobs_polygons"] * sdata_blobs["table"].n_obs
sdata_blobs["table"].uns["spatialdata_attrs"]["region"] = "blobs_polygons"
sdata_blobs.shapes["blobs_polygons"]["cluster"] = [1, 2, 3, 5, 20]
sdata_blobs.pl.render_shapes("blobs_polygons", color="cluster", groups=["c1"], norm=True).pl.show()
norm = Normalize(vmin=0, vmax=5, clip=True)
sdata_blobs.pl.render_shapes("blobs_polygons", color="cluster", groups=["c1"], norm=norm).pl.show()

def test_plot_can_plot_shapes_after_spatial_query(self, sdata_blobs: SpatialData):
# subset to only shapes, should be unnecessary after rasterizeation of multiscale images is included
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
a55318c
fix
timtreis Sep 3, 2024
381b895
fixed case without info for column
timtreis Sep 3, 2024
487f6a5
fixed case for no-column, but modified na_color
timtreis Sep 3, 2024
a9c7609
added images from runner
timtreis Sep 3, 2024
c7e3260
bugfix for NA color
timtreis Sep 3, 2024
d0b7a1a
lowered testing threshold because mismatches are not being flagged
timtreis Sep 3, 2024
a9d5dbb
modified test to be visually easier to compare
timtreis Sep 3, 2024
fd762a9
further lowered testing threshold
timtreis Sep 3, 2024
729d465
Changed points behaviour and lowered test threshold
timtreis Sep 3, 2024
4421383
modified tests for better display
timtreis Sep 3, 2024
608bea0
fixed bug in typecheck
timtreis Sep 3, 2024
69bfc35
fixed test, added images from runner
timtreis Sep 3, 2024
7db0860
Updated CHANGELOG, added pic from runner
timtreis Sep 3, 2024
01a4f73
Removed dead code
timtreis Sep 3, 2024
eb2fc12
simplified test
timtreis Sep 3, 2024
4d40479
added images from runner
timtreis Sep 3, 2024
f8d6bac
fixed test
timtreis Sep 4, 2024
f6a2d16
fix
timtreis Sep 4, 2024
268a1b2
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Sep 4, 2024
81204ea
removed cbar
timtreis Sep 4, 2024
efb0f19
Merge branch 'bugfix/342-coloring-labels-by-a-continuous-variable-app…
timtreis Sep 4, 2024
ca0ff4e
merge
timtreis Sep 4, 2024
62d7cb9
added img from runner
timtreis Sep 4, 2024
6f4a634
Removed percentiles_for_norm parameter, delegating to cmap.norm
timtreis Sep 4, 2024
9377422
fixed shapes cbar logic
timtreis Sep 4, 2024
d4cfc2a
fixed test img generation
timtreis Sep 4, 2024
385dac3
fixed cmap limits for shapes
timtreis Sep 4, 2024
cc21303
modified test
timtreis Sep 4, 2024
2309404
updated CHANGELOG, added img from runner
timtreis Sep 4, 2024
a97eec1
Update CHANGELOG.md
timtreis Sep 4, 2024
667c689
Merge branch 'main' into 324-unable-to-set-vmin-vmax-when-plotting-ve…
timtreis Sep 4, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -18,11 +18,15 @@ and this project adheres to [Semantic Versioning][].

- Lowered RMSE-threshold for plot-based tests from 45 to 15 (#344)
- When subsetting to `groups`, `NA` isn't automatically added to legend (#344)
- When rendering a single image channel, a colorbar is now shown (#346)
- Removed `percentiles_for_norm` parameter (#346)
- Changed `norm` to no longer accept bools, only `mpl.colors.Normalise` or `None` (#346)

### Fixed

- Filtering with `groups` now preserves original cmap (#344)
- Non-selected `groups` are now not shown in `na_color` (#344)
- Several issues associated with `norm` and `colorbar` (#346)

## [0.2.5] - 2024-08-23

Expand Down
16 changes: 3 additions & 13 deletions src/spatialdata_plot/pl/basic.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -166,7 +166,7 @@ def render_shapes(
outline_color: str | list[float] = "#000000ff",
outline_alpha: float | int = 0.0,
cmap: Colormap | str | None = None,
norm: bool | Normalize = False,
norm: Normalize | None = None,
scale: float | int = 1.0,
method: str | None = None,
table_name: str | None = None,
Expand DownExpand Up@@ -301,7 +301,7 @@ def render_points(
palette: list[str] | str | None = None,
na_color: ColorLike | None = "default",
cmap: Colormap | str | None = None,
norm: None | Normalize = None,
norm: Normalize | None = None,
size: float | int = 1.0,
method: str | None = None,
table_name: str | None = None,
Expand DownExpand Up@@ -422,7 +422,6 @@ def render_images(
na_color: ColorLike | None = "default",
palette: list[str] | str | None = None,
alpha: float | int = 1.0,
percentiles_for_norm: tuple[float, float] | None = None,
scale: str | None = None,
**kwargs: Any,
) -> sd.SpatialData:
Expand DownExpand Up@@ -457,8 +456,6 @@ def render_images(
Palette to color images. The number of palettes should be equal to the number of channels.
alpha : float | int, default 1.0
Alpha value for the images. Must be a numeric between 0 and 1.
percentiles_for_norm : tuple[float, float] | None
Optional pair of floats (pmin < pmax, 0-100) which will be used for quantile normalization.
scale : str | None
Influences the resolution of the rendering. Possibilities include:
1) `None` (default): The image is rasterized to fit the canvas size. For
Expand DownExpand Up@@ -486,20 +483,14 @@ def render_images(
cmap=cmap,
norm=norm,
scale=scale,
percentiles_for_norm=percentiles_for_norm,
)

sdata = self._copy()
sdata = _verify_plotting_tree(sdata)
n_steps = len(sdata.plotting_tree.keys())

for element, param_values in params_dict.items():
# cmap_params = _prepare_cmap_norm(
# cmap=params_dict[element]["cmap"],
# norm=norm,
# na_color=params_dict[element]["na_color"], # type: ignore[arg-type]
# **kwargs,
# )

cmap_params: list[CmapParams] | CmapParams
if isinstance(cmap, list):
cmap_params = [
Expand All@@ -525,7 +516,6 @@ def render_images(
cmap_params=cmap_params,
palette=param_values["palette"],
alpha=param_values["alpha"],
percentiles_for_norm=param_values["percentiles_for_norm"],
scale=param_values["scale"],
zorder=n_steps,
)
Expand Down
30 changes: 13 additions & 17 deletions src/spatialdata_plot/pl/render.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,6 +9,7 @@
import datashader as ds
import geopandas as gpd
import matplotlib
import matplotlib.pyplot as plt
import matplotlib.transforms as mtransforms
import numpy as np
import pandas as pd
Expand DownExpand Up@@ -47,7 +48,6 @@
_maybe_set_colors,
_mpl_ax_contains_elements,
_multiscale_to_spatial_image,
_normalize,
_rasterize_if_necessary,
_set_color_source_vec,
to_hex,
Expand DownExpand Up@@ -128,6 +128,7 @@ def _render_shapes(
shapes = shapes.reset_index()
color_source_vector = color_source_vector[mask]
color_vector = color_vector[mask]

shapes = gpd.GeoDataFrame(shapes, geometry="geometry")

# Using dict.fromkeys here since set returns in arbitrary order
Expand DownExpand Up@@ -255,9 +256,13 @@ def _render_shapes(
for path in _cax.get_paths():
path.vertices = trans.transform(path.vertices)

# Sets the limits of the colorbar to the values instead of [0, 1]
if not norm and not values_are_categorical:
_cax.set_clim(min(color_vector), max(color_vector))
if not values_are_categorical:
# If the user passed a Normalize object with vmin/vmax we'll use those,
# # if not we'll use the min/max of the color_vector
_cax.set_clim(
vmin=render_params.cmap_params.norm.vmin or min(color_vector),
vmax=render_params.cmap_params.norm.vmax or max(color_vector),
)

if len(set(color_vector)) != 1 or list(set(color_vector))[0] != to_hex(render_params.cmap_params.na_color):
# necessary in case different shapes elements are annotated with one table
Expand DownExpand Up@@ -603,11 +608,6 @@ def _render_images(
if n_channels == 1 and not isinstance(render_params.cmap_params, list):
layer = img.sel(c=channels[0]).squeeze() if isinstance(channels[0], str) else img.isel(c=channels[0]).squeeze()

if render_params.percentiles_for_norm != (None, None):
layer = _normalize(
layer, pmin=render_params.percentiles_for_norm[0], pmax=render_params.percentiles_for_norm[1], clip=True
)

if render_params.cmap_params.norm: # type: ignore[attr-defined]
layer = render_params.cmap_params.norm(layer) # type: ignore[attr-defined]

Expand All@@ -623,20 +623,16 @@ def _render_images(

_ax_show_and_transform(layer, trans_data, ax, cmap=cmap, zorder=render_params.zorder)

if legend_params.colorbar:
sm = plt.cm.ScalarMappable(cmap=cmap, norm=render_params.cmap_params.norm)
fig_params.fig.colorbar(sm, ax=ax)

# 2) Image has any number of channels but 1
else:
layers = {}
for ch_index, c in enumerate(channels):
layers[c] = img.sel(c=c).copy(deep=True).squeeze()

if render_params.percentiles_for_norm != (None, None):
layers[c] = _normalize(
layers[c],
pmin=render_params.percentiles_for_norm[0],
pmax=render_params.percentiles_for_norm[1],
clip=True,
)

if not isinstance(render_params.cmap_params, list):
if render_params.cmap_params.norm is not None:
layers[c] = render_params.cmap_params.norm(layers[c])
Expand Down
29 changes: 1 addition & 28 deletions src/spatialdata_plot/pl/utils.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -489,7 +489,7 @@ def _get_scalebar(

def _prepare_cmap_norm(
cmap: Colormap | str | None = None,
norm: Normalize | bool = False,
norm: Normalize | None = None,
na_color: ColorLike | None = None,
vmin: float | None = None,
vmax: float | None = None,
Expand DownExpand Up@@ -1623,29 +1623,6 @@ def _type_check_params(param_dict: dict[str, Any], element_type: str) -> dict[st
if scale < 0:
raise ValueError("Parameter 'scale' must be a positive number.")

if (percentiles_for_norm := param_dict.get("percentiles_for_norm")) is None:
percentiles_for_norm = (None, None)
elif not (isinstance(percentiles_for_norm, (list, tuple)) or len(percentiles_for_norm) != 2):
raise TypeError("Parameter 'percentiles_for_norm' must be a list or tuple of exactly two floats or None.")
elif not all(
isinstance(p, (float, int, type(None)))
and isinstance(p, type(percentiles_for_norm[0]))
and (p is None or 0 <= p <= 100)
for p in percentiles_for_norm
):
raise TypeError(
"Each item in 'percentiles_for_norm' must be of the same dtype and must be a float or int within [0, 100], "
"or None"
)
elif (
percentiles_for_norm[0] is not None
and percentiles_for_norm[1] is not None
and percentiles_for_norm[0] > percentiles_for_norm[1]
):
raise ValueError("The first number in 'percentiles_for_norm' must not be smaller than the second.")
if "percentiles_for_norm" in param_dict:
param_dict["percentiles_for_norm"] = percentiles_for_norm

if size := param_dict.get("size"):
if not isinstance(size, (float, int)):
raise TypeError("Parameter 'size' must be numeric.")
Expand DownExpand Up@@ -1886,7 +1863,6 @@ def _validate_image_render_params(
cmap: list[Colormap | str] | Colormap | str | None,
norm: Normalize | None,
scale: str | None,
percentiles_for_norm: tuple[float | None, float | None] | None,
) -> dict[str, dict[str, Any]]:
param_dict: dict[str, Any] = {
"sdata": sdata,
Expand All@@ -1898,7 +1874,6 @@ def _validate_image_render_params(
"cmap": cmap,
"norm": norm,
"scale": scale,
"percentiles_for_norm": percentiles_for_norm,
}
param_dict = _type_check_params(param_dict, "images")

Expand DownExpand Up@@ -1945,8 +1920,6 @@ def _validate_image_render_params(
else:
element_params[el]["scale"] = scale

element_params[el]["percentiles_for_norm"] = param_dict["percentiles_for_norm"]

return element_params


Expand Down
Binary file modifiedtests/_images/Images_can_pass_cmap_to_single_channel.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modifiedtests/_images/Images_can_pass_color_to_each_channel.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modifiedtests/_images/Images_can_pass_color_to_single_channel.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modifiedtests/_images/Images_can_stack_render_images.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modifiedtests/_images/Shapes_colorbar_can_be_normalised.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modifiedtests/_images/Shapes_colorbar_respects_input_limits.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 3 additions & 1 deletion tests/pl/test_render.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -23,7 +23,9 @@ def test_render_images_can_plot_one_cyx_image(request):
def test_render_images_can_plot_multiple_cyx_images(share_coordinate_system: str, request):
fun = request.getfixturevalue("get_sdata_with_multiple_images")
sdata = fun(share_coordinate_system)
sdata.pl.render_images().pl.show()
sdata.pl.render_images().pl.show(
colorbar=False, # otherwise we'll get one cbar per image in the same cs
)
axs = plt.gcf().get_axes()

if share_coordinate_system == "all":
Expand Down
22 changes: 6 additions & 16 deletions tests/pl/test_render_images.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,7 +2,6 @@
import matplotlib
import numpy as np
import scanpy as sc
from matplotlib import pyplot as plt
from matplotlib.colors import Normalize
from spatial_image import to_spatial_image
from spatialdata import SpatialData
Expand DownExpand Up@@ -49,9 +48,6 @@ def test_plot_can_render_a_single_channel_from_image(self, sdata_blobs: SpatialD
def test_plot_can_render_a_single_channel_from_multiscale_image(self, sdata_blobs: SpatialData):
sdata_blobs.pl.render_images(element="blobs_multiscale_image", channel=0).pl.show()

def test_plot_can_render_a_single_channel_from_image_no_el(self, sdata_blobs: SpatialData):
sdata_blobs.pl.render_images(channel=0).pl.show()

def test_plot_can_render_a_single_channel_str_from_image(self, sdata_blobs_str: SpatialData):
sdata_blobs_str.pl.render_images(element="blobs_image", channel="c1").pl.show()

Expand All@@ -70,16 +66,13 @@ def test_plot_can_render_two_channels_str_from_image(self, sdata_blobs_str: Spat
def test_plot_can_render_two_channels_str_from_multiscale_image(self, sdata_blobs_str: SpatialData):
sdata_blobs_str.pl.render_images(element="blobs_multiscale_image", channel=["c1", "c2"]).pl.show()

def test_plot_can_pass_vmin_vmax(self, sdata_blobs: SpatialData):
fig, axs = plt.subplots(ncols=2, figsize=(6, 3))
sdata_blobs.pl.render_images(element="blobs_image", channel=1).pl.show(ax=axs[0])
sdata_blobs.pl.render_images(element="blobs_image", channel=1, vmin=0, vmax=0.4).pl.show(ax=axs[1])

def test_plot_can_pass_normalize(self, sdata_blobs: SpatialData):
fig, axs = plt.subplots(ncols=2, figsize=(6, 3))
def test_plot_can_pass_normalize_clip_True(self, sdata_blobs: SpatialData):
norm = Normalize(vmin=0, vmax=0.4, clip=True)
sdata_blobs.pl.render_images(element="blobs_image", channel=1).pl.show(ax=axs[0])
sdata_blobs.pl.render_images(element="blobs_image", channel=1, norm=norm).pl.show(ax=axs[1])
sdata_blobs.pl.render_images(element="blobs_image", channel=0, norm=norm).pl.show()

def test_plot_can_pass_normalize_clip_False(self, sdata_blobs: SpatialData):
norm = Normalize(vmin=0, vmax=0.4, clip=False)
sdata_blobs.pl.render_images(element="blobs_image", channel=0, norm=norm).pl.show()

def test_plot_can_pass_color_to_single_channel(self, sdata_blobs: SpatialData):
sdata_blobs.pl.render_images(element="blobs_image", channel=1, palette="red").pl.show()
Expand All@@ -97,9 +90,6 @@ def test_plot_can_pass_cmap_to_each_channel(self, sdata_blobs: SpatialData):
element="blobs_image", channel=[0, 1, 2], cmap=["Reds", "Greens", "Blues"]
).pl.show()

def test_plot_can_normalize_image(self, sdata_blobs: SpatialData):
sdata_blobs.pl.render_images(element="blobs_image", percentiles_for_norm=(5, 90)).pl.show()

def test_plot_can_render_multiscale_image(self, sdata_blobs: SpatialData):
sdata_blobs.pl.render_images("blobs_multiscale_image").pl.show()

Expand Down
4 changes: 3 additions & 1 deletion tests/pl/test_render_shapes.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,7 @@
import pandas as pd
import scanpy as sc
from anndata import AnnData
from matplotlib.colors import Normalize
from shapely.geometry import MultiPolygon, Point, Polygon
from spatialdata import SpatialData, deepcopy
from spatialdata.models import ShapesModel, TableModel
Expand DownExpand Up@@ -146,7 +147,8 @@ def test_plot_colorbar_can_be_normalised(self, sdata_blobs: SpatialData):
sdata_blobs["table"].obs["region"] = ["blobs_polygons"] * sdata_blobs["table"].n_obs
sdata_blobs["table"].uns["spatialdata_attrs"]["region"] = "blobs_polygons"
sdata_blobs.shapes["blobs_polygons"]["cluster"] = [1, 2, 3, 5, 20]
sdata_blobs.pl.render_shapes("blobs_polygons", color="cluster", groups=["c1"], norm=True).pl.show()
norm = Normalize(vmin=0, vmax=5, clip=True)
sdata_blobs.pl.render_shapes("blobs_polygons", color="cluster", groups=["c1"], norm=norm).pl.show()

def test_plot_can_plot_shapes_after_spatial_query(self, sdata_blobs: SpatialData):
# subset to only shapes, should be unnecessary after rasterizeation of multiscale images is included
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
a55318c
fix
timtreis Sep 3, 2024
381b895
fixed case without info for column
timtreis Sep 3, 2024
487f6a5
fixed case for no-column, but modified na_color
timtreis Sep 3, 2024
a9c7609
added images from runner
timtreis Sep 3, 2024
c7e3260
bugfix for NA color
timtreis Sep 3, 2024
d0b7a1a
lowered testing threshold because mismatches are not being flagged
timtreis Sep 3, 2024
a9d5dbb
modified test to be visually easier to compare
timtreis Sep 3, 2024
fd762a9
further lowered testing threshold
timtreis Sep 3, 2024
729d465
Changed points behaviour and lowered test threshold
timtreis Sep 3, 2024
4421383
modified tests for better display
timtreis Sep 3, 2024
608bea0
fixed bug in typecheck
timtreis Sep 3, 2024
69bfc35
fixed test, added images from runner
timtreis Sep 3, 2024
7db0860
Updated CHANGELOG, added pic from runner
timtreis Sep 3, 2024
01a4f73
Removed dead code
timtreis Sep 3, 2024
eb2fc12
simplified test
timtreis Sep 3, 2024
4d40479
added images from runner
timtreis Sep 3, 2024
f8d6bac
fixed test
timtreis Sep 4, 2024
f6a2d16
fix
timtreis Sep 4, 2024
268a1b2
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Sep 4, 2024
81204ea
removed cbar
timtreis Sep 4, 2024
efb0f19
Merge branch 'bugfix/342-coloring-labels-by-a-continuous-variable-app…
timtreis Sep 4, 2024
ca0ff4e
merge
timtreis Sep 4, 2024
62d7cb9
added img from runner
timtreis Sep 4, 2024
6f4a634
Removed percentiles_for_norm parameter, delegating to cmap.norm
timtreis Sep 4, 2024
9377422
fixed shapes cbar logic
timtreis Sep 4, 2024
d4cfc2a
fixed test img generation
timtreis Sep 4, 2024
385dac3
fixed cmap limits for shapes
timtreis Sep 4, 2024
cc21303
modified test
timtreis Sep 4, 2024
2309404
updated CHANGELOG, added img from runner
timtreis Sep 4, 2024
a97eec1
Update CHANGELOG.md
timtreis Sep 4, 2024
667c689
Merge branch 'main' into 324-unable-to-set-vmin-vmax-when-plotting-ve…
timtreis Sep 4, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -18,11 +18,15 @@ and this project adheres to [Semantic Versioning][].

- Lowered RMSE-threshold for plot-based tests from 45 to 15 (#344)
- When subsetting to `groups`, `NA` isn't automatically added to legend (#344)
- When rendering a single image channel, a colorbar is now shown (#346)
- Removed `percentiles_for_norm` parameter (#346)
- Changed `norm` to no longer accept bools, only `mpl.colors.Normalise` or `None` (#346)

### Fixed

- Filtering with `groups` now preserves original cmap (#344)
- Non-selected `groups` are now not shown in `na_color` (#344)
- Several issues associated with `norm` and `colorbar` (#346)

## [0.2.5] - 2024-08-23

Expand Down
16 changes: 3 additions & 13 deletions src/spatialdata_plot/pl/basic.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -166,7 +166,7 @@ def render_shapes(
outline_color: str | list[float] = "#000000ff",
outline_alpha: float | int = 0.0,
cmap: Colormap | str | None = None,
norm: bool | Normalize = False,
norm: Normalize | None = None,
scale: float | int = 1.0,
method: str | None = None,
table_name: str | None = None,
Expand DownExpand Up@@ -301,7 +301,7 @@ def render_points(
palette: list[str] | str | None = None,
na_color: ColorLike | None = "default",
cmap: Colormap | str | None = None,
norm: None | Normalize = None,
norm: Normalize | None = None,
size: float | int = 1.0,
method: str | None = None,
table_name: str | None = None,
Expand DownExpand Up@@ -422,7 +422,6 @@ def render_images(
na_color: ColorLike | None = "default",
palette: list[str] | str | None = None,
alpha: float | int = 1.0,
percentiles_for_norm: tuple[float, float] | None = None,
scale: str | None = None,
**kwargs: Any,
) -> sd.SpatialData:
Expand DownExpand Up@@ -457,8 +456,6 @@ def render_images(
Palette to color images. The number of palettes should be equal to the number of channels.
alpha : float | int, default 1.0
Alpha value for the images. Must be a numeric between 0 and 1.
percentiles_for_norm : tuple[float, float] | None
Optional pair of floats (pmin < pmax, 0-100) which will be used for quantile normalization.
scale : str | None
Influences the resolution of the rendering. Possibilities include:
1) `None` (default): The image is rasterized to fit the canvas size. For
Expand DownExpand Up@@ -486,20 +483,14 @@ def render_images(
cmap=cmap,
norm=norm,
scale=scale,
percentiles_for_norm=percentiles_for_norm,
)

sdata = self._copy()
sdata = _verify_plotting_tree(sdata)
n_steps = len(sdata.plotting_tree.keys())

for element, param_values in params_dict.items():
# cmap_params = _prepare_cmap_norm(
# cmap=params_dict[element]["cmap"],
# norm=norm,
# na_color=params_dict[element]["na_color"], # type: ignore[arg-type]
# **kwargs,
# )

cmap_params: list[CmapParams] | CmapParams
if isinstance(cmap, list):
cmap_params = [
Expand All@@ -525,7 +516,6 @@ def render_images(
cmap_params=cmap_params,
palette=param_values["palette"],
alpha=param_values["alpha"],
percentiles_for_norm=param_values["percentiles_for_norm"],
scale=param_values["scale"],
zorder=n_steps,
)
Expand Down
30 changes: 13 additions & 17 deletions src/spatialdata_plot/pl/render.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,6 +9,7 @@
import datashader as ds
import geopandas as gpd
import matplotlib
import matplotlib.pyplot as plt
import matplotlib.transforms as mtransforms
import numpy as np
import pandas as pd
Expand DownExpand Up@@ -47,7 +48,6 @@
_maybe_set_colors,
_mpl_ax_contains_elements,
_multiscale_to_spatial_image,
_normalize,
_rasterize_if_necessary,
_set_color_source_vec,
to_hex,
Expand DownExpand Up@@ -128,6 +128,7 @@ def _render_shapes(
shapes = shapes.reset_index()
color_source_vector = color_source_vector[mask]
color_vector = color_vector[mask]

shapes = gpd.GeoDataFrame(shapes, geometry="geometry")

# Using dict.fromkeys here since set returns in arbitrary order
Expand DownExpand Up@@ -255,9 +256,13 @@ def _render_shapes(
for path in _cax.get_paths():
path.vertices = trans.transform(path.vertices)

# Sets the limits of the colorbar to the values instead of [0, 1]
if not norm and not values_are_categorical:
_cax.set_clim(min(color_vector), max(color_vector))
if not values_are_categorical:
# If the user passed a Normalize object with vmin/vmax we'll use those,
# # if not we'll use the min/max of the color_vector
_cax.set_clim(
vmin=render_params.cmap_params.norm.vmin or min(color_vector),
vmax=render_params.cmap_params.norm.vmax or max(color_vector),
)

if len(set(color_vector)) != 1 or list(set(color_vector))[0] != to_hex(render_params.cmap_params.na_color):
# necessary in case different shapes elements are annotated with one table
Expand DownExpand Up@@ -603,11 +608,6 @@ def _render_images(
if n_channels == 1 and not isinstance(render_params.cmap_params, list):
layer = img.sel(c=channels[0]).squeeze() if isinstance(channels[0], str) else img.isel(c=channels[0]).squeeze()

if render_params.percentiles_for_norm != (None, None):
layer = _normalize(
layer, pmin=render_params.percentiles_for_norm[0], pmax=render_params.percentiles_for_norm[1], clip=True
)

if render_params.cmap_params.norm: # type: ignore[attr-defined]
layer = render_params.cmap_params.norm(layer) # type: ignore[attr-defined]

Expand All@@ -623,20 +623,16 @@ def _render_images(

_ax_show_and_transform(layer, trans_data, ax, cmap=cmap, zorder=render_params.zorder)

if legend_params.colorbar:
sm = plt.cm.ScalarMappable(cmap=cmap, norm=render_params.cmap_params.norm)
fig_params.fig.colorbar(sm, ax=ax)

# 2) Image has any number of channels but 1
else:
layers = {}
for ch_index, c in enumerate(channels):
layers[c] = img.sel(c=c).copy(deep=True).squeeze()

if render_params.percentiles_for_norm != (None, None):
layers[c] = _normalize(
layers[c],
pmin=render_params.percentiles_for_norm[0],
pmax=render_params.percentiles_for_norm[1],
clip=True,
)

if not isinstance(render_params.cmap_params, list):
if render_params.cmap_params.norm is not None:
layers[c] = render_params.cmap_params.norm(layers[c])
Expand Down
29 changes: 1 addition & 28 deletions src/spatialdata_plot/pl/utils.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -489,7 +489,7 @@ def _get_scalebar(

def _prepare_cmap_norm(
cmap: Colormap | str | None = None,
norm: Normalize | bool = False,
norm: Normalize | None = None,
na_color: ColorLike | None = None,
vmin: float | None = None,
vmax: float | None = None,
Expand DownExpand Up@@ -1623,29 +1623,6 @@ def _type_check_params(param_dict: dict[str, Any], element_type: str) -> dict[st
if scale < 0:
raise ValueError("Parameter 'scale' must be a positive number.")

if (percentiles_for_norm := param_dict.get("percentiles_for_norm")) is None:
percentiles_for_norm = (None, None)
elif not (isinstance(percentiles_for_norm, (list, tuple)) or len(percentiles_for_norm) != 2):
raise TypeError("Parameter 'percentiles_for_norm' must be a list or tuple of exactly two floats or None.")
elif not all(
isinstance(p, (float, int, type(None)))
and isinstance(p, type(percentiles_for_norm[0]))
and (p is None or 0 <= p <= 100)
for p in percentiles_for_norm
):
raise TypeError(
"Each item in 'percentiles_for_norm' must be of the same dtype and must be a float or int within [0, 100], "
"or None"
)
elif (
percentiles_for_norm[0] is not None
and percentiles_for_norm[1] is not None
and percentiles_for_norm[0] > percentiles_for_norm[1]
):
raise ValueError("The first number in 'percentiles_for_norm' must not be smaller than the second.")
if "percentiles_for_norm" in param_dict:
param_dict["percentiles_for_norm"] = percentiles_for_norm

if size := param_dict.get("size"):
if not isinstance(size, (float, int)):
raise TypeError("Parameter 'size' must be numeric.")
Expand DownExpand Up@@ -1886,7 +1863,6 @@ def _validate_image_render_params(
cmap: list[Colormap | str] | Colormap | str | None,
norm: Normalize | None,
scale: str | None,
percentiles_for_norm: tuple[float | None, float | None] | None,
) -> dict[str, dict[str, Any]]:
param_dict: dict[str, Any] = {
"sdata": sdata,
Expand All@@ -1898,7 +1874,6 @@ def _validate_image_render_params(
"cmap": cmap,
"norm": norm,
"scale": scale,
"percentiles_for_norm": percentiles_for_norm,
}
param_dict = _type_check_params(param_dict, "images")

Expand DownExpand Up@@ -1945,8 +1920,6 @@ def _validate_image_render_params(
else:
element_params[el]["scale"] = scale

element_params[el]["percentiles_for_norm"] = param_dict["percentiles_for_norm"]

return element_params


Expand Down
Binary file modifiedtests/_images/Images_can_pass_cmap_to_single_channel.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modifiedtests/_images/Images_can_pass_color_to_each_channel.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modifiedtests/_images/Images_can_pass_color_to_single_channel.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modifiedtests/_images/Images_can_stack_render_images.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modifiedtests/_images/Shapes_colorbar_can_be_normalised.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modifiedtests/_images/Shapes_colorbar_respects_input_limits.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 3 additions & 1 deletion tests/pl/test_render.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -23,7 +23,9 @@ def test_render_images_can_plot_one_cyx_image(request):
def test_render_images_can_plot_multiple_cyx_images(share_coordinate_system: str, request):
fun = request.getfixturevalue("get_sdata_with_multiple_images")
sdata = fun(share_coordinate_system)
sdata.pl.render_images().pl.show()
sdata.pl.render_images().pl.show(
colorbar=False, # otherwise we'll get one cbar per image in the same cs
)
axs = plt.gcf().get_axes()

if share_coordinate_system == "all":
Expand Down
22 changes: 6 additions & 16 deletions tests/pl/test_render_images.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,7 +2,6 @@
import matplotlib
import numpy as np
import scanpy as sc
from matplotlib import pyplot as plt
from matplotlib.colors import Normalize
from spatial_image import to_spatial_image
from spatialdata import SpatialData
Expand DownExpand Up@@ -49,9 +48,6 @@ def test_plot_can_render_a_single_channel_from_image(self, sdata_blobs: SpatialD
def test_plot_can_render_a_single_channel_from_multiscale_image(self, sdata_blobs: SpatialData):
sdata_blobs.pl.render_images(element="blobs_multiscale_image", channel=0).pl.show()

def test_plot_can_render_a_single_channel_from_image_no_el(self, sdata_blobs: SpatialData):
sdata_blobs.pl.render_images(channel=0).pl.show()

def test_plot_can_render_a_single_channel_str_from_image(self, sdata_blobs_str: SpatialData):
sdata_blobs_str.pl.render_images(element="blobs_image", channel="c1").pl.show()

Expand All@@ -70,16 +66,13 @@ def test_plot_can_render_two_channels_str_from_image(self, sdata_blobs_str: Spat
def test_plot_can_render_two_channels_str_from_multiscale_image(self, sdata_blobs_str: SpatialData):
sdata_blobs_str.pl.render_images(element="blobs_multiscale_image", channel=["c1", "c2"]).pl.show()

def test_plot_can_pass_vmin_vmax(self, sdata_blobs: SpatialData):
fig, axs = plt.subplots(ncols=2, figsize=(6, 3))
sdata_blobs.pl.render_images(element="blobs_image", channel=1).pl.show(ax=axs[0])
sdata_blobs.pl.render_images(element="blobs_image", channel=1, vmin=0, vmax=0.4).pl.show(ax=axs[1])

def test_plot_can_pass_normalize(self, sdata_blobs: SpatialData):
fig, axs = plt.subplots(ncols=2, figsize=(6, 3))
def test_plot_can_pass_normalize_clip_True(self, sdata_blobs: SpatialData):
norm = Normalize(vmin=0, vmax=0.4, clip=True)
sdata_blobs.pl.render_images(element="blobs_image", channel=1).pl.show(ax=axs[0])
sdata_blobs.pl.render_images(element="blobs_image", channel=1, norm=norm).pl.show(ax=axs[1])
sdata_blobs.pl.render_images(element="blobs_image", channel=0, norm=norm).pl.show()

def test_plot_can_pass_normalize_clip_False(self, sdata_blobs: SpatialData):
norm = Normalize(vmin=0, vmax=0.4, clip=False)
sdata_blobs.pl.render_images(element="blobs_image", channel=0, norm=norm).pl.show()

def test_plot_can_pass_color_to_single_channel(self, sdata_blobs: SpatialData):
sdata_blobs.pl.render_images(element="blobs_image", channel=1, palette="red").pl.show()
Expand All@@ -97,9 +90,6 @@ def test_plot_can_pass_cmap_to_each_channel(self, sdata_blobs: SpatialData):
element="blobs_image", channel=[0, 1, 2], cmap=["Reds", "Greens", "Blues"]
).pl.show()

def test_plot_can_normalize_image(self, sdata_blobs: SpatialData):
sdata_blobs.pl.render_images(element="blobs_image", percentiles_for_norm=(5, 90)).pl.show()

def test_plot_can_render_multiscale_image(self, sdata_blobs: SpatialData):
sdata_blobs.pl.render_images("blobs_multiscale_image").pl.show()

Expand Down
4 changes: 3 additions & 1 deletion tests/pl/test_render_shapes.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,7 @@
import pandas as pd
import scanpy as sc
from anndata import AnnData
from matplotlib.colors import Normalize
from shapely.geometry import MultiPolygon, Point, Polygon
from spatialdata import SpatialData, deepcopy
from spatialdata.models import ShapesModel, TableModel
Expand DownExpand Up@@ -146,7 +147,8 @@ def test_plot_colorbar_can_be_normalised(self, sdata_blobs: SpatialData):
sdata_blobs["table"].obs["region"] = ["blobs_polygons"] * sdata_blobs["table"].n_obs
sdata_blobs["table"].uns["spatialdata_attrs"]["region"] = "blobs_polygons"
sdata_blobs.shapes["blobs_polygons"]["cluster"] = [1, 2, 3, 5, 20]
sdata_blobs.pl.render_shapes("blobs_polygons", color="cluster", groups=["c1"], norm=True).pl.show()
norm = Normalize(vmin=0, vmax=5, clip=True)
sdata_blobs.pl.render_shapes("blobs_polygons", color="cluster", groups=["c1"], norm=norm).pl.show()

def test_plot_can_plot_shapes_after_spatial_query(self, sdata_blobs: SpatialData):
# subset to only shapes, should be unnecessary after rasterizeation of multiscale images is included
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
a55318c
fix
timtreis Sep 3, 2024
381b895
fixed case without info for column
timtreis Sep 3, 2024
487f6a5
fixed case for no-column, but modified na_color
timtreis Sep 3, 2024
a9c7609
added images from runner
timtreis Sep 3, 2024
c7e3260
bugfix for NA color
timtreis Sep 3, 2024
d0b7a1a
lowered testing threshold because mismatches are not being flagged
timtreis Sep 3, 2024
a9d5dbb
modified test to be visually easier to compare
timtreis Sep 3, 2024
fd762a9
further lowered testing threshold
timtreis Sep 3, 2024
729d465
Changed points behaviour and lowered test threshold
timtreis Sep 3, 2024
4421383
modified tests for better display
timtreis Sep 3, 2024
608bea0
fixed bug in typecheck
timtreis Sep 3, 2024
69bfc35
fixed test, added images from runner
timtreis Sep 3, 2024
7db0860
Updated CHANGELOG, added pic from runner
timtreis Sep 3, 2024
01a4f73
Removed dead code
timtreis Sep 3, 2024
eb2fc12
simplified test
timtreis Sep 3, 2024
4d40479
added images from runner
timtreis Sep 3, 2024
f8d6bac
fixed test
timtreis Sep 4, 2024
f6a2d16
fix
timtreis Sep 4, 2024
268a1b2
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Sep 4, 2024
81204ea
removed cbar
timtreis Sep 4, 2024
efb0f19
Merge branch 'bugfix/342-coloring-labels-by-a-continuous-variable-app…
timtreis Sep 4, 2024
ca0ff4e
merge
timtreis Sep 4, 2024
62d7cb9
added img from runner
timtreis Sep 4, 2024
6f4a634
Removed percentiles_for_norm parameter, delegating to cmap.norm
timtreis Sep 4, 2024
9377422
fixed shapes cbar logic
timtreis Sep 4, 2024
d4cfc2a
fixed test img generation
timtreis Sep 4, 2024
385dac3
fixed cmap limits for shapes
timtreis Sep 4, 2024
cc21303
modified test
timtreis Sep 4, 2024
2309404
updated CHANGELOG, added img from runner
timtreis Sep 4, 2024
a97eec1
Update CHANGELOG.md
timtreis Sep 4, 2024
667c689
Merge branch 'main' into 324-unable-to-set-vmin-vmax-when-plotting-ve…
timtreis Sep 4, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -18,11 +18,15 @@ and this project adheres to [Semantic Versioning][].

- Lowered RMSE-threshold for plot-based tests from 45 to 15 (#344)
- When subsetting to `groups`, `NA` isn't automatically added to legend (#344)
- When rendering a single image channel, a colorbar is now shown (#346)
- Removed `percentiles_for_norm` parameter (#346)
- Changed `norm` to no longer accept bools, only `mpl.colors.Normalise` or `None` (#346)

### Fixed

- Filtering with `groups` now preserves original cmap (#344)
- Non-selected `groups` are now not shown in `na_color` (#344)
- Several issues associated with `norm` and `colorbar` (#346)

## [0.2.5] - 2024-08-23

Expand Down
16 changes: 3 additions & 13 deletions src/spatialdata_plot/pl/basic.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -166,7 +166,7 @@ def render_shapes(
outline_color: str | list[float] = "#000000ff",
outline_alpha: float | int = 0.0,
cmap: Colormap | str | None = None,
norm: bool | Normalize = False,
norm: Normalize | None = None,
scale: float | int = 1.0,
method: str | None = None,
table_name: str | None = None,
Expand DownExpand Up@@ -301,7 +301,7 @@ def render_points(
palette: list[str] | str | None = None,
na_color: ColorLike | None = "default",
cmap: Colormap | str | None = None,
norm: None | Normalize = None,
norm: Normalize | None = None,
size: float | int = 1.0,
method: str | None = None,
table_name: str | None = None,
Expand DownExpand Up@@ -422,7 +422,6 @@ def render_images(
na_color: ColorLike | None = "default",
palette: list[str] | str | None = None,
alpha: float | int = 1.0,
percentiles_for_norm: tuple[float, float] | None = None,
scale: str | None = None,
**kwargs: Any,
) -> sd.SpatialData:
Expand DownExpand Up@@ -457,8 +456,6 @@ def render_images(
Palette to color images. The number of palettes should be equal to the number of channels.
alpha : float | int, default 1.0
Alpha value for the images. Must be a numeric between 0 and 1.
percentiles_for_norm : tuple[float, float] | None
Optional pair of floats (pmin < pmax, 0-100) which will be used for quantile normalization.
scale : str | None
Influences the resolution of the rendering. Possibilities include:
1) `None` (default): The image is rasterized to fit the canvas size. For
Expand DownExpand Up@@ -486,20 +483,14 @@ def render_images(
cmap=cmap,
norm=norm,
scale=scale,
percentiles_for_norm=percentiles_for_norm,
)

sdata = self._copy()
sdata = _verify_plotting_tree(sdata)
n_steps = len(sdata.plotting_tree.keys())

for element, param_values in params_dict.items():
# cmap_params = _prepare_cmap_norm(
# cmap=params_dict[element]["cmap"],
# norm=norm,
# na_color=params_dict[element]["na_color"], # type: ignore[arg-type]
# **kwargs,
# )

cmap_params: list[CmapParams] | CmapParams
if isinstance(cmap, list):
cmap_params = [
Expand All@@ -525,7 +516,6 @@ def render_images(
cmap_params=cmap_params,
palette=param_values["palette"],
alpha=param_values["alpha"],
percentiles_for_norm=param_values["percentiles_for_norm"],
scale=param_values["scale"],
zorder=n_steps,
)
Expand Down
30 changes: 13 additions & 17 deletions src/spatialdata_plot/pl/render.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,6 +9,7 @@
import datashader as ds
import geopandas as gpd
import matplotlib
import matplotlib.pyplot as plt
import matplotlib.transforms as mtransforms
import numpy as np
import pandas as pd
Expand DownExpand Up@@ -47,7 +48,6 @@
_maybe_set_colors,
_mpl_ax_contains_elements,
_multiscale_to_spatial_image,
_normalize,
_rasterize_if_necessary,
_set_color_source_vec,
to_hex,
Expand DownExpand Up@@ -128,6 +128,7 @@ def _render_shapes(
shapes = shapes.reset_index()
color_source_vector = color_source_vector[mask]
color_vector = color_vector[mask]

shapes = gpd.GeoDataFrame(shapes, geometry="geometry")

# Using dict.fromkeys here since set returns in arbitrary order
Expand DownExpand Up@@ -255,9 +256,13 @@ def _render_shapes(
for path in _cax.get_paths():
path.vertices = trans.transform(path.vertices)

# Sets the limits of the colorbar to the values instead of [0, 1]
if not norm and not values_are_categorical:
_cax.set_clim(min(color_vector), max(color_vector))
if not values_are_categorical:
# If the user passed a Normalize object with vmin/vmax we'll use those,
# # if not we'll use the min/max of the color_vector
_cax.set_clim(
vmin=render_params.cmap_params.norm.vmin or min(color_vector),
vmax=render_params.cmap_params.norm.vmax or max(color_vector),
)

if len(set(color_vector)) != 1 or list(set(color_vector))[0] != to_hex(render_params.cmap_params.na_color):
# necessary in case different shapes elements are annotated with one table
Expand DownExpand Up@@ -603,11 +608,6 @@ def _render_images(
if n_channels == 1 and not isinstance(render_params.cmap_params, list):
layer = img.sel(c=channels[0]).squeeze() if isinstance(channels[0], str) else img.isel(c=channels[0]).squeeze()

if render_params.percentiles_for_norm != (None, None):
layer = _normalize(
layer, pmin=render_params.percentiles_for_norm[0], pmax=render_params.percentiles_for_norm[1], clip=True
)

if render_params.cmap_params.norm: # type: ignore[attr-defined]
layer = render_params.cmap_params.norm(layer) # type: ignore[attr-defined]

Expand All@@ -623,20 +623,16 @@ def _render_images(

_ax_show_and_transform(layer, trans_data, ax, cmap=cmap, zorder=render_params.zorder)

if legend_params.colorbar:
sm = plt.cm.ScalarMappable(cmap=cmap, norm=render_params.cmap_params.norm)
fig_params.fig.colorbar(sm, ax=ax)

# 2) Image has any number of channels but 1
else:
layers = {}
for ch_index, c in enumerate(channels):
layers[c] = img.sel(c=c).copy(deep=True).squeeze()

if render_params.percentiles_for_norm != (None, None):
layers[c] = _normalize(
layers[c],
pmin=render_params.percentiles_for_norm[0],
pmax=render_params.percentiles_for_norm[1],
clip=True,
)

if not isinstance(render_params.cmap_params, list):
if render_params.cmap_params.norm is not None:
layers[c] = render_params.cmap_params.norm(layers[c])
Expand Down
29 changes: 1 addition & 28 deletions src/spatialdata_plot/pl/utils.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -489,7 +489,7 @@ def _get_scalebar(

def _prepare_cmap_norm(
cmap: Colormap | str | None = None,
norm: Normalize | bool = False,
norm: Normalize | None = None,
na_color: ColorLike | None = None,
vmin: float | None = None,
vmax: float | None = None,
Expand DownExpand Up@@ -1623,29 +1623,6 @@ def _type_check_params(param_dict: dict[str, Any], element_type: str) -> dict[st
if scale < 0:
raise ValueError("Parameter 'scale' must be a positive number.")

if (percentiles_for_norm := param_dict.get("percentiles_for_norm")) is None:
percentiles_for_norm = (None, None)
elif not (isinstance(percentiles_for_norm, (list, tuple)) or len(percentiles_for_norm) != 2):
raise TypeError("Parameter 'percentiles_for_norm' must be a list or tuple of exactly two floats or None.")
elif not all(
isinstance(p, (float, int, type(None)))
and isinstance(p, type(percentiles_for_norm[0]))
and (p is None or 0 <= p <= 100)
for p in percentiles_for_norm
):
raise TypeError(
"Each item in 'percentiles_for_norm' must be of the same dtype and must be a float or int within [0, 100], "
"or None"
)
elif (
percentiles_for_norm[0] is not None
and percentiles_for_norm[1] is not None
and percentiles_for_norm[0] > percentiles_for_norm[1]
):
raise ValueError("The first number in 'percentiles_for_norm' must not be smaller than the second.")
if "percentiles_for_norm" in param_dict:
param_dict["percentiles_for_norm"] = percentiles_for_norm

if size := param_dict.get("size"):
if not isinstance(size, (float, int)):
raise TypeError("Parameter 'size' must be numeric.")
Expand DownExpand Up@@ -1886,7 +1863,6 @@ def _validate_image_render_params(
cmap: list[Colormap | str] | Colormap | str | None,
norm: Normalize | None,
scale: str | None,
percentiles_for_norm: tuple[float | None, float | None] | None,
) -> dict[str, dict[str, Any]]:
param_dict: dict[str, Any] = {
"sdata": sdata,
Expand All@@ -1898,7 +1874,6 @@ def _validate_image_render_params(
"cmap": cmap,
"norm": norm,
"scale": scale,
"percentiles_for_norm": percentiles_for_norm,
}
param_dict = _type_check_params(param_dict, "images")

Expand DownExpand Up@@ -1945,8 +1920,6 @@ def _validate_image_render_params(
else:
element_params[el]["scale"] = scale

element_params[el]["percentiles_for_norm"] = param_dict["percentiles_for_norm"]

return element_params


Expand Down
Binary file modifiedtests/_images/Images_can_pass_cmap_to_single_channel.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modifiedtests/_images/Images_can_pass_color_to_each_channel.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modifiedtests/_images/Images_can_pass_color_to_single_channel.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modifiedtests/_images/Images_can_stack_render_images.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modifiedtests/_images/Shapes_colorbar_can_be_normalised.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modifiedtests/_images/Shapes_colorbar_respects_input_limits.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 3 additions & 1 deletion tests/pl/test_render.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -23,7 +23,9 @@ def test_render_images_can_plot_one_cyx_image(request):
def test_render_images_can_plot_multiple_cyx_images(share_coordinate_system: str, request):
fun = request.getfixturevalue("get_sdata_with_multiple_images")
sdata = fun(share_coordinate_system)
sdata.pl.render_images().pl.show()
sdata.pl.render_images().pl.show(
colorbar=False, # otherwise we'll get one cbar per image in the same cs
)
axs = plt.gcf().get_axes()

if share_coordinate_system == "all":
Expand Down
22 changes: 6 additions & 16 deletions tests/pl/test_render_images.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,7 +2,6 @@
import matplotlib
import numpy as np
import scanpy as sc
from matplotlib import pyplot as plt
from matplotlib.colors import Normalize
from spatial_image import to_spatial_image
from spatialdata import SpatialData
Expand DownExpand Up@@ -49,9 +48,6 @@ def test_plot_can_render_a_single_channel_from_image(self, sdata_blobs: SpatialD
def test_plot_can_render_a_single_channel_from_multiscale_image(self, sdata_blobs: SpatialData):
sdata_blobs.pl.render_images(element="blobs_multiscale_image", channel=0).pl.show()

def test_plot_can_render_a_single_channel_from_image_no_el(self, sdata_blobs: SpatialData):
sdata_blobs.pl.render_images(channel=0).pl.show()

def test_plot_can_render_a_single_channel_str_from_image(self, sdata_blobs_str: SpatialData):
sdata_blobs_str.pl.render_images(element="blobs_image", channel="c1").pl.show()

Expand All@@ -70,16 +66,13 @@ def test_plot_can_render_two_channels_str_from_image(self, sdata_blobs_str: Spat
def test_plot_can_render_two_channels_str_from_multiscale_image(self, sdata_blobs_str: SpatialData):
sdata_blobs_str.pl.render_images(element="blobs_multiscale_image", channel=["c1", "c2"]).pl.show()

def test_plot_can_pass_vmin_vmax(self, sdata_blobs: SpatialData):
fig, axs = plt.subplots(ncols=2, figsize=(6, 3))
sdata_blobs.pl.render_images(element="blobs_image", channel=1).pl.show(ax=axs[0])
sdata_blobs.pl.render_images(element="blobs_image", channel=1, vmin=0, vmax=0.4).pl.show(ax=axs[1])

def test_plot_can_pass_normalize(self, sdata_blobs: SpatialData):
fig, axs = plt.subplots(ncols=2, figsize=(6, 3))
def test_plot_can_pass_normalize_clip_True(self, sdata_blobs: SpatialData):
norm = Normalize(vmin=0, vmax=0.4, clip=True)
sdata_blobs.pl.render_images(element="blobs_image", channel=1).pl.show(ax=axs[0])
sdata_blobs.pl.render_images(element="blobs_image", channel=1, norm=norm).pl.show(ax=axs[1])
sdata_blobs.pl.render_images(element="blobs_image", channel=0, norm=norm).pl.show()

def test_plot_can_pass_normalize_clip_False(self, sdata_blobs: SpatialData):
norm = Normalize(vmin=0, vmax=0.4, clip=False)
sdata_blobs.pl.render_images(element="blobs_image", channel=0, norm=norm).pl.show()

def test_plot_can_pass_color_to_single_channel(self, sdata_blobs: SpatialData):
sdata_blobs.pl.render_images(element="blobs_image", channel=1, palette="red").pl.show()
Expand All@@ -97,9 +90,6 @@ def test_plot_can_pass_cmap_to_each_channel(self, sdata_blobs: SpatialData):
element="blobs_image", channel=[0, 1, 2], cmap=["Reds", "Greens", "Blues"]
).pl.show()

def test_plot_can_normalize_image(self, sdata_blobs: SpatialData):
sdata_blobs.pl.render_images(element="blobs_image", percentiles_for_norm=(5, 90)).pl.show()

def test_plot_can_render_multiscale_image(self, sdata_blobs: SpatialData):
sdata_blobs.pl.render_images("blobs_multiscale_image").pl.show()

Expand Down
4 changes: 3 additions & 1 deletion tests/pl/test_render_shapes.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,7 @@
import pandas as pd
import scanpy as sc
from anndata import AnnData
from matplotlib.colors import Normalize
from shapely.geometry import MultiPolygon, Point, Polygon
from spatialdata import SpatialData, deepcopy
from spatialdata.models import ShapesModel, TableModel
Expand DownExpand Up@@ -146,7 +147,8 @@ def test_plot_colorbar_can_be_normalised(self, sdata_blobs: SpatialData):
sdata_blobs["table"].obs["region"] = ["blobs_polygons"] * sdata_blobs["table"].n_obs
sdata_blobs["table"].uns["spatialdata_attrs"]["region"] = "blobs_polygons"
sdata_blobs.shapes["blobs_polygons"]["cluster"] = [1, 2, 3, 5, 20]
sdata_blobs.pl.render_shapes("blobs_polygons", color="cluster", groups=["c1"], norm=True).pl.show()
norm = Normalize(vmin=0, vmax=5, clip=True)
sdata_blobs.pl.render_shapes("blobs_polygons", color="cluster", groups=["c1"], norm=norm).pl.show()

def test_plot_can_plot_shapes_after_spatial_query(self, sdata_blobs: SpatialData):
# subset to only shapes, should be unnecessary after rasterizeation of multiscale images is included
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
a55318c
fix
timtreis Sep 3, 2024
381b895
fixed case without info for column
timtreis Sep 3, 2024
487f6a5
fixed case for no-column, but modified na_color
timtreis Sep 3, 2024
a9c7609
added images from runner
timtreis Sep 3, 2024
c7e3260
bugfix for NA color
timtreis Sep 3, 2024
d0b7a1a
lowered testing threshold because mismatches are not being flagged
timtreis Sep 3, 2024
a9d5dbb
modified test to be visually easier to compare
timtreis Sep 3, 2024
fd762a9
further lowered testing threshold
timtreis Sep 3, 2024
729d465
Changed points behaviour and lowered test threshold
timtreis Sep 3, 2024
4421383
modified tests for better display
timtreis Sep 3, 2024
608bea0
fixed bug in typecheck
timtreis Sep 3, 2024
69bfc35
fixed test, added images from runner
timtreis Sep 3, 2024
7db0860
Updated CHANGELOG, added pic from runner
timtreis Sep 3, 2024
01a4f73
Removed dead code
timtreis Sep 3, 2024
eb2fc12
simplified test
timtreis Sep 3, 2024
4d40479
added images from runner
timtreis Sep 3, 2024
f8d6bac
fixed test
timtreis Sep 4, 2024
f6a2d16
fix
timtreis Sep 4, 2024
268a1b2
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Sep 4, 2024
81204ea
removed cbar
timtreis Sep 4, 2024
efb0f19
Merge branch 'bugfix/342-coloring-labels-by-a-continuous-variable-app…
timtreis Sep 4, 2024
ca0ff4e
merge
timtreis Sep 4, 2024
62d7cb9
added img from runner
timtreis Sep 4, 2024
6f4a634
Removed percentiles_for_norm parameter, delegating to cmap.norm
timtreis Sep 4, 2024
9377422
fixed shapes cbar logic
timtreis Sep 4, 2024
d4cfc2a
fixed test img generation
timtreis Sep 4, 2024
385dac3
fixed cmap limits for shapes
timtreis Sep 4, 2024
cc21303
modified test
timtreis Sep 4, 2024
2309404
updated CHANGELOG, added img from runner
timtreis Sep 4, 2024
a97eec1
Update CHANGELOG.md
timtreis Sep 4, 2024
667c689
Merge branch 'main' into 324-unable-to-set-vmin-vmax-when-plotting-ve…
timtreis Sep 4, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -18,11 +18,15 @@ and this project adheres to [Semantic Versioning][].

- Lowered RMSE-threshold for plot-based tests from 45 to 15 (#344)
- When subsetting to `groups`, `NA` isn't automatically added to legend (#344)
- When rendering a single image channel, a colorbar is now shown (#346)
- Removed `percentiles_for_norm` parameter (#346)
- Changed `norm` to no longer accept bools, only `mpl.colors.Normalise` or `None` (#346)

### Fixed

- Filtering with `groups` now preserves original cmap (#344)
- Non-selected `groups` are now not shown in `na_color` (#344)
- Several issues associated with `norm` and `colorbar` (#346)

## [0.2.5] - 2024-08-23

Expand Down
16 changes: 3 additions & 13 deletions src/spatialdata_plot/pl/basic.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -166,7 +166,7 @@ def render_shapes(
outline_color: str | list[float] = "#000000ff",
outline_alpha: float | int = 0.0,
cmap: Colormap | str | None = None,
norm: bool | Normalize = False,
norm: Normalize | None = None,
scale: float | int = 1.0,
method: str | None = None,
table_name: str | None = None,
Expand DownExpand Up@@ -301,7 +301,7 @@ def render_points(
palette: list[str] | str | None = None,
na_color: ColorLike | None = "default",
cmap: Colormap | str | None = None,
norm: None | Normalize = None,
norm: Normalize | None = None,
size: float | int = 1.0,
method: str | None = None,
table_name: str | None = None,
Expand DownExpand Up@@ -422,7 +422,6 @@ def render_images(
na_color: ColorLike | None = "default",
palette: list[str] | str | None = None,
alpha: float | int = 1.0,
percentiles_for_norm: tuple[float, float] | None = None,
scale: str | None = None,
**kwargs: Any,
) -> sd.SpatialData:
Expand DownExpand Up@@ -457,8 +456,6 @@ def render_images(
Palette to color images. The number of palettes should be equal to the number of channels.
alpha : float | int, default 1.0
Alpha value for the images. Must be a numeric between 0 and 1.
percentiles_for_norm : tuple[float, float] | None
Optional pair of floats (pmin < pmax, 0-100) which will be used for quantile normalization.
scale : str | None
Influences the resolution of the rendering. Possibilities include:
1) `None` (default): The image is rasterized to fit the canvas size. For
Expand DownExpand Up@@ -486,20 +483,14 @@ def render_images(
cmap=cmap,
norm=norm,
scale=scale,
percentiles_for_norm=percentiles_for_norm,
)

sdata = self._copy()
sdata = _verify_plotting_tree(sdata)
n_steps = len(sdata.plotting_tree.keys())

for element, param_values in params_dict.items():
# cmap_params = _prepare_cmap_norm(
# cmap=params_dict[element]["cmap"],
# norm=norm,
# na_color=params_dict[element]["na_color"], # type: ignore[arg-type]
# **kwargs,
# )

cmap_params: list[CmapParams] | CmapParams
if isinstance(cmap, list):
cmap_params = [
Expand All@@ -525,7 +516,6 @@ def render_images(
cmap_params=cmap_params,
palette=param_values["palette"],
alpha=param_values["alpha"],
percentiles_for_norm=param_values["percentiles_for_norm"],
scale=param_values["scale"],
zorder=n_steps,
)
Expand Down
30 changes: 13 additions & 17 deletions src/spatialdata_plot/pl/render.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,6 +9,7 @@
import datashader as ds
import geopandas as gpd
import matplotlib
import matplotlib.pyplot as plt
import matplotlib.transforms as mtransforms
import numpy as np
import pandas as pd
Expand DownExpand Up@@ -47,7 +48,6 @@
_maybe_set_colors,
_mpl_ax_contains_elements,
_multiscale_to_spatial_image,
_normalize,
_rasterize_if_necessary,
_set_color_source_vec,
to_hex,
Expand DownExpand Up@@ -128,6 +128,7 @@ def _render_shapes(
shapes = shapes.reset_index()
color_source_vector = color_source_vector[mask]
color_vector = color_vector[mask]

shapes = gpd.GeoDataFrame(shapes, geometry="geometry")

# Using dict.fromkeys here since set returns in arbitrary order
Expand DownExpand Up@@ -255,9 +256,13 @@ def _render_shapes(
for path in _cax.get_paths():
path.vertices = trans.transform(path.vertices)

# Sets the limits of the colorbar to the values instead of [0, 1]
if not norm and not values_are_categorical:
_cax.set_clim(min(color_vector), max(color_vector))
if not values_are_categorical:
# If the user passed a Normalize object with vmin/vmax we'll use those,
# # if not we'll use the min/max of the color_vector
_cax.set_clim(
vmin=render_params.cmap_params.norm.vmin or min(color_vector),
vmax=render_params.cmap_params.norm.vmax or max(color_vector),
)

if len(set(color_vector)) != 1 or list(set(color_vector))[0] != to_hex(render_params.cmap_params.na_color):
# necessary in case different shapes elements are annotated with one table
Expand DownExpand Up@@ -603,11 +608,6 @@ def _render_images(
if n_channels == 1 and not isinstance(render_params.cmap_params, list):
layer = img.sel(c=channels[0]).squeeze() if isinstance(channels[0], str) else img.isel(c=channels[0]).squeeze()

if render_params.percentiles_for_norm != (None, None):
layer = _normalize(
layer, pmin=render_params.percentiles_for_norm[0], pmax=render_params.percentiles_for_norm[1], clip=True
)

if render_params.cmap_params.norm: # type: ignore[attr-defined]
layer = render_params.cmap_params.norm(layer) # type: ignore[attr-defined]

Expand All@@ -623,20 +623,16 @@ def _render_images(

_ax_show_and_transform(layer, trans_data, ax, cmap=cmap, zorder=render_params.zorder)

if legend_params.colorbar:
sm = plt.cm.ScalarMappable(cmap=cmap, norm=render_params.cmap_params.norm)
fig_params.fig.colorbar(sm, ax=ax)

# 2) Image has any number of channels but 1
else:
layers = {}
for ch_index, c in enumerate(channels):
layers[c] = img.sel(c=c).copy(deep=True).squeeze()

if render_params.percentiles_for_norm != (None, None):
layers[c] = _normalize(
layers[c],
pmin=render_params.percentiles_for_norm[0],
pmax=render_params.percentiles_for_norm[1],
clip=True,
)

if not isinstance(render_params.cmap_params, list):
if render_params.cmap_params.norm is not None:
layers[c] = render_params.cmap_params.norm(layers[c])
Expand Down
29 changes: 1 addition & 28 deletions src/spatialdata_plot/pl/utils.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -489,7 +489,7 @@ def _get_scalebar(

def _prepare_cmap_norm(
cmap: Colormap | str | None = None,
norm: Normalize | bool = False,
norm: Normalize | None = None,
na_color: ColorLike | None = None,
vmin: float | None = None,
vmax: float | None = None,
Expand DownExpand Up@@ -1623,29 +1623,6 @@ def _type_check_params(param_dict: dict[str, Any], element_type: str) -> dict[st
if scale < 0:
raise ValueError("Parameter 'scale' must be a positive number.")

if (percentiles_for_norm := param_dict.get("percentiles_for_norm")) is None:
percentiles_for_norm = (None, None)
elif not (isinstance(percentiles_for_norm, (list, tuple)) or len(percentiles_for_norm) != 2):
raise TypeError("Parameter 'percentiles_for_norm' must be a list or tuple of exactly two floats or None.")
elif not all(
isinstance(p, (float, int, type(None)))
and isinstance(p, type(percentiles_for_norm[0]))
and (p is None or 0 <= p <= 100)
for p in percentiles_for_norm
):
raise TypeError(
"Each item in 'percentiles_for_norm' must be of the same dtype and must be a float or int within [0, 100], "
"or None"
)
elif (
percentiles_for_norm[0] is not None
and percentiles_for_norm[1] is not None
and percentiles_for_norm[0] > percentiles_for_norm[1]
):
raise ValueError("The first number in 'percentiles_for_norm' must not be smaller than the second.")
if "percentiles_for_norm" in param_dict:
param_dict["percentiles_for_norm"] = percentiles_for_norm

if size := param_dict.get("size"):
if not isinstance(size, (float, int)):
raise TypeError("Parameter 'size' must be numeric.")
Expand DownExpand Up@@ -1886,7 +1863,6 @@ def _validate_image_render_params(
cmap: list[Colormap | str] | Colormap | str | None,
norm: Normalize | None,
scale: str | None,
percentiles_for_norm: tuple[float | None, float | None] | None,
) -> dict[str, dict[str, Any]]:
param_dict: dict[str, Any] = {
"sdata": sdata,
Expand All@@ -1898,7 +1874,6 @@ def _validate_image_render_params(
"cmap": cmap,
"norm": norm,
"scale": scale,
"percentiles_for_norm": percentiles_for_norm,
}
param_dict = _type_check_params(param_dict, "images")

Expand DownExpand Up@@ -1945,8 +1920,6 @@ def _validate_image_render_params(
else:
element_params[el]["scale"] = scale

element_params[el]["percentiles_for_norm"] = param_dict["percentiles_for_norm"]

return element_params


Expand Down
Binary file modifiedtests/_images/Images_can_pass_cmap_to_single_channel.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modifiedtests/_images/Images_can_pass_color_to_each_channel.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modifiedtests/_images/Images_can_pass_color_to_single_channel.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modifiedtests/_images/Images_can_stack_render_images.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modifiedtests/_images/Shapes_colorbar_can_be_normalised.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modifiedtests/_images/Shapes_colorbar_respects_input_limits.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 3 additions & 1 deletion tests/pl/test_render.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -23,7 +23,9 @@ def test_render_images_can_plot_one_cyx_image(request):
def test_render_images_can_plot_multiple_cyx_images(share_coordinate_system: str, request):
fun = request.getfixturevalue("get_sdata_with_multiple_images")
sdata = fun(share_coordinate_system)
sdata.pl.render_images().pl.show()
sdata.pl.render_images().pl.show(
colorbar=False, # otherwise we'll get one cbar per image in the same cs
)
axs = plt.gcf().get_axes()

if share_coordinate_system == "all":
Expand Down
22 changes: 6 additions & 16 deletions tests/pl/test_render_images.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,7 +2,6 @@
import matplotlib
import numpy as np
import scanpy as sc
from matplotlib import pyplot as plt
from matplotlib.colors import Normalize
from spatial_image import to_spatial_image
from spatialdata import SpatialData
Expand DownExpand Up@@ -49,9 +48,6 @@ def test_plot_can_render_a_single_channel_from_image(self, sdata_blobs: SpatialD
def test_plot_can_render_a_single_channel_from_multiscale_image(self, sdata_blobs: SpatialData):
sdata_blobs.pl.render_images(element="blobs_multiscale_image", channel=0).pl.show()

def test_plot_can_render_a_single_channel_from_image_no_el(self, sdata_blobs: SpatialData):
sdata_blobs.pl.render_images(channel=0).pl.show()

def test_plot_can_render_a_single_channel_str_from_image(self, sdata_blobs_str: SpatialData):
sdata_blobs_str.pl.render_images(element="blobs_image", channel="c1").pl.show()

Expand All@@ -70,16 +66,13 @@ def test_plot_can_render_two_channels_str_from_image(self, sdata_blobs_str: Spat
def test_plot_can_render_two_channels_str_from_multiscale_image(self, sdata_blobs_str: SpatialData):
sdata_blobs_str.pl.render_images(element="blobs_multiscale_image", channel=["c1", "c2"]).pl.show()

def test_plot_can_pass_vmin_vmax(self, sdata_blobs: SpatialData):
fig, axs = plt.subplots(ncols=2, figsize=(6, 3))
sdata_blobs.pl.render_images(element="blobs_image", channel=1).pl.show(ax=axs[0])
sdata_blobs.pl.render_images(element="blobs_image", channel=1, vmin=0, vmax=0.4).pl.show(ax=axs[1])

def test_plot_can_pass_normalize(self, sdata_blobs: SpatialData):
fig, axs = plt.subplots(ncols=2, figsize=(6, 3))
def test_plot_can_pass_normalize_clip_True(self, sdata_blobs: SpatialData):
norm = Normalize(vmin=0, vmax=0.4, clip=True)
sdata_blobs.pl.render_images(element="blobs_image", channel=1).pl.show(ax=axs[0])
sdata_blobs.pl.render_images(element="blobs_image", channel=1, norm=norm).pl.show(ax=axs[1])
sdata_blobs.pl.render_images(element="blobs_image", channel=0, norm=norm).pl.show()

def test_plot_can_pass_normalize_clip_False(self, sdata_blobs: SpatialData):
norm = Normalize(vmin=0, vmax=0.4, clip=False)
sdata_blobs.pl.render_images(element="blobs_image", channel=0, norm=norm).pl.show()

def test_plot_can_pass_color_to_single_channel(self, sdata_blobs: SpatialData):
sdata_blobs.pl.render_images(element="blobs_image", channel=1, palette="red").pl.show()
Expand All@@ -97,9 +90,6 @@ def test_plot_can_pass_cmap_to_each_channel(self, sdata_blobs: SpatialData):
element="blobs_image", channel=[0, 1, 2], cmap=["Reds", "Greens", "Blues"]
).pl.show()

def test_plot_can_normalize_image(self, sdata_blobs: SpatialData):
sdata_blobs.pl.render_images(element="blobs_image", percentiles_for_norm=(5, 90)).pl.show()

def test_plot_can_render_multiscale_image(self, sdata_blobs: SpatialData):
sdata_blobs.pl.render_images("blobs_multiscale_image").pl.show()

Expand Down
4 changes: 3 additions & 1 deletion tests/pl/test_render_shapes.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,7 @@
import pandas as pd
import scanpy as sc
from anndata import AnnData
from matplotlib.colors import Normalize
from shapely.geometry import MultiPolygon, Point, Polygon
from spatialdata import SpatialData, deepcopy
from spatialdata.models import ShapesModel, TableModel
Expand DownExpand Up@@ -146,7 +147,8 @@ def test_plot_colorbar_can_be_normalised(self, sdata_blobs: SpatialData):
sdata_blobs["table"].obs["region"] = ["blobs_polygons"] * sdata_blobs["table"].n_obs
sdata_blobs["table"].uns["spatialdata_attrs"]["region"] = "blobs_polygons"
sdata_blobs.shapes["blobs_polygons"]["cluster"] = [1, 2, 3, 5, 20]
sdata_blobs.pl.render_shapes("blobs_polygons", color="cluster", groups=["c1"], norm=True).pl.show()
norm = Normalize(vmin=0, vmax=5, clip=True)
sdata_blobs.pl.render_shapes("blobs_polygons", color="cluster", groups=["c1"], norm=norm).pl.show()

def test_plot_can_plot_shapes_after_spatial_query(self, sdata_blobs: SpatialData):
# subset to only shapes, should be unnecessary after rasterizeation of multiscale images is included
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
a55318c
fix
timtreis Sep 3, 2024
381b895
fixed case without info for column
timtreis Sep 3, 2024
487f6a5
fixed case for no-column, but modified na_color
timtreis Sep 3, 2024
a9c7609
added images from runner
timtreis Sep 3, 2024
c7e3260
bugfix for NA color
timtreis Sep 3, 2024
d0b7a1a
lowered testing threshold because mismatches are not being flagged
timtreis Sep 3, 2024
a9d5dbb
modified test to be visually easier to compare
timtreis Sep 3, 2024
fd762a9
further lowered testing threshold
timtreis Sep 3, 2024
729d465
Changed points behaviour and lowered test threshold
timtreis Sep 3, 2024
4421383
modified tests for better display
timtreis Sep 3, 2024
608bea0
fixed bug in typecheck
timtreis Sep 3, 2024
69bfc35
fixed test, added images from runner
timtreis Sep 3, 2024
7db0860
Updated CHANGELOG, added pic from runner
timtreis Sep 3, 2024
01a4f73
Removed dead code
timtreis Sep 3, 2024
eb2fc12
simplified test
timtreis Sep 3, 2024
4d40479
added images from runner
timtreis Sep 3, 2024
f8d6bac
fixed test
timtreis Sep 4, 2024
f6a2d16
fix
timtreis Sep 4, 2024
268a1b2
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Sep 4, 2024
81204ea
removed cbar
timtreis Sep 4, 2024
efb0f19
Merge branch 'bugfix/342-coloring-labels-by-a-continuous-variable-app…
timtreis Sep 4, 2024
ca0ff4e
merge
timtreis Sep 4, 2024
62d7cb9
added img from runner
timtreis Sep 4, 2024
6f4a634
Removed percentiles_for_norm parameter, delegating to cmap.norm
timtreis Sep 4, 2024
9377422
fixed shapes cbar logic
timtreis Sep 4, 2024
d4cfc2a
fixed test img generation
timtreis Sep 4, 2024
385dac3
fixed cmap limits for shapes
timtreis Sep 4, 2024
cc21303
modified test
timtreis Sep 4, 2024
2309404
updated CHANGELOG, added img from runner
timtreis Sep 4, 2024
a97eec1
Update CHANGELOG.md
timtreis Sep 4, 2024
667c689
Merge branch 'main' into 324-unable-to-set-vmin-vmax-when-plotting-ve…
timtreis Sep 4, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -18,11 +18,15 @@ and this project adheres to [Semantic Versioning][].

- Lowered RMSE-threshold for plot-based tests from 45 to 15 (#344)
- When subsetting to `groups`, `NA` isn't automatically added to legend (#344)
- When rendering a single image channel, a colorbar is now shown (#346)
- Removed `percentiles_for_norm` parameter (#346)
- Changed `norm` to no longer accept bools, only `mpl.colors.Normalise` or `None` (#346)

### Fixed

- Filtering with `groups` now preserves original cmap (#344)
- Non-selected `groups` are now not shown in `na_color` (#344)
- Several issues associated with `norm` and `colorbar` (#346)

## [0.2.5] - 2024-08-23

Expand Down
16 changes: 3 additions & 13 deletions src/spatialdata_plot/pl/basic.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -166,7 +166,7 @@ def render_shapes(
outline_color: str | list[float] = "#000000ff",
outline_alpha: float | int = 0.0,
cmap: Colormap | str | None = None,
norm: bool | Normalize = False,
norm: Normalize | None = None,
scale: float | int = 1.0,
method: str | None = None,
table_name: str | None = None,
Expand DownExpand Up@@ -301,7 +301,7 @@ def render_points(
palette: list[str] | str | None = None,
na_color: ColorLike | None = "default",
cmap: Colormap | str | None = None,
norm: None | Normalize = None,
norm: Normalize | None = None,
size: float | int = 1.0,
method: str | None = None,
table_name: str | None = None,
Expand DownExpand Up@@ -422,7 +422,6 @@ def render_images(
na_color: ColorLike | None = "default",
palette: list[str] | str | None = None,
alpha: float | int = 1.0,
percentiles_for_norm: tuple[float, float] | None = None,
scale: str | None = None,
**kwargs: Any,
) -> sd.SpatialData:
Expand DownExpand Up@@ -457,8 +456,6 @@ def render_images(
Palette to color images. The number of palettes should be equal to the number of channels.
alpha : float | int, default 1.0
Alpha value for the images. Must be a numeric between 0 and 1.
percentiles_for_norm : tuple[float, float] | None
Optional pair of floats (pmin < pmax, 0-100) which will be used for quantile normalization.
scale : str | None
Influences the resolution of the rendering. Possibilities include:
1) `None` (default): The image is rasterized to fit the canvas size. For
Expand DownExpand Up@@ -486,20 +483,14 @@ def render_images(
cmap=cmap,
norm=norm,
scale=scale,
percentiles_for_norm=percentiles_for_norm,
)

sdata = self._copy()
sdata = _verify_plotting_tree(sdata)
n_steps = len(sdata.plotting_tree.keys())

for element, param_values in params_dict.items():
# cmap_params = _prepare_cmap_norm(
# cmap=params_dict[element]["cmap"],
# norm=norm,
# na_color=params_dict[element]["na_color"], # type: ignore[arg-type]
# **kwargs,
# )

cmap_params: list[CmapParams] | CmapParams
if isinstance(cmap, list):
cmap_params = [
Expand All@@ -525,7 +516,6 @@ def render_images(
cmap_params=cmap_params,
palette=param_values["palette"],
alpha=param_values["alpha"],
percentiles_for_norm=param_values["percentiles_for_norm"],
scale=param_values["scale"],
zorder=n_steps,
)
Expand Down
30 changes: 13 additions & 17 deletions src/spatialdata_plot/pl/render.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,6 +9,7 @@
import datashader as ds
import geopandas as gpd
import matplotlib
import matplotlib.pyplot as plt
import matplotlib.transforms as mtransforms
import numpy as np
import pandas as pd
Expand DownExpand Up@@ -47,7 +48,6 @@
_maybe_set_colors,
_mpl_ax_contains_elements,
_multiscale_to_spatial_image,
_normalize,
_rasterize_if_necessary,
_set_color_source_vec,
to_hex,
Expand DownExpand Up@@ -128,6 +128,7 @@ def _render_shapes(
shapes = shapes.reset_index()
color_source_vector = color_source_vector[mask]
color_vector = color_vector[mask]

shapes = gpd.GeoDataFrame(shapes, geometry="geometry")

# Using dict.fromkeys here since set returns in arbitrary order
Expand DownExpand Up@@ -255,9 +256,13 @@ def _render_shapes(
for path in _cax.get_paths():
path.vertices = trans.transform(path.vertices)

# Sets the limits of the colorbar to the values instead of [0, 1]
if not norm and not values_are_categorical:
_cax.set_clim(min(color_vector), max(color_vector))
if not values_are_categorical:
# If the user passed a Normalize object with vmin/vmax we'll use those,
# # if not we'll use the min/max of the color_vector
_cax.set_clim(
vmin=render_params.cmap_params.norm.vmin or min(color_vector),
vmax=render_params.cmap_params.norm.vmax or max(color_vector),
)

if len(set(color_vector)) != 1 or list(set(color_vector))[0] != to_hex(render_params.cmap_params.na_color):
# necessary in case different shapes elements are annotated with one table
Expand DownExpand Up@@ -603,11 +608,6 @@ def _render_images(
if n_channels == 1 and not isinstance(render_params.cmap_params, list):
layer = img.sel(c=channels[0]).squeeze() if isinstance(channels[0], str) else img.isel(c=channels[0]).squeeze()

if render_params.percentiles_for_norm != (None, None):
layer = _normalize(
layer, pmin=render_params.percentiles_for_norm[0], pmax=render_params.percentiles_for_norm[1], clip=True
)

if render_params.cmap_params.norm: # type: ignore[attr-defined]
layer = render_params.cmap_params.norm(layer) # type: ignore[attr-defined]

Expand All@@ -623,20 +623,16 @@ def _render_images(

_ax_show_and_transform(layer, trans_data, ax, cmap=cmap, zorder=render_params.zorder)

if legend_params.colorbar:
sm = plt.cm.ScalarMappable(cmap=cmap, norm=render_params.cmap_params.norm)
fig_params.fig.colorbar(sm, ax=ax)

# 2) Image has any number of channels but 1
else:
layers = {}
for ch_index, c in enumerate(channels):
layers[c] = img.sel(c=c).copy(deep=True).squeeze()

if render_params.percentiles_for_norm != (None, None):
layers[c] = _normalize(
layers[c],
pmin=render_params.percentiles_for_norm[0],
pmax=render_params.percentiles_for_norm[1],
clip=True,
)

if not isinstance(render_params.cmap_params, list):
if render_params.cmap_params.norm is not None:
layers[c] = render_params.cmap_params.norm(layers[c])
Expand Down
29 changes: 1 addition & 28 deletions src/spatialdata_plot/pl/utils.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -489,7 +489,7 @@ def _get_scalebar(

def _prepare_cmap_norm(
cmap: Colormap | str | None = None,
norm: Normalize | bool = False,
norm: Normalize | None = None,
na_color: ColorLike | None = None,
vmin: float | None = None,
vmax: float | None = None,
Expand DownExpand Up@@ -1623,29 +1623,6 @@ def _type_check_params(param_dict: dict[str, Any], element_type: str) -> dict[st
if scale < 0:
raise ValueError("Parameter 'scale' must be a positive number.")

if (percentiles_for_norm := param_dict.get("percentiles_for_norm")) is None:
percentiles_for_norm = (None, None)
elif not (isinstance(percentiles_for_norm, (list, tuple)) or len(percentiles_for_norm) != 2):
raise TypeError("Parameter 'percentiles_for_norm' must be a list or tuple of exactly two floats or None.")
elif not all(
isinstance(p, (float, int, type(None)))
and isinstance(p, type(percentiles_for_norm[0]))
and (p is None or 0 <= p <= 100)
for p in percentiles_for_norm
):
raise TypeError(
"Each item in 'percentiles_for_norm' must be of the same dtype and must be a float or int within [0, 100], "
"or None"
)
elif (
percentiles_for_norm[0] is not None
and percentiles_for_norm[1] is not None
and percentiles_for_norm[0] > percentiles_for_norm[1]
):
raise ValueError("The first number in 'percentiles_for_norm' must not be smaller than the second.")
if "percentiles_for_norm" in param_dict:
param_dict["percentiles_for_norm"] = percentiles_for_norm

if size := param_dict.get("size"):
if not isinstance(size, (float, int)):
raise TypeError("Parameter 'size' must be numeric.")
Expand DownExpand Up@@ -1886,7 +1863,6 @@ def _validate_image_render_params(
cmap: list[Colormap | str] | Colormap | str | None,
norm: Normalize | None,
scale: str | None,
percentiles_for_norm: tuple[float | None, float | None] | None,
) -> dict[str, dict[str, Any]]:
param_dict: dict[str, Any] = {
"sdata": sdata,
Expand All@@ -1898,7 +1874,6 @@ def _validate_image_render_params(
"cmap": cmap,
"norm": norm,
"scale": scale,
"percentiles_for_norm": percentiles_for_norm,
}
param_dict = _type_check_params(param_dict, "images")

Expand DownExpand Up@@ -1945,8 +1920,6 @@ def _validate_image_render_params(
else:
element_params[el]["scale"] = scale

element_params[el]["percentiles_for_norm"] = param_dict["percentiles_for_norm"]

return element_params


Expand Down
Binary file modifiedtests/_images/Images_can_pass_cmap_to_single_channel.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modifiedtests/_images/Images_can_pass_color_to_each_channel.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modifiedtests/_images/Images_can_pass_color_to_single_channel.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modifiedtests/_images/Images_can_stack_render_images.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modifiedtests/_images/Shapes_colorbar_can_be_normalised.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modifiedtests/_images/Shapes_colorbar_respects_input_limits.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 3 additions & 1 deletion tests/pl/test_render.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -23,7 +23,9 @@ def test_render_images_can_plot_one_cyx_image(request):
def test_render_images_can_plot_multiple_cyx_images(share_coordinate_system: str, request):
fun = request.getfixturevalue("get_sdata_with_multiple_images")
sdata = fun(share_coordinate_system)
sdata.pl.render_images().pl.show()
sdata.pl.render_images().pl.show(
colorbar=False, # otherwise we'll get one cbar per image in the same cs
)
axs = plt.gcf().get_axes()

if share_coordinate_system == "all":
Expand Down
22 changes: 6 additions & 16 deletions tests/pl/test_render_images.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,7 +2,6 @@
import matplotlib
import numpy as np
import scanpy as sc
from matplotlib import pyplot as plt
from matplotlib.colors import Normalize
from spatial_image import to_spatial_image
from spatialdata import SpatialData
Expand DownExpand Up@@ -49,9 +48,6 @@ def test_plot_can_render_a_single_channel_from_image(self, sdata_blobs: SpatialD
def test_plot_can_render_a_single_channel_from_multiscale_image(self, sdata_blobs: SpatialData):
sdata_blobs.pl.render_images(element="blobs_multiscale_image", channel=0).pl.show()

def test_plot_can_render_a_single_channel_from_image_no_el(self, sdata_blobs: SpatialData):
sdata_blobs.pl.render_images(channel=0).pl.show()

def test_plot_can_render_a_single_channel_str_from_image(self, sdata_blobs_str: SpatialData):
sdata_blobs_str.pl.render_images(element="blobs_image", channel="c1").pl.show()

Expand All@@ -70,16 +66,13 @@ def test_plot_can_render_two_channels_str_from_image(self, sdata_blobs_str: Spat
def test_plot_can_render_two_channels_str_from_multiscale_image(self, sdata_blobs_str: SpatialData):
sdata_blobs_str.pl.render_images(element="blobs_multiscale_image", channel=["c1", "c2"]).pl.show()

def test_plot_can_pass_vmin_vmax(self, sdata_blobs: SpatialData):
fig, axs = plt.subplots(ncols=2, figsize=(6, 3))
sdata_blobs.pl.render_images(element="blobs_image", channel=1).pl.show(ax=axs[0])
sdata_blobs.pl.render_images(element="blobs_image", channel=1, vmin=0, vmax=0.4).pl.show(ax=axs[1])

def test_plot_can_pass_normalize(self, sdata_blobs: SpatialData):
fig, axs = plt.subplots(ncols=2, figsize=(6, 3))
def test_plot_can_pass_normalize_clip_True(self, sdata_blobs: SpatialData):
norm = Normalize(vmin=0, vmax=0.4, clip=True)
sdata_blobs.pl.render_images(element="blobs_image", channel=1).pl.show(ax=axs[0])
sdata_blobs.pl.render_images(element="blobs_image", channel=1, norm=norm).pl.show(ax=axs[1])
sdata_blobs.pl.render_images(element="blobs_image", channel=0, norm=norm).pl.show()

def test_plot_can_pass_normalize_clip_False(self, sdata_blobs: SpatialData):
norm = Normalize(vmin=0, vmax=0.4, clip=False)
sdata_blobs.pl.render_images(element="blobs_image", channel=0, norm=norm).pl.show()

def test_plot_can_pass_color_to_single_channel(self, sdata_blobs: SpatialData):
sdata_blobs.pl.render_images(element="blobs_image", channel=1, palette="red").pl.show()
Expand All@@ -97,9 +90,6 @@ def test_plot_can_pass_cmap_to_each_channel(self, sdata_blobs: SpatialData):
element="blobs_image", channel=[0, 1, 2], cmap=["Reds", "Greens", "Blues"]
).pl.show()

def test_plot_can_normalize_image(self, sdata_blobs: SpatialData):
sdata_blobs.pl.render_images(element="blobs_image", percentiles_for_norm=(5, 90)).pl.show()

def test_plot_can_render_multiscale_image(self, sdata_blobs: SpatialData):
sdata_blobs.pl.render_images("blobs_multiscale_image").pl.show()

Expand Down
4 changes: 3 additions & 1 deletion tests/pl/test_render_shapes.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,7 @@
import pandas as pd
import scanpy as sc
from anndata import AnnData
from matplotlib.colors import Normalize
from shapely.geometry import MultiPolygon, Point, Polygon
from spatialdata import SpatialData, deepcopy
from spatialdata.models import ShapesModel, TableModel
Expand DownExpand Up@@ -146,7 +147,8 @@ def test_plot_colorbar_can_be_normalised(self, sdata_blobs: SpatialData):
sdata_blobs["table"].obs["region"] = ["blobs_polygons"] * sdata_blobs["table"].n_obs
sdata_blobs["table"].uns["spatialdata_attrs"]["region"] = "blobs_polygons"
sdata_blobs.shapes["blobs_polygons"]["cluster"] = [1, 2, 3, 5, 20]
sdata_blobs.pl.render_shapes("blobs_polygons", color="cluster", groups=["c1"], norm=True).pl.show()
norm = Normalize(vmin=0, vmax=5, clip=True)
sdata_blobs.pl.render_shapes("blobs_polygons", color="cluster", groups=["c1"], norm=norm).pl.show()

def test_plot_can_plot_shapes_after_spatial_query(self, sdata_blobs: SpatialData):
# subset to only shapes, should be unnecessary after rasterizeation of multiscale images is included
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
a55318c
fix
timtreis Sep 3, 2024
381b895
fixed case without info for column
timtreis Sep 3, 2024
487f6a5
fixed case for no-column, but modified na_color
timtreis Sep 3, 2024
a9c7609
added images from runner
timtreis Sep 3, 2024
c7e3260
bugfix for NA color
timtreis Sep 3, 2024
d0b7a1a
lowered testing threshold because mismatches are not being flagged
timtreis Sep 3, 2024
a9d5dbb
modified test to be visually easier to compare
timtreis Sep 3, 2024
fd762a9
further lowered testing threshold
timtreis Sep 3, 2024
729d465
Changed points behaviour and lowered test threshold
timtreis Sep 3, 2024
4421383
modified tests for better display
timtreis Sep 3, 2024
608bea0
fixed bug in typecheck
timtreis Sep 3, 2024
69bfc35
fixed test, added images from runner
timtreis Sep 3, 2024
7db0860
Updated CHANGELOG, added pic from runner
timtreis Sep 3, 2024
01a4f73
Removed dead code
timtreis Sep 3, 2024
eb2fc12
simplified test
timtreis Sep 3, 2024
4d40479
added images from runner
timtreis Sep 3, 2024
f8d6bac
fixed test
timtreis Sep 4, 2024
f6a2d16
fix
timtreis Sep 4, 2024
268a1b2
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Sep 4, 2024
81204ea
removed cbar
timtreis Sep 4, 2024
efb0f19
Merge branch 'bugfix/342-coloring-labels-by-a-continuous-variable-app…
timtreis Sep 4, 2024
ca0ff4e
merge
timtreis Sep 4, 2024
62d7cb9
added img from runner
timtreis Sep 4, 2024
6f4a634
Removed percentiles_for_norm parameter, delegating to cmap.norm
timtreis Sep 4, 2024
9377422
fixed shapes cbar logic
timtreis Sep 4, 2024
d4cfc2a
fixed test img generation
timtreis Sep 4, 2024
385dac3
fixed cmap limits for shapes
timtreis Sep 4, 2024
cc21303
modified test
timtreis Sep 4, 2024
2309404
updated CHANGELOG, added img from runner
timtreis Sep 4, 2024
a97eec1
Update CHANGELOG.md
timtreis Sep 4, 2024
667c689
Merge branch 'main' into 324-unable-to-set-vmin-vmax-when-plotting-ve…
timtreis Sep 4, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -18,11 +18,15 @@ and this project adheres to [Semantic Versioning][].

- Lowered RMSE-threshold for plot-based tests from 45 to 15 (#344)
- When subsetting to `groups`, `NA` isn't automatically added to legend (#344)
- When rendering a single image channel, a colorbar is now shown (#346)
- Removed `percentiles_for_norm` parameter (#346)
- Changed `norm` to no longer accept bools, only `mpl.colors.Normalise` or `None` (#346)

### Fixed

- Filtering with `groups` now preserves original cmap (#344)
- Non-selected `groups` are now not shown in `na_color` (#344)
- Several issues associated with `norm` and `colorbar` (#346)

## [0.2.5] - 2024-08-23

Expand Down
16 changes: 3 additions & 13 deletions src/spatialdata_plot/pl/basic.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -166,7 +166,7 @@ def render_shapes(
outline_color: str | list[float] = "#000000ff",
outline_alpha: float | int = 0.0,
cmap: Colormap | str | None = None,
norm: bool | Normalize = False,
norm: Normalize | None = None,
scale: float | int = 1.0,
method: str | None = None,
table_name: str | None = None,
Expand DownExpand Up@@ -301,7 +301,7 @@ def render_points(
palette: list[str] | str | None = None,
na_color: ColorLike | None = "default",
cmap: Colormap | str | None = None,
norm: None | Normalize = None,
norm: Normalize | None = None,
size: float | int = 1.0,
method: str | None = None,
table_name: str | None = None,
Expand DownExpand Up@@ -422,7 +422,6 @@ def render_images(
na_color: ColorLike | None = "default",
palette: list[str] | str | None = None,
alpha: float | int = 1.0,
percentiles_for_norm: tuple[float, float] | None = None,
scale: str | None = None,
**kwargs: Any,
) -> sd.SpatialData:
Expand DownExpand Up@@ -457,8 +456,6 @@ def render_images(
Palette to color images. The number of palettes should be equal to the number of channels.
alpha : float | int, default 1.0
Alpha value for the images. Must be a numeric between 0 and 1.
percentiles_for_norm : tuple[float, float] | None
Optional pair of floats (pmin < pmax, 0-100) which will be used for quantile normalization.
scale : str | None
Influences the resolution of the rendering. Possibilities include:
1) `None` (default): The image is rasterized to fit the canvas size. For
Expand DownExpand Up@@ -486,20 +483,14 @@ def render_images(
cmap=cmap,
norm=norm,
scale=scale,
percentiles_for_norm=percentiles_for_norm,
)

sdata = self._copy()
sdata = _verify_plotting_tree(sdata)
n_steps = len(sdata.plotting_tree.keys())

for element, param_values in params_dict.items():
# cmap_params = _prepare_cmap_norm(
# cmap=params_dict[element]["cmap"],
# norm=norm,
# na_color=params_dict[element]["na_color"], # type: ignore[arg-type]
# **kwargs,
# )

cmap_params: list[CmapParams] | CmapParams
if isinstance(cmap, list):
cmap_params = [
Expand All@@ -525,7 +516,6 @@ def render_images(
cmap_params=cmap_params,
palette=param_values["palette"],
alpha=param_values["alpha"],
percentiles_for_norm=param_values["percentiles_for_norm"],
scale=param_values["scale"],
zorder=n_steps,
)
Expand Down
30 changes: 13 additions & 17 deletions src/spatialdata_plot/pl/render.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,6 +9,7 @@
import datashader as ds
import geopandas as gpd
import matplotlib
import matplotlib.pyplot as plt
import matplotlib.transforms as mtransforms
import numpy as np
import pandas as pd
Expand DownExpand Up@@ -47,7 +48,6 @@
_maybe_set_colors,
_mpl_ax_contains_elements,
_multiscale_to_spatial_image,
_normalize,
_rasterize_if_necessary,
_set_color_source_vec,
to_hex,
Expand DownExpand Up@@ -128,6 +128,7 @@ def _render_shapes(
shapes = shapes.reset_index()
color_source_vector = color_source_vector[mask]
color_vector = color_vector[mask]

shapes = gpd.GeoDataFrame(shapes, geometry="geometry")

# Using dict.fromkeys here since set returns in arbitrary order
Expand DownExpand Up@@ -255,9 +256,13 @@ def _render_shapes(
for path in _cax.get_paths():
path.vertices = trans.transform(path.vertices)

# Sets the limits of the colorbar to the values instead of [0, 1]
if not norm and not values_are_categorical:
_cax.set_clim(min(color_vector), max(color_vector))
if not values_are_categorical:
# If the user passed a Normalize object with vmin/vmax we'll use those,
# # if not we'll use the min/max of the color_vector
_cax.set_clim(
vmin=render_params.cmap_params.norm.vmin or min(color_vector),
vmax=render_params.cmap_params.norm.vmax or max(color_vector),
)

if len(set(color_vector)) != 1 or list(set(color_vector))[0] != to_hex(render_params.cmap_params.na_color):
# necessary in case different shapes elements are annotated with one table
Expand DownExpand Up@@ -603,11 +608,6 @@ def _render_images(
if n_channels == 1 and not isinstance(render_params.cmap_params, list):
layer = img.sel(c=channels[0]).squeeze() if isinstance(channels[0], str) else img.isel(c=channels[0]).squeeze()

if render_params.percentiles_for_norm != (None, None):
layer = _normalize(
layer, pmin=render_params.percentiles_for_norm[0], pmax=render_params.percentiles_for_norm[1], clip=True
)

if render_params.cmap_params.norm: # type: ignore[attr-defined]
layer = render_params.cmap_params.norm(layer) # type: ignore[attr-defined]

Expand All@@ -623,20 +623,16 @@ def _render_images(

_ax_show_and_transform(layer, trans_data, ax, cmap=cmap, zorder=render_params.zorder)

if legend_params.colorbar:
sm = plt.cm.ScalarMappable(cmap=cmap, norm=render_params.cmap_params.norm)
fig_params.fig.colorbar(sm, ax=ax)

# 2) Image has any number of channels but 1
else:
layers = {}
for ch_index, c in enumerate(channels):
layers[c] = img.sel(c=c).copy(deep=True).squeeze()

if render_params.percentiles_for_norm != (None, None):
layers[c] = _normalize(
layers[c],
pmin=render_params.percentiles_for_norm[0],
pmax=render_params.percentiles_for_norm[1],
clip=True,
)

if not isinstance(render_params.cmap_params, list):
if render_params.cmap_params.norm is not None:
layers[c] = render_params.cmap_params.norm(layers[c])
Expand Down
29 changes: 1 addition & 28 deletions src/spatialdata_plot/pl/utils.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -489,7 +489,7 @@ def _get_scalebar(

def _prepare_cmap_norm(
cmap: Colormap | str | None = None,
norm: Normalize | bool = False,
norm: Normalize | None = None,
na_color: ColorLike | None = None,
vmin: float | None = None,
vmax: float | None = None,
Expand DownExpand Up@@ -1623,29 +1623,6 @@ def _type_check_params(param_dict: dict[str, Any], element_type: str) -> dict[st
if scale < 0:
raise ValueError("Parameter 'scale' must be a positive number.")

if (percentiles_for_norm := param_dict.get("percentiles_for_norm")) is None:
percentiles_for_norm = (None, None)
elif not (isinstance(percentiles_for_norm, (list, tuple)) or len(percentiles_for_norm) != 2):
raise TypeError("Parameter 'percentiles_for_norm' must be a list or tuple of exactly two floats or None.")
elif not all(
isinstance(p, (float, int, type(None)))
and isinstance(p, type(percentiles_for_norm[0]))
and (p is None or 0 <= p <= 100)
for p in percentiles_for_norm
):
raise TypeError(
"Each item in 'percentiles_for_norm' must be of the same dtype and must be a float or int within [0, 100], "
"or None"
)
elif (
percentiles_for_norm[0] is not None
and percentiles_for_norm[1] is not None
and percentiles_for_norm[0] > percentiles_for_norm[1]
):
raise ValueError("The first number in 'percentiles_for_norm' must not be smaller than the second.")
if "percentiles_for_norm" in param_dict:
param_dict["percentiles_for_norm"] = percentiles_for_norm

if size := param_dict.get("size"):
if not isinstance(size, (float, int)):
raise TypeError("Parameter 'size' must be numeric.")
Expand DownExpand Up@@ -1886,7 +1863,6 @@ def _validate_image_render_params(
cmap: list[Colormap | str] | Colormap | str | None,
norm: Normalize | None,
scale: str | None,
percentiles_for_norm: tuple[float | None, float | None] | None,
) -> dict[str, dict[str, Any]]:
param_dict: dict[str, Any] = {
"sdata": sdata,
Expand All@@ -1898,7 +1874,6 @@ def _validate_image_render_params(
"cmap": cmap,
"norm": norm,
"scale": scale,
"percentiles_for_norm": percentiles_for_norm,
}
param_dict = _type_check_params(param_dict, "images")

Expand DownExpand Up@@ -1945,8 +1920,6 @@ def _validate_image_render_params(
else:
element_params[el]["scale"] = scale

element_params[el]["percentiles_for_norm"] = param_dict["percentiles_for_norm"]

return element_params


Expand Down
Binary file modifiedtests/_images/Images_can_pass_cmap_to_single_channel.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modifiedtests/_images/Images_can_pass_color_to_each_channel.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modifiedtests/_images/Images_can_pass_color_to_single_channel.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modifiedtests/_images/Images_can_stack_render_images.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modifiedtests/_images/Shapes_colorbar_can_be_normalised.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modifiedtests/_images/Shapes_colorbar_respects_input_limits.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 3 additions & 1 deletion tests/pl/test_render.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -23,7 +23,9 @@ def test_render_images_can_plot_one_cyx_image(request):
def test_render_images_can_plot_multiple_cyx_images(share_coordinate_system: str, request):
fun = request.getfixturevalue("get_sdata_with_multiple_images")
sdata = fun(share_coordinate_system)
sdata.pl.render_images().pl.show()
sdata.pl.render_images().pl.show(
colorbar=False, # otherwise we'll get one cbar per image in the same cs
)
axs = plt.gcf().get_axes()

if share_coordinate_system == "all":
Expand Down
22 changes: 6 additions & 16 deletions tests/pl/test_render_images.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,7 +2,6 @@
import matplotlib
import numpy as np
import scanpy as sc
from matplotlib import pyplot as plt
from matplotlib.colors import Normalize
from spatial_image import to_spatial_image
from spatialdata import SpatialData
Expand DownExpand Up@@ -49,9 +48,6 @@ def test_plot_can_render_a_single_channel_from_image(self, sdata_blobs: SpatialD
def test_plot_can_render_a_single_channel_from_multiscale_image(self, sdata_blobs: SpatialData):
sdata_blobs.pl.render_images(element="blobs_multiscale_image", channel=0).pl.show()

def test_plot_can_render_a_single_channel_from_image_no_el(self, sdata_blobs: SpatialData):
sdata_blobs.pl.render_images(channel=0).pl.show()

def test_plot_can_render_a_single_channel_str_from_image(self, sdata_blobs_str: SpatialData):
sdata_blobs_str.pl.render_images(element="blobs_image", channel="c1").pl.show()

Expand All@@ -70,16 +66,13 @@ def test_plot_can_render_two_channels_str_from_image(self, sdata_blobs_str: Spat
def test_plot_can_render_two_channels_str_from_multiscale_image(self, sdata_blobs_str: SpatialData):
sdata_blobs_str.pl.render_images(element="blobs_multiscale_image", channel=["c1", "c2"]).pl.show()

def test_plot_can_pass_vmin_vmax(self, sdata_blobs: SpatialData):
fig, axs = plt.subplots(ncols=2, figsize=(6, 3))
sdata_blobs.pl.render_images(element="blobs_image", channel=1).pl.show(ax=axs[0])
sdata_blobs.pl.render_images(element="blobs_image", channel=1, vmin=0, vmax=0.4).pl.show(ax=axs[1])

def test_plot_can_pass_normalize(self, sdata_blobs: SpatialData):
fig, axs = plt.subplots(ncols=2, figsize=(6, 3))
def test_plot_can_pass_normalize_clip_True(self, sdata_blobs: SpatialData):
norm = Normalize(vmin=0, vmax=0.4, clip=True)
sdata_blobs.pl.render_images(element="blobs_image", channel=1).pl.show(ax=axs[0])
sdata_blobs.pl.render_images(element="blobs_image", channel=1, norm=norm).pl.show(ax=axs[1])
sdata_blobs.pl.render_images(element="blobs_image", channel=0, norm=norm).pl.show()

def test_plot_can_pass_normalize_clip_False(self, sdata_blobs: SpatialData):
norm = Normalize(vmin=0, vmax=0.4, clip=False)
sdata_blobs.pl.render_images(element="blobs_image", channel=0, norm=norm).pl.show()

def test_plot_can_pass_color_to_single_channel(self, sdata_blobs: SpatialData):
sdata_blobs.pl.render_images(element="blobs_image", channel=1, palette="red").pl.show()
Expand All@@ -97,9 +90,6 @@ def test_plot_can_pass_cmap_to_each_channel(self, sdata_blobs: SpatialData):
element="blobs_image", channel=[0, 1, 2], cmap=["Reds", "Greens", "Blues"]
).pl.show()

def test_plot_can_normalize_image(self, sdata_blobs: SpatialData):
sdata_blobs.pl.render_images(element="blobs_image", percentiles_for_norm=(5, 90)).pl.show()

def test_plot_can_render_multiscale_image(self, sdata_blobs: SpatialData):
sdata_blobs.pl.render_images("blobs_multiscale_image").pl.show()

Expand Down
4 changes: 3 additions & 1 deletion tests/pl/test_render_shapes.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,7 @@
import pandas as pd
import scanpy as sc
from anndata import AnnData
from matplotlib.colors import Normalize
from shapely.geometry import MultiPolygon, Point, Polygon
from spatialdata import SpatialData, deepcopy
from spatialdata.models import ShapesModel, TableModel
Expand DownExpand Up@@ -146,7 +147,8 @@ def test_plot_colorbar_can_be_normalised(self, sdata_blobs: SpatialData):
sdata_blobs["table"].obs["region"] = ["blobs_polygons"] * sdata_blobs["table"].n_obs
sdata_blobs["table"].uns["spatialdata_attrs"]["region"] = "blobs_polygons"
sdata_blobs.shapes["blobs_polygons"]["cluster"] = [1, 2, 3, 5, 20]
sdata_blobs.pl.render_shapes("blobs_polygons", color="cluster", groups=["c1"], norm=True).pl.show()
norm = Normalize(vmin=0, vmax=5, clip=True)
sdata_blobs.pl.render_shapes("blobs_polygons", color="cluster", groups=["c1"], norm=norm).pl.show()

def test_plot_can_plot_shapes_after_spatial_query(self, sdata_blobs: SpatialData):
# subset to only shapes, should be unnecessary after rasterizeation of multiscale images is included
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
a55318c
fix
timtreis Sep 3, 2024
381b895
fixed case without info for column
timtreis Sep 3, 2024
487f6a5
fixed case for no-column, but modified na_color
timtreis Sep 3, 2024
a9c7609
added images from runner
timtreis Sep 3, 2024
c7e3260
bugfix for NA color
timtreis Sep 3, 2024
d0b7a1a
lowered testing threshold because mismatches are not being flagged
timtreis Sep 3, 2024
a9d5dbb
modified test to be visually easier to compare
timtreis Sep 3, 2024
fd762a9
further lowered testing threshold
timtreis Sep 3, 2024
729d465
Changed points behaviour and lowered test threshold
timtreis Sep 3, 2024
4421383
modified tests for better display
timtreis Sep 3, 2024
608bea0
fixed bug in typecheck
timtreis Sep 3, 2024
69bfc35
fixed test, added images from runner
timtreis Sep 3, 2024
7db0860
Updated CHANGELOG, added pic from runner
timtreis Sep 3, 2024
01a4f73
Removed dead code
timtreis Sep 3, 2024
eb2fc12
simplified test
timtreis Sep 3, 2024
4d40479
added images from runner
timtreis Sep 3, 2024
f8d6bac
fixed test
timtreis Sep 4, 2024
f6a2d16
fix
timtreis Sep 4, 2024
268a1b2
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Sep 4, 2024
81204ea
removed cbar
timtreis Sep 4, 2024
efb0f19
Merge branch 'bugfix/342-coloring-labels-by-a-continuous-variable-app…
timtreis Sep 4, 2024
ca0ff4e
merge
timtreis Sep 4, 2024
62d7cb9
added img from runner
timtreis Sep 4, 2024
6f4a634
Removed percentiles_for_norm parameter, delegating to cmap.norm
timtreis Sep 4, 2024
9377422
fixed shapes cbar logic
timtreis Sep 4, 2024
d4cfc2a
fixed test img generation
timtreis Sep 4, 2024
385dac3
fixed cmap limits for shapes
timtreis Sep 4, 2024
cc21303
modified test
timtreis Sep 4, 2024
2309404
updated CHANGELOG, added img from runner
timtreis Sep 4, 2024
a97eec1
Update CHANGELOG.md
timtreis Sep 4, 2024
667c689
Merge branch 'main' into 324-unable-to-set-vmin-vmax-when-plotting-ve…
timtreis Sep 4, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -18,11 +18,15 @@ and this project adheres to [Semantic Versioning][].

- Lowered RMSE-threshold for plot-based tests from 45 to 15 (#344)
- When subsetting to `groups`, `NA` isn't automatically added to legend (#344)
- When rendering a single image channel, a colorbar is now shown (#346)
- Removed `percentiles_for_norm` parameter (#346)
- Changed `norm` to no longer accept bools, only `mpl.colors.Normalise` or `None` (#346)

### Fixed

- Filtering with `groups` now preserves original cmap (#344)
- Non-selected `groups` are now not shown in `na_color` (#344)
- Several issues associated with `norm` and `colorbar` (#346)

## [0.2.5] - 2024-08-23

Expand Down
16 changes: 3 additions & 13 deletions src/spatialdata_plot/pl/basic.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -166,7 +166,7 @@ def render_shapes(
outline_color: str | list[float] = "#000000ff",
outline_alpha: float | int = 0.0,
cmap: Colormap | str | None = None,
norm: bool | Normalize = False,
norm: Normalize | None = None,
scale: float | int = 1.0,
method: str | None = None,
table_name: str | None = None,
Expand DownExpand Up@@ -301,7 +301,7 @@ def render_points(
palette: list[str] | str | None = None,
na_color: ColorLike | None = "default",
cmap: Colormap | str | None = None,
norm: None | Normalize = None,
norm: Normalize | None = None,
size: float | int = 1.0,
method: str | None = None,
table_name: str | None = None,
Expand DownExpand Up@@ -422,7 +422,6 @@ def render_images(
na_color: ColorLike | None = "default",
palette: list[str] | str | None = None,
alpha: float | int = 1.0,
percentiles_for_norm: tuple[float, float] | None = None,
scale: str | None = None,
**kwargs: Any,
) -> sd.SpatialData:
Expand DownExpand Up@@ -457,8 +456,6 @@ def render_images(
Palette to color images. The number of palettes should be equal to the number of channels.
alpha : float | int, default 1.0
Alpha value for the images. Must be a numeric between 0 and 1.
percentiles_for_norm : tuple[float, float] | None
Optional pair of floats (pmin < pmax, 0-100) which will be used for quantile normalization.
scale : str | None
Influences the resolution of the rendering. Possibilities include:
1) `None` (default): The image is rasterized to fit the canvas size. For
Expand DownExpand Up@@ -486,20 +483,14 @@ def render_images(
cmap=cmap,
norm=norm,
scale=scale,
percentiles_for_norm=percentiles_for_norm,
)

sdata = self._copy()
sdata = _verify_plotting_tree(sdata)
n_steps = len(sdata.plotting_tree.keys())

for element, param_values in params_dict.items():
# cmap_params = _prepare_cmap_norm(
# cmap=params_dict[element]["cmap"],
# norm=norm,
# na_color=params_dict[element]["na_color"], # type: ignore[arg-type]
# **kwargs,
# )

cmap_params: list[CmapParams] | CmapParams
if isinstance(cmap, list):
cmap_params = [
Expand All@@ -525,7 +516,6 @@ def render_images(
cmap_params=cmap_params,
palette=param_values["palette"],
alpha=param_values["alpha"],
percentiles_for_norm=param_values["percentiles_for_norm"],
scale=param_values["scale"],
zorder=n_steps,
)
Expand Down
30 changes: 13 additions & 17 deletions src/spatialdata_plot/pl/render.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,6 +9,7 @@
import datashader as ds
import geopandas as gpd
import matplotlib
import matplotlib.pyplot as plt
import matplotlib.transforms as mtransforms
import numpy as np
import pandas as pd
Expand DownExpand Up@@ -47,7 +48,6 @@
_maybe_set_colors,
_mpl_ax_contains_elements,
_multiscale_to_spatial_image,
_normalize,
_rasterize_if_necessary,
_set_color_source_vec,
to_hex,
Expand DownExpand Up@@ -128,6 +128,7 @@ def _render_shapes(
shapes = shapes.reset_index()
color_source_vector = color_source_vector[mask]
color_vector = color_vector[mask]

shapes = gpd.GeoDataFrame(shapes, geometry="geometry")

# Using dict.fromkeys here since set returns in arbitrary order
Expand DownExpand Up@@ -255,9 +256,13 @@ def _render_shapes(
for path in _cax.get_paths():
path.vertices = trans.transform(path.vertices)

# Sets the limits of the colorbar to the values instead of [0, 1]
if not norm and not values_are_categorical:
_cax.set_clim(min(color_vector), max(color_vector))
if not values_are_categorical:
# If the user passed a Normalize object with vmin/vmax we'll use those,
# # if not we'll use the min/max of the color_vector
_cax.set_clim(
vmin=render_params.cmap_params.norm.vmin or min(color_vector),
vmax=render_params.cmap_params.norm.vmax or max(color_vector),
)

if len(set(color_vector)) != 1 or list(set(color_vector))[0] != to_hex(render_params.cmap_params.na_color):
# necessary in case different shapes elements are annotated with one table
Expand DownExpand Up@@ -603,11 +608,6 @@ def _render_images(
if n_channels == 1 and not isinstance(render_params.cmap_params, list):
layer = img.sel(c=channels[0]).squeeze() if isinstance(channels[0], str) else img.isel(c=channels[0]).squeeze()

if render_params.percentiles_for_norm != (None, None):
layer = _normalize(
layer, pmin=render_params.percentiles_for_norm[0], pmax=render_params.percentiles_for_norm[1], clip=True
)

if render_params.cmap_params.norm: # type: ignore[attr-defined]
layer = render_params.cmap_params.norm(layer) # type: ignore[attr-defined]

Expand All@@ -623,20 +623,16 @@ def _render_images(

_ax_show_and_transform(layer, trans_data, ax, cmap=cmap, zorder=render_params.zorder)

if legend_params.colorbar:
sm = plt.cm.ScalarMappable(cmap=cmap, norm=render_params.cmap_params.norm)
fig_params.fig.colorbar(sm, ax=ax)

# 2) Image has any number of channels but 1
else:
layers = {}
for ch_index, c in enumerate(channels):
layers[c] = img.sel(c=c).copy(deep=True).squeeze()

if render_params.percentiles_for_norm != (None, None):
layers[c] = _normalize(
layers[c],
pmin=render_params.percentiles_for_norm[0],
pmax=render_params.percentiles_for_norm[1],
clip=True,
)

if not isinstance(render_params.cmap_params, list):
if render_params.cmap_params.norm is not None:
layers[c] = render_params.cmap_params.norm(layers[c])
Expand Down
29 changes: 1 addition & 28 deletions src/spatialdata_plot/pl/utils.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -489,7 +489,7 @@ def _get_scalebar(

def _prepare_cmap_norm(
cmap: Colormap | str | None = None,
norm: Normalize | bool = False,
norm: Normalize | None = None,
na_color: ColorLike | None = None,
vmin: float | None = None,
vmax: float | None = None,
Expand DownExpand Up@@ -1623,29 +1623,6 @@ def _type_check_params(param_dict: dict[str, Any], element_type: str) -> dict[st
if scale < 0:
raise ValueError("Parameter 'scale' must be a positive number.")

if (percentiles_for_norm := param_dict.get("percentiles_for_norm")) is None:
percentiles_for_norm = (None, None)
elif not (isinstance(percentiles_for_norm, (list, tuple)) or len(percentiles_for_norm) != 2):
raise TypeError("Parameter 'percentiles_for_norm' must be a list or tuple of exactly two floats or None.")
elif not all(
isinstance(p, (float, int, type(None)))
and isinstance(p, type(percentiles_for_norm[0]))
and (p is None or 0 <= p <= 100)
for p in percentiles_for_norm
):
raise TypeError(
"Each item in 'percentiles_for_norm' must be of the same dtype and must be a float or int within [0, 100], "
"or None"
)
elif (
percentiles_for_norm[0] is not None
and percentiles_for_norm[1] is not None
and percentiles_for_norm[0] > percentiles_for_norm[1]
):
raise ValueError("The first number in 'percentiles_for_norm' must not be smaller than the second.")
if "percentiles_for_norm" in param_dict:
param_dict["percentiles_for_norm"] = percentiles_for_norm

if size := param_dict.get("size"):
if not isinstance(size, (float, int)):
raise TypeError("Parameter 'size' must be numeric.")
Expand DownExpand Up@@ -1886,7 +1863,6 @@ def _validate_image_render_params(
cmap: list[Colormap | str] | Colormap | str | None,
norm: Normalize | None,
scale: str | None,
percentiles_for_norm: tuple[float | None, float | None] | None,
) -> dict[str, dict[str, Any]]:
param_dict: dict[str, Any] = {
"sdata": sdata,
Expand All@@ -1898,7 +1874,6 @@ def _validate_image_render_params(
"cmap": cmap,
"norm": norm,
"scale": scale,
"percentiles_for_norm": percentiles_for_norm,
}
param_dict = _type_check_params(param_dict, "images")

Expand DownExpand Up@@ -1945,8 +1920,6 @@ def _validate_image_render_params(
else:
element_params[el]["scale"] = scale

element_params[el]["percentiles_for_norm"] = param_dict["percentiles_for_norm"]

return element_params


Expand Down
Binary file modifiedtests/_images/Images_can_pass_cmap_to_single_channel.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modifiedtests/_images/Images_can_pass_color_to_each_channel.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modifiedtests/_images/Images_can_pass_color_to_single_channel.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modifiedtests/_images/Images_can_stack_render_images.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modifiedtests/_images/Shapes_colorbar_can_be_normalised.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modifiedtests/_images/Shapes_colorbar_respects_input_limits.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 3 additions & 1 deletion tests/pl/test_render.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -23,7 +23,9 @@ def test_render_images_can_plot_one_cyx_image(request):
def test_render_images_can_plot_multiple_cyx_images(share_coordinate_system: str, request):
fun = request.getfixturevalue("get_sdata_with_multiple_images")
sdata = fun(share_coordinate_system)
sdata.pl.render_images().pl.show()
sdata.pl.render_images().pl.show(
colorbar=False, # otherwise we'll get one cbar per image in the same cs
)
axs = plt.gcf().get_axes()

if share_coordinate_system == "all":
Expand Down
22 changes: 6 additions & 16 deletions tests/pl/test_render_images.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,7 +2,6 @@
import matplotlib
import numpy as np
import scanpy as sc
from matplotlib import pyplot as plt
from matplotlib.colors import Normalize
from spatial_image import to_spatial_image
from spatialdata import SpatialData
Expand DownExpand Up@@ -49,9 +48,6 @@ def test_plot_can_render_a_single_channel_from_image(self, sdata_blobs: SpatialD
def test_plot_can_render_a_single_channel_from_multiscale_image(self, sdata_blobs: SpatialData):
sdata_blobs.pl.render_images(element="blobs_multiscale_image", channel=0).pl.show()

def test_plot_can_render_a_single_channel_from_image_no_el(self, sdata_blobs: SpatialData):
sdata_blobs.pl.render_images(channel=0).pl.show()

def test_plot_can_render_a_single_channel_str_from_image(self, sdata_blobs_str: SpatialData):
sdata_blobs_str.pl.render_images(element="blobs_image", channel="c1").pl.show()

Expand All@@ -70,16 +66,13 @@ def test_plot_can_render_two_channels_str_from_image(self, sdata_blobs_str: Spat
def test_plot_can_render_two_channels_str_from_multiscale_image(self, sdata_blobs_str: SpatialData):
sdata_blobs_str.pl.render_images(element="blobs_multiscale_image", channel=["c1", "c2"]).pl.show()

def test_plot_can_pass_vmin_vmax(self, sdata_blobs: SpatialData):
fig, axs = plt.subplots(ncols=2, figsize=(6, 3))
sdata_blobs.pl.render_images(element="blobs_image", channel=1).pl.show(ax=axs[0])
sdata_blobs.pl.render_images(element="blobs_image", channel=1, vmin=0, vmax=0.4).pl.show(ax=axs[1])

def test_plot_can_pass_normalize(self, sdata_blobs: SpatialData):
fig, axs = plt.subplots(ncols=2, figsize=(6, 3))
def test_plot_can_pass_normalize_clip_True(self, sdata_blobs: SpatialData):
norm = Normalize(vmin=0, vmax=0.4, clip=True)
sdata_blobs.pl.render_images(element="blobs_image", channel=1).pl.show(ax=axs[0])
sdata_blobs.pl.render_images(element="blobs_image", channel=1, norm=norm).pl.show(ax=axs[1])
sdata_blobs.pl.render_images(element="blobs_image", channel=0, norm=norm).pl.show()

def test_plot_can_pass_normalize_clip_False(self, sdata_blobs: SpatialData):
norm = Normalize(vmin=0, vmax=0.4, clip=False)
sdata_blobs.pl.render_images(element="blobs_image", channel=0, norm=norm).pl.show()

def test_plot_can_pass_color_to_single_channel(self, sdata_blobs: SpatialData):
sdata_blobs.pl.render_images(element="blobs_image", channel=1, palette="red").pl.show()
Expand All@@ -97,9 +90,6 @@ def test_plot_can_pass_cmap_to_each_channel(self, sdata_blobs: SpatialData):
element="blobs_image", channel=[0, 1, 2], cmap=["Reds", "Greens", "Blues"]
).pl.show()

def test_plot_can_normalize_image(self, sdata_blobs: SpatialData):
sdata_blobs.pl.render_images(element="blobs_image", percentiles_for_norm=(5, 90)).pl.show()

def test_plot_can_render_multiscale_image(self, sdata_blobs: SpatialData):
sdata_blobs.pl.render_images("blobs_multiscale_image").pl.show()

Expand Down
4 changes: 3 additions & 1 deletion tests/pl/test_render_shapes.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,7 @@
import pandas as pd
import scanpy as sc
from anndata import AnnData
from matplotlib.colors import Normalize
from shapely.geometry import MultiPolygon, Point, Polygon
from spatialdata import SpatialData, deepcopy
from spatialdata.models import ShapesModel, TableModel
Expand DownExpand Up@@ -146,7 +147,8 @@ def test_plot_colorbar_can_be_normalised(self, sdata_blobs: SpatialData):
sdata_blobs["table"].obs["region"] = ["blobs_polygons"] * sdata_blobs["table"].n_obs
sdata_blobs["table"].uns["spatialdata_attrs"]["region"] = "blobs_polygons"
sdata_blobs.shapes["blobs_polygons"]["cluster"] = [1, 2, 3, 5, 20]
sdata_blobs.pl.render_shapes("blobs_polygons", color="cluster", groups=["c1"], norm=True).pl.show()
norm = Normalize(vmin=0, vmax=5, clip=True)
sdata_blobs.pl.render_shapes("blobs_polygons", color="cluster", groups=["c1"], norm=norm).pl.show()

def test_plot_can_plot_shapes_after_spatial_query(self, sdata_blobs: SpatialData):
# subset to only shapes, should be unnecessary after rasterizeation of multiscale images is included
Expand Down