Skip to content

Commit 493653f

Browse files
author
Stefan Hahmann
committed
Add support for filtering by selected spots in Spot and Division Count Chart
Introduce a feature to filter spots and divisions by selected spots and persist this setting. Refactor related code to use `ProjectModel` for better integration. Update tests and UI to reflect new functionality.
1 parent 6821de5 commit 493653f

4 files changed

Lines changed: 109 additions & 61 deletions

File tree

src/main/java/org/mastodon/mamut/tomancak/divisioncount/SpotAndDivisionCount.java

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,11 @@
3232
import java.util.List;
3333

3434
import org.apache.commons.lang3.tuple.Triple;
35+
import org.mastodon.mamut.ProjectModel;
36+
import org.mastodon.mamut.model.Link;
3537
import org.mastodon.mamut.model.Model;
3638
import org.mastodon.mamut.model.Spot;
39+
import org.mastodon.model.SelectionModel;
3740
import org.mastodon.util.TreeUtils;
3841

3942
public class SpotAndDivisionCount
@@ -46,23 +49,33 @@ private SpotAndDivisionCount()
4649
/**
4750
* Calculates the number of spots and divisions per time point in the given model.
4851
*
49-
* @param model The model containing the spots and edges.
52+
* @param projectModel The project model containing the spots and edges.
53+
* @param onlySelectedSpots If {@code true}, only counts spots that are selected.
5054
* @return A list of triples, where each triple contains (in that order) the timepoint, the number of spots at that timepoint, and the number of divisions at that timepoint.
5155
*/
52-
public static List< Triple< Integer, Integer, Integer > > getSpotAndDivisionsPerTimepoint( final Model model )
56+
public static List< Triple< Integer, Integer, Integer > > getSpotAndDivisionsPerTimepoint( final ProjectModel projectModel,
57+
final boolean onlySelectedSpots )
5358
{
59+
final Model model = projectModel.getModel();
60+
final SelectionModel< Spot, Link > selectionModel = projectModel.getSelectionModel();
5461
int minTimepoint = TreeUtils.getMinTimepoint( model );
5562
int maxTimepoint = TreeUtils.getMaxTimepoint( model );
5663
List< Triple< Integer, Integer, Integer > > timepointAndDivisions = new ArrayList<>();
5764
for ( int timepoint = minTimepoint; timepoint <= maxTimepoint; timepoint++ )
5865
{
66+
int spots = 0;
5967
int divisions = 0;
6068
for ( Spot spot : model.getSpatioTemporalIndex().getSpatialIndex( timepoint ) )
6169
{
70+
if ( onlySelectedSpots && !selectionModel.isSelected( spot ) )
71+
continue;
6272
if ( spot.outgoingEdges().size() > 1 )
6373
divisions++;
74+
if ( onlySelectedSpots )
75+
spots++;
6476
}
65-
int spots = model.getSpatioTemporalIndex().getSpatialIndex( timepoint ).size();
77+
if ( !onlySelectedSpots )
78+
spots = model.getSpatioTemporalIndex().getSpatialIndex( timepoint ).size();
6679
timepointAndDivisions.add( Triple.of( timepoint, spots, divisions ) );
6780
}
6881
return timepointAndDivisions;

src/main/java/org/mastodon/mamut/tomancak/divisioncount/SpotAndDivisionCountChart.java

Lines changed: 78 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,14 @@
3838
import java.awt.geom.Rectangle2D;
3939
import java.util.List;
4040

41+
import javax.swing.ButtonGroup;
4142
import javax.swing.JButton;
4243
import javax.swing.JCheckBox;
4344
import javax.swing.JColorChooser;
4445
import javax.swing.JFrame;
4546
import javax.swing.JLabel;
4647
import javax.swing.JPanel;
48+
import javax.swing.JRadioButton;
4749
import javax.swing.JSpinner;
4850
import javax.swing.SpinnerNumberModel;
4951
import javax.swing.WindowConstants;
@@ -83,6 +85,8 @@ public class SpotAndDivisionCountChart extends JFrame
8385

8486
private static final String DIVISION_COUNT_SLIDING_AVERAGE_WINDOW_SIZE = "divisionCountSlidingAverageWindowSize";
8587

88+
private static final String ONLY_SELECTED_SPOTS = "onlySelectedSpots";
89+
8690
private static final int SPOT_COUNT_DEFAULT_COLOR = new Color( 230, 159, 0 ).getRGB(); // Dark Orange
8791

8892
private static final int DIVISION_COUNT_DEFAULT_COLOR = new Color( 86, 180, 233 ).getRGB(); // Light Blue
@@ -93,9 +97,11 @@ public class SpotAndDivisionCountChart extends JFrame
9397

9498
private Color divisionCountColor; // Light Blue
9599

96-
private final static String TITLE = "Spot and Division Counts over Time";
97-
private final static String SPOTS_COUNT_SERIES_NAME = "Spot Counts";
98-
private final static String DIVISION_COUNT_SERIES_NAME = "Division Counts";
100+
private static final String TITLE = "Spot and Division Counts over Time";
101+
102+
private static final String SPOTS_COUNT_SERIES_NAME = "Spot Counts";
103+
104+
private static final String DIVISION_COUNT_SERIES_NAME = "Division Counts";
99105

100106
private final XYPlot plot;
101107

@@ -107,11 +113,19 @@ public class SpotAndDivisionCountChart extends JFrame
107113

108114
private int divisionWindowSize;
109115

116+
private boolean onlySelectedSpots;
117+
110118
private final PrefService prefs;
111119

112-
SpotAndDivisionCountChart( final double[] timepoints, final double[] spotCounts, final double[] divisionCounts,
113-
final PrefService prefs )
120+
private final ProjectModel projectModel;
121+
122+
private XYSeriesCollection spotCountsSeries;
123+
124+
private XYSeriesCollection divisionCountsSeries;
125+
126+
SpotAndDivisionCountChart( final ProjectModel projectModel, final PrefService prefs )
114127
{
128+
this.projectModel = projectModel;
115129
this.prefs = prefs;
116130

117131
this.spotCountColor = new Color(
@@ -122,12 +136,9 @@ public class SpotAndDivisionCountChart extends JFrame
122136
prefs.getInt( SpotAndDivisionCountChart.class, SPOT_COUNT_SLIDING_AVERAGE_WINDOW_SIZE, DEFAULT_SLIDING_WINDOW_SIZE );
123137
this.divisionWindowSize =
124138
prefs.getInt( SpotAndDivisionCountChart.class, DIVISION_COUNT_SLIDING_AVERAGE_WINDOW_SIZE, DEFAULT_SLIDING_WINDOW_SIZE );
139+
this.onlySelectedSpots = prefs.getBoolean( SpotAndDivisionCountChart.class, ONLY_SELECTED_SPOTS, false );
125140

126-
XYSeriesCollection spotCountsSeries = createSeries(
127-
timepoints, spotCounts, SPOTS_COUNT_SERIES_NAME, spotWindowSize );
128-
129-
XYSeriesCollection divisionCountsSeries = createSeries(
130-
timepoints, divisionCounts, DIVISION_COUNT_SERIES_NAME, divisionWindowSize );
141+
updateChartData();
131142

132143
JFreeChart chart = ChartFactory.createXYLineChart(
133144
TITLE,
@@ -168,7 +179,7 @@ public class SpotAndDivisionCountChart extends JFrame
168179
chartPanel.setPreferredSize( new Dimension( 800, 600 ) );
169180

170181
// Add color chooser and visibility controls
171-
JPanel controlPanel = createControlPanel( timepoints, spotCounts, divisionCounts );
182+
JPanel controlPanel = createControlPanel();
172183

173184
// Set up the frame layout
174185
setLayout( new BorderLayout() );
@@ -184,23 +195,34 @@ public class SpotAndDivisionCountChart extends JFrame
184195
repaint();
185196
}
186197

187-
public static void show( final ProjectModel projectModel, final PrefService prefService )
198+
private void updateChartData()
188199
{
189-
List< Triple< Integer, Integer, Integer > > divisionCounts =
190-
SpotAndDivisionCount.getSpotAndDivisionsPerTimepoint( projectModel.getModel() );
191-
double[] timepoints = divisionCounts.stream().mapToDouble( Triple::getLeft ).toArray();
192-
double[] spots = divisionCounts.stream().mapToDouble( Triple::getMiddle ).toArray();
193-
double[] divisions = divisionCounts.stream().mapToDouble( Triple::getRight ).toArray();
200+
List< Triple< Integer, Integer, Integer > > counts =
201+
SpotAndDivisionCount.getSpotAndDivisionsPerTimepoint( projectModel, this.onlySelectedSpots );
202+
double[] timepoints = counts.stream().mapToDouble( Triple::getLeft ).toArray();
203+
double[] spotCounts = counts.stream().mapToDouble( Triple::getMiddle ).toArray();
204+
double[] divisionCounts = counts.stream().mapToDouble( Triple::getRight ).toArray();
205+
spotCountsSeries = createSeries( timepoints, spotCounts, SPOTS_COUNT_SERIES_NAME, spotWindowSize );
206+
divisionCountsSeries = createSeries( timepoints, divisionCounts, DIVISION_COUNT_SERIES_NAME, divisionWindowSize );
207+
208+
if ( plot != null )
209+
{
210+
plot.setDataset( 0, spotCountsSeries );
211+
plot.setDataset( 1, divisionCountsSeries );
212+
}
213+
}
194214

195-
SpotAndDivisionCountChart chart = new SpotAndDivisionCountChart( timepoints, spots, divisions, prefService );
215+
public static void show( final ProjectModel projectModel, final PrefService prefService )
216+
{
217+
SpotAndDivisionCountChart chart = new SpotAndDivisionCountChart( projectModel, prefService );
196218
chart.setDefaultCloseOperation( WindowConstants.DISPOSE_ON_CLOSE );
197219
chart.setVisible( true );
198220
}
199221

200222
/**
201223
* Creates a control panel with color choosers and checkboxes for visibility controls.
202224
*/
203-
private JPanel createControlPanel( final double[] timepoints, final double[] spotCounts, final double[] divisionCounts )
225+
private JPanel createControlPanel()
204226
{
205227
JPanel controlPanel = new JPanel( new MigLayout( "fill, wrap 5", "[grow]", "[]10[]10[]10[]10[]" ) );
206228

@@ -237,7 +259,7 @@ private JPanel createControlPanel( final double[] timepoints, final double[] spo
237259
spotWindowSpinner.addChangeListener( e -> {
238260
spotWindowSize = ( int ) spotWindowSpinner.getValue();
239261
prefs.put( SpotAndDivisionCountChart.class, SPOT_COUNT_SLIDING_AVERAGE_WINDOW_SIZE, spotWindowSize );
240-
updateSlidingAverage( timepoints, spotCounts, divisionCounts );
262+
updateChartData();
241263
} );
242264

243265
// Division-related controls
@@ -273,7 +295,25 @@ private JPanel createControlPanel( final double[] timepoints, final double[] spo
273295
divisionWindowSpinner.addChangeListener( e -> {
274296
divisionWindowSize = ( int ) divisionWindowSpinner.getValue();
275297
prefs.put( SpotAndDivisionCountChart.class, DIVISION_COUNT_SLIDING_AVERAGE_WINDOW_SIZE, divisionWindowSize );
276-
updateSlidingAverage( timepoints, spotCounts, divisionCounts );
298+
updateChartData();
299+
} );
300+
301+
JRadioButton allSpots = new JRadioButton( "All spots", !onlySelectedSpots );
302+
JRadioButton selectedSpots = new JRadioButton( "Only selected spots", onlySelectedSpots );
303+
ButtonGroup group = new ButtonGroup();
304+
group.add( allSpots );
305+
group.add( selectedSpots );
306+
allSpots.addActionListener( e -> {
307+
onlySelectedSpots = false;
308+
prefs.put( SpotAndDivisionCountChart.class, ONLY_SELECTED_SPOTS, onlySelectedSpots );
309+
updateChartData();
310+
repaint();
311+
} );
312+
selectedSpots.addActionListener( e -> {
313+
this.onlySelectedSpots = true;
314+
prefs.put( SpotAndDivisionCountChart.class, ONLY_SELECTED_SPOTS, onlySelectedSpots );
315+
updateChartData();
316+
repaint();
277317
} );
278318

279319
// Add components to the control panel
@@ -289,6 +329,9 @@ private JPanel createControlPanel( final double[] timepoints, final double[] spo
289329
controlPanel.add( new JLabel( "Window Size:" ), "align right" );
290330
controlPanel.add( divisionWindowSpinner, "wmax 50" );
291331

332+
controlPanel.add( allSpots, "growx" );
333+
controlPanel.add( selectedSpots, "span, growx" );
334+
292335
// Add description
293336
controlPanel.add( new JLabel(
294337
"<html>This windows shows the number of spots and divisions at each timepoint together with a sliding average.<br>A division is defined as a spot with more than one outgoing edge.</html>" ),
@@ -331,27 +374,23 @@ private void updateChartColors()
331374
*/
332375
private void updateAxisVisibility()
333376
{
334-
XYItemRenderer spotCountRenderer = plot.getRenderer( 0 );
335-
Boolean spotCountVisible = spotCountRenderer.getSeriesVisible( 0 );
336-
Boolean spotAverageVisible = spotCountRenderer.getSeriesVisible( 1 );
337-
boolean isSpotCountVisible = spotCountVisible != null && spotCountVisible;
338-
boolean isSpotAverageVisible = spotAverageVisible != null && spotAverageVisible;
339-
boolean showSpotAxis = isSpotCountVisible || isSpotAverageVisible;
340-
leftAxis.setLabel( showSpotAxis ? SPOTS_COUNT_SERIES_NAME : null );
341-
leftAxis.setVisible( showSpotAxis );
342-
343-
XYItemRenderer divisionCountRenderer = plot.getRenderer( 1 );
344-
Boolean divisionCountVisible = divisionCountRenderer.getSeriesVisible( 0 );
345-
Boolean divisionAverageVisible = divisionCountRenderer.getSeriesVisible( 1 );
346-
boolean isDivisionCountVisible = divisionCountVisible != null && divisionCountVisible;
347-
boolean isDivisionAverageVisible = divisionAverageVisible != null && divisionAverageVisible;
348-
boolean showDivisionAxis = isDivisionCountVisible || isDivisionAverageVisible;
349-
rightAxis.setLabel( showDivisionAxis ? DIVISION_COUNT_SERIES_NAME : null );
350-
rightAxis.setVisible( showDivisionAxis );
351-
377+
updateAxisVisibility( plot.getRenderer( 0 ), leftAxis, SPOTS_COUNT_SERIES_NAME );
378+
updateAxisVisibility( plot.getRenderer( 1 ), rightAxis, DIVISION_COUNT_SERIES_NAME );
352379
repaint();
353380
}
354381

382+
private void updateAxisVisibility( final XYItemRenderer renderer, final NumberAxis axis,
383+
final String labelIfVisible )
384+
{
385+
Boolean countVisible = renderer.getSeriesVisible( 0 );
386+
Boolean averageVisible = renderer.getSeriesVisible( 1 );
387+
boolean isCountVisible = countVisible != null && countVisible;
388+
boolean isAverageVisible = averageVisible != null && averageVisible;
389+
boolean showAxis = isCountVisible || isAverageVisible;
390+
axis.setLabel( showAxis ? labelIfVisible : null );
391+
axis.setVisible( showAxis );
392+
}
393+
355394
private XYLineAndShapeRenderer createRenderer( final Color color, final Shape shape )
356395
{
357396
XYLineAndShapeRenderer renderer = new XYLineAndShapeRenderer();
@@ -406,20 +445,6 @@ private static XYSeriesCollection createSeries( final double[] xValues, final do
406445
return dataset;
407446
}
408447

409-
/**
410-
* Updates the sliding average series based on the new window sizes.
411-
*/
412-
private void updateSlidingAverage( double[] timepoints, double[] spotCounts, double[] divisionCounts )
413-
{
414-
XYSeriesCollection spotCountsSeries = createSeries(
415-
timepoints, spotCounts, SPOTS_COUNT_SERIES_NAME, spotWindowSize );
416-
XYSeriesCollection divisionCountsSeries = createSeries(
417-
timepoints, divisionCounts, DIVISION_COUNT_SERIES_NAME, divisionWindowSize );
418-
419-
plot.setDataset( 0, spotCountsSeries );
420-
plot.setDataset( 1, divisionCountsSeries );
421-
}
422-
423448
private static double[] calculateSlidingAverage( double[] values, int windowSize )
424449
{
425450
double[] result = new double[ values.length ];

src/main/java/org/mastodon/mamut/tomancak/export/ExportDivisionCountsPerTimepointCommand.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@
3737

3838
import org.apache.commons.lang3.tuple.Triple;
3939
import org.mastodon.mamut.ProjectModel;
40-
import org.mastodon.mamut.model.Model;
4140
import org.mastodon.mamut.tomancak.divisioncount.SpotAndDivisionCount;
4241
import org.scijava.Context;
4342
import org.scijava.ItemVisibility;
@@ -75,7 +74,7 @@ public void run()
7574
{
7675
try
7776
{
78-
writeDivisionCountsToFile( projectModel.getModel(), saveTo, context.service( StatusService.class ) );
77+
writeDivisionCountsToFile( projectModel, saveTo, context.service( StatusService.class ) );
7978
}
8079
catch ( IOException e )
8180
{
@@ -93,7 +92,8 @@ public void run()
9392
* <li>The first line is the header.</li>
9493
* </ul>
9594
*/
96-
public static void writeDivisionCountsToFile( final Model model, final File file, final StatusService statusService ) throws IOException
95+
public static void writeDivisionCountsToFile( final ProjectModel projectModel, final File file, final StatusService statusService )
96+
throws IOException
9797
{
9898
if ( file == null )
9999
throw new IllegalArgumentException( "Cannot write division counts to file. Given file is null." );
@@ -102,7 +102,7 @@ public static void writeDivisionCountsToFile( final Model model, final File file
102102
{
103103
csvWriter.writeNext( new String[] { "timepoint", "divisions" } );
104104
List< Triple< Integer, Integer, Integer > > timepointAndDivisions =
105-
SpotAndDivisionCount.getSpotAndDivisionsPerTimepoint( model );
105+
SpotAndDivisionCount.getSpotAndDivisionsPerTimepoint( projectModel, false );
106106
for ( Triple< Integer, Integer, Integer > pair : timepointAndDivisions )
107107
{
108108
csvWriter.writeNext( new String[] { String.valueOf( pair.getLeft() ), String.valueOf( pair.getRight() ) }, false );

src/test/java/org/mastodon/mamut/tomancak/export/ExportDivisionCountsPerTimepointCommandTest.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,14 @@
3434
import java.io.IOException;
3535
import java.nio.charset.Charset;
3636

37+
import net.imglib2.img.Img;
38+
import net.imglib2.img.array.ArrayImgs;
39+
import net.imglib2.type.numeric.real.FloatType;
40+
3741
import org.apache.commons.io.FileUtils;
3842
import org.junit.Test;
43+
import org.mastodon.mamut.ProjectModel;
44+
import org.mastodon.mamut.ProjectModelTestUtils;
3945
import org.mastodon.mamut.feature.branch.exampleGraph.ExampleGraph2;
4046
import org.mastodon.mamut.model.Model;
4147
import org.scijava.Context;
@@ -52,12 +58,16 @@ public void testWriteDivisionCountsToFile() throws IOException
5258
{
5359
try (Context context = new Context())
5460
{
61+
final Img< FloatType > img = ArrayImgs.floats( 1, 1, 1 );
5562
Model model = new ExampleGraph2().getModel();
63+
File mastodonFile = File.createTempFile( "test", ".mastodon" );
64+
ProjectModel appModel = ProjectModelTestUtils.wrapAsAppModel( img, model, context, mastodonFile );
65+
5666
File outputFile = File.createTempFile( "divisioncounts", ".csv" );
5767
outputFile.deleteOnExit();
5868
StatusService service = context.service( StatusService.class );
5969

60-
ExportDivisionCountsPerTimepointCommand.writeDivisionCountsToFile( model, outputFile, service );
70+
ExportDivisionCountsPerTimepointCommand.writeDivisionCountsToFile( appModel, outputFile, service );
6171

6272
String content = FileUtils.readFileToString( outputFile, Charset.defaultCharset() );
6373
String expected = "\"timepoint\",\"divisions\"\n"

0 commit comments

Comments
 (0)