From f7c60d5c4a9d6220eeafba506329820754b46cb9 Mon Sep 17 00:00:00 2001 From: Akshat Sharma Date: Sun, 1 Mar 2026 12:38:35 +0530 Subject: [PATCH 1/4] Fix 2D morphology coloring to use per-compartment values MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, 2D morphology plotting applied a single color per section, effectively using only the first compartment’s mapped value. This caused incorrect visualization when values differed across compartments. In _plot_morphology2D, colors are now computed and applied per compartment. - For show_diameter=False: each compartment is drawn as an individual segment with its own color. - For show_diameter=True: each compartment is drawn as its own polygon patch with compartment-specific diameters and color. - Scalar-like values are still supported by repeating one value across the section to preserve intentional single-color behavior. - 3D plotting logic remains unchanged. Added a regression test to verify per-compartment color differentiation. All morphology plotting tests pass. --- brian2tools/plotting/morphology.py | 64 ++++++++++++++++++++---------- brian2tools/tests/test_plotting.py | 21 ++++++++++ 2 files changed, 65 insertions(+), 20 deletions(-) diff --git a/brian2tools/plotting/morphology.py b/brian2tools/plotting/morphology.py index 08cdbfdf..550909d1 100644 --- a/brian2tools/plotting/morphology.py +++ b/brian2tools/plotting/morphology.py @@ -25,13 +25,26 @@ def _plot_morphology2D(morpho, axes, colors, voltage_colormap, show_diameter=False, show_compartments=True, color_counter=0): - if values is not None: - # Determine colors based on compartment values - normed_values = value_norm(values[morpho.indices[:]]) - colors = voltage_colormap(normed_values) - color = colors[0] - else: - color = colors[color_counter % len(colors)] + compartment_count = len(morpho.x) + + def _section_colors(): + if values is None: + color = colors[color_counter % len(colors)] + return [color] * compartment_count + try: + section_values = values[morpho.indices[:]] + except Exception: + # Keep scalar behavior: one value means one color everywhere. + section_values = np.repeat(values, compartment_count) + normed_values = value_norm(section_values) + section_colors = voltage_colormap(normed_values) + if section_colors.ndim == 1: + section_colors = np.repeat(section_colors[np.newaxis, :], + compartment_count, axis=0) + return section_colors + + compartment_colors = _section_colors() + color = compartment_colors[0] if isinstance(morpho, Soma): x, y = morpho.x/um, morpho.y/um @@ -46,26 +59,37 @@ def _plot_morphology2D(morpho, axes, colors, if show_diameter: coords_2d = coords[:, :2] directions = np.diff(coords_2d, axis=0) - orthogonal = np.vstack([-directions[:, 1], directions[:, 0]]) - orthogonal = np.vstack([orthogonal.T, orthogonal[:, -1:].T]) - radius = np.hstack([morpho.start_diameter[0]/um/2, - morpho.end_diameter/um/2]) + orthogonal = np.vstack([-directions[:, 1], directions[:, 0]]).T orthogonal /= np.sqrt(np.sum(orthogonal**2, axis=1))[:, np.newaxis] - points = np.vstack([coords_2d + orthogonal*radius[:, np.newaxis], - (coords_2d - orthogonal*radius[:, np.newaxis])[::-1]]) - patch = Polygon(points, color=color) - axes.add_artist(patch) - # FIXME: Ugly workaround to make the auto-scaling work - axes.plot(points[:, 0], points[:, 1], color='white', alpha=0.) + start_radius = morpho.start_diameter/um/2 + end_radius = morpho.end_diameter/um/2 + for idx, color in enumerate(compartment_colors): + start_point = coords_2d[idx] + end_point = coords_2d[idx + 1] + ortho = orthogonal[idx] + points = np.vstack([start_point + ortho*start_radius[idx], + end_point + ortho*end_radius[idx], + end_point - ortho*end_radius[idx], + start_point - ortho*start_radius[idx]]) + patch = Polygon(points, color=color) + axes.add_artist(patch) + # FIXME: Ugly workaround to make the auto-scaling work + axes.plot(points[:, 0], points[:, 1], color='white', alpha=0.) else: - axes.plot(coords[:, 0], coords[:, 1], color=color, lw=2) + for idx, color in enumerate(compartment_colors): + axes.plot(coords[idx:idx + 2, 0], coords[idx:idx + 2, 1], + color=color, lw=2) if show_compartments: # dots at the center of the compartments if show_diameter: color = 'black' - axes.plot(morpho.x/um, morpho.y/um, '.', color=color, - mec='none', alpha=0.75) + axes.plot(morpho.x/um, morpho.y/um, '.', color=color, + mec='none', alpha=0.75) + else: + axes.scatter(morpho.x/um, morpho.y/um, + c=compartment_colors, + marker='.', edgecolors='none', alpha=0.75) for child in morpho.children: _plot_morphology2D(child, axes=axes, diff --git a/brian2tools/tests/test_plotting.py b/brian2tools/tests/test_plotting.py index 1b6fd085..afbf0bea 100644 --- a/brian2tools/tests/test_plotting.py +++ b/brian2tools/tests/test_plotting.py @@ -185,6 +185,27 @@ def test_plot_morphology_values(): plot_3d=False) +def test_plot_morphology_values_per_compartment_2d(): + set_device('runtime') + morpho = Soma(diameter=20*um) + morpho.axon = Cylinder(diameter=2*um, n=3, length=30*um) + morpho = morpho.generate_coordinates() + + # one value for the soma and three different values for the axon compartments + values = np.array([0., 1., 2., 3.]) + ax = plot_morphology(morpho, values=values, plot_3d=False, + show_compartments=False, show_diameter=False) + + # For the axon (n=3) we expect one plotted line segment per compartment. + section_lines = [line for line in ax.lines if line.get_linewidth() == 2] + assert len(section_lines) == 3 + + # Compartment values differ, therefore at least two colors should differ. + section_colors = [tuple(line.get_color()) for line in section_lines] + assert len(set(section_colors)) > 1 + plt.close() + + if __name__ == '__main__': test_plot_monitors() test_plot_synapses() From cb7fc90db3fe0910555be712151d5da1819bd3db Mon Sep 17 00:00:00 2001 From: Akshat Sharma Date: Wed, 18 Mar 2026 10:28:13 +0530 Subject: [PATCH 2/4] Inline section color logic and replace broad exception handling - Replaced broad `except Exception` with more specific exception handling - Inlined `_section_colors` function into `_plot_morphology2D` - Preserved existing behavior for both scalar and per-compartment values --- brian2tools/plotting/morphology.py | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/brian2tools/plotting/morphology.py b/brian2tools/plotting/morphology.py index 65149332..c2ce4c7c 100644 --- a/brian2tools/plotting/morphology.py +++ b/brian2tools/plotting/morphology.py @@ -27,23 +27,19 @@ def _plot_morphology2D(morpho, axes, colors, color_counter=0): compartment_count = len(morpho.x) - def _section_colors(): - if values is None: - color = colors[color_counter % len(colors)] - return [color] * compartment_count + if values is None: + compartment_colors = [colors[color_counter % len(colors)]] * compartment_count + else: try: section_values = values[morpho.indices[:]] - except Exception: + except (IndexError, TypeError): # Keep scalar behavior: one value means one color everywhere. section_values = np.repeat(values, compartment_count) normed_values = value_norm(section_values) - section_colors = voltage_colormap(normed_values) - if section_colors.ndim == 1: - section_colors = np.repeat(section_colors[np.newaxis, :], - compartment_count, axis=0) - return section_colors - - compartment_colors = _section_colors() + compartment_colors = voltage_colormap(normed_values) + if compartment_colors.ndim == 1: + compartment_colors = np.repeat(compartment_colors[np.newaxis, :], + compartment_count, axis=0) color = compartment_colors[0] if isinstance(morpho, Soma): From a21bf43818a0454367883a6a3def10b9bcd5b36a Mon Sep 17 00:00:00 2001 From: Akshat Sharma Date: Mon, 30 Mar 2026 22:59:40 +0530 Subject: [PATCH 3/4] Fix incorrect merge by restoring add_patch usage and removing obsolete autoscaling workaround This commit fixes an issue introduced during the merge with upstream where `axes.add_artist()` was mistakenly used instead of `axes.add_patch()` in `_plot_morphology2D`. Using `add_artist()` prevented matplotlib from including the polygon in axis autoscaling, which required an additional invisible plotting workaround (`axes.plot(..., alpha=0.)`). This workaround is no longer necessary. Changes: - Replaced `axes.add_artist(patch)` with `axes.add_patch(patch)` - Removed obsolete FIXME comment and autoscaling workaround line The fix restores correct autoscaling behavior while preserving the per-compartment coloring logic introduced in this branch. --- brian2tools/plotting/morphology.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/brian2tools/plotting/morphology.py b/brian2tools/plotting/morphology.py index f1d5954d..496d845b 100644 --- a/brian2tools/plotting/morphology.py +++ b/brian2tools/plotting/morphology.py @@ -69,9 +69,7 @@ def _plot_morphology2D(morpho, axes, colors, end_point - ortho*end_radius[idx], start_point - ortho*start_radius[idx]]) patch = Polygon(points, color=color) - axes.add_artist(patch) - # FIXME: Ugly workaround to make the auto-scaling work - axes.plot(points[:, 0], points[:, 1], color='white', alpha=0.) + axes.add_patch(patch) else: for idx, color in enumerate(compartment_colors): axes.plot(coords[idx:idx + 2, 0], coords[idx:idx + 2, 1], From caba489843e5050765f83103db540c590326c722 Mon Sep 17 00:00:00 2001 From: Akshat Sharma Date: Tue, 31 Mar 2026 16:34:18 +0530 Subject: [PATCH 4/4] Fix: Remove dead code in _plot_morphology2D Remove the defensive ndim == 1 check on compartment_colors, which is unreachable since voltage_colormap always receives an array and returns a 2D (N, 4) RGBA result. --- brian2tools/plotting/morphology.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/brian2tools/plotting/morphology.py b/brian2tools/plotting/morphology.py index 496d845b..632dca0a 100644 --- a/brian2tools/plotting/morphology.py +++ b/brian2tools/plotting/morphology.py @@ -37,9 +37,6 @@ def _plot_morphology2D(morpho, axes, colors, section_values = np.repeat(values, compartment_count) normed_values = value_norm(section_values) compartment_colors = voltage_colormap(normed_values) - if compartment_colors.ndim == 1: - compartment_colors = np.repeat(compartment_colors[np.newaxis, :], - compartment_count, axis=0) color = compartment_colors[0] if isinstance(morpho, Soma):