Skip to content

support for multiple slice positions in 2D heatmaps with automatic layout #87

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
169 changes: 142 additions & 27 deletions brainglobe_heatmap/heatmaps.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@
magnitudes as the values.
position : list, tuple, np.ndarray, float
Position of the plane in the atlas.
list of positions create multiple slices.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
list of positions create multiple slices.
List of positions create multiple slices.

typo.

orientation : str or tuple, optional
Orientation of the plane in the atlas. Either, "frontal",
"sagittal", "horizontal" or a tuple with the normal vector.
Expand Down Expand Up @@ -182,6 +183,8 @@
self.label_regions = label_regions
self.annotate_regions = annotate_regions
self.annotate_text_options_2d = annotate_text_options_2d
self.slicer: Optional[Slicer] = None
self.multiple_slicers: Optional[List[Slicer]] = None

# create a scene
self.scene = Scene(
Expand All @@ -204,8 +207,27 @@
if r.name != "root"
]

# prepare slicer object
self.slicer = Slicer(position, orientation, thickness, self.scene.root)
# prepare slicer object or objects when list(position)
if isinstance(position, list):
if self.format == "3D":
raise ValueError(

Check warning on line 213 in brainglobe_heatmap/heatmaps.py

View check run for this annotation

Codecov / codecov/patch

brainglobe_heatmap/heatmaps.py#L212-L213

Added lines #L212 - L213 were not covered by tests
"List of positions not supported in 3D format. "
"Did you mean to use a tuple as a 3D position?"
)
if len(position) <= 1:
Copy link
Member

Choose a reason for hiding this comment

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

I think it should be OK to pass a list of length 1 (but not an empty list)? If you agree, please adapt accordingly?

raise ValueError(

Check warning on line 218 in brainglobe_heatmap/heatmaps.py

View check run for this annotation

Codecov / codecov/patch

brainglobe_heatmap/heatmaps.py#L217-L218

Added lines #L217 - L218 were not covered by tests
"List of positions should contain more than one position. "
"Did you mean to pass a single value?"
)
self.positions = position
self.multiple_slicers = [

Check warning on line 223 in brainglobe_heatmap/heatmaps.py

View check run for this annotation

Codecov / codecov/patch

brainglobe_heatmap/heatmaps.py#L222-L223

Added lines #L222 - L223 were not covered by tests
Slicer(pos, orientation, thickness, self.scene.root)
for pos in position
]
else:
self.slicer = Slicer(
position, orientation, thickness, self.scene.root
)

def prepare_colors(
self,
Expand Down Expand Up @@ -275,6 +297,9 @@
Creates a 2D plot or 3D rendering of the heatmap
"""
if self.format == "3D":
assert (
Copy link
Member

Choose a reason for hiding this comment

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

is there a case where self.slicer is ever still None after running __init__? I don't see it, so not sure this is needed?

self.slicer is not None
), "Cannot access slice, check your parameters"
self.slicer.slice_scene(self.scene, self.regions_meshes)
view = self.render(**kwargs)
else:
Expand Down Expand Up @@ -327,6 +352,9 @@
camera = self.orientation
else:
self.orientation = np.array(self.orientation)
assert (

Check warning on line 355 in brainglobe_heatmap/heatmaps.py

View check run for this annotation

Codecov / codecov/patch

brainglobe_heatmap/heatmaps.py#L355

Added line #L355 was not covered by tests
self.slicer is not None
), "Cannot access plane0: slicer is None"
com = self.slicer.plane0.center_of_mass()
camera = {
"pos": com - self.orientation * 2 * np.linalg.norm(com),
Expand All @@ -349,7 +377,7 @@
cbar_label: Optional[str] = None,
show_cbar: bool = True,
**kwargs,
) -> plt.Figure:
) -> Union[plt.Figure, List[plt.Figure]]:
"""
Plots the heatmap in 2D using matplotlib.

Expand Down Expand Up @@ -381,39 +409,118 @@

Returns
-------
plt.Figure
The matplotlib figure object for the plot.
Union[plt.Figure, List[plt.Figure]]
The matplotlib figure object for the plot,
or a list of figure objects
when list(positions) are provided.

Notes
-----
This method is used to generate a standalone plot of
the heatmap data.
When list(positions) are provided to class constructor,
it creates multiple figures with optimized grid layouts.
"""

f, ax = plt.subplots(figsize=(9, 9))

f, ax = self.plot_subplot(
fig=f,
ax=ax,
show_legend=show_legend,
xlabel=xlabel,
ylabel=ylabel,
hide_axes=hide_axes,
cbar_label=cbar_label,
show_cbar=show_cbar,
**kwargs,
)
if self.multiple_slicers is not None:
num_slices = len(self.multiple_slicers)
max_plots_per_fig = min(25, num_slices)
num_figures = (

Check warning on line 428 in brainglobe_heatmap/heatmaps.py

View check run for this annotation

Codecov / codecov/patch

brainglobe_heatmap/heatmaps.py#L425-L428

Added lines #L425 - L428 were not covered by tests
num_slices + max_plots_per_fig - 1
) // max_plots_per_fig

all_figures = []
for fig_idx in range(num_figures):

Check warning on line 433 in brainglobe_heatmap/heatmaps.py

View check run for this annotation

Codecov / codecov/patch

brainglobe_heatmap/heatmaps.py#L432-L433

Added lines #L432 - L433 were not covered by tests
# calculate grid layout for the figure
start_idx = fig_idx * max_plots_per_fig
end_idx = min((fig_idx + 1) * max_plots_per_fig, num_slices)
current_num_slices = end_idx - start_idx

Check warning on line 437 in brainglobe_heatmap/heatmaps.py

View check run for this annotation

Codecov / codecov/patch

brainglobe_heatmap/heatmaps.py#L435-L437

Added lines #L435 - L437 were not covered by tests

nrows = int(np.ceil(np.sqrt(current_num_slices)))
ncols = int(np.ceil(current_num_slices / nrows))

Check warning on line 440 in brainglobe_heatmap/heatmaps.py

View check run for this annotation

Codecov / codecov/patch

brainglobe_heatmap/heatmaps.py#L439-L440

Added lines #L439 - L440 were not covered by tests

f, axes = plt.subplots(

Check warning on line 442 in brainglobe_heatmap/heatmaps.py

View check run for this annotation

Codecov / codecov/patch

brainglobe_heatmap/heatmaps.py#L442

Added line #L442 was not covered by tests
nrows,
ncols,
layout="constrained",
figsize=(26.25, 15), # 7:4 aspect ratio works well
)

if filename is not None:
plt.savefig(filename, dpi=300)
# padding (left, bottom, right, top) [0-1]%
f.get_layout_engine().set(rect=(0, 0.01, 1, 0.97))
axes_flat = axes.flatten()

Check warning on line 451 in brainglobe_heatmap/heatmaps.py

View check run for this annotation

Codecov / codecov/patch

brainglobe_heatmap/heatmaps.py#L450-L451

Added lines #L450 - L451 were not covered by tests

# plot each position into the figure
for i in range(current_num_slices):
global_i = start_idx + i
projected, _ = self.multiple_slicers[

Check warning on line 456 in brainglobe_heatmap/heatmaps.py

View check run for this annotation

Codecov / codecov/patch

brainglobe_heatmap/heatmaps.py#L454-L456

Added lines #L454 - L456 were not covered by tests
global_i
].get_structures_slice_coords(
self.regions_meshes, self.scene.root
)

plt.show()
return f
self.plot_subplot(

Check warning on line 462 in brainglobe_heatmap/heatmaps.py

View check run for this annotation

Codecov / codecov/patch

brainglobe_heatmap/heatmaps.py#L462

Added line #L462 was not covered by tests
f,
axes_flat[i],
projected,
show_legend,
xlabel,
ylabel,
hide_axes,
cbar_label,
show_cbar,
**kwargs,
)

axes_flat[i].set_title(

Check warning on line 475 in brainglobe_heatmap/heatmaps.py

View check run for this annotation

Codecov / codecov/patch

brainglobe_heatmap/heatmaps.py#L475

Added line #L475 was not covered by tests
self.title
if self.title is not None
else f"Position {self.positions[global_i]}"
)

# hide any empty subplots
for i in range(current_num_slices, len(axes_flat)):
axes_flat[i].axis("off")

Check warning on line 483 in brainglobe_heatmap/heatmaps.py

View check run for this annotation

Codecov / codecov/patch

brainglobe_heatmap/heatmaps.py#L482-L483

Added lines #L482 - L483 were not covered by tests

# save if filename provided
if filename and num_figures > 1:
print("Saving ", f"fig{fig_idx+1}_{filename}")
plt.savefig(f"fig{fig_idx+1}_{filename}", dpi=200)
elif filename:
print("Saving ", filename)
plt.savefig(filename, dpi=200)

Check warning on line 491 in brainglobe_heatmap/heatmaps.py

View check run for this annotation

Codecov / codecov/patch

brainglobe_heatmap/heatmaps.py#L486-L491

Added lines #L486 - L491 were not covered by tests

all_figures.append(f)
plt.show()

Check warning on line 494 in brainglobe_heatmap/heatmaps.py

View check run for this annotation

Codecov / codecov/patch

brainglobe_heatmap/heatmaps.py#L493-L494

Added lines #L493 - L494 were not covered by tests

return all_figures

Check warning on line 496 in brainglobe_heatmap/heatmaps.py

View check run for this annotation

Codecov / codecov/patch

brainglobe_heatmap/heatmaps.py#L496

Added line #L496 was not covered by tests
else:
f, ax = plt.subplots(figsize=(9, 9))

Check warning on line 498 in brainglobe_heatmap/heatmaps.py

View check run for this annotation

Codecov / codecov/patch

brainglobe_heatmap/heatmaps.py#L498

Added line #L498 was not covered by tests

f, ax = self.plot_subplot(

Check warning on line 500 in brainglobe_heatmap/heatmaps.py

View check run for this annotation

Codecov / codecov/patch

brainglobe_heatmap/heatmaps.py#L500

Added line #L500 was not covered by tests
fig=f,
ax=ax,
show_legend=show_legend,
xlabel=xlabel,
ylabel=ylabel,
hide_axes=hide_axes,
cbar_label=cbar_label,
show_cbar=show_cbar,
**kwargs,
)

if filename is not None:
print("Saving ", filename)
plt.savefig(filename, dpi=300)

Check warning on line 514 in brainglobe_heatmap/heatmaps.py

View check run for this annotation

Codecov / codecov/patch

brainglobe_heatmap/heatmaps.py#L512-L514

Added lines #L512 - L514 were not covered by tests

plt.show()
return f

Check warning on line 517 in brainglobe_heatmap/heatmaps.py

View check run for this annotation

Codecov / codecov/patch

brainglobe_heatmap/heatmaps.py#L516-L517

Added lines #L516 - L517 were not covered by tests

def plot_subplot(
self,
fig: plt.Figure,
ax: plt.Axes,
projected=None,
show_legend: bool = False,
xlabel: str = "µm",
ylabel: str = "µm",
Expand All @@ -431,10 +538,14 @@

Parameters
----------
fig : plt.Figure, optional
fig : plt.Figure
The figure object in which the subplot is plotted.
ax : plt.Axes, optional
ax : plt.Axes
The axes object in which the subplot is plotted.
projected : dict, optional
Pre-computed slice coordinates.
If None, coordinates will be
calculated using the self.slicer.
show_legend : bool, optional
If True, displays a legend for the plotted regions.
Default is False.
Expand All @@ -461,9 +572,13 @@
-----
This method modifies the provided figure and axes objects in-place.
"""
projected, _ = self.slicer.get_structures_slice_coords(
self.regions_meshes, self.scene.root
)
if projected is None:
assert (
self.slicer is not None
), "Cannot plot: slicer is None and no projected data provided"
projected, _ = self.slicer.get_structures_slice_coords(
self.regions_meshes, self.scene.root
)

segments: List[Dict[str, Union[str, np.ndarray, float]]] = []
for r, coords in projected.items():
Expand Down
4 changes: 4 additions & 0 deletions brainglobe_heatmap/planner.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,10 @@
)

# print planes information
assert self.slicer is not None, (

Check warning on line 75 in brainglobe_heatmap/planner.py

View check run for this annotation

Codecov / codecov/patch

brainglobe_heatmap/planner.py#L75

Added line #L75 was not covered by tests
"Cannot access plane0/plane1: slicer is None, "
"check your position parameter"
)
print_plane("Plane 0", self.slicer.plane0, blue_dark)
print_plane("Plane 1", self.slicer.plane1, pink_dark)

Expand Down
38 changes: 11 additions & 27 deletions examples/heatmap_2d_subplots.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import matplotlib.pyplot as plt

import brainglobe_heatmap as bgh

data_dict = {
Expand All @@ -13,28 +11,14 @@
"VISam": 1.0,
}

# Create a list of scenes to plot
# Note: it's important to keep reference to the scenes to avoid a
# segmentation fault
scenes = []
for distance in range(7500, 10500, 500):
Copy link
Member

Choose a reason for hiding this comment

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

maybe still worth keeping this example too, for advanced users (like when users want more control over figure size, or more than 25 subplots... or something else) to have more flexibility?

scene = bgh.Heatmap(
data_dict,
position=distance,
orientation="frontal",
thickness=10,
format="2D",
cmap="Reds",
vmin=0,
vmax=1,
label_regions=False,
)
scenes.append(scene)

# Create a figure with 6 subplots and plot the scenes
fig, axs = plt.subplots(3, 2, figsize=(18, 12))
for scene, ax in zip(scenes, axs.flatten(), strict=False):
scene.plot_subplot(fig=fig, ax=ax, show_cbar=True, hide_axes=False)

plt.tight_layout()
plt.show()
f = bgh.Heatmap(
data_dict,
position=[7000, 7250, 7500, 8000, 8500, 9000, 9500, 10000],
orientation="frontal",
cmap="Reds",
vmin=0,
vmax=1,
title="", # title=None for title with positions number
label_regions=False,
format="2D",
).show(show_cbar=True, hide_axes=False)
Loading