From 77f136a1165abf02b7fcb38fbb9c0bd11b07303c Mon Sep 17 00:00:00 2001 From: Dan Vasilescu Date: Tue, 24 Feb 2026 15:26:29 -0500 Subject: [PATCH 01/31] Fixing class name ambiguity --- .../ode/gui/OutputSpeciesResultsPanel.java | 41 +++++++++---------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/OutputSpeciesResultsPanel.java b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/OutputSpeciesResultsPanel.java index b9ddf0bbf4..c564a2f9eb 100644 --- a/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/OutputSpeciesResultsPanel.java +++ b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/OutputSpeciesResultsPanel.java @@ -72,7 +72,6 @@ import org.vcell.model.rbm.RbmUtils; import org.vcell.model.rbm.SpeciesPattern; import org.vcell.model.rbm.RbmUtils.BnglObjectConstructionVisitor; -import org.vcell.model.rbm.gui.GeneratedSpeciesTableRow; import org.vcell.solver.nfsim.NFSimMolecularConfigurations; import org.vcell.solver.nfsim.NFSimSolver; import org.vcell.util.Pair; @@ -134,7 +133,7 @@ public class OutputSpeciesResultsPanel extends DocumentEditorSubPanel { private SpeciesPatternLargeShape spls; // =================================================================================================== - private class OutputSpeciesResultsTableModel extends VCellSortTableModel + private class OutputSpeciesResultsTableModel extends VCellSortTableModel implements PropertyChangeListener, AutoCompleteTableModel { public final int colCount = 4; @@ -147,7 +146,7 @@ private class OutputSpeciesResultsTableModel extends VCellSortTableModel allGeneratedSpeciesList; + private ArrayList allGeneratedSpeciesList; public OutputSpeciesResultsTableModel() { super(table, new String[] {"Count", "Structure", "Depiction", "BioNetGen Definition"}); @@ -168,7 +167,7 @@ public Class getColumnClass(int iCol) { return Object.class; } - public ArrayList getTableRows() { + public ArrayList getTableRows() { return allGeneratedSpeciesList; } @@ -186,7 +185,7 @@ public void setValueAt(Object valueNew, int iRow, int iCol) { } @Override public Object getValueAt(int iRow, int iCol) { - GeneratedSpeciesTableRow speciesTableRow = getValueAt(iRow); + LocalGeneratedSpeciesTableRow speciesTableRow = getValueAt(iRow); switch(iCol) { case iColCount: return speciesTableRow.count; @@ -227,10 +226,10 @@ public Set getAutoCompletionWords(int row, int column) { public void propertyChange(PropertyChangeEvent arg0) { } @Override - protected Comparator getComparator(int col, boolean ascending) { + protected Comparator getComparator(int col, boolean ascending) { final int scale = ascending ? 1 : -1; - return new Comparator() { - public int compare(GeneratedSpeciesTableRow o1, GeneratedSpeciesTableRow o2) { + return new Comparator() { + public int compare(LocalGeneratedSpeciesTableRow o1, LocalGeneratedSpeciesTableRow o2) { switch (col) { case iColCount: return scale * o1.count.compareTo(o2.count); @@ -299,7 +298,7 @@ public void refreshData() { Map molecularConfigurations = nfsmc.getMolecularConfigurations(); for(String pattern : molecularConfigurations.keySet()) { Integer number = molecularConfigurations.get(pattern); - GeneratedSpeciesTableRow newRow = createTableRow(pattern, number); + LocalGeneratedSpeciesTableRow newRow = createTableRow(pattern, number); allGeneratedSpeciesList.add(newRow); } } else { @@ -310,12 +309,12 @@ public void refreshData() { System.out.println("Simulation: " + sim.getName()); // apply text search function for particular columns - List speciesObjectList = new ArrayList<>(); + List speciesObjectList = new ArrayList<>(); if (searchText == null || searchText.length() == 0) { speciesObjectList.addAll(allGeneratedSpeciesList); } else { String lowerCaseSearchText = searchText.toLowerCase(); - for (GeneratedSpeciesTableRow rs : allGeneratedSpeciesList) { + for (LocalGeneratedSpeciesTableRow rs : allGeneratedSpeciesList) { if (rs.expression.toLowerCase().contains(lowerCaseSearchText) ) { speciesObjectList.add(rs); // search for pure expression continue; // found already, no point to keep searching for another match for this row @@ -352,7 +351,7 @@ public void refreshData() { } // end OutputSpeciesResultsTableModel class // ====================================================================================== - private class GeneratedSpeciesTableRow { + private class LocalGeneratedSpeciesTableRow { private Integer count = 0; // used only in relation to an observable private String expression = "?"; private String structure = "?"; @@ -360,7 +359,7 @@ private class GeneratedSpeciesTableRow { // never call this directly, the object is not fully constructed // always call createTableRow() instead - protected GeneratedSpeciesTableRow(Integer count) { + protected LocalGeneratedSpeciesTableRow(Integer count) { if(count != null) { this.count = count; } @@ -412,7 +411,7 @@ private void deriveSpecies(String originalExpression) { species = null; } } - } // end GeneratedSpeciesTableRow class + } // end LocalGeneratedSpeciesTableRow class // ====================================================================================== private class EventHandler implements ActionListener, DocumentListener, ListSelectionListener, TableModelListener { @@ -476,8 +475,8 @@ public OutputSpeciesResultsPanel(ODEDataViewer owner) { initialize(); } -private GeneratedSpeciesTableRow createTableRow(String expression, Integer count) { - GeneratedSpeciesTableRow row = new GeneratedSpeciesTableRow(count); +private LocalGeneratedSpeciesTableRow createTableRow(String expression, Integer count) { + LocalGeneratedSpeciesTableRow row = new LocalGeneratedSpeciesTableRow(count); try { row.deriveSpecies(expression); }catch (Exception e) { @@ -488,7 +487,7 @@ private GeneratedSpeciesTableRow createTableRow(String expression, Integer count } -public ArrayList getTableRows() { +public ArrayList getTableRows() { return tableModel.getTableRows(); } @@ -726,8 +725,8 @@ public Component getTableCellRendererComponent(JTable table, Object value, selectedObject = tableModel.getValueAt(row); } if (selectedObject != null) { - if(selectedObject instanceof GeneratedSpeciesTableRow) { - SpeciesContext sc = ((GeneratedSpeciesTableRow)selectedObject).species; + if(selectedObject instanceof LocalGeneratedSpeciesTableRow) { + SpeciesContext sc = ((LocalGeneratedSpeciesTableRow)selectedObject).species; if(sc == null || sc.getSpeciesPattern() == null) { spss = null; } else { @@ -764,8 +763,8 @@ public void paintComponent(Graphics g) { } public void updateShape(int selectedRow) { - - GeneratedSpeciesTableRow speciesTableRow = tableModel.getValueAt(selectedRow); + + LocalGeneratedSpeciesTableRow speciesTableRow = tableModel.getValueAt(selectedRow); SpeciesContext sc = speciesTableRow.species; if(sc == null || sc.getSpeciesPattern() == null) { spls = new SpeciesPatternLargeShape(20, 20, -1, shapePanel, true, issueManager); // error (red circle) From 48a71c49194ef618979238e02ba88e94cb0a9285 Mon Sep 17 00:00:00 2001 From: Dan Vasilescu Date: Wed, 25 Feb 2026 14:46:25 -0500 Subject: [PATCH 02/31] Clusters panel --- .../cbit/vcell/client/data/ODEDataViewer.java | 31 +++++++++++++++++-- .../vcell/client/data/SimResultsViewer.java | 5 +++ .../ode/gui/LangevinClustersResultsPanel.java | 19 ++++++++++++ .../cbit/vcell/simdata/ODEDataManager.java | 4 +++ 4 files changed, 57 insertions(+), 2 deletions(-) create mode 100644 vcell-client/src/main/java/cbit/vcell/solver/ode/gui/LangevinClustersResultsPanel.java diff --git a/vcell-client/src/main/java/cbit/vcell/client/data/ODEDataViewer.java b/vcell-client/src/main/java/cbit/vcell/client/data/ODEDataViewer.java index a267f7208a..930735541b 100644 --- a/vcell-client/src/main/java/cbit/vcell/client/data/ODEDataViewer.java +++ b/vcell-client/src/main/java/cbit/vcell/client/data/ODEDataViewer.java @@ -18,6 +18,8 @@ import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; +import cbit.vcell.simdata.LangevinSolverResultSet; +import cbit.vcell.solver.ode.gui.LangevinClustersResultsPanel; import org.vcell.solver.nfsim.NFSimMolecularConfigurations; import org.vcell.util.document.VCDataIdentifier; import org.vcell.util.gui.DialogUtils; @@ -53,12 +55,16 @@ public class ODEDataViewer extends DataViewer { private PlotPane ivjPlotPane1 = null; private JTabbedPane ivjJTabbedPane = null; private ODESolverResultSet fieldOdeSolverResultSet = null; + public boolean hasLangevinBatchResults = false; + private LangevinSolverResultSet fieldLangevinSolverResultSet = null; private NFSimMolecularConfigurations nFSimMolecularConfigurations = null; private javax.swing.JPanel ivjViewData = null; + private LangevinClustersResultsPanel langevinClustersResultsPanel = null; private OutputSpeciesResultsPanel outputSpeciesResultsPanel = null; private VCDataIdentifier fieldVcDataIdentifier = null; private static final String OUTPUT_SPECIES_TABNAME = "Output Species"; + private static final String LANGEVIN_CLUSTER_RESULTS_TABNAME = "Langevin Clusters"; class IvjEventHandler implements java.beans.PropertyChangeListener { public void propertyChange(java.beans.PropertyChangeEvent evt) { @@ -217,6 +223,9 @@ public ODESolverPlotSpecificationPanel getODESolverPlotSpecificationPanel1() { public ODESolverResultSet getOdeSolverResultSet() { return fieldOdeSolverResultSet; } +public LangevinSolverResultSet getLangevinSolverResultSet() { + return fieldLangevinSolverResultSet; +} public NFSimMolecularConfigurations getNFSimMolecularConfigurations() { return nFSimMolecularConfigurations; } @@ -265,7 +274,9 @@ public VCDataIdentifier getVcDataIdentifier() { public void stateChanged(ChangeEvent e) { if(ivjJTabbedPane.getSelectedIndex() == ivjJTabbedPane.indexOfTab(OUTPUT_SPECIES_TABNAME)){ // TODO: here - }else { + } else if(ivjJTabbedPane.getSelectedIndex() == ivjJTabbedPane.indexOfTab(LANGEVIN_CLUSTER_RESULTS_TABNAME)) { + + } else { } } @@ -277,9 +288,15 @@ private javax.swing.JTabbedPane getJTabbedPane() { ivjJTabbedPane = new javax.swing.JTabbedPane(); ivjJTabbedPane.setName("JTabbedPane1"); ivjJTabbedPane.insertTab("View Data", null, getViewData(), null, 0); + + langevinClustersResultsPanel = new LangevinClustersResultsPanel(this); + langevinClustersResultsPanel.addPropertyChangeListener(ivjEventHandler); + ivjJTabbedPane.addTab(LANGEVIN_CLUSTER_RESULTS_TABNAME, langevinClustersResultsPanel); + outputSpeciesResultsPanel = new OutputSpeciesResultsPanel(this); outputSpeciesResultsPanel.addPropertyChangeListener(ivjEventHandler); ivjJTabbedPane.addTab(OUTPUT_SPECIES_TABNAME, outputSpeciesResultsPanel); + ivjJTabbedPane.addChangeListener(mainTabChangeListener); } catch (java.lang.Throwable ivjExc) { handleException(ivjExc); @@ -390,6 +407,11 @@ public void setOdeSolverResultSet(ODESolverResultSet odeSolverResultSet) { fieldOdeSolverResultSet = odeSolverResultSet; firePropertyChange("odeSolverResultSet", oldValue, odeSolverResultSet); } +public void setLangevinSolverResultSet(LangevinSolverResultSet langevinSolverResultSet) { + LangevinSolverResultSet oldValue = fieldLangevinSolverResultSet; + fieldLangevinSolverResultSet = langevinSolverResultSet; + firePropertyChange("langevinSolverResultSet", oldValue, langevinSolverResultSet); +} public void setNFSimMolecularConfigurations(NFSimMolecularConfigurations nFSimMolecularConfigurations) { NFSimMolecularConfigurations oldValue = nFSimMolecularConfigurations; this.nFSimMolecularConfigurations = nFSimMolecularConfigurations; @@ -430,10 +452,15 @@ public void setVcDataIdentifier(VCDataIdentifier vcDataIdentifier) { } public void setOdeDataContext() { - if(getSimulation() != null && getNFSimMolecularConfigurations() != null) { + if(getSimulation() != null && hasLangevinBatchResults) { + getJTabbedPane().setEnabledAt(getJTabbedPane().indexOfTab(OUTPUT_SPECIES_TABNAME), false); + getJTabbedPane().setEnabledAt(getJTabbedPane().indexOfTab(LANGEVIN_CLUSTER_RESULTS_TABNAME), true); + } else if(getSimulation() != null && getNFSimMolecularConfigurations() != null) { getJTabbedPane().setEnabledAt(getJTabbedPane().indexOfTab(OUTPUT_SPECIES_TABNAME), true); + getJTabbedPane().setEnabledAt(getJTabbedPane().indexOfTab(LANGEVIN_CLUSTER_RESULTS_TABNAME), false); } else { getJTabbedPane().setEnabledAt(getJTabbedPane().indexOfTab(OUTPUT_SPECIES_TABNAME), false); + getJTabbedPane().setEnabledAt(getJTabbedPane().indexOfTab(LANGEVIN_CLUSTER_RESULTS_TABNAME), false); } } diff --git a/vcell-client/src/main/java/cbit/vcell/client/data/SimResultsViewer.java b/vcell-client/src/main/java/cbit/vcell/client/data/SimResultsViewer.java index e4382369fc..0db1152c15 100644 --- a/vcell-client/src/main/java/cbit/vcell/client/data/SimResultsViewer.java +++ b/vcell-client/src/main/java/cbit/vcell/client/data/SimResultsViewer.java @@ -79,6 +79,11 @@ private DataViewer createODEDataViewer() throws DataAccessException { odeDataViewer = new ODEDataViewer(); odeDataViewer.setSimulation(getSimulation()); ODESolverResultSet odesrs = ((ODEDataManager)dataManager).getODESolverResultSet(); + LangevinSolverResultSet langevinSolverResultSet = ((ODEDataManager)dataManager).getLangevinSolverResultSet(); + if(getSimulation().getSimulationOwner().getMathDescription().isLangevin() && langevinSolverResultSet.isAverageDataAvailable() && langevinSolverResultSet.isClusterDataAvailable()) { + odeDataViewer.setLangevinSolverResultSet(langevinSolverResultSet); + odeDataViewer.hasLangevinBatchResults = true; + } odeDataViewer.setOdeSolverResultSet(odesrs); odeDataViewer.setNFSimMolecularConfigurations(((ODEDataManager)dataManager).getNFSimMolecularConfigurations()); odeDataViewer.setVcDataIdentifier(dataManager.getVCDataIdentifier()); diff --git a/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/LangevinClustersResultsPanel.java b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/LangevinClustersResultsPanel.java new file mode 100644 index 0000000000..8eb1f75362 --- /dev/null +++ b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/LangevinClustersResultsPanel.java @@ -0,0 +1,19 @@ +package cbit.vcell.solver.ode.gui; + +import cbit.vcell.client.data.ODEDataViewer; +import cbit.vcell.client.desktop.biomodel.DocumentEditorSubPanel; + +public class LangevinClustersResultsPanel extends DocumentEditorSubPanel { + + private final ODEDataViewer owner; + + public LangevinClustersResultsPanel(ODEDataViewer odeDataViewer) { + this.owner = odeDataViewer; + } + + + @Override + protected void onSelectedObjectsChange(Object[] selectedObjects) { + + } +} diff --git a/vcell-core/src/main/java/cbit/vcell/simdata/ODEDataManager.java b/vcell-core/src/main/java/cbit/vcell/simdata/ODEDataManager.java index 418d034b5c..0cc7ecbdd3 100644 --- a/vcell-core/src/main/java/cbit/vcell/simdata/ODEDataManager.java +++ b/vcell-core/src/main/java/cbit/vcell/simdata/ODEDataManager.java @@ -137,6 +137,9 @@ public ODESolverResultSet getODESolverResultSet() throws DataAccessException { public NFSimMolecularConfigurations getNFSimMolecularConfigurations() throws DataAccessException { return nFSimMolecularConfigurations; } +public LangevinSolverResultSet getLangevinSolverResultSet() throws DataAccessException { + return langevinSolverResultSet; +} /** * Gets the simulationInfo property (cbit.vcell.solver.SimulationInfo) value. @@ -184,6 +187,7 @@ private void connect() throws DataAccessException { langevinSolverResultSet = new LangevinSolverResultSet(raw); // may be null if( langevinSolverResultSet.isAverageDataAvailable()) { odeSolverResultSet = langevinSolverResultSet.getAvg(); +// odeSolverResultSet = langevinSolverResultSet.getClusterMean(); } } From ff17e3a02de433ad0810f498dc4ab422ff9498fd Mon Sep 17 00:00:00 2001 From: Dan Vasilescu Date: Fri, 27 Feb 2026 17:58:52 -0500 Subject: [PATCH 03/31] implementing the cluster specifications panel --- .../cbit/vcell/client/data/ODEDataViewer.java | 46 +++-- .../ode/gui/LangevinClustersResultsPanel.java | 191 +++++++++++++++++- 2 files changed, 214 insertions(+), 23 deletions(-) diff --git a/vcell-client/src/main/java/cbit/vcell/client/data/ODEDataViewer.java b/vcell-client/src/main/java/cbit/vcell/client/data/ODEDataViewer.java index 930735541b..281b9bae89 100644 --- a/vcell-client/src/main/java/cbit/vcell/client/data/ODEDataViewer.java +++ b/vcell-client/src/main/java/cbit/vcell/client/data/ODEDataViewer.java @@ -9,12 +9,10 @@ */ package cbit.vcell.client.data; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; import java.util.HashMap; import java.util.Hashtable; -import javax.swing.JTabbedPane; +import javax.swing.*; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; @@ -39,7 +37,6 @@ import cbit.vcell.solver.DataSymbolMetadata; import cbit.vcell.solver.Simulation; import cbit.vcell.solver.SimulationModelInfo; -import cbit.vcell.solver.ode.ODESimData; import cbit.vcell.solver.ode.ODESolverResultSet; import cbit.vcell.solver.ode.gui.ODESolverPlotSpecificationPanel; import cbit.vcell.solver.ode.gui.OutputSpeciesResultsPanel; @@ -58,8 +55,8 @@ public class ODEDataViewer extends DataViewer { public boolean hasLangevinBatchResults = false; private LangevinSolverResultSet fieldLangevinSolverResultSet = null; private NFSimMolecularConfigurations nFSimMolecularConfigurations = null; - private javax.swing.JPanel ivjViewData = null; - private LangevinClustersResultsPanel langevinClustersResultsPanel = null; + private javax.swing.JPanel viewData = null; + private LangevinClustersResultsPanel viewClustersPanel = null; private OutputSpeciesResultsPanel outputSpeciesResultsPanel = null; private VCDataIdentifier fieldVcDataIdentifier = null; @@ -289,9 +286,7 @@ private javax.swing.JTabbedPane getJTabbedPane() { ivjJTabbedPane.setName("JTabbedPane1"); ivjJTabbedPane.insertTab("View Data", null, getViewData(), null, 0); - langevinClustersResultsPanel = new LangevinClustersResultsPanel(this); - langevinClustersResultsPanel.addPropertyChangeListener(ivjEventHandler); - ivjJTabbedPane.addTab(LANGEVIN_CLUSTER_RESULTS_TABNAME, langevinClustersResultsPanel); + ivjJTabbedPane.addTab(LANGEVIN_CLUSTER_RESULTS_TABNAME, getViewClusters()); outputSpeciesResultsPanel = new OutputSpeciesResultsPanel(this); outputSpeciesResultsPanel.addPropertyChangeListener(ivjEventHandler); @@ -304,23 +299,31 @@ private javax.swing.JTabbedPane getJTabbedPane() { } return ivjJTabbedPane; } -/** - * Return the ViewData property value. - * @return javax.swing.JPanel - */ -private javax.swing.JPanel getViewData() { - if (ivjViewData == null) { + +private JPanel getViewClusters() { + if (viewClustersPanel == null) { + try { + viewClustersPanel = new LangevinClustersResultsPanel(this); + viewClustersPanel.addPropertyChangeListener(ivjEventHandler); + } catch (java.lang.Throwable ivjExc) { + handleException(ivjExc); + } + } + return viewClustersPanel; +} +private JPanel getViewData() { + if (viewData == null) { try { - ivjViewData = new javax.swing.JPanel(); - ivjViewData.setName("ViewData"); - ivjViewData.setLayout(new java.awt.BorderLayout()); - getViewData().add(getODESolverPlotSpecificationPanel1(), "West"); - getViewData().add(getPlotPane1(), "Center"); + viewData = new JPanel(); + viewData.setName("ViewData"); + viewData.setLayout(new java.awt.BorderLayout()); + viewData.add(getODESolverPlotSpecificationPanel1(), "West"); + viewData.add(getPlotPane1(), "Center"); } catch (java.lang.Throwable ivjExc) { handleException(ivjExc); } } - return ivjViewData; + return viewData; } @@ -449,6 +452,7 @@ public void setVcDataIdentifier(VCDataIdentifier vcDataIdentifier) { setOdeDataContext(); firePropertyChange("vcDataIdentifier", oldValue, vcDataIdentifier); outputSpeciesResultsPanel.refreshData(); + viewClustersPanel.refreshData(); } public void setOdeDataContext() { diff --git a/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/LangevinClustersResultsPanel.java b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/LangevinClustersResultsPanel.java index 8eb1f75362..5f34de9793 100644 --- a/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/LangevinClustersResultsPanel.java +++ b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/LangevinClustersResultsPanel.java @@ -2,18 +2,205 @@ import cbit.vcell.client.data.ODEDataViewer; import cbit.vcell.client.desktop.biomodel.DocumentEditorSubPanel; +import org.vcell.util.gui.CollapsiblePanel; + +import javax.swing.*; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; +import javax.swing.event.ListSelectionEvent; +import javax.swing.event.ListSelectionListener; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; public class LangevinClustersResultsPanel extends DocumentEditorSubPanel { private final ODEDataViewer owner; + IvjEventHandler ivjEventHandler = new IvjEventHandler(); + + private DefaultListModel defaultListModelY = null; + + private JPanel clusterSpecificationPanel = null; + private JPanel clusterVisualizationPanel = null; + + private CollapsiblePanel displayOptionsCollapsiblePanel = null; + private JScrollPane jScrollPaneYAxis = null; + private JList yAxisChoiceList = null; + + + class IvjEventHandler implements java.awt.event.ActionListener, PropertyChangeListener, ChangeListener, ListSelectionListener { + @Override + public void actionPerformed(ActionEvent e) { + System.out.println("LangevinClustersResultsPanel.IvjEventHandler.actionPerformed() called"); + System.out.println(" Source: " + e.getSource() + ", Action Command: " + e.getActionCommand()); + } + @Override + public void propertyChange(PropertyChangeEvent evt) { + System.out.println("LangevinClustersResultsPanel.IvjEventHandler.propertyChange() called"); + System.out.println(" Source: " + evt.getSource() + ", Property Name: " + evt.getPropertyName() + ", Old Value: " + evt.getOldValue() + ", New Value: " + evt.getNewValue()); + } + @Override + public void stateChanged(ChangeEvent e) { + System.out.println("LangevinClustersResultsPanel.IvjEventHandler.stateChanged() called"); + System.out.println(" Source: " + e.getSource() + ", Class: " + e.getSource().getClass().getName()); + } + + @Override + public void valueChanged(ListSelectionEvent e) { + System.out.println("LangevinClustersResultsPanel.IvjEventHandler.valueChanged() called"); + System.out.println(" Source: " + e.getSource() + ", Class: " + e.getSource().getClass().getName()); + } + }; + + public LangevinClustersResultsPanel(ODEDataViewer odeDataViewer) { + super(); + this.owner = odeDataViewer; + initialize(); + } - public LangevinClustersResultsPanel(ODEDataViewer odeDataViewer) { - this.owner = odeDataViewer; + private void initialize() { + System.out.println("LangevinClustersResultsPanel.initialize() called"); + setName("LangevinClustersResultsPanel"); + setLayout(new BorderLayout()); + add(getClusterSpecificationPanel(), BorderLayout.WEST); + add(getClusterVisualizationPanel(), BorderLayout.CENTER); + } + + private JPanel getClusterSpecificationPanel() { + if (clusterSpecificationPanel == null) { + clusterSpecificationPanel = new JPanel(); + clusterSpecificationPanel.setName("ClusterSpecificationPanel"); + clusterSpecificationPanel.setPreferredSize(new Dimension(213, 600)); + clusterSpecificationPanel.setLayout(new java.awt.GridBagLayout()); + clusterSpecificationPanel.setSize(248, 604); + clusterSpecificationPanel.setMinimumSize(new java.awt.Dimension(125, 300)); + + JLabel xAxisLabel = new JLabel("X Axis:"); + java.awt.GridBagConstraints constraintsXAxisLabel = new java.awt.GridBagConstraints(); + constraintsXAxisLabel.anchor = GridBagConstraints.WEST; + constraintsXAxisLabel.gridx = 0; constraintsXAxisLabel.gridy = 0; + constraintsXAxisLabel.insets = new Insets(4, 4, 0, 4); + clusterSpecificationPanel.add(xAxisLabel, constraintsXAxisLabel); + + JTextField xAxisTextBox = new JTextField("t"); + xAxisTextBox.setEnabled(false); + xAxisTextBox.setEditable(false); + GridBagConstraints constraintsXAxisTextBox = new java.awt.GridBagConstraints(); + constraintsXAxisTextBox.gridx = 0; constraintsXAxisTextBox.gridy = 1; + constraintsXAxisTextBox.fill = GridBagConstraints.HORIZONTAL; + constraintsXAxisTextBox.weightx = 1.0; + constraintsXAxisTextBox.insets = new Insets(0, 4, 4, 4); + clusterSpecificationPanel.add(xAxisTextBox, constraintsXAxisTextBox); + + JLabel yAxisLabel = new JLabel("Y Axis:"); + GridBagConstraints gbc_YAxisLabel = new GridBagConstraints(); + gbc_YAxisLabel.anchor = GridBagConstraints.WEST; + gbc_YAxisLabel.insets = new Insets(4, 4, 0, 4); + gbc_YAxisLabel.gridx = 0; gbc_YAxisLabel.gridy = 2; + clusterSpecificationPanel.add(yAxisLabel, gbc_YAxisLabel); + + GridBagConstraints gbc_panel = new GridBagConstraints(); + gbc_panel.fill = GridBagConstraints.BOTH; + gbc_panel.insets = new Insets(4, 4, 5, 4); + gbc_panel.gridx = 0; + gbc_panel.gridy = 3; + clusterSpecificationPanel.add(getDisplayOptionsPanel(), gbc_panel); + + java.awt.GridBagConstraints constraintsJScrollPaneYAxis = new java.awt.GridBagConstraints(); + constraintsJScrollPaneYAxis.gridx = 0; constraintsJScrollPaneYAxis.gridy = 4; + constraintsJScrollPaneYAxis.fill = GridBagConstraints.BOTH; + constraintsJScrollPaneYAxis.weightx = 1.0; + constraintsJScrollPaneYAxis.weighty = 1.0; + constraintsJScrollPaneYAxis.insets = new Insets(0, 4, 4, 4); + clusterSpecificationPanel.add(getJScrollPaneYAxis(), constraintsJScrollPaneYAxis); + + initConnections(); + } + return clusterSpecificationPanel; + } + + private CollapsiblePanel getDisplayOptionsPanel() { + if(displayOptionsCollapsiblePanel == null) { + displayOptionsCollapsiblePanel = new CollapsiblePanel("Display Options", true); + JPanel content = displayOptionsCollapsiblePanel.getContentPanel(); + content.setLayout(new GridBagLayout()); + + ButtonGroup group = new ButtonGroup(); + JRadioButton rbCounts = new JRadioButton("Cluster Counts"); + JRadioButton rbMean = new JRadioButton("Cluster Mean"); + JRadioButton rbOverall = new JRadioButton("Cluster Overall"); + group.add(rbCounts); + group.add(rbMean); + group.add(rbOverall); + + rbCounts.setSelected(true); + + GridBagConstraints gbc = new GridBagConstraints(); + gbc.gridx = 0; + gbc.anchor = GridBagConstraints.WEST; + gbc.insets = new Insets(2, 4, 2, 4); + gbc.gridy = 0; + content.add(rbCounts, gbc); + gbc.gridy = 1; + content.add(rbMean, gbc); + gbc.gridy = 2; + content.add(rbOverall, gbc); + } + return displayOptionsCollapsiblePanel; + } + + private JScrollPane getJScrollPaneYAxis() { + if(jScrollPaneYAxis == null) { + jScrollPaneYAxis = new JScrollPane(); + jScrollPaneYAxis.setName("JScrollPaneYAxis"); + jScrollPaneYAxis.setViewportView(getYAxisChoice()); + } + return jScrollPaneYAxis; + } + private JList getYAxisChoice() { + if ((yAxisChoiceList == null)) { + yAxisChoiceList = new JList(); + yAxisChoiceList.setName("YAxisChoice"); + yAxisChoiceList.setBounds(0, 0, 160, 120); + yAxisChoiceList.setSelectionMode(javax.swing.ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); + } + return yAxisChoiceList; + } + + private void initConnections() { + + getDisplayOptionsPanel().addPropertyChangeListener(ivjEventHandler); + getYAxisChoice().addListSelectionListener(ivjEventHandler); + this.addPropertyChangeListener(ivjEventHandler); + getYAxisChoice().setModel(getDefaultListModelY()); + + } + private javax.swing.DefaultListModel getDefaultListModelY() { + if (defaultListModelY == null) { + defaultListModelY = new javax.swing.DefaultListModel(); + } + return defaultListModelY; + } + + // ================================================================================================ + + private JPanel getClusterVisualizationPanel() { + if (clusterVisualizationPanel == null) { + clusterVisualizationPanel = new JPanel(); + clusterVisualizationPanel.setName("ClusterVisualizationPanel"); + } + return clusterVisualizationPanel; } @Override protected void onSelectedObjectsChange(Object[] selectedObjects) { + System.out.println("LangevinClustersResultsPanel.onSelectedObjectsChange() called with " + selectedObjects.length + " objects"); + } + public void refreshData() { + System.out.println("LangevinClustersResultsPanel.refreshData() called"); } + } From 1d6b2e455594755f7c845fc4cf8fb6cc54bb200b Mon Sep 17 00:00:00 2001 From: Dan Vasilescu Date: Tue, 3 Mar 2026 15:43:16 -0500 Subject: [PATCH 04/31] separating the cluster results panel --- .../cbit/vcell/client/data/ODEDataViewer.java | 43 ++- .../ode/gui/ClusterSpecificationPanel.java | 275 +++++++++++++++ .../ode/gui/ClusterVisualizationPanel.java | 286 ++++++++++++++++ .../ode/gui/LangevinClustersResultsPanel.java | 324 +++++++++++++++++- 4 files changed, 914 insertions(+), 14 deletions(-) create mode 100644 vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterSpecificationPanel.java create mode 100644 vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterVisualizationPanel.java diff --git a/vcell-client/src/main/java/cbit/vcell/client/data/ODEDataViewer.java b/vcell-client/src/main/java/cbit/vcell/client/data/ODEDataViewer.java index 281b9bae89..0bbc889c1f 100644 --- a/vcell-client/src/main/java/cbit/vcell/client/data/ODEDataViewer.java +++ b/vcell-client/src/main/java/cbit/vcell/client/data/ODEDataViewer.java @@ -17,7 +17,7 @@ import javax.swing.event.ChangeListener; import cbit.vcell.simdata.LangevinSolverResultSet; -import cbit.vcell.solver.ode.gui.LangevinClustersResultsPanel; +import cbit.vcell.solver.ode.gui.*; import org.vcell.solver.nfsim.NFSimMolecularConfigurations; import org.vcell.util.document.VCDataIdentifier; import org.vcell.util.gui.DialogUtils; @@ -38,8 +38,6 @@ import cbit.vcell.solver.Simulation; import cbit.vcell.solver.SimulationModelInfo; import cbit.vcell.solver.ode.ODESolverResultSet; -import cbit.vcell.solver.ode.gui.ODESolverPlotSpecificationPanel; -import cbit.vcell.solver.ode.gui.OutputSpeciesResultsPanel; import cbit.vcell.util.ColumnDescription; /** * Insert the type's description here. @@ -50,13 +48,16 @@ public class ODEDataViewer extends DataViewer { IvjEventHandler ivjEventHandler = new IvjEventHandler(); private ODESolverPlotSpecificationPanel ivjODESolverPlotSpecificationPanel1 = null; private PlotPane ivjPlotPane1 = null; + private ClusterSpecificationPanel clusterSpecificationPanel = null; + private ClusterVisualizationPanel clusterVisualizationPanel = null; + private JTabbedPane ivjJTabbedPane = null; private ODESolverResultSet fieldOdeSolverResultSet = null; public boolean hasLangevinBatchResults = false; private LangevinSolverResultSet fieldLangevinSolverResultSet = null; private NFSimMolecularConfigurations nFSimMolecularConfigurations = null; private javax.swing.JPanel viewData = null; - private LangevinClustersResultsPanel viewClustersPanel = null; + private JPanel viewClustersPanel = null; private OutputSpeciesResultsPanel outputSpeciesResultsPanel = null; private VCDataIdentifier fieldVcDataIdentifier = null; @@ -303,14 +304,42 @@ private javax.swing.JTabbedPane getJTabbedPane() { private JPanel getViewClusters() { if (viewClustersPanel == null) { try { - viewClustersPanel = new LangevinClustersResultsPanel(this); - viewClustersPanel.addPropertyChangeListener(ivjEventHandler); + viewClustersPanel = new JPanel(); + viewClustersPanel.setName("ViewClusters"); + viewClustersPanel.setLayout(new java.awt.BorderLayout()); + viewClustersPanel.add(getClusterSpecificationPanel(), "West"); + viewClustersPanel.add(getClusterVisualizationPanel(), "Center"); +// viewClustersPanel = new LangevinClustersResultsPanel(this); +// viewClustersPanel.addPropertyChangeListener(ivjEventHandler); } catch (java.lang.Throwable ivjExc) { handleException(ivjExc); } } return viewClustersPanel; } +public ClusterSpecificationPanel getClusterSpecificationPanel() { + if (clusterSpecificationPanel == null) { + try { + clusterSpecificationPanel = new ClusterSpecificationPanel(this); + clusterSpecificationPanel.setName("ClusterSpecificationPanel"); + } catch (java.lang.Throwable ivjExc) { + handleException(ivjExc); + } + } + return clusterSpecificationPanel; +} +public ClusterVisualizationPanel getClusterVisualizationPanel() { + if (clusterVisualizationPanel == null) { + try { + clusterVisualizationPanel = new ClusterVisualizationPanel(this); + clusterVisualizationPanel.setName("ClusterVisualizationPanel"); + } catch (java.lang.Throwable ivjExc) { + handleException(ivjExc); + } + } + return clusterVisualizationPanel; +} + private JPanel getViewData() { if (viewData == null) { try { @@ -452,7 +481,7 @@ public void setVcDataIdentifier(VCDataIdentifier vcDataIdentifier) { setOdeDataContext(); firePropertyChange("vcDataIdentifier", oldValue, vcDataIdentifier); outputSpeciesResultsPanel.refreshData(); - viewClustersPanel.refreshData(); + getClusterSpecificationPanel().refreshData(); } public void setOdeDataContext() { diff --git a/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterSpecificationPanel.java b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterSpecificationPanel.java new file mode 100644 index 0000000000..29a70c9d2f --- /dev/null +++ b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterSpecificationPanel.java @@ -0,0 +1,275 @@ +package cbit.vcell.solver.ode.gui; + +import cbit.vcell.client.data.ODEDataViewer; +import cbit.vcell.client.desktop.biomodel.DocumentEditorSubPanel; +import cbit.vcell.simdata.LangevinSolverResultSet; +import cbit.vcell.solver.SimulationModelInfo; +import cbit.vcell.solver.ode.ODESolverResultSet; +import cbit.vcell.util.ColumnDescription; +import org.vcell.util.gui.CollapsiblePanel; + +import javax.swing.*; +import javax.swing.border.EmptyBorder; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; +import javax.swing.event.ListSelectionEvent; +import javax.swing.event.ListSelectionListener; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; + +public class ClusterSpecificationPanel extends DocumentEditorSubPanel { + + private final ODEDataViewer owner; + ClusterSpecificationPanel.IvjEventHandler ivjEventHandler = new ClusterSpecificationPanel.IvjEventHandler(); + + private DefaultListModel defaultListModelY = null; + + private CollapsiblePanel displayOptionsCollapsiblePanel = null; + private JScrollPane jScrollPaneYAxis = null; + private JList yAxisChoiceList = null; + + + private enum DisplayMode { COUNTS, MEAN, OVERALL }; + LangevinSolverResultSet langevinSolverResultSet = null; + SimulationModelInfo simulationModelInfo = null; + + + class IvjEventHandler implements ActionListener, PropertyChangeListener, ChangeListener, ListSelectionListener { + @Override + public void actionPerformed(ActionEvent e) { + System.out.println("ClusterSpecificationPanel.IvjEventHandler.actionPerformed() called"); + System.out.println(" Source: " + e.getSource() + ", Action Command: " + e.getActionCommand()); + Object source = e.getSource(); + String cmd = e.getActionCommand(); + + if (source instanceof JRadioButton) { + JRadioButton rb = (JRadioButton) source; + System.out.println(" Source is JRadioButton with text: " + rb.getText()); + switch (cmd) { + case "COUNTS": + populateYAxisChoices(DisplayMode.COUNTS); + break; + case "MEAN": + populateYAxisChoices(DisplayMode.MEAN); + break; + case "OVERALL": + populateYAxisChoices(DisplayMode.OVERALL); + break; + } + } + } + @Override + public void propertyChange(PropertyChangeEvent evt) { + System.out.println("ClusterSpecificationPanel.IvjEventHandler.propertyChange() called"); + System.out.println(" Source: " + evt.getSource() + ", Property Name: " + evt.getPropertyName() + ", Old Value: " + evt.getOldValue() + ", New Value: " + evt.getNewValue()); + } + @Override + public void stateChanged(ChangeEvent e) { + System.out.println("ClusterSpecificationPanel.IvjEventHandler.stateChanged() called"); + System.out.println(" Source: " + e.getSource() + ", Class: " + e.getSource().getClass().getName()); + } + + @Override + public void valueChanged(ListSelectionEvent e) { + System.out.println("ClusterSpecificationPanel.IvjEventHandler.valueChanged() called"); + System.out.println(" Source: " + e.getSource() + ", Class: " + e.getSource().getClass().getName()); + Object source = e.getSource(); + + } + }; + + private void populateYAxisChoices(DisplayMode mode) { + DefaultListModel model = getDefaultListModelY(); + model.clear(); + getYAxisChoice().setEnabled(false); + + ODESolverResultSet srs = null; + ColumnDescription[] cd = null; + + if (langevinSolverResultSet == null) { + model.addElement(""); + return; + } + switch (mode) { + case COUNTS: + // we may never get non-trivial clusters if there's no binding reaction + srs = langevinSolverResultSet.getClusterCounts(); + cd = srs.getColumnDescriptions(); + break; + case MEAN: + srs = langevinSolverResultSet.getClusterMean(); + cd = srs.getColumnDescriptions(); + break; + case OVERALL: + srs = langevinSolverResultSet.getClusterOverall(); + cd = srs.getColumnDescriptions(); + break; + } + if(cd == null || cd.length == 1) { + model.addElement(""); // if only "t" column is present, treat as no data + return; + } + for (ColumnDescription columnDescription : cd) { + if(columnDescription.getName().equals("t")) { + continue; // skip time column + } + model.addElement(columnDescription.getName()); + getYAxisChoice().setEnabled(true); + } + } + + public ClusterSpecificationPanel(ODEDataViewer odeDataViewer) { + super(); + this.owner = odeDataViewer; + initialize(); + } + + private void initialize() { + System.out.println("ClusterSpecificationPanel.initialize() called"); + setPreferredSize(new Dimension(213, 600)); + setLayout(new GridBagLayout()); + setSize(248, 604); + setMinimumSize(new Dimension(125, 300)); + + JLabel xAxisLabel = new JLabel("X Axis:"); + GridBagConstraints gbc = new GridBagConstraints(); + gbc.anchor = GridBagConstraints.WEST; + gbc.gridx = 0; gbc.gridy = 0; + gbc.insets = new Insets(4, 4, 0, 4); + add(xAxisLabel, gbc); + + JTextField xAxisTextBox = new JTextField("t"); + xAxisTextBox.setEnabled(false); + xAxisTextBox.setEditable(false); + gbc = new GridBagConstraints(); + gbc.gridx = 0; gbc.gridy = 1; + gbc.fill = GridBagConstraints.HORIZONTAL; + gbc.weightx = 1.0; + gbc.insets = new Insets(0, 4, 4, 4); + add(xAxisTextBox, gbc); + + JLabel yAxisLabel = new JLabel("Y Axis:"); + gbc = new GridBagConstraints(); + gbc.anchor = GridBagConstraints.WEST; + gbc.insets = new Insets(4, 4, 0, 4); + gbc.gridx = 0; gbc.gridy = 2; + add(yAxisLabel, gbc); + + gbc = new GridBagConstraints(); + gbc.fill = GridBagConstraints.BOTH; + gbc.insets = new Insets(4, 4, 5, 4); + gbc.gridx = 0; + gbc.gridy = 3; + add(getDisplayOptionsPanel(), gbc); + + gbc = new GridBagConstraints(); + gbc.gridx = 0; gbc.gridy = 4; + gbc.fill = GridBagConstraints.BOTH; + gbc.weightx = 1.0; + gbc.weighty = 1.0; + gbc.insets = new Insets(0, 4, 4, 4); + add(getJScrollPaneYAxis(), gbc); + + initConnections(); + } + + private CollapsiblePanel getDisplayOptionsPanel() { + if(displayOptionsCollapsiblePanel == null) { + displayOptionsCollapsiblePanel = new CollapsiblePanel("Display Options", true); + JPanel content = displayOptionsCollapsiblePanel.getContentPanel(); + content.setLayout(new GridBagLayout()); + + ButtonGroup group = new ButtonGroup(); + JRadioButton rbCounts = new JRadioButton("Cluster Counts"); + JRadioButton rbMean = new JRadioButton("Cluster Mean"); + JRadioButton rbOverall = new JRadioButton("Cluster Overall"); + + rbCounts.setActionCommand("COUNTS"); + rbMean.setActionCommand("MEAN"); + rbOverall.setActionCommand("OVERALL"); + + rbCounts.addActionListener(ivjEventHandler); + rbMean.addActionListener(ivjEventHandler); + rbOverall.addActionListener(ivjEventHandler); + + group.add(rbCounts); + group.add(rbMean); + group.add(rbOverall); + + rbCounts.setSelected(true); + + GridBagConstraints gbc = new GridBagConstraints(); + gbc.gridx = 0; + gbc.anchor = GridBagConstraints.WEST; + gbc.insets = new Insets(2, 4, 2, 4); + gbc.gridy = 0; + content.add(rbCounts, gbc); + gbc.gridy = 1; + content.add(rbMean, gbc); + gbc.gridy = 2; + content.add(rbOverall, gbc); + } + return displayOptionsCollapsiblePanel; + } + + private JScrollPane getJScrollPaneYAxis() { + if(jScrollPaneYAxis == null) { + jScrollPaneYAxis = new JScrollPane(); + jScrollPaneYAxis.setName("JScrollPaneYAxis"); + jScrollPaneYAxis.setViewportView(getYAxisChoice()); + } + return jScrollPaneYAxis; + } + private JList getYAxisChoice() { + if ((yAxisChoiceList == null)) { + yAxisChoiceList = new JList(); + yAxisChoiceList.setName("YAxisChoice"); + yAxisChoiceList.setBounds(0, 0, 160, 120); + yAxisChoiceList.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); + } + return yAxisChoiceList; + } + + private void initConnections() { + getDisplayOptionsPanel().addPropertyChangeListener(ivjEventHandler); + getYAxisChoice().addListSelectionListener(ivjEventHandler); + this.addPropertyChangeListener(ivjEventHandler); + getYAxisChoice().setModel(getDefaultListModelY()); + } + + private DefaultListModel getDefaultListModelY() { + if (defaultListModelY == null) { + defaultListModelY = new DefaultListModel(); + } + return defaultListModelY; + } + + private void handleException(java.lang.Throwable exception) { + System.out.println("--------- UNCAUGHT EXCEPTION ---------"); + exception.printStackTrace(System.out); + } + + @Override + protected void onSelectedObjectsChange(Object[] selectedObjects) { + System.out.println("ClusterSpecificationPanel.onSelectedObjectsChange() called with " + selectedObjects.length + " objects"); + } + + public void refreshData() { + simulationModelInfo = owner.getSimulationModelInfo(); + langevinSolverResultSet = owner.getLangevinSolverResultSet(); + System.out.println("ClusterSpecificationPanel.refreshData() called"); + + // find the selected radio button inside the collapsible panel + JPanel content = displayOptionsCollapsiblePanel.getContentPanel(); + for (Component c : content.getComponents()) { + if (c instanceof JRadioButton rb && rb.isSelected()) { + ivjEventHandler.actionPerformed(new ActionEvent(rb, ActionEvent.ACTION_PERFORMED, rb.getActionCommand())); + break; + } + } + } + +} diff --git a/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterVisualizationPanel.java b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterVisualizationPanel.java new file mode 100644 index 0000000000..22778bfd73 --- /dev/null +++ b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterVisualizationPanel.java @@ -0,0 +1,286 @@ +package cbit.vcell.solver.ode.gui; + +import cbit.vcell.client.data.ODEDataViewer; +import cbit.vcell.client.desktop.biomodel.DocumentEditorSubPanel; +import org.vcell.util.gui.JToolBarToggleButton; +import org.vcell.util.gui.VCellIcons; + +import javax.swing.*; +import javax.swing.border.EmptyBorder; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; +import javax.swing.event.ListSelectionEvent; +import javax.swing.event.ListSelectionListener; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; + + +public class ClusterVisualizationPanel extends DocumentEditorSubPanel { + + ODEDataViewer owner; + IvjEventHandler ivjEventHandler = new IvjEventHandler(); + + private JPanel ivjJPanel1 = null; + private JPanel ivjJPanelPlot = null; + private JPanel ivjPlot2DPanel1 = null; // here + private JLabel ivjJLabelBottom = null; + private JPanel ivjJPanelData = null; + private JPanel ivjPlot2DDataPanel1 = null; // here + private JPanel bottomRightPanel = null; + private JPanel ivjJPanelLegend = null; + private JScrollPane ivjPlotLegendsScrollPane = null; + private JPanel ivjJPanelPlotLegends = null; + private JLabel bottomLabel = null; + private JToolBarToggleButton ivjPlotButton = null; + private JToolBarToggleButton ivjDataButton = null; + + class IvjEventHandler implements ActionListener, PropertyChangeListener, ChangeListener, ListSelectionListener { + @Override + public void actionPerformed(ActionEvent e) { + System.out.println("ClusterVisualizationPanel.IvjEventHandler.actionPerformed() called"); + System.out.println(" Source: " + e.getSource() + ", Action Command: " + e.getActionCommand()); + Object source = e.getSource(); + String cmd = e.getActionCommand(); + + if (cmd.equals("JPanelPlot") || cmd.equals("JPanelData")) { + CardLayout cl = (CardLayout) ivjJPanel1.getLayout(); + cl.show(ivjJPanel1, cmd); // show the plot or the data panel + ivjPlotButton.setSelected(cmd.equals("JPanelPlot")); // update button selection state + ivjDataButton.setSelected(cmd.equals("JPanelData")); + ivjJPanelLegend.setVisible(cmd.equals("JPanelPlot")); // show legend only in plot mode + return; + } + } + @Override + public void propertyChange(PropertyChangeEvent evt) { + System.out.println("ClusterVisualizationPanel.IvjEventHandler.propertyChange() called"); + System.out.println(" Source: " + evt.getSource() + ", Property Name: " + evt.getPropertyName() + ", Old Value: " + evt.getOldValue() + ", New Value: " + evt.getNewValue()); + } + @Override + public void stateChanged(ChangeEvent e) { + System.out.println("ClusterVisualizationPanel.IvjEventHandler.stateChanged() called"); + System.out.println(" Source: " + e.getSource() + ", Class: " + e.getSource().getClass().getName()); + } + + @Override + public void valueChanged(ListSelectionEvent e) { + System.out.println("ClusterVisualizationPanel.IvjEventHandler.valueChanged() called"); + System.out.println(" Source: " + e.getSource() + ", Class: " + e.getSource().getClass().getName()); + Object source = e.getSource(); + + } + }; + + public ClusterVisualizationPanel(ODEDataViewer odeDataViewer) { + super(); + this.owner = odeDataViewer; + initialize(); + } + + private void initialize() { + setPreferredSize(new Dimension(420, 400)); + setLayout(new BorderLayout()); + setSize(513, 457); + add(getJPanel1(), "Center"); + add(getBottomRightPanel(), "South"); + add(getJPanelLegend(), "East"); + initConnectionsRight(); + } + + private JPanel getJPanel1() { + if (ivjJPanel1 == null) { + ivjJPanel1 = new JPanel(); + ivjJPanel1.setName("JPanel1"); + ivjJPanel1.setLayout(new CardLayout()); + ivjJPanel1.add(getJPanelPlot(), getJPanelPlot().getName()); + ivjJPanel1.add(getJPanelData(), getJPanelData().getName()); + } + return ivjJPanel1; + } + private JPanel getJPanelPlot() { + if (ivjJPanelPlot == null) { + ivjJPanelPlot = new JPanel(); + ivjJPanelPlot.setName("JPanelPlot"); + ivjJPanelPlot.setLayout(new BorderLayout()); + ivjJPanelPlot.add(getPlot2DPanel1(), "Center"); + ivjJPanelPlot.add(getJLabelBottom(), "South"); + } + return ivjJPanelPlot; + } + private JPanel getJPanelData() { + if (ivjJPanelData == null) { + ivjJPanelData = new JPanel(); + ivjJPanelData.setName("JPanelData"); + ivjJPanelData.setLayout(new BorderLayout()); + ivjJPanelData.add(getPlot2DDataPanel1(), "Center"); + } + return ivjJPanelData; + } + + private JLabel getJLabelBottom() { + if (ivjJLabelBottom == null) { + ivjJLabelBottom = new JLabel(); + ivjJLabelBottom.setName("JLabelBottom"); + ivjJLabelBottom.setText("t"); + ivjJLabelBottom.setForeground(Color.black); + ivjJLabelBottom.setHorizontalTextPosition(SwingConstants.CENTER); + ivjJLabelBottom.setHorizontalAlignment(SwingConstants.CENTER); + } + return ivjJLabelBottom; + } + private JPanel getPlot2DPanel1() { + if (ivjPlot2DPanel1 == null) { + try { + ivjPlot2DPanel1 = new JPanel(); + ivjPlot2DPanel1.setName("Plot2DPanel1"); + } catch (java.lang.Throwable ivjExc) { + handleException(ivjExc); + } + } + return ivjPlot2DPanel1; + } + public JPanel getPlot2DDataPanel1() { + if (ivjPlot2DDataPanel1 == null) { + try { + ivjPlot2DDataPanel1 = new JPanel(); + ivjPlot2DDataPanel1.setName("Plot2DDataPanel1"); + } catch (java.lang.Throwable ivjExc) { + handleException(ivjExc); + } + } + return ivjPlot2DDataPanel1; + } + + // --------------------------------------------------------------------- + private JPanel getBottomRightPanel() { + if (bottomRightPanel == null) { + bottomRightPanel = new JPanel(); + bottomRightPanel.setName("JPanelBottom"); + bottomRightPanel.setLayout(new GridBagLayout()); + + GridBagConstraints gbc = new GridBagConstraints(); + gbc.gridx = 1; gbc.gridy = 0; + gbc.fill = GridBagConstraints.HORIZONTAL; + gbc.weightx = 1.0; + gbc.insets = new Insets(4, 4, 4, 4); + bottomRightPanel.add(getJBottomLabel(), gbc); + + gbc = new GridBagConstraints(); + gbc.gridx = 2; gbc.gridy = 0; + gbc.insets = new Insets(4, 4, 4, 4); + bottomRightPanel.add(getPlotButton(), gbc); + + gbc = new GridBagConstraints(); + gbc.gridx = 3; gbc.gridy = 0; + gbc.insets = new Insets(4, 4, 4, 4); + bottomRightPanel.add(getDataButton(), gbc); + } + return bottomRightPanel; + } + private JLabel getJBottomLabel() { + if (bottomLabel == null) { + bottomLabel = new JLabel(); + bottomLabel.setName("JBottomLabel"); + bottomLabel.setText(" "); + bottomLabel.setForeground(Color.blue); + bottomLabel.setPreferredSize(new Dimension(44, 20)); + bottomLabel.setFont(new Font("dialog", 0, 12)); + bottomLabel.setMinimumSize(new Dimension(44, 20)); + } + return bottomLabel; + } + private JToolBarToggleButton getPlotButton() { + if (ivjPlotButton == null) { + ivjPlotButton = new JToolBarToggleButton(); + ivjPlotButton.setName("PlotButton"); + ivjPlotButton.setToolTipText("Show plot(s)"); + ivjPlotButton.setText(""); + ivjPlotButton.setMaximumSize(new Dimension(28, 28)); + ivjPlotButton.setActionCommand("JPanelPlot"); + ivjPlotButton.setSelected(true); + ivjPlotButton.setPreferredSize(new Dimension(28, 28)); + ivjPlotButton.setIcon(VCellIcons.dataExporterIcon); + ivjPlotButton.setMinimumSize(new Dimension(28, 28)); + } + return ivjPlotButton; + } + private JToolBarToggleButton getDataButton() { + if (ivjDataButton == null) { + ivjDataButton = new JToolBarToggleButton(); + ivjDataButton.setName("DataButton"); + ivjDataButton.setToolTipText("Show data"); + ivjDataButton.setText(""); + ivjDataButton.setMaximumSize(new Dimension(28, 28)); + ivjDataButton.setActionCommand("JPanelData"); + ivjDataButton.setIcon(VCellIcons.dataSetsIcon); + ivjDataButton.setPreferredSize(new Dimension(28, 28)); + ivjDataButton.setMinimumSize(new Dimension(28, 28)); + } + return ivjDataButton; + } + + private JPanel getJPanelLegend() { + if (ivjJPanelLegend == null) { + ivjJPanelLegend = new JPanel(); + ivjJPanelLegend.setName("JPanelLegend"); + ivjJPanelLegend.setLayout(new BorderLayout()); + getJPanelLegend().add(new JLabel(" "), "South"); + getJPanelLegend().add(new JLabel("Plot Legend:"), "North"); + getJPanelLegend().add(getPlotLegendsScrollPane(), "Center"); + } + return ivjJPanelLegend; + } + private JScrollPane getPlotLegendsScrollPane() { + if (ivjPlotLegendsScrollPane == null) { + ivjPlotLegendsScrollPane = new JScrollPane(); + ivjPlotLegendsScrollPane.setName("PlotLegendsScrollPane"); + ivjPlotLegendsScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED); + ivjPlotLegendsScrollPane.setBorder(new EmptyBorder(0, 0, 0, 0)); + getPlotLegendsScrollPane().setViewportView(getJPanelPlotLegends()); + } + return ivjPlotLegendsScrollPane; + } + private JPanel getJPanelPlotLegends() { + if (ivjJPanelPlotLegends == null) { + ivjJPanelPlotLegends = new JPanel(); + ivjJPanelPlotLegends.setName("JPanelPlotLegends"); + ivjJPanelPlotLegends.setLayout(new BoxLayout(ivjJPanelPlotLegends, BoxLayout.Y_AXIS)); + ivjJPanelPlotLegends.setBounds(0, 0, 72, 360); + } + return ivjJPanelPlotLegends; + } + + + private void initConnectionsRight() { + // Group the two buttons so only one stays selected + ButtonGroup bg = new ButtonGroup(); + bg.add(getPlotButton()); + bg.add(getDataButton()); + + // Add the shared handler + getPlotButton().addActionListener(ivjEventHandler); + getDataButton().addActionListener(ivjEventHandler); + } + + + private void handleException(java.lang.Throwable exception) { + System.out.println("--------- UNCAUGHT EXCEPTION ---------"); + exception.printStackTrace(System.out); + } + + @Override + protected void onSelectedObjectsChange(Object[] selectedObjects) { + System.out.println("ClusterVisualizationPanel.onSelectedObjectsChange() called with " + selectedObjects.length + " objects"); + } + + public void refreshData() { +// simulationModelInfo = owner.getSimulationModelInfo(); +// langevinSolverResultSet = owner.getLangevinSolverResultSet(); + System.out.println("ClusterVisualizationPanel.refreshData() called"); + + } + +} diff --git a/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/LangevinClustersResultsPanel.java b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/LangevinClustersResultsPanel.java index 5f34de9793..d52b29a746 100644 --- a/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/LangevinClustersResultsPanel.java +++ b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/LangevinClustersResultsPanel.java @@ -1,10 +1,19 @@ package cbit.vcell.solver.ode.gui; +import cbit.plot.gui.Plot2DDataPanel; +import cbit.plot.gui.Plot2DPanel; import cbit.vcell.client.data.ODEDataViewer; import cbit.vcell.client.desktop.biomodel.DocumentEditorSubPanel; +import cbit.vcell.simdata.LangevinSolverResultSet; +import cbit.vcell.solver.SimulationModelInfo; +import cbit.vcell.solver.ode.ODESolverResultSet; +import cbit.vcell.util.ColumnDescription; import org.vcell.util.gui.CollapsiblePanel; +import org.vcell.util.gui.JToolBarToggleButton; +import org.vcell.util.gui.VCellIcons; import javax.swing.*; +import javax.swing.border.EmptyBorder; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import javax.swing.event.ListSelectionEvent; @@ -14,6 +23,7 @@ import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; +@Deprecated public class LangevinClustersResultsPanel extends DocumentEditorSubPanel { private final ODEDataViewer owner; @@ -28,12 +38,58 @@ public class LangevinClustersResultsPanel extends DocumentEditorSubPanel { private JScrollPane jScrollPaneYAxis = null; private JList yAxisChoiceList = null; + private JPanel ivjJPanel1 = null; + private JPanel ivjJPanelPlot = null; + private JPanel ivjPlot2DPanel1 = null; // here + private JLabel ivjJLabelBottom = null; + private JPanel ivjJPanelData = null; + private JPanel ivjPlot2DDataPanel1 = null; // here + private JPanel bottomRightPanel = null; + private JPanel ivjJPanelLegend = null; + private JScrollPane ivjPlotLegendsScrollPane = null; + private JPanel ivjJPanelPlotLegends = null; + private JLabel bottomLabel = null; + private JToolBarToggleButton ivjPlotButton = null; + private JToolBarToggleButton ivjDataButton = null; + + + private enum DisplayMode { COUNTS, MEAN, OVERALL } + LangevinSolverResultSet langevinSolverResultSet = null; + SimulationModelInfo simulationModelInfo = null; + class IvjEventHandler implements java.awt.event.ActionListener, PropertyChangeListener, ChangeListener, ListSelectionListener { @Override public void actionPerformed(ActionEvent e) { System.out.println("LangevinClustersResultsPanel.IvjEventHandler.actionPerformed() called"); System.out.println(" Source: " + e.getSource() + ", Action Command: " + e.getActionCommand()); + Object source = e.getSource(); + String cmd = e.getActionCommand(); + + if (cmd.equals("JPanelPlot") || cmd.equals("JPanelData")) { + CardLayout cl = (CardLayout) ivjJPanel1.getLayout(); + cl.show(ivjJPanel1, cmd); // show the plot or the data panel + ivjPlotButton.setSelected(cmd.equals("JPanelPlot")); // update button selection state + ivjDataButton.setSelected(cmd.equals("JPanelData")); + ivjJPanelLegend.setVisible(cmd.equals("JPanelPlot")); // show legend only in plot mode + return; + } + + if (source instanceof JRadioButton) { + JRadioButton rb = (JRadioButton) source; + System.out.println(" Source is JRadioButton with text: " + rb.getText()); + switch (cmd) { + case "COUNTS": + populateYAxisChoices(DisplayMode.COUNTS); + break; + case "MEAN": + populateYAxisChoices(DisplayMode.MEAN); + break; + case "OVERALL": + populateYAxisChoices(DisplayMode.OVERALL); + break; + } + } } @Override public void propertyChange(PropertyChangeEvent evt) { @@ -50,13 +106,55 @@ public void stateChanged(ChangeEvent e) { public void valueChanged(ListSelectionEvent e) { System.out.println("LangevinClustersResultsPanel.IvjEventHandler.valueChanged() called"); System.out.println(" Source: " + e.getSource() + ", Class: " + e.getSource().getClass().getName()); + Object source = e.getSource(); + } }; - public LangevinClustersResultsPanel(ODEDataViewer odeDataViewer) { - super(); - this.owner = odeDataViewer; - initialize(); + private void populateYAxisChoices(DisplayMode mode) { + DefaultListModel model = getDefaultListModelY(); + model.clear(); + getYAxisChoice().setEnabled(false); + + ODESolverResultSet srs = null; + ColumnDescription[] cd = null; + + if (langevinSolverResultSet == null) { + model.addElement(""); + return; + } + switch (mode) { + case COUNTS: + // we may never get non-trivial clusters if there's no binding reaction + srs = langevinSolverResultSet.getClusterCounts(); + cd = srs.getColumnDescriptions(); + break; + case MEAN: + srs = langevinSolverResultSet.getClusterMean(); + cd = srs.getColumnDescriptions(); + break; + case OVERALL: + srs = langevinSolverResultSet.getClusterOverall(); + cd = srs.getColumnDescriptions(); + break; + } + if(cd == null || cd.length == 1) { + model.addElement(""); // if only "t" column is present, treat as no data + return; + } + for (ColumnDescription columnDescription : cd) { + if(columnDescription.getName().equals("t")) { + continue; // skip time column + } + model.addElement(columnDescription.getName()); + getYAxisChoice().setEnabled(true); + } + } + + public LangevinClustersResultsPanel(ODEDataViewer odeDataViewer) { + super(); + this.owner = odeDataViewer; + initialize(); } private void initialize() { @@ -115,7 +213,7 @@ private JPanel getClusterSpecificationPanel() { constraintsJScrollPaneYAxis.insets = new Insets(0, 4, 4, 4); clusterSpecificationPanel.add(getJScrollPaneYAxis(), constraintsJScrollPaneYAxis); - initConnections(); + initConnectionsLeft(); } return clusterSpecificationPanel; } @@ -130,6 +228,15 @@ private CollapsiblePanel getDisplayOptionsPanel() { JRadioButton rbCounts = new JRadioButton("Cluster Counts"); JRadioButton rbMean = new JRadioButton("Cluster Mean"); JRadioButton rbOverall = new JRadioButton("Cluster Overall"); + + rbCounts.setActionCommand("COUNTS"); + rbMean.setActionCommand("MEAN"); + rbOverall.setActionCommand("OVERALL"); + + rbCounts.addActionListener(ivjEventHandler); + rbMean.addActionListener(ivjEventHandler); + rbOverall.addActionListener(ivjEventHandler); + group.add(rbCounts); group.add(rbMean); group.add(rbOverall); @@ -168,7 +275,7 @@ private JList getYAxisChoice() { return yAxisChoiceList; } - private void initConnections() { + private void initConnectionsLeft() { getDisplayOptionsPanel().addPropertyChangeListener(ivjEventHandler); getYAxisChoice().addListSelectionListener(ivjEventHandler); @@ -189,10 +296,200 @@ private JPanel getClusterVisualizationPanel() { if (clusterVisualizationPanel == null) { clusterVisualizationPanel = new JPanel(); clusterVisualizationPanel.setName("ClusterVisualizationPanel"); + clusterVisualizationPanel.setPreferredSize(new java.awt.Dimension(420, 400)); + clusterVisualizationPanel.setLayout(new BorderLayout()); + clusterVisualizationPanel.setSize(513, 457); + clusterVisualizationPanel.add(getJPanel1(), "Center"); + clusterVisualizationPanel.add(getBottomRightPanel(), "South"); + clusterVisualizationPanel.add(getJPanelLegend(), "East"); + initConnectionsRight(); + } return clusterVisualizationPanel; } + private JPanel getJPanel1() { + if (ivjJPanel1 == null) { + ivjJPanel1 = new JPanel(); + ivjJPanel1.setName("JPanel1"); + ivjJPanel1.setLayout(new CardLayout()); + ivjJPanel1.add(getJPanelPlot(), getJPanelPlot().getName()); + ivjJPanel1.add(getJPanelData(), getJPanelData().getName()); + } + return ivjJPanel1; + } + private javax.swing.JPanel getJPanelPlot() { + if (ivjJPanelPlot == null) { + ivjJPanelPlot = new javax.swing.JPanel(); + ivjJPanelPlot.setName("JPanelPlot"); + ivjJPanelPlot.setLayout(new java.awt.BorderLayout()); + ivjJPanelPlot.add(getPlot2DPanel1(), "Center"); + ivjJPanelPlot.add(getJLabelBottom(), "South"); + } + return ivjJPanelPlot; + } + private javax.swing.JPanel getJPanelData() { + if (ivjJPanelData == null) { + ivjJPanelData = new javax.swing.JPanel(); + ivjJPanelData.setName("JPanelData"); + ivjJPanelData.setLayout(new java.awt.BorderLayout()); + ivjJPanelData.add(getPlot2DDataPanel1(), "Center"); + } + return ivjJPanelData; + } + + private javax.swing.JLabel getJLabelBottom() { + if (ivjJLabelBottom == null) { + ivjJLabelBottom = new javax.swing.JLabel(); + ivjJLabelBottom.setName("JLabelBottom"); + ivjJLabelBottom.setText("t"); + ivjJLabelBottom.setForeground(java.awt.Color.black); + ivjJLabelBottom.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER); + ivjJLabelBottom.setHorizontalAlignment(javax.swing.SwingConstants.CENTER); + } + return ivjJLabelBottom; + } + private JPanel getPlot2DPanel1() { + if (ivjPlot2DPanel1 == null) { + try { + ivjPlot2DPanel1 = new JPanel(); + ivjPlot2DPanel1.setName("Plot2DPanel1"); + } catch (java.lang.Throwable ivjExc) { + handleException(ivjExc); + } + } + return ivjPlot2DPanel1; + } + public JPanel getPlot2DDataPanel1() { + if (ivjPlot2DDataPanel1 == null) { + try { + ivjPlot2DDataPanel1 = new JPanel(); + ivjPlot2DDataPanel1.setName("Plot2DDataPanel1"); + } catch (java.lang.Throwable ivjExc) { + handleException(ivjExc); + } + } + return ivjPlot2DDataPanel1; + } + + + // --------------------------------------------------------------------- + private JPanel getBottomRightPanel() { + if (bottomRightPanel == null) { + bottomRightPanel = new JPanel(); + bottomRightPanel.setName("JPanelBottom"); + bottomRightPanel.setLayout(new GridBagLayout()); + + java.awt.GridBagConstraints gbc = new GridBagConstraints(); + gbc.gridx = 1; gbc.gridy = 0; + gbc.fill = GridBagConstraints.HORIZONTAL; + gbc.weightx = 1.0; + gbc.insets = new java.awt.Insets(4, 4, 4, 4); + bottomRightPanel.add(getJBottomLabel(), gbc); + + gbc = new GridBagConstraints(); + gbc.gridx = 2; gbc.gridy = 0; + gbc.insets = new Insets(4, 4, 4, 4); + bottomRightPanel.add(getPlotButton(), gbc); + + gbc = new GridBagConstraints(); + gbc.gridx = 3; gbc.gridy = 0; + gbc.insets = new Insets(4, 4, 4, 4); + bottomRightPanel.add(getDataButton(), gbc); + } + return bottomRightPanel; + } + private JLabel getJBottomLabel() { + if (bottomLabel == null) { + bottomLabel = new JLabel(); + bottomLabel.setName("JBottomLabel"); + bottomLabel.setText(" "); + bottomLabel.setForeground(Color.blue); + bottomLabel.setPreferredSize(new Dimension(44, 20)); + bottomLabel.setFont(new Font("dialog", 0, 12)); + bottomLabel.setMinimumSize(new Dimension(44, 20)); + } + return bottomLabel; + } + private JToolBarToggleButton getPlotButton() { + if (ivjPlotButton == null) { + ivjPlotButton = new JToolBarToggleButton(); + ivjPlotButton.setName("PlotButton"); + ivjPlotButton.setToolTipText("Show plot(s)"); + ivjPlotButton.setText(""); + ivjPlotButton.setMaximumSize(new Dimension(28, 28)); + ivjPlotButton.setActionCommand("JPanelPlot"); + ivjPlotButton.setSelected(true); + ivjPlotButton.setPreferredSize(new Dimension(28, 28)); + ivjPlotButton.setIcon(VCellIcons.dataExporterIcon); + ivjPlotButton.setMinimumSize(new Dimension(28, 28)); + } + return ivjPlotButton; + } + private JToolBarToggleButton getDataButton() { + if (ivjDataButton == null) { + ivjDataButton = new JToolBarToggleButton(); + ivjDataButton.setName("DataButton"); + ivjDataButton.setToolTipText("Show data"); + ivjDataButton.setText(""); + ivjDataButton.setMaximumSize(new Dimension(28, 28)); + ivjDataButton.setActionCommand("JPanelData"); + ivjDataButton.setIcon(VCellIcons.dataSetsIcon); + ivjDataButton.setPreferredSize(new Dimension(28, 28)); + ivjDataButton.setMinimumSize(new Dimension(28, 28)); + } + return ivjDataButton; + } + + private javax.swing.JPanel getJPanelLegend() { + if (ivjJPanelLegend == null) { + ivjJPanelLegend = new javax.swing.JPanel(); + ivjJPanelLegend.setName("JPanelLegend"); + ivjJPanelLegend.setLayout(new java.awt.BorderLayout()); + getJPanelLegend().add(new JLabel(" "), "South"); + getJPanelLegend().add(new JLabel("Plot Legend:"), "North"); + getJPanelLegend().add(getPlotLegendsScrollPane(), "Center"); + } + return ivjJPanelLegend; + } + private JScrollPane getPlotLegendsScrollPane() { + if (ivjPlotLegendsScrollPane == null) { + ivjPlotLegendsScrollPane = new javax.swing.JScrollPane(); + ivjPlotLegendsScrollPane.setName("PlotLegendsScrollPane"); + ivjPlotLegendsScrollPane.setHorizontalScrollBarPolicy(javax.swing.JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED); + ivjPlotLegendsScrollPane.setBorder(new EmptyBorder(0, 0, 0, 0)); + getPlotLegendsScrollPane().setViewportView(getJPanelPlotLegends()); + } + return ivjPlotLegendsScrollPane; + } + private JPanel getJPanelPlotLegends() { + if (ivjJPanelPlotLegends == null) { + ivjJPanelPlotLegends = new javax.swing.JPanel(); + ivjJPanelPlotLegends.setName("JPanelPlotLegends"); + ivjJPanelPlotLegends.setLayout(new BoxLayout(ivjJPanelPlotLegends, javax.swing.BoxLayout.Y_AXIS)); + ivjJPanelPlotLegends.setBounds(0, 0, 72, 360); + } + return ivjJPanelPlotLegends; + } + + + private void initConnectionsRight() { + // Group the two buttons so only one stays selected + ButtonGroup bg = new ButtonGroup(); + bg.add(getPlotButton()); + bg.add(getDataButton()); + + // Add the shared handler + getPlotButton().addActionListener(ivjEventHandler); + getDataButton().addActionListener(ivjEventHandler); + } + + // ================================================================================================= + + private void handleException(java.lang.Throwable exception) { + System.out.println("--------- UNCAUGHT EXCEPTION ---------"); + exception.printStackTrace(System.out); + } @Override protected void onSelectedObjectsChange(Object[] selectedObjects) { @@ -200,7 +497,20 @@ protected void onSelectedObjectsChange(Object[] selectedObjects) { } public void refreshData() { - System.out.println("LangevinClustersResultsPanel.refreshData() called"); + simulationModelInfo = owner.getSimulationModelInfo(); + langevinSolverResultSet = owner.getLangevinSolverResultSet(); + System.out.println("LangevinClustersResultsPanel.refreshData() called"); + + // find the selected radio button inside the collapsible panel + JPanel content = displayOptionsCollapsiblePanel.getContentPanel(); + for (Component c : content.getComponents()) { + if (c instanceof JRadioButton rb && rb.isSelected()) { + ivjEventHandler.actionPerformed(new ActionEvent(rb, ActionEvent.ACTION_PERFORMED, rb.getActionCommand())); + break; + } + } + } + } From 41840eeeeb5bf7abda51b9e3e8cafe96cb91a896 Mon Sep 17 00:00:00 2001 From: Dan Vasilescu Date: Wed, 4 Mar 2026 18:00:11 -0500 Subject: [PATCH 05/31] decoupling the pannels, one way communication between panels --- .../ode/gui/ClusterSpecificationPanel.java | 112 ++++++++++++------ .../ode/gui/ClusterVisualizationPanel.java | 68 +++++++---- 2 files changed, 123 insertions(+), 57 deletions(-) diff --git a/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterSpecificationPanel.java b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterSpecificationPanel.java index 29a70c9d2f..6094199885 100644 --- a/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterSpecificationPanel.java +++ b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterSpecificationPanel.java @@ -22,32 +22,24 @@ public class ClusterSpecificationPanel extends DocumentEditorSubPanel { - private final ODEDataViewer owner; - ClusterSpecificationPanel.IvjEventHandler ivjEventHandler = new ClusterSpecificationPanel.IvjEventHandler(); - - private DefaultListModel defaultListModelY = null; - - private CollapsiblePanel displayOptionsCollapsiblePanel = null; - private JScrollPane jScrollPaneYAxis = null; - private JList yAxisChoiceList = null; - - private enum DisplayMode { COUNTS, MEAN, OVERALL }; - LangevinSolverResultSet langevinSolverResultSet = null; - SimulationModelInfo simulationModelInfo = null; - + public static class ClusterSelection { // used to communicate y-list selection to the ClusterVisualizationPanel + public final DisplayMode mode; + public final java.util.List columns; + public final ODESolverResultSet resultSet; + public ClusterSelection(DisplayMode mode, java.util.List columns, ODESolverResultSet resultSet) { + this.mode = mode; + this.columns = columns; + this.resultSet = resultSet; + } + } - class IvjEventHandler implements ActionListener, PropertyChangeListener, ChangeListener, ListSelectionListener { + class IvjEventHandler implements ActionListener, PropertyChangeListener, ListSelectionListener { @Override public void actionPerformed(ActionEvent e) { - System.out.println("ClusterSpecificationPanel.IvjEventHandler.actionPerformed() called"); - System.out.println(" Source: " + e.getSource() + ", Action Command: " + e.getActionCommand()); - Object source = e.getSource(); String cmd = e.getActionCommand(); - - if (source instanceof JRadioButton) { - JRadioButton rb = (JRadioButton) source; - System.out.println(" Source is JRadioButton with text: " + rb.getText()); + if (e.getSource() instanceof JRadioButton rb && SwingUtilities.isDescendingFrom(rb, ClusterSpecificationPanel.this)) { + System.out.println("ClusterSpecificationPanel.actionPerformed() called. Source is JRadioButton: " + rb.getText()); switch (cmd) { case "COUNTS": populateYAxisChoices(DisplayMode.COUNTS); @@ -63,24 +55,60 @@ public void actionPerformed(ActionEvent e) { } @Override public void propertyChange(PropertyChangeEvent evt) { - System.out.println("ClusterSpecificationPanel.IvjEventHandler.propertyChange() called"); - System.out.println(" Source: " + evt.getSource() + ", Property Name: " + evt.getPropertyName() + ", Old Value: " + evt.getOldValue() + ", New Value: " + evt.getNewValue()); - } - @Override - public void stateChanged(ChangeEvent e) { - System.out.println("ClusterSpecificationPanel.IvjEventHandler.stateChanged() called"); - System.out.println(" Source: " + e.getSource() + ", Class: " + e.getSource().getClass().getName()); + if(evt.getSource() == ClusterSpecificationPanel.this) { + System.out.println("ClusterSpecificationPanel.IvjEventHandler.propertyChange() called"); + } } - @Override public void valueChanged(ListSelectionEvent e) { - System.out.println("ClusterSpecificationPanel.IvjEventHandler.valueChanged() called"); - System.out.println(" Source: " + e.getSource() + ", Class: " + e.getSource().getClass().getName()); - Object source = e.getSource(); - + if (e.getSource() == ClusterSpecificationPanel.this.getYAxisChoice() && !e.getValueIsAdjusting()) { + // extract selected ColumnDescriptions + java.util.List selected = (java.util.List) yAxisChoiceList.getSelectedValuesList(); + DisplayMode mode = null; + JPanel content = displayOptionsCollapsiblePanel.getContentPanel(); + for (Component c : content.getComponents()) { + if (c instanceof JRadioButton rb && rb.isSelected()) { + switch (rb.getActionCommand()) { + case "COUNTS": + mode = DisplayMode.COUNTS; + break; + case "MEAN": + mode = DisplayMode.MEAN; + break; + case "OVERALL": + mode = DisplayMode.OVERALL; + break; + } + } + } + ODESolverResultSet srs = null; + switch (mode) { + case COUNTS: + srs = langevinSolverResultSet.getClusterCounts(); + break; + case MEAN: + srs = langevinSolverResultSet.getClusterMean(); + break; + case OVERALL: + srs = langevinSolverResultSet.getClusterOverall(); + break; + } + // fire the event upward + firePropertyChange("ClusterSelection", null, new ClusterSelection(mode, selected, srs)); + } } }; + private final ODEDataViewer owner; LangevinSolverResultSet langevinSolverResultSet = null; + SimulationModelInfo simulationModelInfo = null; + ClusterSpecificationPanel.IvjEventHandler ivjEventHandler = new ClusterSpecificationPanel.IvjEventHandler(); + + private CollapsiblePanel displayOptionsCollapsiblePanel = null; + private JScrollPane jScrollPaneYAxis = null; + private JList yAxisChoiceList = null; + private DefaultListModel defaultListModelY = null; + + private void populateYAxisChoices(DisplayMode mode) { DefaultListModel model = getDefaultListModelY(); model.clear(); @@ -108,6 +136,7 @@ private void populateYAxisChoices(DisplayMode mode) { cd = srs.getColumnDescriptions(); break; } + if(cd == null || cd.length == 1) { model.addElement(""); // if only "t" column is present, treat as no data return; @@ -116,7 +145,7 @@ private void populateYAxisChoices(DisplayMode mode) { if(columnDescription.getName().equals("t")) { continue; // skip time column } - model.addElement(columnDescription.getName()); + model.addElement(columnDescription); getYAxisChoice().setEnabled(true); } } @@ -229,6 +258,21 @@ private JList getYAxisChoice() { yAxisChoiceList.setName("YAxisChoice"); yAxisChoiceList.setBounds(0, 0, 160, 120); yAxisChoiceList.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); + yAxisChoiceList.setCellRenderer(new DefaultListCellRenderer() { + @Override + public Component getListCellRendererComponent( + JList list, Object value, int index, + boolean isSelected, boolean cellHasFocus) { + + JLabel label = (JLabel) super.getListCellRendererComponent( + list, value, index, isSelected, cellHasFocus); + + ColumnDescription cd = (ColumnDescription) value; + label.setText(cd.getName()); // later: cd.getName() + " (molecules)" + + return label; + } + }); } return yAxisChoiceList; } diff --git a/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterVisualizationPanel.java b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterVisualizationPanel.java index 22778bfd73..dd59540030 100644 --- a/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterVisualizationPanel.java +++ b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterVisualizationPanel.java @@ -2,6 +2,7 @@ import cbit.vcell.client.data.ODEDataViewer; import cbit.vcell.client.desktop.biomodel.DocumentEditorSubPanel; +import cbit.vcell.util.ColumnDescription; import org.vcell.util.gui.JToolBarToggleButton; import org.vcell.util.gui.VCellIcons; @@ -40,37 +41,39 @@ public class ClusterVisualizationPanel extends DocumentEditorSubPanel { class IvjEventHandler implements ActionListener, PropertyChangeListener, ChangeListener, ListSelectionListener { @Override public void actionPerformed(ActionEvent e) { - System.out.println("ClusterVisualizationPanel.IvjEventHandler.actionPerformed() called"); - System.out.println(" Source: " + e.getSource() + ", Action Command: " + e.getActionCommand()); - Object source = e.getSource(); - String cmd = e.getActionCommand(); - - if (cmd.equals("JPanelPlot") || cmd.equals("JPanelData")) { - CardLayout cl = (CardLayout) ivjJPanel1.getLayout(); - cl.show(ivjJPanel1, cmd); // show the plot or the data panel - ivjPlotButton.setSelected(cmd.equals("JPanelPlot")); // update button selection state - ivjDataButton.setSelected(cmd.equals("JPanelData")); - ivjJPanelLegend.setVisible(cmd.equals("JPanelPlot")); // show legend only in plot mode - return; + if (e.getSource() instanceof Component c && SwingUtilities.isDescendingFrom(c, ClusterVisualizationPanel.this)) { + String cmd = e.getActionCommand(); + if (cmd.equals("JPanelPlot") || cmd.equals("JPanelData")) { + CardLayout cl = (CardLayout) ivjJPanel1.getLayout(); + cl.show(ivjJPanel1, cmd); // show the plot or the data panel + ivjPlotButton.setSelected(cmd.equals("JPanelPlot")); // update button selection state + ivjDataButton.setSelected(cmd.equals("JPanelData")); + ivjJPanelLegend.setVisible(cmd.equals("JPanelPlot")); // show legend only in plot mode + return; + } } } @Override public void propertyChange(PropertyChangeEvent evt) { - System.out.println("ClusterVisualizationPanel.IvjEventHandler.propertyChange() called"); - System.out.println(" Source: " + evt.getSource() + ", Property Name: " + evt.getPropertyName() + ", Old Value: " + evt.getOldValue() + ", New Value: " + evt.getNewValue()); + if (evt.getSource() == owner.getClusterSpecificationPanel() && "ClusterSelection".equals(evt.getPropertyName())) { + ClusterSpecificationPanel.ClusterSelection sel = (ClusterSpecificationPanel.ClusterSelection) evt.getNewValue(); + updateLegend(sel); // update legend (one plot, multiple curves) + redrawPlot(sel); // redraw plot (one plot, multiple curves) + updateDataTable(sel); // update data table + return; + } } @Override public void stateChanged(ChangeEvent e) { - System.out.println("ClusterVisualizationPanel.IvjEventHandler.stateChanged() called"); - System.out.println(" Source: " + e.getSource() + ", Class: " + e.getSource().getClass().getName()); + if (e.getSource() instanceof Component c && SwingUtilities.isDescendingFrom(c, ClusterVisualizationPanel.this)) { + System.out.println("ClusterVisualizationPanel.IvjEventHandler.stateChanged() called"); + } } - @Override public void valueChanged(ListSelectionEvent e) { - System.out.println("ClusterVisualizationPanel.IvjEventHandler.valueChanged() called"); - System.out.println(" Source: " + e.getSource() + ", Class: " + e.getSource().getClass().getName()); - Object source = e.getSource(); - + if (e.getSource() instanceof Component c && SwingUtilities.isDescendingFrom(c, ClusterVisualizationPanel.this)) { + System.out.println("ClusterVisualizationPanel.IvjEventHandler.valueChanged() called"); + } } }; @@ -255,14 +258,17 @@ private JPanel getJPanelPlotLegends() { private void initConnectionsRight() { - // Group the two buttons so only one stays selected + // group the two buttons so only one stays selected ButtonGroup bg = new ButtonGroup(); bg.add(getPlotButton()); bg.add(getDataButton()); - // Add the shared handler + // add the shared handler getPlotButton().addActionListener(ivjEventHandler); getDataButton().addActionListener(ivjEventHandler); + + // listen to the left panel + owner.getClusterSpecificationPanel().addPropertyChangeListener(ivjEventHandler); } @@ -280,7 +286,23 @@ public void refreshData() { // simulationModelInfo = owner.getSimulationModelInfo(); // langevinSolverResultSet = owner.getLangevinSolverResultSet(); System.out.println("ClusterVisualizationPanel.refreshData() called"); + } + + // --------------------------------------------------------------------- + private void updateLegend(ClusterSpecificationPanel.ClusterSelection sel) { + System.out.println("ClusterVisualizationPanel.updateLegend() called"); } + private void redrawPlot(ClusterSpecificationPanel.ClusterSelection sel) { + System.out.println("ClusterVisualizationPanel.redrawPlot() called"); + java.util.List columnDescriptions = sel.columns; + for(ColumnDescription cd : columnDescriptions) { + System.out.println(" column name: '" + cd.getName() + "'"); + } + } + private void updateDataTable(ClusterSpecificationPanel.ClusterSelection sel) { + System.out.println("ClusterVisualizationPanel.updateDataTable() called"); + } + } From f1b028d2a62624606f3d82e79aad5e3369ff5867 Mon Sep 17 00:00:00 2001 From: Dan Vasilescu Date: Fri, 6 Mar 2026 17:41:23 -0500 Subject: [PATCH 06/31] graceful behavior if no data --- .../ode/gui/ClusterSpecificationPanel.java | 55 +++++++++++++++---- 1 file changed, 45 insertions(+), 10 deletions(-) diff --git a/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterSpecificationPanel.java b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterSpecificationPanel.java index 6094199885..80f8c08567 100644 --- a/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterSpecificationPanel.java +++ b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterSpecificationPanel.java @@ -19,6 +19,8 @@ import java.awt.event.ActionListener; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; +import java.util.LinkedHashMap; +import java.util.Map; public class ClusterSpecificationPanel extends DocumentEditorSubPanel { @@ -99,12 +101,16 @@ public void valueChanged(ListSelectionEvent e) { } }; - private final ODEDataViewer owner; LangevinSolverResultSet langevinSolverResultSet = null; + private final ODEDataViewer owner; + LangevinSolverResultSet langevinSolverResultSet = null; SimulationModelInfo simulationModelInfo = null; + private final Map yAxisCounts = new LinkedHashMap<>(); ClusterSpecificationPanel.IvjEventHandler ivjEventHandler = new ClusterSpecificationPanel.IvjEventHandler(); private CollapsiblePanel displayOptionsCollapsiblePanel = null; private JScrollPane jScrollPaneYAxis = null; + private static final String YAxisLabelText = "Y Axis: "; + private JLabel yAxisLabel = null; private JList yAxisChoiceList = null; private DefaultListModel defaultListModelY = null; @@ -117,8 +123,9 @@ private void populateYAxisChoices(DisplayMode mode) { ODESolverResultSet srs = null; ColumnDescription[] cd = null; + updateYAxisLabel(mode); + if (langevinSolverResultSet == null) { - model.addElement(""); return; } switch (mode) { @@ -138,7 +145,6 @@ private void populateYAxisChoices(DisplayMode mode) { } if(cd == null || cd.length == 1) { - model.addElement(""); // if only "t" column is present, treat as no data return; } for (ColumnDescription columnDescription : cd) { @@ -149,6 +155,11 @@ private void populateYAxisChoices(DisplayMode mode) { getYAxisChoice().setEnabled(true); } } + private void updateYAxisLabel(DisplayMode mode) { + int count = yAxisCounts.getOrDefault(mode, 0); + String text = "" + YAxisLabelText + "(" + count + " entries)"; + yAxisLabel.setText(text); + } public ClusterSpecificationPanel(ODEDataViewer odeDataViewer) { super(); @@ -163,7 +174,7 @@ private void initialize() { setSize(248, 604); setMinimumSize(new Dimension(125, 300)); - JLabel xAxisLabel = new JLabel("X Axis:"); + JLabel xAxisLabel = new JLabel("X Axis: "); // non-breaking space is   GridBagConstraints gbc = new GridBagConstraints(); gbc.anchor = GridBagConstraints.WEST; gbc.gridx = 0; gbc.gridy = 0; @@ -180,12 +191,11 @@ private void initialize() { gbc.insets = new Insets(0, 4, 4, 4); add(xAxisTextBox, gbc); - JLabel yAxisLabel = new JLabel("Y Axis:"); gbc = new GridBagConstraints(); gbc.anchor = GridBagConstraints.WEST; gbc.insets = new Insets(4, 4, 0, 4); gbc.gridx = 0; gbc.gridy = 2; - add(yAxisLabel, gbc); + add(getYAxisLabel(), gbc); gbc = new GridBagConstraints(); gbc.fill = GridBagConstraints.BOTH; @@ -228,7 +238,7 @@ private CollapsiblePanel getDisplayOptionsPanel() { group.add(rbMean); group.add(rbOverall); - rbCounts.setSelected(true); + rbCounts.setSelected(true); // select the first option by default, which will populate the y-axis choices GridBagConstraints gbc = new GridBagConstraints(); gbc.gridx = 0; @@ -249,9 +259,21 @@ private JScrollPane getJScrollPaneYAxis() { jScrollPaneYAxis = new JScrollPane(); jScrollPaneYAxis.setName("JScrollPaneYAxis"); jScrollPaneYAxis.setViewportView(getYAxisChoice()); + // prevent collapse when list is empty + jScrollPaneYAxis.setMinimumSize(new Dimension(100, 120)); + jScrollPaneYAxis.setPreferredSize(new Dimension(100, 120)); } return jScrollPaneYAxis; } + private JLabel getYAxisLabel() { + if (yAxisLabel == null) { + yAxisLabel = new JLabel(); + yAxisLabel.setName("YAxisLabel"); + String text = "" + YAxisLabelText + ""; + yAxisLabel.setText(text); + } + return yAxisLabel; + } private JList getYAxisChoice() { if ((yAxisChoiceList == null)) { yAxisChoiceList = new JList(); @@ -284,9 +306,9 @@ private void initConnections() { getYAxisChoice().setModel(getDefaultListModelY()); } - private DefaultListModel getDefaultListModelY() { + private DefaultListModel getDefaultListModelY() { if (defaultListModelY == null) { - defaultListModelY = new DefaultListModel(); + defaultListModelY = new DefaultListModel<>(); } return defaultListModelY; } @@ -304,9 +326,16 @@ protected void onSelectedObjectsChange(Object[] selectedObjects) { public void refreshData() { simulationModelInfo = owner.getSimulationModelInfo(); langevinSolverResultSet = owner.getLangevinSolverResultSet(); + yAxisCounts.clear(); + if (langevinSolverResultSet != null) { + yAxisCounts.put(DisplayMode.COUNTS, countColumns(langevinSolverResultSet.getClusterCounts())); + yAxisCounts.put(DisplayMode.MEAN, countColumns(langevinSolverResultSet.getClusterMean())); + yAxisCounts.put(DisplayMode.OVERALL, countColumns(langevinSolverResultSet.getClusterOverall())); + } System.out.println("ClusterSpecificationPanel.refreshData() called"); - // find the selected radio button inside the collapsible panel + // find the selected radio button inside the collapsible panel and fire event as if it were just selected by mouse click + // which will populate the y-axis choices based on the new data JPanel content = displayOptionsCollapsiblePanel.getContentPanel(); for (Component c : content.getComponents()) { if (c instanceof JRadioButton rb && rb.isSelected()) { @@ -315,5 +344,11 @@ public void refreshData() { } } } + private int countColumns(ODESolverResultSet srs) { + if (srs == null) return 0; + ColumnDescription[] cds = srs.getColumnDescriptions(); + if (cds == null) return 0; + return cds.length > 1 ? cds.length-1 : 0; // subtract one for time column, but don't return negative if no column at all + } } From 1b3548dddf86dab387e4b63bf6bbcd9a26da0ba5 Mon Sep 17 00:00:00 2001 From: Dan Vasilescu Date: Mon, 9 Mar 2026 18:55:16 -0400 Subject: [PATCH 07/31] standard color tables - tableau20, color blind, etc --- .../main/java/org/vcell/util/ColorUtil.java | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/vcell-util/src/main/java/org/vcell/util/ColorUtil.java b/vcell-util/src/main/java/org/vcell/util/ColorUtil.java index ca87a95aee..ab911ac441 100644 --- a/vcell-util/src/main/java/org/vcell/util/ColorUtil.java +++ b/vcell-util/src/main/java/org/vcell/util/ColorUtil.java @@ -124,4 +124,73 @@ public static int calcBrightness(int red,int grn,int blu){ return (red*299+grn*587+blu*114)/1000; } + public static final Color[] TABLEAU20 = { + new Color(31,119,180), // deep blue + new Color(255,127,14), // orange + new Color(44,160,44), // green + new Color(214,39,40), // red + new Color(148,103,189), // purple + new Color(140,86,75), // brown + new Color(227,119,194), // pink + new Color(127,127,127), // gray + new Color(188,189,34), // olive + new Color(23,190,207), // teal + new Color(174,199,232), // light blue + new Color(255,187,120), // light orange + new Color(152,223,138), // light green + new Color(255,152,150), // light red + new Color(197,176,213), // light purple + new Color(196,156,148), // light brown + new Color(247,182,210), // light pink + new Color(199,199,199), // light gray + new Color(219,219,141), // light olive + new Color(158,218,229) // light teal + }; + + public static final Color[] COLORBLIND20 = { + new Color(0,114,178), // deep blue + new Color(213,94,0), // orange + new Color(0,158,115), // green + new Color(170,51,119), // magenta + new Color(0,170,170), // teal + new Color(153,153,153), // gray + new Color(0,150,255), // azure + new Color(0,170,0), // emerald + new Color(200,55,0), // brick red + new Color(102,0,153), // purple + new Color(86,180,233), // sky blue + new Color(120,190,32), // lime green + new Color(230,159,0), // goldenrod + new Color(204,121,167), // rose + new Color(0,200,200), // cyan + new Color(102,102,102), // dark gray + new Color(0,90,160), // deep navy + new Color(40,120,40), // forest green + new Color(153,102,204), // lavender + new Color(136,34,85) // plum + }; + + public static final Color[] DARK20 = { + new Color(31,119,180), // dark blue + new Color(214,39,40), // dark red + new Color(44,160,44), // dark green + new Color(148,103,189), // dark purple + new Color(140,86,75), // brown + new Color(23,190,207), // dark cyan + new Color(188,189,34), // olive + new Color(127,127,127), // gray + new Color(57,59,121), // indigo + new Color(82,84,163), // muted violet + new Color(107,110,207), // periwinkle + new Color(156,158,222), // muted lavender + new Color(99,121,57), // moss green + new Color(140,162,82), // muted lime + new Color(181,207,107), // muted olive green + new Color(206,109,189), // muted magenta + new Color(140,109,49), // dark gold/brown + new Color(189,158,57), // mustard + new Color(231,186,82), // muted amber + new Color(231,203,148) // muted beige + }; + } From ca8471413e56fcdb0624b561a3e18bb0d9e17775 Mon Sep 17 00:00:00 2001 From: Dan Vasilescu Date: Mon, 9 Mar 2026 18:55:55 -0400 Subject: [PATCH 08/31] color management, legend panel --- .../src/main/java/cbit/plot/gui/PlotPane.java | 2 +- .../ode/gui/ClusterSpecificationPanel.java | 99 +++++------- .../ode/gui/ClusterVisualizationPanel.java | 141 ++++++++++++++++-- 3 files changed, 170 insertions(+), 72 deletions(-) diff --git a/vcell-client/src/main/java/cbit/plot/gui/PlotPane.java b/vcell-client/src/main/java/cbit/plot/gui/PlotPane.java index 7f7bcc8c81..44f999e93c 100644 --- a/vcell-client/src/main/java/cbit/plot/gui/PlotPane.java +++ b/vcell-client/src/main/java/cbit/plot/gui/PlotPane.java @@ -52,7 +52,7 @@ */ public class PlotPane extends JPanel { -class LineIcon implements Icon { +public class LineIcon implements Icon { private Paint lineColor; private LineIcon(Paint paint) { lineColor = paint; diff --git a/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterSpecificationPanel.java b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterSpecificationPanel.java index 80f8c08567..1f96086564 100644 --- a/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterSpecificationPanel.java +++ b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterSpecificationPanel.java @@ -24,7 +24,7 @@ public class ClusterSpecificationPanel extends DocumentEditorSubPanel { - private enum DisplayMode { COUNTS, MEAN, OVERALL }; + public enum DisplayMode { COUNTS, MEAN, OVERALL }; public static class ClusterSelection { // used to communicate y-list selection to the ClusterVisualizationPanel public final DisplayMode mode; public final java.util.List columns; @@ -65,36 +65,10 @@ public void propertyChange(PropertyChangeEvent evt) { public void valueChanged(ListSelectionEvent e) { if (e.getSource() == ClusterSpecificationPanel.this.getYAxisChoice() && !e.getValueIsAdjusting()) { // extract selected ColumnDescriptions - java.util.List selected = (java.util.List) yAxisChoiceList.getSelectedValuesList(); - DisplayMode mode = null; - JPanel content = displayOptionsCollapsiblePanel.getContentPanel(); - for (Component c : content.getComponents()) { - if (c instanceof JRadioButton rb && rb.isSelected()) { - switch (rb.getActionCommand()) { - case "COUNTS": - mode = DisplayMode.COUNTS; - break; - case "MEAN": - mode = DisplayMode.MEAN; - break; - case "OVERALL": - mode = DisplayMode.OVERALL; - break; - } - } - } - ODESolverResultSet srs = null; - switch (mode) { - case COUNTS: - srs = langevinSolverResultSet.getClusterCounts(); - break; - case MEAN: - srs = langevinSolverResultSet.getClusterMean(); - break; - case OVERALL: - srs = langevinSolverResultSet.getClusterOverall(); - break; - } + java.util.List selected = yAxisChoiceList.getSelectedValuesList(); + DisplayMode mode = getCurrentDisplayMode(); + ODESolverResultSet srs = getResultSetForMode(mode); + // fire the event upward firePropertyChange("ClusterSelection", null, new ClusterSelection(mode, selected, srs)); } @@ -114,47 +88,30 @@ public void valueChanged(ListSelectionEvent e) { private JList yAxisChoiceList = null; private DefaultListModel defaultListModelY = null; - private void populateYAxisChoices(DisplayMode mode) { - DefaultListModel model = getDefaultListModelY(); + DefaultListModel model = getDefaultListModelY(); model.clear(); getYAxisChoice().setEnabled(false); - ODESolverResultSet srs = null; - ColumnDescription[] cd = null; - updateYAxisLabel(mode); - if (langevinSolverResultSet == null) { + ColumnDescription[] cds = getColumnDescriptionsForMode(mode); + if (cds == null || cds.length <= 1) { return; } - switch (mode) { - case COUNTS: - // we may never get non-trivial clusters if there's no binding reaction - srs = langevinSolverResultSet.getClusterCounts(); - cd = srs.getColumnDescriptions(); - break; - case MEAN: - srs = langevinSolverResultSet.getClusterMean(); - cd = srs.getColumnDescriptions(); - break; - case OVERALL: - srs = langevinSolverResultSet.getClusterOverall(); - cd = srs.getColumnDescriptions(); - break; - } - if(cd == null || cd.length == 1) { - return; - } - for (ColumnDescription columnDescription : cd) { - if(columnDescription.getName().equals("t")) { - continue; // skip time column + for (ColumnDescription cd : cds) { + if (!"t".equals(cd.getName())) { + model.addElement(cd); } - model.addElement(columnDescription); + } + + if (!model.isEmpty()) { getYAxisChoice().setEnabled(true); + getYAxisChoice().setSelectedIndex(0); // triggers valueChanged() } } + private void updateYAxisLabel(DisplayMode mode) { int count = yAxisCounts.getOrDefault(mode, 0); String text = "" + YAxisLabelText + "(" + count + " entries)"; @@ -350,5 +307,27 @@ private int countColumns(ODESolverResultSet srs) { if (cds == null) return 0; return cds.length > 1 ? cds.length-1 : 0; // subtract one for time column, but don't return negative if no column at all } - + private ODESolverResultSet getResultSetForMode(DisplayMode mode) { + if (langevinSolverResultSet == null) { + return null; + } + return switch (mode) { + case COUNTS -> langevinSolverResultSet.getClusterCounts(); + case MEAN -> langevinSolverResultSet.getClusterMean(); + case OVERALL-> langevinSolverResultSet.getClusterOverall(); + }; + } + private ColumnDescription[] getColumnDescriptionsForMode(DisplayMode mode) { + ODESolverResultSet srs = getResultSetForMode(mode); + return (srs == null ? null : srs.getColumnDescriptions()); + } + private DisplayMode getCurrentDisplayMode() { + JPanel content = displayOptionsCollapsiblePanel.getContentPanel(); + for (Component c : content.getComponents()) { + if (c instanceof JRadioButton rb && rb.isSelected()) { + return DisplayMode.valueOf(rb.getActionCommand()); + } + } + return DisplayMode.COUNTS; // default fallback + } } diff --git a/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterVisualizationPanel.java b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterVisualizationPanel.java index dd59540030..bda898bce1 100644 --- a/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterVisualizationPanel.java +++ b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterVisualizationPanel.java @@ -2,7 +2,9 @@ import cbit.vcell.client.data.ODEDataViewer; import cbit.vcell.client.desktop.biomodel.DocumentEditorSubPanel; +import cbit.vcell.parser.ExpressionException; import cbit.vcell.util.ColumnDescription; +import org.vcell.util.ColorUtil; import org.vcell.util.gui.JToolBarToggleButton; import org.vcell.util.gui.VCellIcons; @@ -17,6 +19,8 @@ import java.awt.event.ActionListener; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; +import java.util.*; +import java.util.List; public class ClusterVisualizationPanel extends DocumentEditorSubPanel { @@ -24,6 +28,12 @@ public class ClusterVisualizationPanel extends DocumentEditorSubPanel { ODEDataViewer owner; IvjEventHandler ivjEventHandler = new IvjEventHandler(); + private final Map persistentColorMap = new LinkedHashMap<>(); + private final java.util.List globalPalette = new ArrayList<>(); + private int nextColorIndex = 0; + + + private JPanel ivjJPanel1 = null; private JPanel ivjJPanelPlot = null; private JPanel ivjPlot2DPanel1 = null; // here @@ -57,8 +67,13 @@ public void actionPerformed(ActionEvent e) { public void propertyChange(PropertyChangeEvent evt) { if (evt.getSource() == owner.getClusterSpecificationPanel() && "ClusterSelection".equals(evt.getPropertyName())) { ClusterSpecificationPanel.ClusterSelection sel = (ClusterSpecificationPanel.ClusterSelection) evt.getNewValue(); - updateLegend(sel); // update legend (one plot, multiple curves) - redrawPlot(sel); // redraw plot (one plot, multiple curves) + ensureColorsAssigned(sel.columns); + try { + redrawPlot(sel); // redraw plot (one plot, multiple curves) + } catch (ExpressionException e) { + throw new RuntimeException(e); + } + redrawLegend(sel); // update legend (one plot, multiple curves) updateDataTable(sel); // update data table return; } @@ -90,7 +105,8 @@ private void initialize() { add(getJPanel1(), "Center"); add(getBottomRightPanel(), "South"); add(getJPanelLegend(), "East"); - initConnectionsRight(); + setBackground(Color.white); + initConnections(); } private JPanel getJPanel1() { @@ -231,11 +247,14 @@ private JPanel getJPanelLegend() { ivjJPanelLegend.setName("JPanelLegend"); ivjJPanelLegend.setLayout(new BorderLayout()); getJPanelLegend().add(new JLabel(" "), "South"); - getJPanelLegend().add(new JLabel("Plot Legend:"), "North"); + JLabel labelLegendTitle = new JLabel("Plot Legend:"); + labelLegendTitle.setBorder(new EmptyBorder(10, 4, 10, 4)); + getJPanelLegend().add(labelLegendTitle, "North"); getJPanelLegend().add(getPlotLegendsScrollPane(), "Center"); } return ivjJPanelLegend; } + private JScrollPane getPlotLegendsScrollPane() { if (ivjPlotLegendsScrollPane == null) { ivjPlotLegendsScrollPane = new JScrollPane(); @@ -256,8 +275,22 @@ private JPanel getJPanelPlotLegends() { return ivjJPanelPlotLegends; } + public void setBackground(Color color) { + super.setBackground(color); + getBottomRightPanel().setBackground(color); + getJBottomLabel().setBackground(color); + getJPanelLegend().setBackground(color); + getJPanelPlotLegends().setBackground(color); + getJPanel1().setBackground(color); + getPlot2DPanel1().setBackground(color); + getPlot2DDataPanel1().setBackground(color); + getJPanelData().setBackground(color); + getJPanelPlot().setBackground(color); + + } - private void initConnectionsRight() { + private void initConnections() { + initializeGlobalPalette(); // get a stable, high contrast palette // group the two buttons so only one stays selected ButtonGroup bg = new ButtonGroup(); bg.add(getPlotButton()); @@ -290,15 +323,101 @@ public void refreshData() { // --------------------------------------------------------------------- - private void updateLegend(ClusterSpecificationPanel.ClusterSelection sel) { - System.out.println("ClusterVisualizationPanel.updateLegend() called"); + private void initializeGlobalPalette() { + // Use a curated palette from ColorUtil + globalPalette.clear(); + globalPalette.addAll(Arrays.asList(ColorUtil.TABLEAU20)); + } + private void ensureColorsAssigned(List columns) { + // assign colors only when needed, and keep them consistent across updates + for (ColumnDescription cd : columns) { + String name = cd.getName(); + if (!persistentColorMap.containsKey(name)) { + Color c = globalPalette.get(nextColorIndex % globalPalette.size()); + persistentColorMap.put(name, c); + nextColorIndex++; + } + } + } + private JComponent createLegendEntry(String name, Color color, ClusterSpecificationPanel.DisplayMode mode) { + JPanel p = new JPanel(); + p.setName("JPanelClusterColorLegends"); + BoxLayout bl = new BoxLayout(p, BoxLayout.Y_AXIS); + p.setLayout(bl); + p.setBounds(0, 0, 72, 360); + p.setOpaque(false); + + String unitSymbol = ""; + if(ClusterSpecificationPanel.DisplayMode.COUNTS == mode) { + unitSymbol = "molecules"; + } + String shortLabel = "" + name + "" + " [" + unitSymbol + "] " + ""; + + JLabel line = new JLabel(new LineIcon(color)); + JLabel text = new JLabel(shortLabel); + line.setBorder(new EmptyBorder(6,0,1,0)); + text.setBorder(new EmptyBorder(1,8,6,0)); + p.add(line); + p.add(text); + + return p; + } + public class LineIcon implements Icon { + private final Color color; + public LineIcon(Color color) { + this.color = color; + } + @Override + public void paintIcon(Component c, Graphics g, int x, int y) { + Graphics2D g2 = (Graphics2D)g; + g2.setStroke(new BasicStroke(3.0f)); + g2.setPaint(color); + int midY = y + getIconHeight() / 2; + g2.drawLine(x, midY, x + getIconWidth(), midY); + } + @Override + public int getIconWidth() { return 50; } + @Override + public int getIconHeight() { + return 4; // more vertical room for a wider stroke + } } - private void redrawPlot(ClusterSpecificationPanel.ClusterSelection sel) { + public static String getUnitSymbol(ClusterSpecificationPanel.ClusterSelection sel) { + if(ClusterSpecificationPanel.DisplayMode.COUNTS == sel.mode) { + + + } + return ""; + } + + private void redrawPlot(ClusterSpecificationPanel.ClusterSelection sel) throws ExpressionException { System.out.println("ClusterVisualizationPanel.redrawPlot() called"); - java.util.List columnDescriptions = sel.columns; - for(ColumnDescription cd : columnDescriptions) { - System.out.println(" column name: '" + cd.getName() + "'"); +// java.util.List columnDescriptions = sel.columns; +// for(ColumnDescription cd : columnDescriptions) { +// System.out.println(" column name: '" + cd.getName() + "'"); +// } +// getPlot2DPanel1().removeAllPlots(); +// +// int index = sel.resultSet.findColumn("t"); +// double[] t = sel.resultSet.extractColumn(index); +// +// for (ColumnDescription cd : sel.columns) { +// index = sel.resultSet.findColumn(cd.getName()); +// double[] y = sel.resultSet.extractColumn(index); +// Color c = persistentColorMap.get(cd.getName()); +// getPlot2DPanel1().addLinePlot(cd.getName(), c, t, y); +// } +// getPlot2DPanel1().repaint(); + } + private void redrawLegend(ClusterSpecificationPanel.ClusterSelection sel) { + System.out.println("ClusterVisualizationPanel.updateLegend() called"); + getJPanelPlotLegends().removeAll(); + for (ColumnDescription cd : sel.columns) { + Color c = persistentColorMap.get(cd.getName()); + getJPanelPlotLegends().add(createLegendEntry(cd.getName(), c, sel.mode)); } + getJPanelPlotLegends().revalidate(); + getJPanelPlotLegends().repaint(); } private void updateDataTable(ClusterSpecificationPanel.ClusterSelection sel) { System.out.println("ClusterVisualizationPanel.updateDataTable() called"); From 793b531fbe09034b65d238a55bdfe81261921dd9 Mon Sep 17 00:00:00 2001 From: Dan Vasilescu Date: Tue, 10 Mar 2026 17:46:50 -0400 Subject: [PATCH 09/31] plot panel, multithreaded. scaling fails during initial display --- .../java/cbit/plot/gui/ClusterPlotPanel.java | 88 ++++++++++ .../ode/gui/ClusterVisualizationPanel.java | 154 ++++++++++++++---- .../main/java/org/vcell/util/ColorUtil.java | 4 +- 3 files changed, 210 insertions(+), 36 deletions(-) create mode 100644 vcell-client/src/main/java/cbit/plot/gui/ClusterPlotPanel.java diff --git a/vcell-client/src/main/java/cbit/plot/gui/ClusterPlotPanel.java b/vcell-client/src/main/java/cbit/plot/gui/ClusterPlotPanel.java new file mode 100644 index 0000000000..65d939ea00 --- /dev/null +++ b/vcell-client/src/main/java/cbit/plot/gui/ClusterPlotPanel.java @@ -0,0 +1,88 @@ +package cbit.plot.gui; + +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.Paint; +import java.awt.Point; +import java.awt.Rectangle; +import java.awt.RenderingHints; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.MouseEvent; +import java.awt.geom.AffineTransform; +import java.awt.geom.Ellipse2D; +import java.awt.geom.GeneralPath; +import java.awt.geom.Line2D; +import java.awt.geom.NoninvertibleTransformException; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.text.DecimalFormat; +import java.text.NumberFormat; + +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.KeyStroke; +import javax.swing.border.Border; + +import org.vcell.util.gui.GeneralGuiUtils; +import org.vcell.util.*; + +public class ClusterPlotPanel extends JPanel { + + private final float lineBS_10 = 1.0f; + private final float lineBS_15 = 1.5f; + private final float lineBS_20 = 2.0f; + + private static class CurveData { + // uniform time course, we compute the x on the fly + final String name; + final int[] y; // scaled pixel y-values + final Color color; + CurveData(String name, int[] y, Color color) { + this.name = name; + this.y = y; + this.color = color; + } + } + private final java.util.List curves = new java.util.ArrayList<>(); + +// ----------------------------------------------------------------------------- + + public void clear() { + curves.clear(); + } + public void addCurve(String name, int[] y, Color color) { + curves.add(new CurveData(name, y, color)); + } + + @Override + protected void paintComponent(Graphics g) { + super.paintComponent(g); + + Graphics2D g2 = (Graphics2D) g; + g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + int w = getWidth(); + int h = getHeight(); + System.out.println("ClusterPlotPanel.paintComponent: w=" + w + ", h=" + h); + g2.setColor(Color.white); + g2.fillRect(0, 0, w, h); + g2.setStroke(new BasicStroke(lineBS_15, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND)); + + for (CurveData curve : curves) { + int n = curve.y.length; + if (n < 2) continue; + + int[] x = new int[n]; + for (int i = 0; i < n; i++) { + x[i] = (int) Math.round((i / (double)(n - 1)) * w); + } + g2.setColor(curve.color); + g2.drawPolyline(x, curve.y, n); + } + } + +} diff --git a/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterVisualizationPanel.java b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterVisualizationPanel.java index bda898bce1..3d31c97574 100644 --- a/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterVisualizationPanel.java +++ b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterVisualizationPanel.java @@ -1,8 +1,12 @@ package cbit.vcell.solver.ode.gui; +import cbit.plot.gui.ClusterPlotPanel; import cbit.vcell.client.data.ODEDataViewer; import cbit.vcell.client.desktop.biomodel.DocumentEditorSubPanel; +import cbit.vcell.client.task.AsynchClientTask; +import cbit.vcell.client.task.ClientTaskDispatcher; import cbit.vcell.parser.ExpressionException; +import cbit.vcell.solver.ode.ODESolverResultSet; import cbit.vcell.util.ColumnDescription; import org.vcell.util.ColorUtil; import org.vcell.util.gui.JToolBarToggleButton; @@ -17,6 +21,8 @@ import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; +import java.awt.event.ComponentAdapter; +import java.awt.event.ComponentEvent; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.util.*; @@ -32,14 +38,14 @@ public class ClusterVisualizationPanel extends DocumentEditorSubPanel { private final java.util.List globalPalette = new ArrayList<>(); private int nextColorIndex = 0; - + private ClusterSpecificationPanel.ClusterSelection currentSelection = null; private JPanel ivjJPanel1 = null; private JPanel ivjJPanelPlot = null; - private JPanel ivjPlot2DPanel1 = null; // here + private ClusterPlotPanel clusterPlotPanel = null; // here are the plots being drawn private JLabel ivjJLabelBottom = null; private JPanel ivjJPanelData = null; - private JPanel ivjPlot2DDataPanel1 = null; // here + private JPanel ivjPlot2DDataPanel1 = null; // here resides the data table private JPanel bottomRightPanel = null; private JPanel ivjJPanelLegend = null; private JScrollPane ivjPlotLegendsScrollPane = null; @@ -124,7 +130,7 @@ private JPanel getJPanelPlot() { ivjJPanelPlot = new JPanel(); ivjJPanelPlot.setName("JPanelPlot"); ivjJPanelPlot.setLayout(new BorderLayout()); - ivjJPanelPlot.add(getPlot2DPanel1(), "Center"); + ivjJPanelPlot.add(getClusterPlotPanel(), "Center"); ivjJPanelPlot.add(getJLabelBottom(), "South"); } return ivjJPanelPlot; @@ -150,16 +156,34 @@ private JLabel getJLabelBottom() { } return ivjJLabelBottom; } - private JPanel getPlot2DPanel1() { - if (ivjPlot2DPanel1 == null) { + private ClusterPlotPanel getClusterPlotPanel() { // actual plotting is done here + if (clusterPlotPanel == null) { try { - ivjPlot2DPanel1 = new JPanel(); - ivjPlot2DPanel1.setName("Plot2DPanel1"); + clusterPlotPanel = new ClusterPlotPanel(); + clusterPlotPanel.setName("ClusterPlotPanel"); + clusterPlotPanel.addComponentListener(new ComponentAdapter() { + @Override + public void componentShown(ComponentEvent e) { + System.out.println("ClusterVisualizationPanel.componentShown() called, height = " + clusterPlotPanel.getHeight()); + // Only redraw when the panel is actually visible and sized + if (currentSelection != null) { + try { + System.out.println("ClusterVisualizationPanel.componentShown() calling redrawPlot() with current selection: " + currentSelection); + redrawPlot(currentSelection); + } catch (ExpressionException ex) { + ex.printStackTrace(); + } + } else { + System.out.println("ClusterVisualizationPanel.componentShown() no current selection, skipping redraw"); + } + } + }); + } catch (java.lang.Throwable ivjExc) { handleException(ivjExc); } } - return ivjPlot2DPanel1; + return clusterPlotPanel; } public JPanel getPlot2DDataPanel1() { if (ivjPlot2DDataPanel1 == null) { @@ -282,7 +306,7 @@ public void setBackground(Color color) { getJPanelLegend().setBackground(color); getJPanelPlotLegends().setBackground(color); getJPanel1().setBackground(color); - getPlot2DPanel1().setBackground(color); + getClusterPlotPanel().setBackground(color); getPlot2DDataPanel1().setBackground(color); getJPanelData().setBackground(color); getJPanelPlot().setBackground(color); @@ -382,35 +406,97 @@ public int getIconHeight() { return 4; // more vertical room for a wider stroke } } - public static String getUnitSymbol(ClusterSpecificationPanel.ClusterSelection sel) { - if(ClusterSpecificationPanel.DisplayMode.COUNTS == sel.mode) { - + private void redrawPlot(ClusterSpecificationPanel.ClusterSelection sel) throws ExpressionException { + System.out.println("ClusterVisualizationPanel.redrawPlot() called, current selection: " + sel); + if (sel != null) { + System.out.println("ClusterVisualizationPanel.redrawPlot() mode: " + sel.mode + ", columns: " + sel.columns.size() + ", resultSet: " + (sel.resultSet != null ? "present" : "null")); + } else { + System.out.println("ClusterVisualizationPanel.redrawPlot() selection is null"); } - return ""; - } + System.out.println("ClusterVisualizationPanel.redrawPlot(), height = " + getClusterPlotPanel().getHeight()); + currentSelection = sel; + java.util.List columnDescriptions = sel.columns; + for(ColumnDescription cd : columnDescriptions) { + System.out.println(" column name: '" + cd.getName() + "'"); + } + Hashtable hashTable = new Hashtable<>(); + hashTable.put("columns", sel.columns); // selected columns + hashTable.put("resultSet", sel.resultSet); + hashTable.put("persistentColorMap", persistentColorMap); + hashTable.put("plotPanel", getClusterPlotPanel()); + hashTable.put("panelHeight", getPlot2DDataPanel1().getHeight()); + hashTable.put("panelWidth", getPlot2DDataPanel1().getWidth()); + + AsynchClientTask computeScaledCurvesTask = new AsynchClientTask("Computing scaled curves...", + AsynchClientTask.TASKTYPE_NONSWING_BLOCKING) { + @Override + public void run(Hashtable hashTable) throws Exception { + + List columns = (List) hashTable.get("columns"); + ODESolverResultSet srs = (ODESolverResultSet) hashTable.get("resultSet"); + int panelWidth = (Integer) hashTable.get("panelWidth"); + + double globalMin = Double.POSITIVE_INFINITY; // compute global min/max + double globalMax = Double.NEGATIVE_INFINITY; + Map rawCurves = new LinkedHashMap<>(); + + for (ColumnDescription cd : columns) { + int index = srs.findColumn(cd.getName()); + double[] y = srs.extractColumn(index); + rawCurves.put(cd.getName(), y); + + for (double v : y) { + if (v < globalMin) globalMin = v; + if (v > globalMax) globalMax = v; + } + } + hashTable.put("globalMin", globalMin); + hashTable.put("globalMax", globalMax); + + int panelHeight = (Integer) hashTable.get("panelHeight"); // scale curves into panel coordinates + Map scaledCurves = new LinkedHashMap<>(); + double range = globalMax - globalMin; + if (range == 0) range = 1; // avoid divide-by-zero + + for (Map.Entry entry : rawCurves.entrySet()) { + String name = entry.getKey(); + double[] y = entry.getValue(); + int[] scaled = new int[y.length]; // this is in pixels, which are int + for (int i = 0; i < y.length; i++) { + double norm = (y[i] - globalMin) / range; + scaled[i] = panelHeight - (int) Math.round(norm * panelHeight); + } + scaledCurves.put(name, scaled); + } + hashTable.put("scaledCurves", scaledCurves); + } + }; + AsynchClientTask drawCurvesTask = new AsynchClientTask("Drawing curves...", + AsynchClientTask.TASKTYPE_SWING_BLOCKING) { + @Override + public void run(Hashtable hashTable) throws Exception { + + Map scaledCurves = (Map) hashTable.get("scaledCurves"); + Map colorMap = (Map) hashTable.get("persistentColorMap"); + ClusterPlotPanel clusterPlotPanel = (ClusterPlotPanel) hashTable.get("plotPanel"); + + clusterPlotPanel.clear(); + for (String name : scaledCurves.keySet()) { + int[] yScaled = scaledCurves.get(name); + Color c = colorMap.get(name); + clusterPlotPanel.addCurve(name, yScaled, c); + } + clusterPlotPanel.repaint(); + } + }; + AsynchClientTask[] tasks = new AsynchClientTask[] { computeScaledCurvesTask, drawCurvesTask }; + ClientTaskDispatcher.dispatch(this, hashTable, tasks, false); + - private void redrawPlot(ClusterSpecificationPanel.ClusterSelection sel) throws ExpressionException { - System.out.println("ClusterVisualizationPanel.redrawPlot() called"); -// java.util.List columnDescriptions = sel.columns; -// for(ColumnDescription cd : columnDescriptions) { -// System.out.println(" column name: '" + cd.getName() + "'"); -// } -// getPlot2DPanel1().removeAllPlots(); -// -// int index = sel.resultSet.findColumn("t"); -// double[] t = sel.resultSet.extractColumn(index); -// -// for (ColumnDescription cd : sel.columns) { -// index = sel.resultSet.findColumn(cd.getName()); -// double[] y = sel.resultSet.extractColumn(index); -// Color c = persistentColorMap.get(cd.getName()); -// getPlot2DPanel1().addLinePlot(cd.getName(), c, t, y); -// } -// getPlot2DPanel1().repaint(); } private void redrawLegend(ClusterSpecificationPanel.ClusterSelection sel) { - System.out.println("ClusterVisualizationPanel.updateLegend() called"); + System.out.println("ClusterVisualizationPanel.redrawLegend() called"); getJPanelPlotLegends().removeAll(); for (ColumnDescription cd : sel.columns) { Color c = persistentColorMap.get(cd.getName()); diff --git a/vcell-util/src/main/java/org/vcell/util/ColorUtil.java b/vcell-util/src/main/java/org/vcell/util/ColorUtil.java index ab911ac441..ed70a49474 100644 --- a/vcell-util/src/main/java/org/vcell/util/ColorUtil.java +++ b/vcell-util/src/main/java/org/vcell/util/ColorUtil.java @@ -130,20 +130,20 @@ public static int calcBrightness(int red,int grn,int blu){ new Color(44,160,44), // green new Color(214,39,40), // red new Color(148,103,189), // purple + new Color(23,190,207), // teal new Color(140,86,75), // brown new Color(227,119,194), // pink new Color(127,127,127), // gray new Color(188,189,34), // olive - new Color(23,190,207), // teal new Color(174,199,232), // light blue new Color(255,187,120), // light orange new Color(152,223,138), // light green new Color(255,152,150), // light red new Color(197,176,213), // light purple + new Color(219,219,141), // light olive new Color(196,156,148), // light brown new Color(247,182,210), // light pink new Color(199,199,199), // light gray - new Color(219,219,141), // light olive new Color(158,218,229) // light teal }; From 193743352cf4d60155fd21af8d948ec6161e6182 Mon Sep 17 00:00:00 2001 From: Dan Vasilescu Date: Tue, 10 Mar 2026 19:15:03 -0400 Subject: [PATCH 10/31] plot panel, nice working implementation --- .../java/cbit/plot/gui/ClusterPlotPanel.java | 142 +++++++++++++++--- .../ode/gui/ClusterVisualizationPanel.java | 98 +++--------- 2 files changed, 139 insertions(+), 101 deletions(-) diff --git a/vcell-client/src/main/java/cbit/plot/gui/ClusterPlotPanel.java b/vcell-client/src/main/java/cbit/plot/gui/ClusterPlotPanel.java index 65d939ea00..4571d643e0 100644 --- a/vcell-client/src/main/java/cbit/plot/gui/ClusterPlotPanel.java +++ b/vcell-client/src/main/java/cbit/plot/gui/ClusterPlotPanel.java @@ -1,13 +1,6 @@ package cbit.plot.gui; -import java.awt.BasicStroke; -import java.awt.Color; -import java.awt.Graphics; -import java.awt.Graphics2D; -import java.awt.Paint; -import java.awt.Point; -import java.awt.Rectangle; -import java.awt.RenderingHints; +import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.MouseEvent; @@ -20,6 +13,8 @@ import java.awt.geom.Rectangle2D; import java.text.DecimalFormat; import java.text.NumberFormat; +import java.util.ArrayList; +import java.util.List; import javax.swing.JFrame; import javax.swing.JLabel; @@ -33,30 +28,71 @@ public class ClusterPlotPanel extends JPanel { - private final float lineBS_10 = 1.0f; - private final float lineBS_15 = 1.5f; - private final float lineBS_20 = 2.0f; + private static final int LEFT_INSET = 50; // insets for axes and labels + private static final int RIGHT_INSET = 20; + private static final int TOP_INSET = 20; + private static final int BOTTOM_INSET = 30; + + private static final float AXIS_STROKE = 1.0f; // stroke widths + private static final float CURVE_STROKE = 1.5f; + private static class CurveData { - // uniform time course, we compute the x on the fly final String name; - final int[] y; // scaled pixel y-values + final double[] yRaw; final Color color; - CurveData(String name, int[] y, Color color) { + CurveData(String name, double[] yRaw, Color color) { this.name = name; - this.y = y; + this.yRaw = yRaw; this.color = color; } } - private final java.util.List curves = new java.util.ArrayList<>(); + private final List curves = new ArrayList<>(); + private double globalMin = 0; + private double globalMax = 1; + private double dt = 1; // ----------------------------------------------------------------------------- public void clear() { curves.clear(); } - public void addCurve(String name, int[] y, Color color) { - curves.add(new CurveData(name, y, color)); + public void addCurve(String name, double[] yRaw, Color color) { + curves.add(new CurveData(name, yRaw, color)); + } + public void setGlobalMinMax(double min, double max) { + this.globalMin = min; + this.globalMax = max; + } + public void setDt(double dt) { + this.dt = dt; + } + private double roundUpNice(double value) { + if (value <= 0) return 1; + + double exp = Math.pow(10, Math.floor(Math.log10(value))); + double n = value / exp; + double rounded; + if (n <= 1) rounded = 1; + else if (n <= 2) rounded = 2; + else if (n <= 5) rounded = 5; + else rounded = 10; + return rounded * exp; + } + + private String formatNumber(double v) { + if (v == 0) return "0"; + + double abs = Math.abs(v); + if (abs >= 1) { + return String.format("%.0f", v); // 0, 10, 20, 30, 40 + } else if (abs >= 0.01) { + return String.format("%.3f", v); // 0.123 + } else if (abs >= 0.0001) { + return String.format("%.5f", v); // 0.00012 + } else { + return String.format("%.2E", v); // 2.0E-5 + } } @Override @@ -65,23 +101,79 @@ protected void paintComponent(Graphics g) { Graphics2D g2 = (Graphics2D) g; g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + int w = getWidth(); int h = getHeight(); - System.out.println("ClusterPlotPanel.paintComponent: w=" + w + ", h=" + h); - g2.setColor(Color.white); + g2.setColor(Color.white); // background g2.fillRect(0, 0, w, h); - g2.setStroke(new BasicStroke(lineBS_15, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND)); + + int x0 = LEFT_INSET; // plot area + int x1 = w - RIGHT_INSET; + int y0 = h - BOTTOM_INSET; + int y1 = TOP_INSET; + + int plotWidth = x1 - x0; + int plotHeight = y0 - y1; + if (plotWidth <= 0 || plotHeight <= 0) { + return; // too small to draw + } + + // determine max curve length + int maxCurveLength = curves.stream() + .mapToInt(c -> c.yRaw.length) + .max() + .orElse(0); + + if (maxCurveLength < 2) { + return; // nothing to draw + } + + double yMaxRounded = roundUpNice(globalMax); // rounded axis limits + double xMax = dt * (maxCurveLength - 1); + double xMaxRounded = roundUpNice(xMax); + + g2.setColor(Color.black); // draw axes + g2.setStroke(new BasicStroke(AXIS_STROKE)); + g2.drawLine(x0, y0, x1, y0); // X axis + g2.drawLine(x0, y0, x0, y1); // Y axis + + FontMetrics fm = g2.getFontMetrics(); // tick labels + int yTicks = 5; // ------- Y ticks (5 ticks) + double yStep = yMaxRounded / yTicks; + for (int i = 0; i <= yTicks; i++) { + double value = i * yStep; + int yPix = y0 - (int) Math.round((value / yMaxRounded) * plotHeight); + g2.drawLine(x0 - 5, yPix, x0, yPix); // tick + String label = formatNumber(value); // label + int sw = fm.stringWidth(label); + g2.drawString(label, x0 - 10 - sw, yPix + fm.getAscent() / 2); + } + + double[] xTickValues = {0, xMaxRounded / 2, xMaxRounded}; // ----- X ticks (0, mid, end) + for (double xv : xTickValues) { + int xPix = x0 + (int) Math.round((xv / xMaxRounded) * plotWidth); + g2.drawLine(xPix, y0, xPix, y0 + 5); // tick + String label = formatNumber(xv); // label + int sw = fm.stringWidth(label); + g2.drawString(label, xPix - sw / 2, y0 + fm.getAscent() + 5); + } + + // draw curves + g2.setStroke(new BasicStroke(CURVE_STROKE, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND)); for (CurveData curve : curves) { - int n = curve.y.length; + int n = curve.yRaw.length; if (n < 2) continue; - int[] x = new int[n]; + int[] y = new int[n]; for (int i = 0; i < n; i++) { - x[i] = (int) Math.round((i / (double)(n - 1)) * w); + double t = i * dt; + x[i] = x0 + (int) Math.round((t / xMaxRounded) * plotWidth); + double norm = curve.yRaw[i] / yMaxRounded; + y[i] = y0 - (int) Math.round(norm * plotHeight); } g2.setColor(curve.color); - g2.drawPolyline(x, curve.y, n); + g2.drawPolyline(x, y, n); } } diff --git a/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterVisualizationPanel.java b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterVisualizationPanel.java index 3d31c97574..14e6308209 100644 --- a/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterVisualizationPanel.java +++ b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterVisualizationPanel.java @@ -416,84 +416,30 @@ private void redrawPlot(ClusterSpecificationPanel.ClusterSelection sel) throws E } System.out.println("ClusterVisualizationPanel.redrawPlot(), height = " + getClusterPlotPanel().getHeight()); currentSelection = sel; - java.util.List columnDescriptions = sel.columns; - for(ColumnDescription cd : columnDescriptions) { - System.out.println(" column name: '" + cd.getName() + "'"); - } - Hashtable hashTable = new Hashtable<>(); - hashTable.put("columns", sel.columns); // selected columns - hashTable.put("resultSet", sel.resultSet); - hashTable.put("persistentColorMap", persistentColorMap); - hashTable.put("plotPanel", getClusterPlotPanel()); - hashTable.put("panelHeight", getPlot2DDataPanel1().getHeight()); - hashTable.put("panelWidth", getPlot2DDataPanel1().getWidth()); - - AsynchClientTask computeScaledCurvesTask = new AsynchClientTask("Computing scaled curves...", - AsynchClientTask.TASKTYPE_NONSWING_BLOCKING) { - @Override - public void run(Hashtable hashTable) throws Exception { - - List columns = (List) hashTable.get("columns"); - ODESolverResultSet srs = (ODESolverResultSet) hashTable.get("resultSet"); - int panelWidth = (Integer) hashTable.get("panelWidth"); - - double globalMin = Double.POSITIVE_INFINITY; // compute global min/max - double globalMax = Double.NEGATIVE_INFINITY; - Map rawCurves = new LinkedHashMap<>(); - - for (ColumnDescription cd : columns) { - int index = srs.findColumn(cd.getName()); - double[] y = srs.extractColumn(index); - rawCurves.put(cd.getName(), y); - - for (double v : y) { - if (v < globalMin) globalMin = v; - if (v > globalMax) globalMax = v; - } - } - hashTable.put("globalMin", globalMin); - hashTable.put("globalMax", globalMax); - - int panelHeight = (Integer) hashTable.get("panelHeight"); // scale curves into panel coordinates - Map scaledCurves = new LinkedHashMap<>(); - double range = globalMax - globalMin; - if (range == 0) range = 1; // avoid divide-by-zero - - for (Map.Entry entry : rawCurves.entrySet()) { - String name = entry.getKey(); - double[] y = entry.getValue(); - int[] scaled = new int[y.length]; // this is in pixels, which are int - for (int i = 0; i < y.length; i++) { - double norm = (y[i] - globalMin) / range; - scaled[i] = panelHeight - (int) Math.round(norm * panelHeight); - } - scaledCurves.put(name, scaled); - } - hashTable.put("scaledCurves", scaledCurves); - } - }; - AsynchClientTask drawCurvesTask = new AsynchClientTask("Drawing curves...", - AsynchClientTask.TASKTYPE_SWING_BLOCKING) { - @Override - public void run(Hashtable hashTable) throws Exception { - - Map scaledCurves = (Map) hashTable.get("scaledCurves"); - Map colorMap = (Map) hashTable.get("persistentColorMap"); - ClusterPlotPanel clusterPlotPanel = (ClusterPlotPanel) hashTable.get("plotPanel"); - - clusterPlotPanel.clear(); - for (String name : scaledCurves.keySet()) { - int[] yScaled = scaledCurves.get(name); - Color c = colorMap.get(name); - clusterPlotPanel.addCurve(name, yScaled, c); - } - clusterPlotPanel.repaint(); - } - }; - AsynchClientTask[] tasks = new AsynchClientTask[] { computeScaledCurvesTask, drawCurvesTask }; - ClientTaskDispatcher.dispatch(this, hashTable, tasks, false); + List columns = sel.columns; + ODESolverResultSet srs = sel.resultSet; + + int indexTime = srs.findColumn("t"); + double[] times = srs.extractColumn(indexTime); +// double globalMin = Double.POSITIVE_INFINITY; + double globalMin = 0; // these are counts, so min is always 0 + double globalMax = Double.NEGATIVE_INFINITY; + clusterPlotPanel.clear(); + for (ColumnDescription cd : columns) { + int index = srs.findColumn(cd.getName()); + double[] y = srs.extractColumn(index); + for (double v : y) { +// if (v < globalMin) globalMin = v; + if (v > globalMax) globalMax = v; + } + Color c = persistentColorMap.get(cd.getName()); + clusterPlotPanel.addCurve(cd.getName(), y, c); + } + clusterPlotPanel.setGlobalMinMax(globalMin, globalMax); + clusterPlotPanel.setDt(times[1]); // times[0] == 0; + clusterPlotPanel.repaint(); } private void redrawLegend(ClusterSpecificationPanel.ClusterSelection sel) { System.out.println("ClusterVisualizationPanel.redrawLegend() called"); From ed998b0293735e16e0b67377d80eda64bcfab955 Mon Sep 17 00:00:00 2001 From: Dan Vasilescu Date: Thu, 12 Mar 2026 12:50:10 -0400 Subject: [PATCH 11/31] cluster analyze panel, testing JFreeChart --- .../cli/run/plotting/Results2DLinePlot.java | 250 ++++++++++++ vcell-client/pom.xml | 6 + .../cbit/vcell/client/data/ODEDataViewer.java | 2 + .../ode/gui/ClusterVisualizationPanel.java | 367 +++++++++++++++++- 4 files changed, 606 insertions(+), 19 deletions(-) diff --git a/vcell-cli/src/main/java/org/vcell/cli/run/plotting/Results2DLinePlot.java b/vcell-cli/src/main/java/org/vcell/cli/run/plotting/Results2DLinePlot.java index b04a69b837..08f009bf96 100644 --- a/vcell-cli/src/main/java/org/vcell/cli/run/plotting/Results2DLinePlot.java +++ b/vcell-cli/src/main/java/org/vcell/cli/run/plotting/Results2DLinePlot.java @@ -5,9 +5,15 @@ import com.lowagie.text.DocumentException; import org.jfree.chart.ChartFactory; +import org.jfree.chart.ChartPanel; import org.jfree.chart.JFreeChart; +import org.jfree.chart.block.BlockBorder; import org.jfree.chart.labels.StandardXYItemLabelGenerator; +import org.jfree.chart.plot.PlotOrientation; import org.jfree.chart.plot.XYPlot; +import org.jfree.chart.renderer.xy.XYDifferenceRenderer; +import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer; +import org.jfree.chart.ui.RectangleEdge; import org.jfree.data.xy.XYDataset; import org.jfree.data.xy.XYSeries; import org.jfree.data.xy.XYSeriesCollection; @@ -16,7 +22,9 @@ import org.apache.logging.log4j.Logger; import javax.imageio.ImageIO; +import javax.swing.*; import java.awt.*; +import java.awt.geom.Path2D; import java.awt.image.BufferedImage; import java.awt.print.PageFormat; import java.awt.print.Paper; @@ -319,4 +327,246 @@ private static PageFormat generateAlternatePageFormat(){ pageFormat.setOrientation(PageFormat.LANDSCAPE); return pageFormat; } + + + public static void main(String[] args) { + SwingUtilities.invokeLater(() -> { + + Random rand = new Random(); + + int n = 50; + double xMin = 0.0; + double xMax = 7.0; + double dx = (xMax - xMin) / (n - 1); + + double xLower = 0.0; // these are the limits for the x values of the axis + double xUpper = 7.5; + + Color sinColor = new Color(31, 119, 180); // blue + Color tanColor = new Color(255, 127, 14); // orange + + // --- SIN series --- + XYSeries sinMin = new XYSeries("sin-min"); + XYSeries sinMax = new XYSeries("sin-max"); + XYSeries sinMain = new XYSeries("sin"); + XYSeries sinStd = new XYSeries("sin-std"); + + for (int i = 0; i < n; i++) { + double x = xMin + i * dx; + double y = Math.sin(x); + + double delta = 0.2 + rand.nextDouble() * 0.2; + double yMin = y - delta; + double yMax = y + delta; + + double std = 0.08 + rand.nextDouble() * 0.08; + + sinMin.add(x, yMin); + sinMax.add(x, yMax); + sinMain.add(x, y); + sinStd.add(x, y + std); + } + + // --- TAN series --- + XYSeries tanMin = new XYSeries("tan-min"); + XYSeries tanMax = new XYSeries("tan-max"); + XYSeries tanMain = new XYSeries("tan"); + XYSeries tanStd = new XYSeries("tan-std"); + + for (int i = 0; i < n; i++) { + double x = xMin + i * dx; + double y = Math.tan(x); + + if (y > 3) y = 3; + if (y < -3) y = -3; + + double delta = 0.3 + rand.nextDouble() * 0.3; + double yMin = y - delta; + double yMax = y + delta; + + double std = 0.2 + rand.nextDouble() * 0.2; + + tanMin.add(x, yMin); + tanMax.add(x, yMax); + tanMain.add(x, y); + tanStd.add(x, y + std); + } + + double globalMin = Double.POSITIVE_INFINITY; + double globalMax = Double.NEGATIVE_INFINITY; + for (int i = 0; i < n; i++) { + globalMin = Math.min(globalMin, sinMin.getY(i).doubleValue()); + globalMin = Math.min(globalMin, tanMin.getY(i).doubleValue()); + globalMax = Math.max(globalMax, sinMax.getY(i).doubleValue()); + globalMax = Math.max(globalMax, tanMax.getY(i).doubleValue()); + } + // Add padding + double pad = 0.1 * (globalMax - globalMin); + globalMin -= pad; + globalMax += pad; + + // --- Datasets --- + + // Dataset 0: sin min/max (for band) + XYSeriesCollection sinMinMaxDataset = new XYSeriesCollection(); + sinMinMaxDataset.addSeries(sinMax); // upper + sinMinMaxDataset.addSeries(sinMin); // lower + + // Dataset 1: tan min/max (for band) + XYSeriesCollection tanMinMaxDataset = new XYSeriesCollection(); + tanMinMaxDataset.addSeries(tanMax); // upper + tanMinMaxDataset.addSeries(tanMin); // lower + + // Dataset 2: main curves + XYSeriesCollection mainDataset = new XYSeriesCollection(); + mainDataset.addSeries(sinMain); + mainDataset.addSeries(tanMain); + + // Dataset 3: std diamonds + XYSeriesCollection stdDataset = new XYSeriesCollection(); + stdDataset.addSeries(sinStd); + stdDataset.addSeries(tanStd); + + // --- Chart skeleton --- + JFreeChart chart = ChartFactory.createXYLineChart( + "Min/Max Bands + STD Demo", + "x", + "y", + null, + PlotOrientation.VERTICAL, + true, + true, + false + ); + XYPlot plot = chart.getXYPlot(); + + // Transparent backgrounds + chart.setBackgroundPaint(Color.WHITE); + plot.setBackgroundPaint(Color.WHITE); + plot.setOutlinePaint(null); + plot.setDomainGridlinePaint(new Color(180, 180, 180)); // very light + plot.setRangeGridlinePaint(new Color(180, 180, 180)); + + plot.getDomainAxis().setAutoRange(false); // lock the axis so that they never resize + plot.getRangeAxis().setAutoRange(false); + plot.getDomainAxis().setRange(xLower, xUpper); + plot.getRangeAxis().setRange(globalMin, globalMax); + + // --- Legend to the right + chart.getLegend().setPosition(RectangleEdge.RIGHT); + chart.getLegend().setBackgroundPaint(Color.WHITE); +// chart.getLegend().setFrame(BlockBorder.NONE); + + // --- Renderer 0: sin band --- + XYDifferenceRenderer sinBandRenderer = new XYDifferenceRenderer(); + Color sinBandColor = new Color(sinColor.getRed(), sinColor.getGreen(), sinColor.getBlue(), 40); + sinBandRenderer.setPositivePaint(sinBandColor); + sinBandRenderer.setNegativePaint(sinBandColor); + sinBandRenderer.setSeriesStroke(0, new BasicStroke(0f)); + sinBandRenderer.setSeriesStroke(1, new BasicStroke(0f)); +// sinBandRenderer.setOutlinePaint(null); + sinBandRenderer.setSeriesVisibleInLegend(0, false); // hide sin-max legend entries + sinBandRenderer.setSeriesVisibleInLegend(1, false); + + plot.setDataset(0, sinMinMaxDataset); + plot.setRenderer(0, sinBandRenderer); + + // --- Renderer 1: tan band --- + XYDifferenceRenderer tanBandRenderer = new XYDifferenceRenderer(); + Color tanBandColor = new Color(tanColor.getRed(), tanColor.getGreen(), tanColor.getBlue(), 40); + tanBandRenderer.setPositivePaint(tanBandColor); + tanBandRenderer.setNegativePaint(tanBandColor); + tanBandRenderer.setSeriesStroke(0, new BasicStroke(0f)); + tanBandRenderer.setSeriesStroke(1, new BasicStroke(0f)); +// tanBandRenderer.setOutlinePaint(null); + tanBandRenderer.setSeriesVisibleInLegend(0, false); + tanBandRenderer.setSeriesVisibleInLegend(1, false); + + plot.setDataset(1, tanMinMaxDataset); + plot.setRenderer(1, tanBandRenderer); + + // --- Renderer 2: main curves --- + XYLineAndShapeRenderer lineRenderer = new XYLineAndShapeRenderer(true, false); + lineRenderer.setSeriesPaint(0, sinColor); + lineRenderer.setSeriesPaint(1, tanColor); + lineRenderer.setSeriesStroke(0, new BasicStroke(2f)); + lineRenderer.setSeriesStroke(1, new BasicStroke(2f)); + lineRenderer.setSeriesVisibleInLegend(0, true); // keep the main curves visible in Legend + lineRenderer.setSeriesVisibleInLegend(1, true); + + plot.setDataset(2, mainDataset); + plot.setRenderer(2, lineRenderer); + + // --- Renderer 3: STD diamonds --- + XYLineAndShapeRenderer stdRenderer = new XYLineAndShapeRenderer(false, true); + Shape diamond = createDiamondShape(4); + + stdRenderer.setSeriesShape(0, diamond); + stdRenderer.setSeriesShape(1, diamond); + stdRenderer.setSeriesPaint(0, sinColor.darker()); + stdRenderer.setSeriesPaint(1, tanColor.darker()); + stdRenderer.setSeriesVisibleInLegend(0, false); + stdRenderer.setSeriesVisibleInLegend(1, false); + + plot.setDataset(3, stdDataset); + plot.setRenderer(3, stdRenderer); + + // --- ChartPanel --- + ChartPanel panel = new ChartPanel(chart); + panel.setOpaque(true); // must be opaque for white to show + panel.setBackground(Color.WHITE); + + // --- checkboxes to control display + JPanel controls = new JPanel(); + JCheckBox cbAvg = new JCheckBox("Averages", true); + JCheckBox cbMinMax = new JCheckBox("Min-Max", false); + JCheckBox cbStd = new JCheckBox("STD", false); + controls.add(cbAvg); + controls.add(cbMinMax); + controls.add(cbStd); + + // Averages (dataset 2) + cbAvg.addActionListener(e -> { + boolean on = cbAvg.isSelected(); + plot.getRenderer(2).setSeriesVisible(0, on); + plot.getRenderer(2).setSeriesVisible(1, on); + lineRenderer.setSeriesVisibleInLegend(0, true); // force legend visible + lineRenderer.setSeriesVisibleInLegend(1, true); + }); + + // Min-Max (datasets 0 and 1) + cbMinMax.addActionListener(e -> { + boolean on = cbMinMax.isSelected(); + plot.setRenderer(0, on ? sinBandRenderer : null); + plot.setRenderer(1, on ? tanBandRenderer : null); + }); + + // STD (dataset 3) + cbStd.addActionListener(e -> { + boolean on = cbStd.isSelected(); + stdRenderer.setSeriesVisible(0, on); + stdRenderer.setSeriesVisible(1, on); + }); + + // --- Frame --- + JFrame frame = new JFrame("JFreeChart Min/Max/STD Demo"); + frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + frame.add(panel, BorderLayout.CENTER); + frame.add(controls, BorderLayout.SOUTH); + frame.setSize(900, 600); + frame.setLocationRelativeTo(null); + frame.setVisible(true); + }); + } + + private static Shape createDiamondShape(int size) { + Path2D.Double p = new Path2D.Double(); + p.moveTo(0, -size); + p.lineTo(size, 0); + p.lineTo(0, size); + p.lineTo(-size, 0); + p.closePath(); + return p; + } + } diff --git a/vcell-client/pom.xml b/vcell-client/pom.xml index 238ddacbcb..051b3ede55 100644 --- a/vcell-client/pom.xml +++ b/vcell-client/pom.xml @@ -104,6 +104,12 @@ 10.0.5 + + org.jfree + jfreechart + 1.5.5 + + org.junit.jupiter junit-jupiter diff --git a/vcell-client/src/main/java/cbit/vcell/client/data/ODEDataViewer.java b/vcell-client/src/main/java/cbit/vcell/client/data/ODEDataViewer.java index 0bbc889c1f..f1acb4e218 100644 --- a/vcell-client/src/main/java/cbit/vcell/client/data/ODEDataViewer.java +++ b/vcell-client/src/main/java/cbit/vcell/client/data/ODEDataViewer.java @@ -333,6 +333,8 @@ public ClusterVisualizationPanel getClusterVisualizationPanel() { try { clusterVisualizationPanel = new ClusterVisualizationPanel(this); clusterVisualizationPanel.setName("ClusterVisualizationPanel"); + SpecialtyTableRenderer str = new RenderDataViewerDoubleWithTooltip(); + clusterVisualizationPanel.setSpecialityRenderer(str); } catch (java.lang.Throwable ivjExc) { handleException(ivjExc); } diff --git a/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterVisualizationPanel.java b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterVisualizationPanel.java index 14e6308209..137f3e8536 100644 --- a/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterVisualizationPanel.java +++ b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterVisualizationPanel.java @@ -1,5 +1,19 @@ package cbit.vcell.solver.ode.gui; +import org.jfree.chart.ChartFactory; +import org.jfree.chart.ChartPanel; +import org.jfree.chart.JFreeChart; +import org.jfree.chart.block.BlockBorder; +import org.jfree.chart.labels.StandardXYItemLabelGenerator; +import org.jfree.chart.plot.PlotOrientation; +import org.jfree.chart.plot.XYPlot; +import org.jfree.chart.renderer.xy.XYDifferenceRenderer; +import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer; +import org.jfree.chart.ui.RectangleEdge; +import org.jfree.data.xy.XYDataset; +import org.jfree.data.xy.XYSeries; +import org.jfree.data.xy.XYSeriesCollection; + import cbit.plot.gui.ClusterPlotPanel; import cbit.vcell.client.data.ODEDataViewer; import cbit.vcell.client.desktop.biomodel.DocumentEditorSubPanel; @@ -10,6 +24,7 @@ import cbit.vcell.util.ColumnDescription; import org.vcell.util.ColorUtil; import org.vcell.util.gui.JToolBarToggleButton; +import org.vcell.util.gui.SpecialtyTableRenderer; import org.vcell.util.gui.VCellIcons; import javax.swing.*; @@ -18,11 +33,14 @@ import javax.swing.event.ChangeListener; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; +import javax.swing.table.TableCellRenderer; +import javax.swing.table.TableColumn; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.ComponentAdapter; import java.awt.event.ComponentEvent; +import java.awt.geom.Path2D; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.util.*; @@ -38,8 +56,6 @@ public class ClusterVisualizationPanel extends DocumentEditorSubPanel { private final java.util.List globalPalette = new ArrayList<>(); private int nextColorIndex = 0; - private ClusterSpecificationPanel.ClusterSelection currentSelection = null; - private JPanel ivjJPanel1 = null; private JPanel ivjJPanelPlot = null; private ClusterPlotPanel clusterPlotPanel = null; // here are the plots being drawn @@ -75,12 +91,12 @@ public void propertyChange(PropertyChangeEvent evt) { ClusterSpecificationPanel.ClusterSelection sel = (ClusterSpecificationPanel.ClusterSelection) evt.getNewValue(); ensureColorsAssigned(sel.columns); try { + redrawLegend(sel); // redraw legend (one plot, multiple curves) redrawPlot(sel); // redraw plot (one plot, multiple curves) + redrawDataTable(sel); // redraw data table } catch (ExpressionException e) { throw new RuntimeException(e); } - redrawLegend(sel); // update legend (one plot, multiple curves) - updateDataTable(sel); // update data table return; } } @@ -156,7 +172,7 @@ private JLabel getJLabelBottom() { } return ivjJLabelBottom; } - private ClusterPlotPanel getClusterPlotPanel() { // actual plotting is done here + private ClusterPlotPanel getClusterPlotPanel() { // actual plotting is shown here if (clusterPlotPanel == null) { try { clusterPlotPanel = new ClusterPlotPanel(); @@ -165,17 +181,6 @@ private ClusterPlotPanel getClusterPlotPanel() { // actual plotting is done @Override public void componentShown(ComponentEvent e) { System.out.println("ClusterVisualizationPanel.componentShown() called, height = " + clusterPlotPanel.getHeight()); - // Only redraw when the panel is actually visible and sized - if (currentSelection != null) { - try { - System.out.println("ClusterVisualizationPanel.componentShown() calling redrawPlot() with current selection: " + currentSelection); - redrawPlot(currentSelection); - } catch (ExpressionException ex) { - ex.printStackTrace(); - } - } else { - System.out.println("ClusterVisualizationPanel.componentShown() no current selection, skipping redraw"); - } } }); @@ -185,7 +190,7 @@ public void componentShown(ComponentEvent e) { } return clusterPlotPanel; } - public JPanel getPlot2DDataPanel1() { + public JPanel getPlot2DDataPanel1() { // actual table shown here if (ivjPlot2DDataPanel1 == null) { try { ivjPlot2DDataPanel1 = new JPanel(); @@ -415,7 +420,7 @@ private void redrawPlot(ClusterSpecificationPanel.ClusterSelection sel) throws E System.out.println("ClusterVisualizationPanel.redrawPlot() selection is null"); } System.out.println("ClusterVisualizationPanel.redrawPlot(), height = " + getClusterPlotPanel().getHeight()); - currentSelection = sel; +// currentSelection = sel; List columns = sel.columns; ODESolverResultSet srs = sel.resultSet; @@ -451,9 +456,333 @@ private void redrawLegend(ClusterSpecificationPanel.ClusterSelection sel) { getJPanelPlotLegends().revalidate(); getJPanelPlotLegends().repaint(); } - private void updateDataTable(ClusterSpecificationPanel.ClusterSelection sel) { + private void redrawDataTable(ClusterSpecificationPanel.ClusterSelection sel) throws ExpressionException { System.out.println("ClusterVisualizationPanel.updateDataTable() called"); + + JPanel container = getPlot2DDataPanel1(); + container.removeAll(); + container.setLayout(new BorderLayout()); + + if (sel == null || sel.resultSet == null || sel.columns == null || sel.columns.isEmpty()) { + container.add(new JLabel("No data to display"), BorderLayout.CENTER); + container.revalidate(); + container.repaint(); + return; + } + + ODESolverResultSet srs = sel.resultSet; + java.util.List columns = sel.columns; + + // time column + int timeIndex = srs.findColumn("t"); + double[] times = srs.extractColumn(timeIndex); + int rowCount = times.length; + + // column names: t + one per selected column + String[] columnNames = new String[1 + columns.size()]; + columnNames[0] = "t"; + for (int i = 0; i < columns.size(); i++) { + ColumnDescription cd = columns.get(i); + // you can decorate with mode if you want, e.g. "[COUNTS] name" + columnNames[i + 1] = cd.getName(); + } + + // data + Object[][] data = new Object[rowCount][columnNames.length]; + for (int r = 0; r < rowCount; r++) { + int c = 0; + data[r][c++] = times[r]; + for (ColumnDescription cd : columns) { + int idx = srs.findColumn(cd.getName()); + double[] y = srs.extractColumn(idx); + data[r][c++] = y[r]; + } + } + + JTable table = new JTable(data, columnNames); + table.setAutoResizeMode(JTable.AUTO_RESIZE_OFF); + table.setFillsViewportHeight(true); + + autoSizeTableColumns(table); // resize to make it look better + + JScrollPane scrollPane = new JScrollPane(table); + container.add(scrollPane, BorderLayout.CENTER); + + container.revalidate(); + container.repaint(); + + } + public void setSpecialityRenderer(SpecialtyTableRenderer str) { + // TODO: implement this +// getPlot2DDataPanel1().setSpecialityRenderer(str); + } + + private void autoSizeTableColumns(JTable table) { + final int margin = 14; // some breathing room + + for (int col = 0; col < table.getColumnCount(); col++) { + TableColumn column = table.getColumnModel().getColumn(col); + + int maxWidth = 0; + + // header width + TableCellRenderer headerRenderer = table.getTableHeader().getDefaultRenderer(); + Component headerComp = headerRenderer.getTableCellRendererComponent( + table, column.getHeaderValue(), false, false, 0, col); + maxWidth = Math.max(maxWidth, headerComp.getPreferredSize().width); + + // cell widths + for (int row = 0; row < table.getRowCount(); row++) { + TableCellRenderer cellRenderer = table.getCellRenderer(row, col); + Component comp = table.prepareRenderer(cellRenderer, row, col); + maxWidth = Math.max(maxWidth, comp.getPreferredSize().width); + } + + column.setPreferredWidth(maxWidth + margin); + } } + // =================================================== Evaluate JFreeChart capabilities with a simple demo === + + public static void main(String[] args) { + SwingUtilities.invokeLater(() -> { + + Random rand = new Random(); + + int n = 50; + double xMin = 0.0; + double xMax = 7.0; + double dx = (xMax - xMin) / (n - 1); + + double xLower = 0.0; // these are the limits for the x values of the axis + double xUpper = 7.5; + + Color sinColor = new Color(31, 119, 180); // blue + Color tanColor = new Color(255, 127, 14); // orange + + // --- SIN series --- + XYSeries sinMin = new XYSeries("sin-min"); + XYSeries sinMax = new XYSeries("sin-max"); + XYSeries sinMain = new XYSeries("sin"); + XYSeries sinStd = new XYSeries("sin-std"); + + for (int i = 0; i < n; i++) { + double x = xMin + i * dx; + double y = Math.sin(x); + + double delta = 0.2 + rand.nextDouble() * 0.2; + double yMin = y - delta; + double yMax = y + delta; + + double std = 0.08 + rand.nextDouble() * 0.08; + + sinMin.add(x, yMin); + sinMax.add(x, yMax); + sinMain.add(x, y); + sinStd.add(x, y + std); + } + + // --- TAN series --- + XYSeries tanMin = new XYSeries("tan-min"); + XYSeries tanMax = new XYSeries("tan-max"); + XYSeries tanMain = new XYSeries("tan"); + XYSeries tanStd = new XYSeries("tan-std"); + + for (int i = 0; i < n; i++) { + double x = xMin + i * dx; + double y = Math.tan(x); + + if (y > 3) y = 3; + if (y < -3) y = -3; + + double delta = 0.3 + rand.nextDouble() * 0.3; + double yMin = y - delta; + double yMax = y + delta; + + double std = 0.2 + rand.nextDouble() * 0.2; + + tanMin.add(x, yMin); + tanMax.add(x, yMax); + tanMain.add(x, y); + tanStd.add(x, y + std); + } + + double globalMin = Double.POSITIVE_INFINITY; + double globalMax = Double.NEGATIVE_INFINITY; + for (int i = 0; i < n; i++) { + globalMin = Math.min(globalMin, sinMin.getY(i).doubleValue()); + globalMin = Math.min(globalMin, tanMin.getY(i).doubleValue()); + globalMax = Math.max(globalMax, sinMax.getY(i).doubleValue()); + globalMax = Math.max(globalMax, tanMax.getY(i).doubleValue()); + } + // Add padding + double pad = 0.1 * (globalMax - globalMin); + globalMin -= pad; + globalMax += pad; + + // --- Datasets --- + + // Dataset 0: sin min/max (for band) + XYSeriesCollection sinMinMaxDataset = new XYSeriesCollection(); + sinMinMaxDataset.addSeries(sinMax); // upper + sinMinMaxDataset.addSeries(sinMin); // lower + + // Dataset 1: tan min/max (for band) + XYSeriesCollection tanMinMaxDataset = new XYSeriesCollection(); + tanMinMaxDataset.addSeries(tanMax); // upper + tanMinMaxDataset.addSeries(tanMin); // lower + + // Dataset 2: main curves + XYSeriesCollection mainDataset = new XYSeriesCollection(); + mainDataset.addSeries(sinMain); + mainDataset.addSeries(tanMain); + + // Dataset 3: std diamonds + XYSeriesCollection stdDataset = new XYSeriesCollection(); + stdDataset.addSeries(sinStd); + stdDataset.addSeries(tanStd); + + // --- Chart skeleton --- + JFreeChart chart = ChartFactory.createXYLineChart( + "Min/Max Bands + STD Demo", + "x", + "y", + null, + PlotOrientation.VERTICAL, + true, + true, + false + ); + XYPlot plot = chart.getXYPlot(); + + // Transparent backgrounds + chart.setBackgroundPaint(Color.WHITE); + plot.setBackgroundPaint(Color.WHITE); + plot.setOutlinePaint(null); + plot.setDomainGridlinePaint(new Color(180, 180, 180)); // very light + plot.setRangeGridlinePaint(new Color(180, 180, 180)); + + plot.getDomainAxis().setAutoRange(false); // lock the axis so that they never resize + plot.getRangeAxis().setAutoRange(false); + plot.getDomainAxis().setRange(xLower, xUpper); + plot.getRangeAxis().setRange(globalMin, globalMax); + + // --- Legend to the right + chart.getLegend().setPosition(RectangleEdge.RIGHT); + chart.getLegend().setBackgroundPaint(Color.WHITE); +// chart.getLegend().setFrame(BlockBorder.NONE); + + // --- Renderer 0: sin band --- + XYDifferenceRenderer sinBandRenderer = new XYDifferenceRenderer(); + Color sinBandColor = new Color(sinColor.getRed(), sinColor.getGreen(), sinColor.getBlue(), 40); + sinBandRenderer.setPositivePaint(sinBandColor); + sinBandRenderer.setNegativePaint(sinBandColor); + sinBandRenderer.setSeriesStroke(0, new BasicStroke(0f)); + sinBandRenderer.setSeriesStroke(1, new BasicStroke(0f)); +// sinBandRenderer.setOutlinePaint(null); + sinBandRenderer.setSeriesVisibleInLegend(0, false); // hide sin-max legend entries + sinBandRenderer.setSeriesVisibleInLegend(1, false); + + plot.setDataset(0, sinMinMaxDataset); + plot.setRenderer(0, sinBandRenderer); + + // --- Renderer 1: tan band --- + XYDifferenceRenderer tanBandRenderer = new XYDifferenceRenderer(); + Color tanBandColor = new Color(tanColor.getRed(), tanColor.getGreen(), tanColor.getBlue(), 40); + tanBandRenderer.setPositivePaint(tanBandColor); + tanBandRenderer.setNegativePaint(tanBandColor); + tanBandRenderer.setSeriesStroke(0, new BasicStroke(0f)); + tanBandRenderer.setSeriesStroke(1, new BasicStroke(0f)); +// tanBandRenderer.setOutlinePaint(null); + tanBandRenderer.setSeriesVisibleInLegend(0, false); + tanBandRenderer.setSeriesVisibleInLegend(1, false); + + plot.setDataset(1, tanMinMaxDataset); + plot.setRenderer(1, tanBandRenderer); + + // --- Renderer 2: main curves --- + XYLineAndShapeRenderer lineRenderer = new XYLineAndShapeRenderer(true, false); + lineRenderer.setSeriesPaint(0, sinColor); + lineRenderer.setSeriesPaint(1, tanColor); + lineRenderer.setSeriesStroke(0, new BasicStroke(2f)); + lineRenderer.setSeriesStroke(1, new BasicStroke(2f)); + lineRenderer.setSeriesVisibleInLegend(0, true); // keep the main curves visible in Legend + lineRenderer.setSeriesVisibleInLegend(1, true); + + plot.setDataset(2, mainDataset); + plot.setRenderer(2, lineRenderer); + + // --- Renderer 3: STD diamonds --- + XYLineAndShapeRenderer stdRenderer = new XYLineAndShapeRenderer(false, true); + Shape diamond = createDiamondShape(4); + + stdRenderer.setSeriesShape(0, diamond); + stdRenderer.setSeriesShape(1, diamond); + stdRenderer.setSeriesPaint(0, sinColor.darker()); + stdRenderer.setSeriesPaint(1, tanColor.darker()); + stdRenderer.setSeriesVisibleInLegend(0, false); + stdRenderer.setSeriesVisibleInLegend(1, false); + + plot.setDataset(3, stdDataset); + plot.setRenderer(3, stdRenderer); + + // --- ChartPanel --- + ChartPanel panel = new ChartPanel(chart); + panel.setOpaque(true); // must be opaque for white to show + panel.setBackground(Color.WHITE); + + // --- checkboxes to control display + JPanel controls = new JPanel(); + JCheckBox cbAvg = new JCheckBox("Averages", true); + JCheckBox cbMinMax = new JCheckBox("Min-Max", false); + JCheckBox cbStd = new JCheckBox("STD", false); + controls.add(cbAvg); + controls.add(cbMinMax); + controls.add(cbStd); + + // Averages (dataset 2) + cbAvg.addActionListener(e -> { + boolean on = cbAvg.isSelected(); + plot.getRenderer(2).setSeriesVisible(0, on); + plot.getRenderer(2).setSeriesVisible(1, on); + lineRenderer.setSeriesVisibleInLegend(0, true); // force legend visible + lineRenderer.setSeriesVisibleInLegend(1, true); + }); + + // Min-Max (datasets 0 and 1) + cbMinMax.addActionListener(e -> { + boolean on = cbMinMax.isSelected(); + plot.setRenderer(0, on ? sinBandRenderer : null); + plot.setRenderer(1, on ? tanBandRenderer : null); + }); + + // STD (dataset 3) + cbStd.addActionListener(e -> { + boolean on = cbStd.isSelected(); + stdRenderer.setSeriesVisible(0, on); + stdRenderer.setSeriesVisible(1, on); + }); + + // --- Frame --- + JFrame frame = new JFrame("JFreeChart Min/Max/STD Demo"); + frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + frame.add(panel, BorderLayout.CENTER); + frame.add(controls, BorderLayout.SOUTH); + frame.setSize(900, 600); + frame.setLocationRelativeTo(null); + frame.setVisible(true); + }); + } + + private static Shape createDiamondShape(int size) { + Path2D.Double p = new Path2D.Double(); + p.moveTo(0, -size); + p.lineTo(size, 0); + p.lineTo(0, size); + p.lineTo(-size, 0); + p.closePath(); + return p; + } + } From de35730a8f5418b01f18f07faa60ef7a7d8e69f1 Mon Sep 17 00:00:00 2001 From: Dan Vasilescu Date: Thu, 12 Mar 2026 17:35:41 -0400 Subject: [PATCH 12/31] cluster data panel - table view --- .../java/cbit/plot/gui/ClusterDataPanel.java | 262 ++++++++++++++++++ .../java/cbit/plot/gui/Plot2DDataPanel.java | 89 ------ .../ode/gui/ClusterVisualizationPanel.java | 102 +------ 3 files changed, 272 insertions(+), 181 deletions(-) create mode 100644 vcell-client/src/main/java/cbit/plot/gui/ClusterDataPanel.java diff --git a/vcell-client/src/main/java/cbit/plot/gui/ClusterDataPanel.java b/vcell-client/src/main/java/cbit/plot/gui/ClusterDataPanel.java new file mode 100644 index 0000000000..b634277218 --- /dev/null +++ b/vcell-client/src/main/java/cbit/plot/gui/ClusterDataPanel.java @@ -0,0 +1,262 @@ +package cbit.plot.gui; + +import cbit.vcell.parser.ExpressionException; +import cbit.vcell.solver.ode.ODESolverResultSet; +import cbit.vcell.solver.ode.gui.ClusterSpecificationPanel; +import cbit.vcell.util.ColumnDescription; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.vcell.util.gui.NonEditableDefaultTableModel; +import org.vcell.util.gui.ScrollTable; +import org.vcell.util.gui.SpecialtyTableRenderer; + +import javax.swing.*; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; +import javax.swing.table.DefaultTableCellRenderer; +import javax.swing.table.TableCellRenderer; +import javax.swing.table.TableColumn; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; + +public class ClusterDataPanel extends JPanel { + + private static final Logger LG = LogManager.getLogger(ClusterDataPanel.class); + + private ScrollTable scrollPaneTable; + private NonEditableDefaultTableModel nonEditableDefaultTableModel = null; + + private final IvjEventHandler ivjEventHandler = new IvjEventHandler(); + + class IvjEventHandler implements ActionListener, MouseListener, PropertyChangeListener, ChangeListener { + + @Override + public void mouseClicked(MouseEvent e) { + if (e.getSource() == getScrollPaneTable()) { + int row = getScrollPaneTable().rowAtPoint(e.getPoint()); + int col = getScrollPaneTable().columnAtPoint(e.getPoint()); + System.out.println("ClusterDataPanel: clicked row=" + row + " col=" + col); + } + } + + @Override public void mousePressed(MouseEvent e) {} + @Override public void mouseReleased(MouseEvent e) {} + @Override public void mouseEntered(MouseEvent e) {} + @Override public void mouseExited(MouseEvent e) {} + + @Override + public void actionPerformed(ActionEvent e) { + // reserved for future buttons or context menu actions + } + + @Override + public void propertyChange(PropertyChangeEvent evt) { + // reserved for future dynamic formatting + } + + @Override + public void stateChanged(ChangeEvent e) { + // reserved for future slider/spinner interactions + } + } + + private class ClusterHeaderRenderer extends DefaultTableCellRenderer { + + private final TableCellRenderer base; + public ClusterHeaderRenderer(TableCellRenderer base) { + this.base = base; + } + + @Override + public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, + boolean hasFocus, int row, int column) { + // First let ScrollTable’s renderer do its work + Component c = base.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); + if (!(c instanceof JLabel)) { + return c; // safety + } + JLabel lbl = (JLabel) c; + String name = value == null ? "" : value.toString(); + + // Read mode from table metadata + ClusterSpecificationPanel.DisplayMode mode = (ClusterSpecificationPanel.DisplayMode) + ((JComponent)table).getClientProperty("ClusterDisplayMode"); + + // First-time creation: no mode yet + if (mode == null) { + lbl.setToolTipText(null); + return lbl; // leave ScrollTable’s default header styling intact + } + + String unit = ""; + String tooltip = ""; + if (column == 0) { + unit = "seconds"; + tooltip = "Simulation time in seconds"; + } else { + switch (mode) { + case COUNTS: + unit = "molecules"; + tooltip = "Number of clusters of size " + name + " molecules"; + break; + case MEAN: + unit = "value"; + tooltip = "Cluster mean statistic for " + name; + break; + case OVERALL: + unit = "value"; + tooltip = "Cluster overall statistic for " + name; + break; + } + } + lbl.setText("" + name + " [" + unit + "]"); + lbl.setToolTipText(tooltip); + return lbl; + } + } + + public ClusterDataPanel() { + super(); + initialize(); + } + private void initialize() { + try { + setName("ClusterDataPanel"); + setLayout(new java.awt.BorderLayout()); + setSize(541, 348); + add(getScrollPaneTable().getEnclosingScrollPane(), BorderLayout.CENTER); + JLabel lblNewLabel = new JLabel("To Copy table data or Export as HDF5, select rows/cells and use the right mouse button menu."); + add(lblNewLabel, BorderLayout.SOUTH); + initConnections(); +// controlKeys(); + } catch (Throwable exc) { + handleException(exc); + } + } + private void initConnections() throws java.lang.Exception { + this.addPropertyChangeListener(ivjEventHandler); + getScrollPaneTable().addMouseListener(ivjEventHandler); + getScrollPaneTable().setModel(getNonEditableDefaultTableModel()); + getScrollPaneTable().createDefaultColumnsFromModel(); + TableCellRenderer baseHeaderRenderer = getScrollPaneTable().getTableHeader().getDefaultRenderer(); + getScrollPaneTable().getTableHeader().setDefaultRenderer(new ClusterHeaderRenderer(baseHeaderRenderer)); + + } + private void handleException(java.lang.Throwable exception) { + System.out.println("--------- UNCAUGHT EXCEPTION ---------"); + exception.printStackTrace(System.out); + } + + // ----------------------------------------------------------- + private ScrollTable getScrollPaneTable() { + if (scrollPaneTable == null) { + try { + scrollPaneTable = new ScrollTable(); + scrollPaneTable.setName("ScrollPaneTable"); + scrollPaneTable.setCellSelectionEnabled(true); + scrollPaneTable.setBounds(0, 0, 200, 200); + } catch (java.lang.Throwable ivjExc) { + handleException(ivjExc); + } + } + return scrollPaneTable; + } + + private NonEditableDefaultTableModel getNonEditableDefaultTableModel() { + if (nonEditableDefaultTableModel == null) { + try { + nonEditableDefaultTableModel = new NonEditableDefaultTableModel(); + } catch (java.lang.Throwable ivjExc) { + handleException(ivjExc); + } + } + return nonEditableDefaultTableModel; + } + + public void setSpecialityRenderer(SpecialtyTableRenderer str) { + // TODO: write some appropriate renderer when we decide what to show in the tooltip + // use RendererViewerDoubleWithTooltip for inspiration +// getScrollPaneTable().setSpecialityRenderer(str); + } + + + public void updateData(ClusterSpecificationPanel.ClusterSelection sel) throws ExpressionException { + if(sel == null) { + getScrollPaneTable().putClientProperty("ClusterDisplayMode", null); + } else { + getScrollPaneTable().putClientProperty("ClusterDisplayMode", sel.mode); + } + if (sel == null || sel.resultSet == null || sel.columns == null || sel.columns.isEmpty()) { + getNonEditableDefaultTableModel().setDataVector(new Object[][]{}, new Object[]{"No data"}); + getScrollPaneTable().createDefaultColumnsFromModel(); + revalidate(); + repaint(); + return; + } + ODESolverResultSet srs = sel.resultSet; + java.util.List columns = sel.columns; + + int timeIndex = srs.findColumn("t"); + double[] times = srs.extractColumn(timeIndex); + int rowCount = times.length; + + // column names + String[] columnNames = new String[1 + columns.size()]; + columnNames[0] = "t"; + for (int i = 0; i < columns.size(); i++) { + columnNames[i + 1] = columns.get(i).getName(); + } + + // data + Object[][] data = new Object[rowCount][columnNames.length]; + for (int r = 0; r < rowCount; r++) { + int c = 0; + data[r][c++] = times[r]; + for (ColumnDescription cd : columns) { + int idx = srs.findColumn(cd.getName()); + double[] y = srs.extractColumn(idx); + data[r][c++] = y[r]; + } + } + + // update existing model + getNonEditableDefaultTableModel().setDataVector(data, columnNames); + + // refresh table columns + getScrollPaneTable().createDefaultColumnsFromModel(); + autoSizeTableColumns(getScrollPaneTable()); + + revalidate(); + repaint(); + } + + private void autoSizeTableColumns(JTable table) { + final int margin = 12; + + for (int col = 0; col < table.getColumnCount(); col++) { + TableColumn column = table.getColumnModel().getColumn(col); + + int maxWidth = 0; + + // header + TableCellRenderer headerRenderer = table.getTableHeader().getDefaultRenderer(); + Component headerComp = headerRenderer.getTableCellRendererComponent( + table, column.getHeaderValue(), false, false, 0, col); + maxWidth = Math.max(maxWidth, headerComp.getPreferredSize().width); + + // cells + for (int row = 0; row < table.getRowCount(); row++) { + TableCellRenderer cellRenderer = table.getCellRenderer(row, col); + Component comp = table.prepareRenderer(cellRenderer, row, col); + maxWidth = Math.max(maxWidth, comp.getPreferredSize().width); + } + + column.setPreferredWidth(maxWidth + margin); + } + } +} \ No newline at end of file diff --git a/vcell-client/src/main/java/cbit/plot/gui/Plot2DDataPanel.java b/vcell-client/src/main/java/cbit/plot/gui/Plot2DDataPanel.java index 0c669b6f08..099171907c 100644 --- a/vcell-client/src/main/java/cbit/plot/gui/Plot2DDataPanel.java +++ b/vcell-client/src/main/java/cbit/plot/gui/Plot2DDataPanel.java @@ -130,17 +130,10 @@ private void exportHDF5() { /** * connEtoC3: (Plot2DDataPanel.initialize() --> Plot2DDataPanel.controlKeys()V) */ -/* WARNING: THIS METHOD WILL BE REGENERATED. */ private void connEtoC3() { try { - // user code begin {1} - // user code end this.controlKeys(); - // user code begin {2} - // user code end } catch (java.lang.Throwable ivjExc) { - // user code begin {3} - // user code end handleException(ivjExc); } } @@ -149,21 +142,14 @@ private void connEtoC3() { * connEtoM1: (plot2D1.this --> DefaultTableModel1.setDataVector([[Ljava.lang.Object;[Ljava.lang.Object;)V) * @param value cbit.plot.Plot2D */ -/* WARNING: THIS METHOD WILL BE REGENERATED. */ private void connEtoM1(Plot2D value) { try { - // user code begin {1} - // user code end if (getplot2D1() != null) { getNonEditableDefaultTableModel1().setDataVector(getplot2D1().getVisiblePlotDataValuesByRow(), getplot2D1().getVisiblePlotColumnTitles()); }else{ getNonEditableDefaultTableModel1().setDataVector((Object [][])null,(Object [])null); } - // user code begin {2} - // user code end } catch (java.lang.Throwable ivjExc) { - // user code begin {3} - // user code end handleException(ivjExc); } } @@ -172,19 +158,12 @@ private void connEtoM1(Plot2D value) { * connEtoM2: (plot2D1.change.stateChanged(javax.swing.event.ChangeEvent) --> NonEditableDefaultTableModel1.setDataVector([[Ljava.lang.Object;[Ljava.lang.Object;)V) * @param arg1 javax.swing.event.ChangeEvent */ -/* WARNING: THIS METHOD WILL BE REGENERATED. */ private void connEtoM2(javax.swing.event.ChangeEvent arg1) { try { - // user code begin {1} - // user code end if (getplot2D1() != null) { getNonEditableDefaultTableModel1().setDataVector(getplot2D1().getVisiblePlotDataValuesByRow(), getplot2D1().getVisiblePlotColumnTitles()); } - // user code begin {2} - // user code end } catch (java.lang.Throwable ivjExc) { - // user code begin {3} - // user code end handleException(ivjExc); } } @@ -193,17 +172,12 @@ private void connEtoM2(javax.swing.event.ChangeEvent arg1) { /** * connPtoP1SetTarget: (DefaultTableModel1.this <--> ScrollPaneTable.model) */ -/* WARNING: THIS METHOD WILL BE REGENERATED. */ private void connPtoP1SetTarget() { /* Set the target from the source */ try { getScrollPaneTable().setModel(getNonEditableDefaultTableModel1()); getScrollPaneTable().createDefaultColumnsFromModel(); - // user code begin {1} - // user code end } catch (java.lang.Throwable ivjExc) { - // user code begin {3} - // user code end handleException(ivjExc); } } @@ -211,25 +185,18 @@ private void connPtoP1SetTarget() { /** * connPtoP2SetSource: (Plot2DDataPanel.plot2D <--> plot2D1.this) */ -/* WARNING: THIS METHOD WILL BE REGENERATED. */ private void connPtoP2SetSource() { /* Set the source from the target */ try { if (ivjConnPtoP2Aligning == false) { - // user code begin {1} - // user code end ivjConnPtoP2Aligning = true; if ((getplot2D1() != null)) { this.setPlot2D(getplot2D1()); } - // user code begin {2} - // user code end ivjConnPtoP2Aligning = false; } } catch (java.lang.Throwable ivjExc) { ivjConnPtoP2Aligning = false; - // user code begin {3} - // user code end handleException(ivjExc); } } @@ -238,23 +205,16 @@ private void connPtoP2SetSource() { /** * connPtoP2SetTarget: (Plot2DDataPanel.plot2D <--> plot2D1.this) */ -/* WARNING: THIS METHOD WILL BE REGENERATED. */ private void connPtoP2SetTarget() { /* Set the target from the source */ try { if (ivjConnPtoP2Aligning == false) { - // user code begin {1} - // user code end ivjConnPtoP2Aligning = true; setplot2D1(this.getPlot2D()); - // user code begin {2} - // user code end ivjConnPtoP2Aligning = false; } } catch (java.lang.Throwable ivjExc) { ivjConnPtoP2Aligning = false; - // user code begin {3} - // user code end handleException(ivjExc); } } @@ -453,18 +413,13 @@ else if (copyAction == CopyAction.copyrow) { * Return the JMenuItemCopy property value. * @return javax.swing.JMenuItem */ -/* WARNING: THIS METHOD WILL BE REGENERATED. */ private javax.swing.JMenuItem getJMenuItemCopy() { if (ivjJMenuItemCopy == null) { try { ivjJMenuItemCopy = new javax.swing.JMenuItem(); ivjJMenuItemCopy.setName("JMenuItemCopy"); ivjJMenuItemCopy.setText("Copy Cells"); - // user code begin {1} - // user code end } catch (java.lang.Throwable ivjExc) { - // user code begin {2} - // user code end handleException(ivjExc); } } @@ -476,18 +431,13 @@ private javax.swing.JMenuItem getJMenuItemCopy() { * Return the JMenuItemCopyAll property value. * @return javax.swing.JMenuItem */ -/* WARNING: THIS METHOD WILL BE REGENERATED. */ private javax.swing.JMenuItem getJMenuItemCopyAll() { if (ivjJMenuItemCopyAll == null) { try { ivjJMenuItemCopyAll = new javax.swing.JMenuItem(); ivjJMenuItemCopyAll.setName("JMenuItemCopyAll"); ivjJMenuItemCopyAll.setText("Copy All"); - // user code begin {1} - // user code end } catch (java.lang.Throwable ivjExc) { - // user code begin {2} - // user code end handleException(ivjExc); } } @@ -501,11 +451,7 @@ private javax.swing.JMenuItem getJMenuItemExportHDF5() { ivjJMenuItemExportHDF5 = new javax.swing.JMenuItem(); ivjJMenuItemExportHDF5.setName("JMenuItemExportHDF5"); ivjJMenuItemExportHDF5.setText("Export Selected cells as HDF5 file"); - // user code begin {1} - // user code end } catch (java.lang.Throwable ivjExc) { - // user code begin {2} - // user code end handleException(ivjExc); } } @@ -518,11 +464,7 @@ private javax.swing.JMenuItem getJMenuItemCopyRow() { ivjJMenuItemCopyRow = new javax.swing.JMenuItem(); ivjJMenuItemCopyRow.setName("JMenuItemCopyRow"); ivjJMenuItemCopyRow.setText("Copy Rows"); - // user code begin {1} - // user code end } catch (java.lang.Throwable ivjExc) { - // user code begin {2} - // user code end handleException(ivjExc); } } @@ -537,11 +479,7 @@ private javax.swing.JPopupMenu getJPopupMenu1() { ivjJPopupMenu1.add(getJMenuItemCopyRow()); ivjJPopupMenu1.add(getJMenuItemCopyAll()); ivjJPopupMenu1.add(getJMenuItemExportHDF5()); - // user code begin {1} - // user code end } catch (java.lang.Throwable ivjExc) { - // user code begin {2} - // user code end handleException(ivjExc); } } @@ -552,16 +490,11 @@ private javax.swing.JPopupMenu getJPopupMenu1() { * Return the NonEditableDefaultTableModel1 property value. * @return cbit.gui.NonEditableDefaultTableModel */ -/* WARNING: THIS METHOD WILL BE REGENERATED. */ private org.vcell.util.gui.NonEditableDefaultTableModel getNonEditableDefaultTableModel1() { if (ivjNonEditableDefaultTableModel1 == null) { try { ivjNonEditableDefaultTableModel1 = new org.vcell.util.gui.NonEditableDefaultTableModel(); - // user code begin {1} - // user code end } catch (java.lang.Throwable ivjExc) { - // user code begin {2} - // user code end handleException(ivjExc); } } @@ -583,10 +516,7 @@ public Plot2D getPlot2D() { * Return the plot2D1 property value. * @return cbit.plot.Plot2D */ -/* WARNING: THIS METHOD WILL BE REGENERATED. */ private Plot2D getplot2D1() { - // user code begin {1} - // user code end return ivjplot2D1; } @@ -595,7 +525,6 @@ private Plot2D getplot2D1() { * Return the ScrollPaneTable property value. * @return javax.swing.JTable */ -/* WARNING: THIS METHOD WILL BE REGENERATED. */ private ScrollTable getScrollPaneTable() { if (ivjScrollPaneTable == null) { try { @@ -609,11 +538,7 @@ private ScrollTable getScrollPaneTable() { ivjScrollPaneTable.setDefaultRenderer(Object.class,rdwtt); ivjScrollPaneTable.setDefaultRenderer(Number.class,rdwtt); */ - // user code begin {1} - // user code end } catch (java.lang.Throwable ivjExc) { - // user code begin {2} - // user code end handleException(ivjExc); } } @@ -636,10 +561,7 @@ private void handleException(java.lang.Throwable exception) { * Initializes connections * @exception java.lang.Exception The exception description. */ -/* WARNING: THIS METHOD WILL BE REGENERATED. */ private void initConnections() throws java.lang.Exception { - // user code begin {1} - // user code end this.addPropertyChangeListener(ivjEventHandler); getScrollPaneTable().addMouseListener(ivjEventHandler); getJMenuItemCopy().addActionListener(ivjEventHandler); @@ -653,11 +575,8 @@ private void initConnections() throws java.lang.Exception { /** * Initialize the class. */ -/* WARNING: THIS METHOD WILL BE REGENERATED. */ private void initialize() { try { - // user code begin {1} - // user code end setName("Plot2DDataPanel"); setLayout(new java.awt.BorderLayout()); setSize(541, 348); @@ -670,8 +589,6 @@ private void initialize() { } catch (java.lang.Throwable ivjExc) { handleException(ivjExc); } - // user code begin {2} - // user code end } /** @@ -735,16 +652,10 @@ private void setplot2D1(Plot2D newValue) { connPtoP2SetSource(); connEtoM1(ivjplot2D1); firePropertyChange("plot2D", oldValue, newValue); - // user code begin {1} - // user code end } catch (java.lang.Throwable ivjExc) { - // user code begin {2} - // user code end handleException(ivjExc); } }; - // user code begin {3} - // user code end } /** diff --git a/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterVisualizationPanel.java b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterVisualizationPanel.java index 137f3e8536..25c875c2ef 100644 --- a/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterVisualizationPanel.java +++ b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterVisualizationPanel.java @@ -1,24 +1,20 @@ package cbit.vcell.solver.ode.gui; +import cbit.plot.gui.ClusterDataPanel; import org.jfree.chart.ChartFactory; import org.jfree.chart.ChartPanel; import org.jfree.chart.JFreeChart; -import org.jfree.chart.block.BlockBorder; -import org.jfree.chart.labels.StandardXYItemLabelGenerator; import org.jfree.chart.plot.PlotOrientation; import org.jfree.chart.plot.XYPlot; import org.jfree.chart.renderer.xy.XYDifferenceRenderer; import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer; import org.jfree.chart.ui.RectangleEdge; -import org.jfree.data.xy.XYDataset; import org.jfree.data.xy.XYSeries; import org.jfree.data.xy.XYSeriesCollection; import cbit.plot.gui.ClusterPlotPanel; import cbit.vcell.client.data.ODEDataViewer; import cbit.vcell.client.desktop.biomodel.DocumentEditorSubPanel; -import cbit.vcell.client.task.AsynchClientTask; -import cbit.vcell.client.task.ClientTaskDispatcher; import cbit.vcell.parser.ExpressionException; import cbit.vcell.solver.ode.ODESolverResultSet; import cbit.vcell.util.ColumnDescription; @@ -61,7 +57,7 @@ public class ClusterVisualizationPanel extends DocumentEditorSubPanel { private ClusterPlotPanel clusterPlotPanel = null; // here are the plots being drawn private JLabel ivjJLabelBottom = null; private JPanel ivjJPanelData = null; - private JPanel ivjPlot2DDataPanel1 = null; // here resides the data table + private ClusterDataPanel clusterDataPanel = null; // here resides the data table private JPanel bottomRightPanel = null; private JPanel ivjJPanelLegend = null; private JScrollPane ivjPlotLegendsScrollPane = null; @@ -156,7 +152,7 @@ private JPanel getJPanelData() { ivjJPanelData = new JPanel(); ivjJPanelData.setName("JPanelData"); ivjJPanelData.setLayout(new BorderLayout()); - ivjJPanelData.add(getPlot2DDataPanel1(), "Center"); + ivjJPanelData.add(getClusterDataPanel(), BorderLayout.CENTER); } return ivjJPanelData; } @@ -190,16 +186,15 @@ public void componentShown(ComponentEvent e) { } return clusterPlotPanel; } - public JPanel getPlot2DDataPanel1() { // actual table shown here - if (ivjPlot2DDataPanel1 == null) { + public ClusterDataPanel getClusterDataPanel() { // actual table shown here + if (clusterDataPanel == null) { try { - ivjPlot2DDataPanel1 = new JPanel(); - ivjPlot2DDataPanel1.setName("Plot2DDataPanel1"); + clusterDataPanel = new ClusterDataPanel(); } catch (java.lang.Throwable ivjExc) { handleException(ivjExc); } } - return ivjPlot2DDataPanel1; + return clusterDataPanel; } // --------------------------------------------------------------------- @@ -312,7 +307,7 @@ public void setBackground(Color color) { getJPanelPlotLegends().setBackground(color); getJPanel1().setBackground(color); getClusterPlotPanel().setBackground(color); - getPlot2DDataPanel1().setBackground(color); + getClusterDataPanel().setBackground(color); getJPanelData().setBackground(color); getJPanelPlot().setBackground(color); @@ -458,88 +453,11 @@ private void redrawLegend(ClusterSpecificationPanel.ClusterSelection sel) { } private void redrawDataTable(ClusterSpecificationPanel.ClusterSelection sel) throws ExpressionException { System.out.println("ClusterVisualizationPanel.updateDataTable() called"); - - JPanel container = getPlot2DDataPanel1(); - container.removeAll(); - container.setLayout(new BorderLayout()); - - if (sel == null || sel.resultSet == null || sel.columns == null || sel.columns.isEmpty()) { - container.add(new JLabel("No data to display"), BorderLayout.CENTER); - container.revalidate(); - container.repaint(); - return; - } - - ODESolverResultSet srs = sel.resultSet; - java.util.List columns = sel.columns; - - // time column - int timeIndex = srs.findColumn("t"); - double[] times = srs.extractColumn(timeIndex); - int rowCount = times.length; - - // column names: t + one per selected column - String[] columnNames = new String[1 + columns.size()]; - columnNames[0] = "t"; - for (int i = 0; i < columns.size(); i++) { - ColumnDescription cd = columns.get(i); - // you can decorate with mode if you want, e.g. "[COUNTS] name" - columnNames[i + 1] = cd.getName(); - } - - // data - Object[][] data = new Object[rowCount][columnNames.length]; - for (int r = 0; r < rowCount; r++) { - int c = 0; - data[r][c++] = times[r]; - for (ColumnDescription cd : columns) { - int idx = srs.findColumn(cd.getName()); - double[] y = srs.extractColumn(idx); - data[r][c++] = y[r]; - } - } - - JTable table = new JTable(data, columnNames); - table.setAutoResizeMode(JTable.AUTO_RESIZE_OFF); - table.setFillsViewportHeight(true); - - autoSizeTableColumns(table); // resize to make it look better - - JScrollPane scrollPane = new JScrollPane(table); - container.add(scrollPane, BorderLayout.CENTER); - - container.revalidate(); - container.repaint(); - + getClusterDataPanel().updateData(sel); } public void setSpecialityRenderer(SpecialtyTableRenderer str) { // TODO: implement this -// getPlot2DDataPanel1().setSpecialityRenderer(str); - } - - private void autoSizeTableColumns(JTable table) { - final int margin = 14; // some breathing room - - for (int col = 0; col < table.getColumnCount(); col++) { - TableColumn column = table.getColumnModel().getColumn(col); - - int maxWidth = 0; - - // header width - TableCellRenderer headerRenderer = table.getTableHeader().getDefaultRenderer(); - Component headerComp = headerRenderer.getTableCellRendererComponent( - table, column.getHeaderValue(), false, false, 0, col); - maxWidth = Math.max(maxWidth, headerComp.getPreferredSize().width); - - // cell widths - for (int row = 0; row < table.getRowCount(); row++) { - TableCellRenderer cellRenderer = table.getCellRenderer(row, col); - Component comp = table.prepareRenderer(cellRenderer, row, col); - maxWidth = Math.max(maxWidth, comp.getPreferredSize().width); - } - - column.setPreferredWidth(maxWidth + margin); - } + getClusterDataPanel().setSpecialityRenderer(str); } From 7df959426e58a83431870ab07f6fd10fb7accb99 Mon Sep 17 00:00:00 2001 From: Dan Vasilescu Date: Fri, 13 Mar 2026 18:04:23 -0400 Subject: [PATCH 13/31] cluster specification and visualization panel - visual refinements --- .../cbit/vcell/client/data/ODEDataViewer.java | 1 + .../ode/gui/ClusterSpecificationPanel.java | 56 ++++++++++++++++--- .../ode/gui/ClusterVisualizationPanel.java | 10 +++- .../simdata/LangevinSolverResultSet.java | 51 +++++++++++++++++ .../cbit/vcell/simdata/ODEDataManager.java | 4 +- 5 files changed, 113 insertions(+), 9 deletions(-) diff --git a/vcell-client/src/main/java/cbit/vcell/client/data/ODEDataViewer.java b/vcell-client/src/main/java/cbit/vcell/client/data/ODEDataViewer.java index f1acb4e218..2ec7948f14 100644 --- a/vcell-client/src/main/java/cbit/vcell/client/data/ODEDataViewer.java +++ b/vcell-client/src/main/java/cbit/vcell/client/data/ODEDataViewer.java @@ -484,6 +484,7 @@ public void setVcDataIdentifier(VCDataIdentifier vcDataIdentifier) { firePropertyChange("vcDataIdentifier", oldValue, vcDataIdentifier); outputSpeciesResultsPanel.refreshData(); getClusterSpecificationPanel().refreshData(); + getClusterVisualizationPanel().refreshData(); } public void setOdeDataContext() { diff --git a/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterSpecificationPanel.java b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterSpecificationPanel.java index 1f96086564..886e6e85ba 100644 --- a/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterSpecificationPanel.java +++ b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterSpecificationPanel.java @@ -2,6 +2,7 @@ import cbit.vcell.client.data.ODEDataViewer; import cbit.vcell.client.desktop.biomodel.DocumentEditorSubPanel; +import cbit.vcell.math.ODESolverResultSetColumnDescription; import cbit.vcell.simdata.LangevinSolverResultSet; import cbit.vcell.solver.SimulationModelInfo; import cbit.vcell.solver.ode.ODESolverResultSet; @@ -64,11 +65,15 @@ public void propertyChange(PropertyChangeEvent evt) { @Override public void valueChanged(ListSelectionEvent e) { if (e.getSource() == ClusterSpecificationPanel.this.getYAxisChoice() && !e.getValueIsAdjusting()) { + // extract selected ColumnDescriptions - java.util.List selected = yAxisChoiceList.getSelectedValuesList(); + java.util.List selected = getYAxisChoice().getSelectedValuesList(); DisplayMode mode = getCurrentDisplayMode(); ODESolverResultSet srs = getResultSetForMode(mode); + // set property to inform the list about current mode (needed for renderer) + yAxisChoiceList.putClientProperty("ClusterDisplayMode", mode); + // fire the event upward firePropertyChange("ClusterSelection", null, new ClusterSelection(mode, selected, srs)); } @@ -242,17 +247,54 @@ private JList getYAxisChoice() { public Component getListCellRendererComponent( JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) { - JLabel label = (JLabel) super.getListCellRendererComponent( list, value, index, isSelected, cellHasFocus); - ColumnDescription cd = (ColumnDescription) value; - label.setText(cd.getName()); // later: cd.getName() + " (molecules)" - + if (value instanceof ODESolverResultSetColumnDescription cd) { + // Always show the plain name + String name = cd.getName(); + label.setText(name); + + // Gray out trivial entries + if (cd.isTrivial()) { + label.setForeground(Color.GRAY); + } else { + label.setForeground(isSelected + ? list.getSelectionForeground() + : list.getForeground()); + } + + // Determine tooltip based on DisplayMode + DisplayMode mode = (DisplayMode) + ((JComponent) list).getClientProperty("ClusterDisplayMode"); + if (mode == null) { + label.setToolTipText(null); + return label; + } + switch (mode) { + case COUNTS: + // cluster size X molecules + label.setToolTipText( + "cluster size " + name + " molecules" + ); + break; + case MEAN: + case OVERALL: + label.setToolTipText("" + expandStatisticName(name) + ""); + break; + } + } return label; } - }); - } + private String expandStatisticName(String name) { + return switch (name) { + case "ACS" -> "Average Cluster Size"; + case "SD" -> "Standard Deviation"; + case "ACO" -> "Average Cluster Occupancy"; + default -> name; // fallback + }; + } + }); } return yAxisChoiceList; } diff --git a/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterVisualizationPanel.java b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterVisualizationPanel.java index 25c875c2ef..50066f5af2 100644 --- a/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterVisualizationPanel.java +++ b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterVisualizationPanel.java @@ -313,7 +313,7 @@ public void setBackground(Color color) { } - private void initConnections() { + private void initConnections() { initializeGlobalPalette(); // get a stable, high contrast palette // group the two buttons so only one stays selected ButtonGroup bg = new ButtonGroup(); @@ -340,6 +340,14 @@ protected void onSelectedObjectsChange(Object[] selectedObjects) { } public void refreshData() { + if(owner != null && owner.getSimulation() != null) { + int jobs = owner.getSimulation().getSolverTaskDescription().getLangevinSimulationOptions().getTotalNumberOfJobs(); + String name = owner.getSimulation().getName(); + String str = "" + name + " [" + jobs + " job" + (jobs != 1 ? "s" : "") + "]"; + getJBottomLabel().setText(str); + } else { + getJBottomLabel().setText(" "); + } // simulationModelInfo = owner.getSimulationModelInfo(); // langevinSolverResultSet = owner.getLangevinSolverResultSet(); System.out.println("ClusterVisualizationPanel.refreshData() called"); diff --git a/vcell-core/src/main/java/cbit/vcell/simdata/LangevinSolverResultSet.java b/vcell-core/src/main/java/cbit/vcell/simdata/LangevinSolverResultSet.java index edf1b5a234..5496a95257 100644 --- a/vcell-core/src/main/java/cbit/vcell/simdata/LangevinSolverResultSet.java +++ b/vcell-core/src/main/java/cbit/vcell/simdata/LangevinSolverResultSet.java @@ -1,6 +1,9 @@ package cbit.vcell.simdata; +import cbit.vcell.math.ODESolverResultSetColumnDescription; +import cbit.vcell.parser.ExpressionException; import cbit.vcell.solver.ode.ODESimData; +import cbit.vcell.util.ColumnDescription; import java.io.*; @@ -71,4 +74,52 @@ private static LangevinBatchResultSet deepCopy(LangevinBatchResultSet original) } } + public void postProcess() { + if(isClusterDataAvailable()) { + ODESimData co = getClusterOverall(); + checkTrivial(co); + co = getClusterMean(); + checkTrivial(co); + co = getClusterCounts(); + checkTrivial(co); + } + if(isAverageDataAvailable()) { + ODESimData co = getAvg(); + checkTrivial(co); + co = getMin(); + checkTrivial(co); + co = getMax(); + checkTrivial(co); + co = getStd(); + checkTrivial(co); + } + } + private static void checkTrivial(ODESimData co) { + ColumnDescription[] cds = co.getColumnDescriptions(); + for(ColumnDescription columnDescription : cds) { + if (columnDescription instanceof ODESolverResultSetColumnDescription cd) { + double[] data = null; + int index = co.findColumn(cd.getName()); + try { + data = co.extractColumn(index); + } catch (ExpressionException e) { + System.out.println("Failed to extract column: " + e.getMessage()); + continue; + } + if(data == null || data.length == 0) { + continue; + } + double initial = data[0]; + boolean isTrivial = true; + for(double d : data) { + if(initial != d) { + isTrivial = false; + break; // one mismatch is enough to know it's not trivial + } + } + cd.setIsTrivial(isTrivial); + } + } + } + } diff --git a/vcell-core/src/main/java/cbit/vcell/simdata/ODEDataManager.java b/vcell-core/src/main/java/cbit/vcell/simdata/ODEDataManager.java index 0cc7ecbdd3..b406f8edaa 100644 --- a/vcell-core/src/main/java/cbit/vcell/simdata/ODEDataManager.java +++ b/vcell-core/src/main/java/cbit/vcell/simdata/ODEDataManager.java @@ -185,9 +185,11 @@ private void connect() throws DataAccessException { nFSimMolecularConfigurations = getVCDataManager().getNFSimMolecularConfigurations(getVCDataIdentifier()); LangevinBatchResultSet raw = getVCDataManager().getLangevinBatchResultSet(getVCDataIdentifier()); langevinSolverResultSet = new LangevinSolverResultSet(raw); // may be null + if(langevinSolverResultSet != null) { + langevinSolverResultSet.postProcess(); + } if( langevinSolverResultSet.isAverageDataAvailable()) { odeSolverResultSet = langevinSolverResultSet.getAvg(); -// odeSolverResultSet = langevinSolverResultSet.getClusterMean(); } } From 7f2fbf3f7e530a4ea2f0c0d0b4f1cc36dbbcb52c Mon Sep 17 00:00:00 2001 From: Dan Vasilescu Date: Tue, 17 Mar 2026 19:01:06 -0400 Subject: [PATCH 14/31] cluster specification and visualization panel - more visual refinements, stub for copy / export --- .../java/cbit/plot/gui/ClusterDataPanel.java | 60 +++++- .../java/cbit/plot/gui/ClusterPlotPanel.java | 194 +++++++++++++++--- .../ode/gui/ClusterSpecificationPanel.java | 139 +++++++++---- .../ode/gui/ClusterVisualizationPanel.java | 173 +++++++++++++--- 4 files changed, 472 insertions(+), 94 deletions(-) diff --git a/vcell-client/src/main/java/cbit/plot/gui/ClusterDataPanel.java b/vcell-client/src/main/java/cbit/plot/gui/ClusterDataPanel.java index b634277218..149cd7f4b8 100644 --- a/vcell-client/src/main/java/cbit/plot/gui/ClusterDataPanel.java +++ b/vcell-client/src/main/java/cbit/plot/gui/ClusterDataPanel.java @@ -31,6 +31,11 @@ public class ClusterDataPanel extends JPanel { private ScrollTable scrollPaneTable; private NonEditableDefaultTableModel nonEditableDefaultTableModel = null; + private JPopupMenu popupMenu = null; + private JMenuItem miCopyAll = null; + private JMenuItem miCopyHDF5 = null; + private static enum CopyAction {copy,copyrow,copyall}; + private final IvjEventHandler ivjEventHandler = new IvjEventHandler(); class IvjEventHandler implements ActionListener, MouseListener, PropertyChangeListener, ChangeListener { @@ -41,6 +46,9 @@ public void mouseClicked(MouseEvent e) { int row = getScrollPaneTable().rowAtPoint(e.getPoint()); int col = getScrollPaneTable().columnAtPoint(e.getPoint()); System.out.println("ClusterDataPanel: clicked row=" + row + " col=" + col); + if (SwingUtilities.isRightMouseButton(e)) { + getPopupMenu().show(e.getComponent(), e.getX(), e.getY()); + } } } @@ -51,7 +59,7 @@ public void mouseClicked(MouseEvent e) { @Override public void actionPerformed(ActionEvent e) { - // reserved for future buttons or context menu actions + // reserved for future } @Override @@ -178,6 +186,54 @@ private NonEditableDefaultTableModel getNonEditableDefaultTableModel() { return nonEditableDefaultTableModel; } + private JPopupMenu getPopupMenu() { + if (popupMenu == null) { + popupMenu = new JPopupMenu(); + + miCopyAll = new JMenuItem("Copy All"); + miCopyAll.addActionListener(e -> copyCells(this, false)); + popupMenu.add(miCopyAll); + + miCopyHDF5 = new JMenuItem("Copy to HDF5"); + miCopyHDF5.addActionListener(e -> copyCells(this,true)); + popupMenu.add(miCopyHDF5); + } + return popupMenu; + } + + // ----------------------------------------------------------------------------------------------- + private static synchronized void copyCells(ClusterDataPanel cdp, boolean isHDF5) { + try { + int r = 0; + int c = 0; + int[] rows = new int[0]; + int[] columns = new int[0]; + r = cdp.getScrollPaneTable().getRowCount(); + c = cdp.getScrollPaneTable().getColumnCount(); + rows = new int[r]; + columns = new int[c]; + for (int i = 0; i < rows.length; i++){ + rows[i] = i; + } + for (int i = 0; i < columns.length; i++){ + columns[i] = i; + } + if(rows.length < 1 || columns.length < 1) + { + throw new Exception("No table cell is selected."); + } + System.out.println("Copying cluster data: rows=" + rows.length + " columns=" + columns.length + " isHDF5=" + isHDF5); + + + + + + } catch (Exception ex) { + LG.error("Error copying cluster data", ex); + JOptionPane.showMessageDialog(cdp, "Error copying cluster data: " + ex.getMessage(), "Copy Error", JOptionPane.ERROR_MESSAGE); + } + } + public void setSpecialityRenderer(SpecialtyTableRenderer str) { // TODO: write some appropriate renderer when we decide what to show in the tooltip // use RendererViewerDoubleWithTooltip for inspiration @@ -207,7 +263,7 @@ public void updateData(ClusterSpecificationPanel.ClusterSelection sel) throws Ex // column names String[] columnNames = new String[1 + columns.size()]; - columnNames[0] = "t"; + columnNames[0] = "time"; for (int i = 0; i < columns.size(); i++) { columnNames[i + 1] = columns.get(i).getName(); } diff --git a/vcell-client/src/main/java/cbit/plot/gui/ClusterPlotPanel.java b/vcell-client/src/main/java/cbit/plot/gui/ClusterPlotPanel.java index 4571d643e0..76d3c009d4 100644 --- a/vcell-client/src/main/java/cbit/plot/gui/ClusterPlotPanel.java +++ b/vcell-client/src/main/java/cbit/plot/gui/ClusterPlotPanel.java @@ -1,9 +1,7 @@ package cbit.plot.gui; import java.awt.*; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; -import java.awt.event.MouseEvent; +import java.awt.event.*; import java.awt.geom.AffineTransform; import java.awt.geom.Ellipse2D; import java.awt.geom.GeneralPath; @@ -15,6 +13,7 @@ import java.text.NumberFormat; import java.util.ArrayList; import java.util.List; +import java.util.function.Consumer; import javax.swing.JFrame; import javax.swing.JLabel; @@ -36,7 +35,6 @@ public class ClusterPlotPanel extends JPanel { private static final float AXIS_STROKE = 1.0f; // stroke widths private static final float CURVE_STROKE = 1.5f; - private static class CurveData { final String name; final double[] yRaw; @@ -52,8 +50,64 @@ private static class CurveData { private double globalMax = 1; private double dt = 1; + private Integer mouseX = null; // mouse crosshair + private Integer mouseY = null; + private int lastX0, lastX1, lastY0, lastY1; + private boolean crosshairEnabled = true; + private Consumer coordCallback; // parent supplies this + private double lastXMaxRounded; + private double lastYMaxRounded; + + + public ClusterPlotPanel() { + + addMouseMotionListener(new MouseMotionAdapter() { + @Override + public void mouseMoved(MouseEvent e) { + int mx = e.getX(); + int my = e.getY(); + // Check if inside last-known plot area + if (mx >= lastX0 && mx <= lastX1 && my >= lastY1 && my <= lastY0) { + mouseX = mx; + mouseY = my; + } else { + mouseX = null; + mouseY = null; + } + if (crosshairEnabled && mouseX != null && mouseY != null) { + double xVal = (mouseX - lastX0) * lastXMaxRounded / (lastX1 - lastX0); + double yVal = (lastY0 - mouseY) * lastYMaxRounded / (lastY0 - lastY1); + + if (coordCallback != null) { + coordCallback.accept(new double[]{xVal, yVal}); + } + } else { + if (coordCallback != null) { + coordCallback.accept(null); // clear + } + } + repaint(); + } + }); + addMouseListener(new MouseAdapter() { + @Override + public void mouseExited(MouseEvent e) { + mouseX = null; + mouseY = null; + repaint(); + } + }); + } + // ----------------------------------------------------------------------------- + public void setCrosshairEnabled(boolean enabled) { + this.crosshairEnabled = enabled; + } + public void setCoordinateCallback(Consumer cb) { + this.coordCallback = cb; + } + public void clear() { curves.clear(); } @@ -80,7 +134,7 @@ private double roundUpNice(double value) { return rounded * exp; } - private String formatNumber(double v) { + public static String formatNumber(double v) { if (v == 0) return "0"; double abs = Math.abs(v); @@ -104,74 +158,156 @@ protected void paintComponent(Graphics g) { int w = getWidth(); int h = getHeight(); - g2.setColor(Color.white); // background + g2.setColor(Color.white); g2.fillRect(0, 0, w, h); - int x0 = LEFT_INSET; // plot area + int x0 = LEFT_INSET; int x1 = w - RIGHT_INSET; int y0 = h - BOTTOM_INSET; int y1 = TOP_INSET; + // store plot area for mouse listeners + lastX0 = x0; + lastX1 = x1; + lastY0 = y0; + lastY1 = y1; + int plotWidth = x1 - x0; int plotHeight = y0 - y1; if (plotWidth <= 0 || plotHeight <= 0) { - return; // too small to draw + return; } - // determine max curve length int maxCurveLength = curves.stream() .mapToInt(c -> c.yRaw.length) .max() .orElse(0); if (maxCurveLength < 2) { - return; // nothing to draw + return; } - double yMaxRounded = roundUpNice(globalMax); // rounded axis limits + double yMaxRounded = roundUpNice(globalMax); double xMax = dt * (maxCurveLength - 1); double xMaxRounded = roundUpNice(xMax); + lastXMaxRounded = xMaxRounded; + lastYMaxRounded = yMaxRounded; + FontMetrics fm = g2.getFontMetrics(); - g2.setColor(Color.black); // draw axes - g2.setStroke(new BasicStroke(AXIS_STROKE)); - g2.drawLine(x0, y0, x1, y0); // X axis - g2.drawLine(x0, y0, x0, y1); // Y axis + // ============================================================ + // GRIDLINES (major + mid) + // ============================================================ + g2.setColor(new Color(220, 220, 220)); + g2.setStroke(new BasicStroke(1f)); - FontMetrics fm = g2.getFontMetrics(); // tick labels - int yTicks = 5; // ------- Y ticks (5 ticks) + // Y gridlines + int yTicks = 5; double yStep = yMaxRounded / yTicks; + + for (int i = 0; i <= yTicks; i++) { + double valueMajor = i * yStep; + int yPixMajor = y0 - (int) Math.round((valueMajor / yMaxRounded) * plotHeight); + + g2.drawLine(x0, yPixMajor, x1, yPixMajor); + + if (i < yTicks) { + double valueMid = (i + 0.5) * yStep; + int yPixMid = y0 - (int) Math.round((valueMid / yMaxRounded) * plotHeight); + g2.drawLine(x0, yPixMid, x1, yPixMid); + } + } + + // X gridlines + double[] xMajor = {0, xMaxRounded / 2, xMaxRounded}; + + for (int i = 0; i < xMajor.length; i++) { + double xvMajor = xMajor[i]; + int xPixMajor = x0 + (int) Math.round((xvMajor / xMaxRounded) * plotWidth); + + g2.drawLine(xPixMajor, y1, xPixMajor, y0); + + if (i < xMajor.length - 1) { + double xvMid = (xMajor[i] + xMajor[i + 1]) / 2.0; + int xPixMid = x0 + (int) Math.round((xvMid / xMaxRounded) * plotWidth); + g2.drawLine(xPixMid, y1, xPixMid, y0); + } + } + + // ============================================================ + // AXES + TICKS + // ============================================================ + g2.setColor(Color.black); + g2.setStroke(new BasicStroke(AXIS_STROKE)); + + g2.drawLine(x0, y0, x1, y0); + g2.drawLine(x0, y0, x0, y1); + + // Y ticks for (int i = 0; i <= yTicks; i++) { - double value = i * yStep; - int yPix = y0 - (int) Math.round((value / yMaxRounded) * plotHeight); - g2.drawLine(x0 - 5, yPix, x0, yPix); // tick - String label = formatNumber(value); // label + double valueMajor = i * yStep; + int yPixMajor = y0 - (int) Math.round((valueMajor / yMaxRounded) * plotHeight); + + g2.drawLine(x0 - 5, yPixMajor, x0, yPixMajor); + + String label = formatNumber(valueMajor); int sw = fm.stringWidth(label); - g2.drawString(label, x0 - 10 - sw, yPix + fm.getAscent() / 2); + g2.drawString(label, x0 - 10 - sw, yPixMajor + fm.getAscent() / 2); + + if (i < yTicks) { + double valueMid = (i + 0.5) * yStep; + int yPixMid = y0 - (int) Math.round((valueMid / yMaxRounded) * plotHeight); + g2.drawLine(x0 - 3, yPixMid, x0, yPixMid); + } } - double[] xTickValues = {0, xMaxRounded / 2, xMaxRounded}; // ----- X ticks (0, mid, end) - for (double xv : xTickValues) { - int xPix = x0 + (int) Math.round((xv / xMaxRounded) * plotWidth); - g2.drawLine(xPix, y0, xPix, y0 + 5); // tick - String label = formatNumber(xv); // label + // X ticks + for (int i = 0; i < xMajor.length; i++) { + double xvMajor = xMajor[i]; + int xPixMajor = x0 + (int) Math.round((xvMajor / xMaxRounded) * plotWidth); + + g2.drawLine(xPixMajor, y0, xPixMajor, y0 + 5); + + String label = formatNumber(xvMajor); int sw = fm.stringWidth(label); - g2.drawString(label, xPix - sw / 2, y0 + fm.getAscent() + 5); + g2.drawString(label, xPixMajor - sw / 2, y0 + fm.getAscent() + 5); + + if (i < xMajor.length - 1) { + double xvMid = (xMajor[i] + xMajor[i + 1]) / 2.0; + int xPixMid = x0 + (int) Math.round((xvMid / xMaxRounded) * plotWidth); + g2.drawLine(xPixMid, y0, xPixMid, y0 + 3); + } } - // draw curves + // ============================================================ + // CROSSHAIR (drawn after gridlines, before curves) + // ============================================================ + if (crosshairEnabled && mouseX != null && mouseY != null) { + g2.setColor(new Color(180, 180, 180)); + g2.setStroke(new BasicStroke(1f)); + g2.drawLine(mouseX, y1, mouseX, y0); + g2.drawLine(x0, mouseY, x1, mouseY); + } + + // ============================================================ + // CURVES + // ============================================================ g2.setStroke(new BasicStroke(CURVE_STROKE, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND)); for (CurveData curve : curves) { int n = curve.yRaw.length; if (n < 2) continue; + int[] x = new int[n]; int[] y = new int[n]; + for (int i = 0; i < n; i++) { double t = i * dt; x[i] = x0 + (int) Math.round((t / xMaxRounded) * plotWidth); + double norm = curve.yRaw[i] / yMaxRounded; y[i] = y0 - (int) Math.round(norm * plotHeight); } + g2.setColor(curve.color); g2.drawPolyline(x, y, n); } diff --git a/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterSpecificationPanel.java b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterSpecificationPanel.java index 886e6e85ba..5b0cb1b7af 100644 --- a/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterSpecificationPanel.java +++ b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterSpecificationPanel.java @@ -25,7 +25,78 @@ public class ClusterSpecificationPanel extends DocumentEditorSubPanel { - public enum DisplayMode { COUNTS, MEAN, OVERALL }; + public enum DisplayMode { + COUNTS( + "COUNTS", + "Cluster Counts", + "Show the number of clusters of each size" + ), + MEAN( + "MEAN", + "Cluster Mean", + "At each timepoint: ACS/ACO averaged over runs; SD = SD of ACS across runs" + ), + OVERALL( + "OVERALL", + "Cluster Overall", + "At each timepoint: all runs pooled; ACS, SD, ACO computed from the combined sample" + ); + private final String actionCommand; + private final String uiLabel; + private final String tooltip; + DisplayMode(String actionCommand, String uiLabel, String tooltip) { + this.actionCommand = actionCommand; + this.uiLabel = uiLabel; + this.tooltip = tooltip; + } + public String actionCommand() { return actionCommand; } + public String uiLabel() { return uiLabel; } + public String tooltip() { return tooltip; } + public static DisplayMode fromActionCommand(String cmd) { + for (DisplayMode m : values()) { + if (m.actionCommand.equals(cmd)) { + return m; + } + } + throw new IllegalArgumentException("Unknown DisplayMode: " + cmd); + } + } + + public enum ClusterStatistic { + ACS( + "Average Cluster Size", + "Average number of molecules per cluster", + "molecules" + ), + ACO( + "Average Cluster Occupancy", + "Average number of molecules per molecule (molecule‑centric cluster size)", + "molecules" + ), + SD( + "Standard Deviation of Cluster Size", + "Variability of cluster sizes around the average cluster size (ACS)", + "molecules" + ); + private final String fullName; + private final String description; + private final String unit; + ClusterStatistic(String fullName, String description, String unit) { + this.fullName = fullName; + this.description = description; + this.unit = unit; + } + public String fullName() { + return fullName; + } + public String description() { + return description; + } + public String unit() { + return unit; + } + } + public static class ClusterSelection { // used to communicate y-list selection to the ClusterVisualizationPanel public final DisplayMode mode; public final java.util.List columns; @@ -43,17 +114,8 @@ public void actionPerformed(ActionEvent e) { String cmd = e.getActionCommand(); if (e.getSource() instanceof JRadioButton rb && SwingUtilities.isDescendingFrom(rb, ClusterSpecificationPanel.this)) { System.out.println("ClusterSpecificationPanel.actionPerformed() called. Source is JRadioButton: " + rb.getText()); - switch (cmd) { - case "COUNTS": - populateYAxisChoices(DisplayMode.COUNTS); - break; - case "MEAN": - populateYAxisChoices(DisplayMode.MEAN); - break; - case "OVERALL": - populateYAxisChoices(DisplayMode.OVERALL); - break; - } + DisplayMode mode = DisplayMode.fromActionCommand(cmd); + populateYAxisChoices(mode); } } @Override @@ -143,7 +205,7 @@ private void initialize() { gbc.insets = new Insets(4, 4, 0, 4); add(xAxisLabel, gbc); - JTextField xAxisTextBox = new JTextField("t"); + JTextField xAxisTextBox = new JTextField("time [seconds]"); xAxisTextBox.setEnabled(false); xAxisTextBox.setEditable(false); gbc = new GridBagConstraints(); @@ -184,18 +246,23 @@ private CollapsiblePanel getDisplayOptionsPanel() { content.setLayout(new GridBagLayout()); ButtonGroup group = new ButtonGroup(); - JRadioButton rbCounts = new JRadioButton("Cluster Counts"); - JRadioButton rbMean = new JRadioButton("Cluster Mean"); - JRadioButton rbOverall = new JRadioButton("Cluster Overall"); - rbCounts.setActionCommand("COUNTS"); - rbMean.setActionCommand("MEAN"); - rbOverall.setActionCommand("OVERALL"); + JRadioButton rbCounts = new JRadioButton(DisplayMode.COUNTS.uiLabel()); + JRadioButton rbMean = new JRadioButton(DisplayMode.MEAN.uiLabel()); + JRadioButton rbOverall = new JRadioButton(DisplayMode.OVERALL.uiLabel()); + + rbCounts.setActionCommand(DisplayMode.COUNTS.actionCommand()); + rbMean.setActionCommand(DisplayMode.MEAN.actionCommand()); + rbOverall.setActionCommand(DisplayMode.OVERALL.actionCommand()); rbCounts.addActionListener(ivjEventHandler); rbMean.addActionListener(ivjEventHandler); rbOverall.addActionListener(ivjEventHandler); + rbCounts.setToolTipText(DisplayMode.COUNTS.tooltip()); + rbMean.setToolTipText(DisplayMode.MEAN.tooltip()); + rbOverall.setToolTipText(DisplayMode.OVERALL.tooltip()); + group.add(rbCounts); group.add(rbMean); group.add(rbOverall); @@ -244,19 +311,15 @@ private JList getYAxisChoice() { yAxisChoiceList.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); yAxisChoiceList.setCellRenderer(new DefaultListCellRenderer() { @Override - public Component getListCellRendererComponent( - JList list, Object value, int index, - boolean isSelected, boolean cellHasFocus) { - JLabel label = (JLabel) super.getListCellRendererComponent( - list, value, index, isSelected, cellHasFocus); + public Component getListCellRendererComponent(JList list, Object value, int index, + boolean isSelected, boolean cellHasFocus) { + JLabel label = (JLabel) super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); if (value instanceof ODESolverResultSetColumnDescription cd) { - // Always show the plain name String name = cd.getName(); label.setText(name); - // Gray out trivial entries - if (cd.isTrivial()) { + if (cd.isTrivial()) { // gray out trivial entries label.setForeground(Color.GRAY); } else { label.setForeground(isSelected @@ -264,8 +327,7 @@ public Component getListCellRendererComponent( : list.getForeground()); } - // Determine tooltip based on DisplayMode - DisplayMode mode = (DisplayMode) + DisplayMode mode = (DisplayMode) // determine tooltip based on DisplayMode ((JComponent) list).getClientProperty("ClusterDisplayMode"); if (mode == null) { label.setToolTipText(null); @@ -274,27 +336,24 @@ public Component getListCellRendererComponent( switch (mode) { case COUNTS: // cluster size X molecules + label.setText(name); label.setToolTipText( - "cluster size " + name + " molecules" + "Number of Clusters of size: " + name + " " + "" + "[molecules] " ); break; case MEAN: case OVERALL: - label.setToolTipText("" + expandStatisticName(name) + ""); + ClusterStatistic stat = ClusterStatistic.valueOf(name); + label.setText(stat.fullName); + String tooltip = "" + stat.description + "" + " [" + stat.unit + "] "; + label.setToolTipText(tooltip); break; } } return label; } - private String expandStatisticName(String name) { - return switch (name) { - case "ACS" -> "Average Cluster Size"; - case "SD" -> "Standard Deviation"; - case "ACO" -> "Average Cluster Occupancy"; - default -> name; // fallback - }; - } - }); } + }); + } return yAxisChoiceList; } diff --git a/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterVisualizationPanel.java b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterVisualizationPanel.java index 50066f5af2..1c0349185c 100644 --- a/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterVisualizationPanel.java +++ b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterVisualizationPanel.java @@ -39,6 +39,8 @@ import java.awt.geom.Path2D; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; import java.util.*; import java.util.List; @@ -63,6 +65,8 @@ public class ClusterVisualizationPanel extends DocumentEditorSubPanel { private JScrollPane ivjPlotLegendsScrollPane = null; private JPanel ivjJPanelPlotLegends = null; private JLabel bottomLabel = null; + private JCheckBox showCrosshairCheckBox = null; + private JLabel crosshairCoordLabel = null; private JToolBarToggleButton ivjPlotButton = null; private JToolBarToggleButton ivjDataButton = null; @@ -77,6 +81,7 @@ public void actionPerformed(ActionEvent e) { ivjPlotButton.setSelected(cmd.equals("JPanelPlot")); // update button selection state ivjDataButton.setSelected(cmd.equals("JPanelData")); ivjJPanelLegend.setVisible(cmd.equals("JPanelPlot")); // show legend only in plot mode + getShowCrosshairCheckBox().setVisible(cmd.equals("JPanelPlot")); // show/hide crosshair checkbox return; } } @@ -161,7 +166,7 @@ private JLabel getJLabelBottom() { if (ivjJLabelBottom == null) { ivjJLabelBottom = new JLabel(); ivjJLabelBottom.setName("JLabelBottom"); - ivjJLabelBottom.setText("t"); + ivjJLabelBottom.setText("time"); ivjJLabelBottom.setForeground(Color.black); ivjJLabelBottom.setHorizontalTextPosition(SwingConstants.CENTER); ivjJLabelBottom.setHorizontalAlignment(SwingConstants.CENTER); @@ -173,13 +178,19 @@ private ClusterPlotPanel getClusterPlotPanel() { // actual plotting is show try { clusterPlotPanel = new ClusterPlotPanel(); clusterPlotPanel.setName("ClusterPlotPanel"); + clusterPlotPanel.setCoordinateCallback(coords -> { + if (coords == null) { + clearCrosshairCoordinates(); + } else { + updateCrosshairCoordinates(coords[0], coords[1]); + } + }); clusterPlotPanel.addComponentListener(new ComponentAdapter() { @Override public void componentShown(ComponentEvent e) { System.out.println("ClusterVisualizationPanel.componentShown() called, height = " + clusterPlotPanel.getHeight()); } }); - } catch (java.lang.Throwable ivjExc) { handleException(ivjExc); } @@ -213,11 +224,21 @@ private JPanel getBottomRightPanel() { gbc = new GridBagConstraints(); gbc.gridx = 2; gbc.gridy = 0; + gbc.insets = new Insets(4, 4, 4, 2); + bottomRightPanel.add(getShowCrosshairCheckBox(), gbc); + + gbc = new GridBagConstraints(); + gbc.gridx = 3; gbc.gridy = 0; + gbc.insets = new Insets(4, 2, 4, 2); + bottomRightPanel.add(getCrosshairCoordLabel(), gbc); + + gbc = new GridBagConstraints(); + gbc.gridx = 4; gbc.gridy = 0; gbc.insets = new Insets(4, 4, 4, 4); bottomRightPanel.add(getPlotButton(), gbc); gbc = new GridBagConstraints(); - gbc.gridx = 3; gbc.gridy = 0; + gbc.gridx = 5; gbc.gridy = 0; gbc.insets = new Insets(4, 4, 4, 4); bottomRightPanel.add(getDataButton(), gbc); } @@ -235,6 +256,28 @@ private JLabel getJBottomLabel() { } return bottomLabel; } + private JCheckBox getShowCrosshairCheckBox() { + if (showCrosshairCheckBox == null) { + showCrosshairCheckBox = new JCheckBox("Show Crosshair"); + showCrosshairCheckBox.setSelected(true); // default ON + + showCrosshairCheckBox.addActionListener(e -> { + boolean enabled = showCrosshairCheckBox.isSelected(); + clusterPlotPanel.setCrosshairEnabled(enabled); + clusterPlotPanel.repaint(); + }); + } + return showCrosshairCheckBox; + } + private JLabel getCrosshairCoordLabel() { + if (crosshairCoordLabel == null) { + crosshairCoordLabel = new JLabel(emptyCoordText); + // no fixed width — dynamic sizing will handle it but we DO want a stable height + int height = crosshairCoordLabel.getFontMetrics(crosshairCoordLabel.getFont()).getHeight(); + crosshairCoordLabel.setPreferredSize(new Dimension(1, height)); + } + return crosshairCoordLabel; + } private JToolBarToggleButton getPlotButton() { if (ivjPlotButton == null) { ivjPlotButton = new JToolBarToggleButton(); @@ -283,7 +326,8 @@ private JScrollPane getPlotLegendsScrollPane() { if (ivjPlotLegendsScrollPane == null) { ivjPlotLegendsScrollPane = new JScrollPane(); ivjPlotLegendsScrollPane.setName("PlotLegendsScrollPane"); - ivjPlotLegendsScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED); + ivjPlotLegendsScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); +// ivjPlotLegendsScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED); ivjPlotLegendsScrollPane.setBorder(new EmptyBorder(0, 0, 0, 0)); getPlotLegendsScrollPane().setViewportView(getJPanelPlotLegends()); } @@ -291,10 +335,17 @@ private JScrollPane getPlotLegendsScrollPane() { } private JPanel getJPanelPlotLegends() { if (ivjJPanelPlotLegends == null) { - ivjJPanelPlotLegends = new JPanel(); + ivjJPanelPlotLegends = new JPanel() { + @Override + public Dimension getPreferredSize() { // allocate from start enough space for vertical scrollbar + Dimension d = super.getPreferredSize(); + int scrollbarWidth = UIManager.getInt("ScrollBar.width"); + return new Dimension(d.width + scrollbarWidth, d.height); + } + }; ivjJPanelPlotLegends.setName("JPanelPlotLegends"); ivjJPanelPlotLegends.setLayout(new BoxLayout(ivjJPanelPlotLegends, BoxLayout.Y_AXIS)); - ivjJPanelPlotLegends.setBounds(0, 0, 72, 360); +// ivjJPanelPlotLegends.setBounds(0, 0, 72, 360); } return ivjJPanelPlotLegends; } @@ -303,6 +354,8 @@ public void setBackground(Color color) { super.setBackground(color); getBottomRightPanel().setBackground(color); getJBottomLabel().setBackground(color); + getShowCrosshairCheckBox().setBackground(color); + getCrosshairCoordLabel().setBackground(color); getJPanelLegend().setBackground(color); getJPanelPlotLegends().setBackground(color); getJPanel1().setBackground(color); @@ -371,29 +424,71 @@ private void ensureColorsAssigned(List columns) { } } } + /* + ACS = Average Cluster Size + ACS answers: "If I pick a cluster at random, how many molecules does it contain on average?" + ACS is the mean size of clusters, computed as: ACS = Math.sumOver(i, n_i * size_i) / Math.sumOver(i, n_i); + Unit: molecules per cluster (molecules) + + SD = Standard Deviation of Cluster Size + SD answers: "How much variability is there in cluster sizes?" + SD = Math.sqrt( Math.sumOver(i, n_i * Math.pow(size_i - ACS, 2)) / Math.sumOver(i, n_i) ) + Unit: molecules per cluster (molecules) + + ACO = Average Cluster Occupancy + ACO answers: "If I pick a random molecule from the entire system, how many other molecules are in its cluster on average?" + ACO = Math.sumOver(i, n_i * size_i * size_i) / Math.sumOver(i, n_i * size_i) + Unit: molecules per molecule (molecules) + + Example: if clusters are: + - {5, 5, 5, 5} -> ACS = 5, SD = 0 + - {3, 4, 5, 6} -> ACS = 4.5, SD ≈ 1.12 + - {1, 1, 1, 10} -> ACS = 3.25, SD is large + SD tells whether the system is: + - homogeneous (low SD) + - heterogeneous (high SD) + ACS alone cannot tell that - SD is the variability measure + */ private JComponent createLegendEntry(String name, Color color, ClusterSpecificationPanel.DisplayMode mode) { JPanel p = new JPanel(); p.setName("JPanelClusterColorLegends"); - BoxLayout bl = new BoxLayout(p, BoxLayout.Y_AXIS); - p.setLayout(bl); - p.setBounds(0, 0, 72, 360); + p.setLayout(new BoxLayout(p, BoxLayout.Y_AXIS)); p.setOpaque(false); - String unitSymbol = ""; - if(ClusterSpecificationPanel.DisplayMode.COUNTS == mode) { - unitSymbol = "molecules"; + String unitSymbol; + String tooltip; + switch (mode) { + case COUNTS: + // name is the cluster size (e.g., "3") + unitSymbol = "molecules"; + tooltip = "cluster size " + name + " molecules"; + break; + case MEAN: + case OVERALL: + // name is ACS / SD / ACO + ClusterSpecificationPanel.ClusterStatistic stat = ClusterSpecificationPanel.ClusterStatistic.valueOf(name); + unitSymbol = stat.unit(); + tooltip = "" + stat.fullName() + "
" + stat.description() + ""; + break; + default: + unitSymbol = ""; + tooltip = null; } - String shortLabel = "" + name + "" + " [" + unitSymbol + "] " + ""; + // Visible label + String shortLabel = "" + name + "" + " [" + unitSymbol + "] " + ""; JLabel line = new JLabel(new LineIcon(color)); JLabel text = new JLabel(shortLabel); - line.setBorder(new EmptyBorder(6,0,1,0)); - text.setBorder(new EmptyBorder(1,8,6,0)); + line.setBorder(new EmptyBorder(6, 0, 1, 0)); + text.setBorder(new EmptyBorder(1, 8, 6, 0)); + line.setToolTipText(tooltip); + text.setToolTipText(tooltip); + p.setToolTipText(tooltip); p.add(line); p.add(text); - return p; } + public class LineIcon implements Icon { private final Color color; public LineIcon(Color color) { @@ -433,7 +528,7 @@ private void redrawPlot(ClusterSpecificationPanel.ClusterSelection sel) throws E // double globalMin = Double.POSITIVE_INFINITY; double globalMin = 0; // these are counts, so min is always 0 double globalMax = Double.NEGATIVE_INFINITY; - clusterPlotPanel.clear(); + getClusterPlotPanel().clear(); for (ColumnDescription cd : columns) { int index = srs.findColumn(cd.getName()); @@ -443,11 +538,11 @@ private void redrawPlot(ClusterSpecificationPanel.ClusterSelection sel) throws E if (v > globalMax) globalMax = v; } Color c = persistentColorMap.get(cd.getName()); - clusterPlotPanel.addCurve(cd.getName(), y, c); + getClusterPlotPanel().addCurve(cd.getName(), y, c); } - clusterPlotPanel.setGlobalMinMax(globalMin, globalMax); - clusterPlotPanel.setDt(times[1]); // times[0] == 0; - clusterPlotPanel.repaint(); + getClusterPlotPanel().setGlobalMinMax(globalMin, globalMax); + getClusterPlotPanel().setDt(times[1]); // times[0] == 0; + getClusterPlotPanel().repaint(); } private void redrawLegend(ClusterSpecificationPanel.ClusterSelection sel) { System.out.println("ClusterVisualizationPanel.redrawLegend() called"); @@ -464,11 +559,43 @@ private void redrawDataTable(ClusterSpecificationPanel.ClusterSelection sel) thr getClusterDataPanel().updateData(sel); } public void setSpecialityRenderer(SpecialtyTableRenderer str) { - // TODO: implement this getClusterDataPanel().setSpecialityRenderer(str); } - + // crosshair coordinates and coordinates label management + public void updateCrosshairCoordinates(double xVal, double yVal) { + String text = formatCoord(xVal) + ", " + formatCoord(yVal); + getCrosshairCoordLabel().setText(text); + adjustCoordLabelWidth(text); + } + public void clearCrosshairCoordinates() { + getCrosshairCoordLabel().setText(emptyCoordText); + adjustCoordLabelWidth(emptyCoordText); + } + private int lastCoordCharCount = emptyCoordText.length(); + private static final String emptyCoordText = " "; // enough spaces to reduce jitter when switching between no coord and coord + private static final DecimalFormat sci = new DecimalFormat("0.000E0", DecimalFormatSymbols.getInstance(Locale.US)); + private static final DecimalFormat fix = new DecimalFormat("0.000", DecimalFormatSymbols.getInstance(Locale.US)); + private static String formatCoord(double v) { + double av = Math.abs(v); + return (av >= 0.001) + ? fix.format(v) + : sci.format(v); + } + private void adjustCoordLabelWidth(String text) { + int charCount = text.length(); + // Only resize if the number of characters changed + if (charCount == lastCoordCharCount) { + return; + } + lastCoordCharCount = charCount; + FontMetrics fm = getCrosshairCoordLabel().getFontMetrics(getCrosshairCoordLabel().getFont()); + int charWidth = fm.charWidth('8'); // good representative width + int width = charWidth * charCount + 4; // +4 px padding + Dimension d = getCrosshairCoordLabel().getPreferredSize(); + getCrosshairCoordLabel().setPreferredSize(new Dimension(width, d.height)); + getCrosshairCoordLabel().revalidate(); // required so GridBagLayout recalculates layout + } // =================================================== Evaluate JFreeChart capabilities with a simple demo === public static void main(String[] args) { From 7ef9705bc4a7781898e438ed4c0940648a2e5ea0 Mon Sep 17 00:00:00 2001 From: Dan Vasilescu Date: Thu, 19 Mar 2026 12:32:27 -0400 Subject: [PATCH 15/31] cluster specification and visualization panel, SD of ACS --- .../java/cbit/plot/gui/ClusterPlotPanel.java | 116 +++++++++--- .../ode/gui/ClusterSpecificationPanel.java | 47 +++++ .../ode/gui/ClusterVisualizationPanel.java | 176 ++++++++++++++++-- .../main/java/org/vcell/util/ColorUtil.java | 2 +- 4 files changed, 291 insertions(+), 50 deletions(-) diff --git a/vcell-client/src/main/java/cbit/plot/gui/ClusterPlotPanel.java b/vcell-client/src/main/java/cbit/plot/gui/ClusterPlotPanel.java index 76d3c009d4..7af6380c9e 100644 --- a/vcell-client/src/main/java/cbit/plot/gui/ClusterPlotPanel.java +++ b/vcell-client/src/main/java/cbit/plot/gui/ClusterPlotPanel.java @@ -2,13 +2,7 @@ import java.awt.*; import java.awt.event.*; -import java.awt.geom.AffineTransform; -import java.awt.geom.Ellipse2D; -import java.awt.geom.GeneralPath; -import java.awt.geom.Line2D; -import java.awt.geom.NoninvertibleTransformException; -import java.awt.geom.Point2D; -import java.awt.geom.Rectangle2D; +import java.awt.geom.*; import java.text.DecimalFormat; import java.text.NumberFormat; import java.util.ArrayList; @@ -45,7 +39,23 @@ private static class CurveData { this.color = color; } } + private static class Envelope { + final String name; + final double[] upper; + final double[] lower; + final Color fillColor; + + Envelope(String name, double[] upper, double[] lower, Color fillColor) { + this.name = name; + this.upper = upper; + this.lower = lower; + this.fillColor = fillColor; + } + } + private final List curves = new ArrayList<>(); + private final List envelopes = new ArrayList<>(); + private double globalMin = 0; private double globalMax = 1; private double dt = 1; @@ -57,6 +67,7 @@ private static class CurveData { private Consumer coordCallback; // parent supplies this private double lastXMaxRounded; private double lastYMaxRounded; + private double lastYMinRounded; public ClusterPlotPanel() { @@ -66,7 +77,7 @@ public ClusterPlotPanel() { public void mouseMoved(MouseEvent e) { int mx = e.getX(); int my = e.getY(); - // Check if inside last-known plot area + // Check if inside plot area if (mx >= lastX0 && mx <= lastX1 && my >= lastY1 && my <= lastY0) { mouseX = mx; mouseY = my; @@ -75,15 +86,18 @@ public void mouseMoved(MouseEvent e) { mouseY = null; } if (crosshairEnabled && mouseX != null && mouseY != null) { - double xVal = (mouseX - lastX0) * lastXMaxRounded / (lastX1 - lastX0); - double yVal = (lastY0 - mouseY) * lastYMaxRounded / (lastY0 - lastY1); - + // ---- X coordinate ---- + double fracX = (mouseX - lastX0) / (double)(lastX1 - lastX0); + double xVal = fracX * lastXMaxRounded; + // ---- Y coordinate (now using yMinRounded and yMaxRounded) ---- + double fracY = (lastY0 - mouseY) / (double)(lastY0 - lastY1); + double yVal = lastYMinRounded + fracY * (lastYMaxRounded - lastYMinRounded); if (coordCallback != null) { coordCallback.accept(new double[]{xVal, yVal}); } } else { if (coordCallback != null) { - coordCallback.accept(null); // clear + coordCallback.accept(null); } } repaint(); @@ -110,7 +124,9 @@ public void setCoordinateCallback(Consumer cb) { public void clear() { curves.clear(); + envelopes.clear(); } + public void addCurve(String name, double[] yRaw, Color color) { curves.add(new CurveData(name, yRaw, color)); } @@ -118,6 +134,10 @@ public void setGlobalMinMax(double min, double max) { this.globalMin = min; this.globalMax = max; } + public void addEnvelope(String name, double[] upper, double[] lower, Color fillColor) { + envelopes.add(new Envelope(name, upper, lower, fillColor)); + } + public void setDt(double dt) { this.dt = dt; } @@ -149,6 +169,15 @@ public static String formatNumber(double v) { } } + private int xPixel(double t, int x0, int plotWidth, double xMaxRounded) { + return x0 + (int) Math.round((t / xMaxRounded) * plotWidth); + } + private int yPixel(double value, int y0, int plotHeight, double yMaxRounded, double yMinRounded) { + double norm = (value - yMinRounded) / (yMaxRounded - yMinRounded); + int yPix = y0 - (int)Math.round(norm * plotHeight); + return yPix; + } + @Override protected void paintComponent(Graphics g) { super.paintComponent(g); @@ -178,20 +207,21 @@ protected void paintComponent(Graphics g) { return; } - int maxCurveLength = curves.stream() - .mapToInt(c -> c.yRaw.length) - .max() - .orElse(0); - - if (maxCurveLength < 2) { + // Determine if we have anything to draw (curves or envelopes) + int maxCurveLength = curves.stream().mapToInt(c -> c.yRaw.length).max().orElse(0); + int maxEnvelopeLength = envelopes.stream().mapToInt(e -> e.upper.length).max().orElse(0); + int maxLength = Math.max(maxCurveLength, maxEnvelopeLength); + if (maxLength < 2) { // If neither curves nor envelopes have at least 2 points, nothing to draw return; } double yMaxRounded = roundUpNice(globalMax); - double xMax = dt * (maxCurveLength - 1); + double yMinRounded = (globalMin < 0) ? -roundUpNice(-globalMin) : 0; + double xMax = dt * (maxLength - 1); double xMaxRounded = roundUpNice(xMax); lastXMaxRounded = xMaxRounded; lastYMaxRounded = yMaxRounded; + lastYMinRounded = yMinRounded; FontMetrics fm = g2.getFontMetrics(); // ============================================================ @@ -202,17 +232,23 @@ protected void paintComponent(Graphics g) { // Y gridlines int yTicks = 5; - double yStep = yMaxRounded / yTicks; + double yRange = yMaxRounded - yMinRounded; + double yStep = yRange / yTicks; for (int i = 0; i <= yTicks; i++) { - double valueMajor = i * yStep; - int yPixMajor = y0 - (int) Math.round((valueMajor / yMaxRounded) * plotHeight); - + double valueMajor = yMinRounded + i * yStep; + int yPixMajor = yPixel(valueMajor, y0, plotHeight, yMaxRounded, yMinRounded); g2.drawLine(x0, yPixMajor, x1, yPixMajor); + String label = formatNumber(valueMajor); + int sw = fm.stringWidth(label); + g2.drawString(label, x0 - 10 - sw, yPixMajor + fm.getAscent() / 2); + if (i < yTicks) { - double valueMid = (i + 0.5) * yStep; - int yPixMid = y0 - (int) Math.round((valueMid / yMaxRounded) * plotHeight); + double valueMid = valueMajor + yStep / 2.0; + int yPixMid = y0 - (int)Math.round( + (valueMid - yMinRounded) / yRange * plotHeight + ); g2.drawLine(x0, yPixMid, x1, yPixMid); } } @@ -223,7 +259,6 @@ protected void paintComponent(Graphics g) { for (int i = 0; i < xMajor.length; i++) { double xvMajor = xMajor[i]; int xPixMajor = x0 + (int) Math.round((xvMajor / xMaxRounded) * plotWidth); - g2.drawLine(xPixMajor, y1, xPixMajor, y0); if (i < xMajor.length - 1) { @@ -278,6 +313,30 @@ protected void paintComponent(Graphics g) { } } + // ============================================================ + // SD ENVELOPES (drawn after gridlines, before curves) + // ============================================================ + for (Envelope env : envelopes) { + g2.setColor(env.fillColor); + + Path2D path = new Path2D.Double(); + int n = env.upper.length; + // Upper boundary + double t0 = 0; + path.moveTo(xPixel(t0, x0, plotWidth, xMaxRounded), yPixel(env.upper[0], y0, plotHeight, yMaxRounded, yMinRounded)); + for (int i = 1; i < n; i++) { + double t = i * dt; + path.lineTo(xPixel(t, x0, plotWidth, xMaxRounded), yPixel(env.upper[i], y0, plotHeight, yMaxRounded, yMinRounded)); + } + // Lower boundary (reverse direction) + for (int i = n - 1; i >= 0; i--) { + double t = i * dt; + path.lineTo(xPixel(t, x0, plotWidth, xMaxRounded), yPixel(env.lower[i], y0, plotHeight, yMaxRounded, yMinRounded)); + } + path.closePath(); + g2.fill(path); + } + // ============================================================ // CROSSHAIR (drawn after gridlines, before curves) // ============================================================ @@ -303,10 +362,7 @@ protected void paintComponent(Graphics g) { for (int i = 0; i < n; i++) { double t = i * dt; x[i] = x0 + (int) Math.round((t / xMaxRounded) * plotWidth); - - double norm = curve.yRaw[i] / yMaxRounded; - y[i] = y0 - (int) Math.round(norm * plotHeight); - } + y[i] = yPixel(curve.yRaw[i], y0, plotHeight, yMaxRounded, yMinRounded); } g2.setColor(curve.color); g2.drawPolyline(x, y, n); diff --git a/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterSpecificationPanel.java b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterSpecificationPanel.java index 5b0cb1b7af..94c6c6598e 100644 --- a/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterSpecificationPanel.java +++ b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterSpecificationPanel.java @@ -128,6 +128,8 @@ public void propertyChange(PropertyChangeEvent evt) { public void valueChanged(ListSelectionEvent e) { if (e.getSource() == ClusterSpecificationPanel.this.getYAxisChoice() && !e.getValueIsAdjusting()) { + enforceAcsSdAcoRule(); + // extract selected ColumnDescriptions java.util.List selected = getYAxisChoice().getSelectedValuesList(); DisplayMode mode = getCurrentDisplayMode(); @@ -185,6 +187,51 @@ private void updateYAxisLabel(DisplayMode mode) { yAxisLabel.setText(text); } + private void enforceAcsSdAcoRule() { + DisplayMode mode = getCurrentDisplayMode(); + if (mode != DisplayMode.MEAN && mode != DisplayMode.OVERALL) { + return; // rule applies only in these modes + } + java.util.List selected = getYAxisChoice().getSelectedValuesList(); + + boolean acs = contains(selected, "ACS"); + boolean sd = contains(selected, "SD"); + boolean aco = contains(selected, "ACO"); + if (!acs && sd && aco) { + // conflict: SD + ACO without ACS + int anchor = getYAxisChoice().getAnchorSelectionIndex(); + ColumnDescription clicked = (ColumnDescription) getYAxisChoice().getModel().getElementAt(anchor); + String name = clicked.getName(); + if (name.equals("SD")) { + deselect("ACO"); + } else if (name.equals("ACO")) { + deselect("SD"); + } else if (name.equals("ACS")) { + // Ctrl-click on ACS caused conflict -> SD must go + deselect("SD"); + } + } + } + + private boolean contains(java.util.List list, String name) { + for (ColumnDescription cd : list) { + if (cd.getName().equals(name)) return true; + } + return false; + } + + private void deselect(String name) { + DefaultListModel model = getDefaultListModelY(); + int size = model.getSize(); + for (int i = 0; i < size; i++) { + ColumnDescription cd = model.get(i); + if (cd.getName().equals(name)) { + getYAxisChoice().removeSelectionInterval(i, i); + return; + } + } + } + public ClusterSpecificationPanel(ODEDataViewer odeDataViewer) { super(); this.owner = odeDataViewer; diff --git a/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterVisualizationPanel.java b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterVisualizationPanel.java index 1c0349185c..dc67c42a2c 100644 --- a/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterVisualizationPanel.java +++ b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterVisualizationPanel.java @@ -412,18 +412,38 @@ private void initializeGlobalPalette() { // Use a curated palette from ColorUtil globalPalette.clear(); globalPalette.addAll(Arrays.asList(ColorUtil.TABLEAU20)); + + // Reserve ACS and ACO immediately + ensureColorsAssigned("ACS"); + ensureColorsAssigned("ACO"); + + // SD derives from ACS (does NOT consume a palette slot) + Color acsColor = persistentColorMap.get("ACS"); + Color sdColor = deriveEnvelopeColor(acsColor); + persistentColorMap.put("SD", sdColor); } private void ensureColorsAssigned(List columns) { // assign colors only when needed, and keep them consistent across updates for (ColumnDescription cd : columns) { String name = cd.getName(); - if (!persistentColorMap.containsKey(name)) { - Color c = globalPalette.get(nextColorIndex % globalPalette.size()); - persistentColorMap.put(name, c); - nextColorIndex++; - } + ensureColorsAssigned(name); } } + private void ensureColorsAssigned(String name) { + if (!persistentColorMap.containsKey(name)) { + Color c = globalPalette.get(nextColorIndex % globalPalette.size()); + persistentColorMap.put(name, c); + nextColorIndex++; + } + } + private Color deriveEnvelopeColor(Color base) { + return new Color( + base.getRed(), + base.getGreen(), + base.getBlue(), + 80 // smaller number means lighter color (more transparent) + ); + } /* ACS = Average Cluster Size ACS answers: "If I pick a cluster at random, how many molecules does it contain on average?" @@ -517,43 +537,154 @@ private void redrawPlot(ClusterSpecificationPanel.ClusterSelection sel) throws E } else { System.out.println("ClusterVisualizationPanel.redrawPlot() selection is null"); } - System.out.println("ClusterVisualizationPanel.redrawPlot(), height = " + getClusterPlotPanel().getHeight()); -// currentSelection = sel; + + if (sel == null || sel.resultSet == null) { + getClusterPlotPanel().clear(); + getClusterPlotPanel().repaint(); + return; + } + List columns = sel.columns; ODESolverResultSet srs = sel.resultSet; int indexTime = srs.findColumn("t"); double[] times = srs.extractColumn(indexTime); -// double globalMin = Double.POSITIVE_INFINITY; - double globalMin = 0; // these are counts, so min is always 0 + // --------------------------------------------------------------------- + // FIRST PASS: load all selected columns normally (except SD curve) + // --------------------------------------------------------------------- + Map yMap = new LinkedHashMap<>(); + + double globalMin = 0.0; // old behavior baseline double globalMax = Double.NEGATIVE_INFINITY; + + for (ColumnDescription cd : columns) { + String name = cd.getName(); + + // SD is special: no curve, but still load its data + int idx = srs.findColumn(name); + if (idx < 0) { + continue; + } + + double[] y = srs.extractColumn(idx); + yMap.put(name, y); + + // SD does not contribute its own curve, but its raw values still matter for globalMax + if (!name.equals("SD")) { + for (double v : y) { + if (v > globalMax) { + globalMax = v; + } + } + } + } + + // --------------------------------------------------------------------- + // SECOND PASS: ACS/SD special logic + // --------------------------------------------------------------------- + boolean acsSelected = yMap.containsKey("ACS"); + boolean sdSelected = yMap.containsKey("SD"); + + double[] acs = null; + double[] sd = null; + + int idxACS = srs.findColumn("ACS"); + int idxSD = srs.findColumn("SD"); + + // We need ACS if either ACS or SD is selected + if (idxACS >= 0) { + acs = (acsSelected ? yMap.get("ACS") : srs.extractColumn(idxACS)); + } + + // We need SD if either ACS or SD is selected + if (idxSD >= 0) { + sd = (sdSelected ? yMap.get("SD") : srs.extractColumn(idxSD)); + } + + // If either ACS or SD is selected, scale using ACS±SD + if ((acsSelected || sdSelected) && acs != null && sd != null) { + for (int i = 0; i < acs.length; i++) { + double upper = acs[i] + sd[i]; + double lower = acs[i] - sd[i]; + + if (upper > globalMax) { + globalMax = upper; + } + if (lower < globalMin) { + globalMin = lower; // may be < 0 in theory + } + } + } + + // --------------------------------------------------------------------- + // DRAWING + // --------------------------------------------------------------------- getClusterPlotPanel().clear(); + // Draw all selected curves EXCEPT SD (SD is envelope only) for (ColumnDescription cd : columns) { - int index = srs.findColumn(cd.getName()); - double[] y = srs.extractColumn(index); - for (double v : y) { -// if (v < globalMin) globalMin = v; - if (v > globalMax) globalMax = v; + String name = cd.getName(); + if (name.equals("SD")) { + continue; // SD never draws a line + } + double[] y = yMap.get(name); + if (y == null) { + continue; + } + Color c = persistentColorMap.get(name); + getClusterPlotPanel().addCurve(name, y, c); + } + + // Draw SD envelope if SD is selected + if (sdSelected && acs != null && sd != null) { + double[] upper = new double[acs.length]; + double[] lower = new double[acs.length]; + for (int i = 0; i < acs.length; i++) { + upper[i] = acs[i] + sd[i]; + lower[i] = acs[i] - sd[i]; } - Color c = persistentColorMap.get(cd.getName()); - getClusterPlotPanel().addCurve(cd.getName(), y, c); + // Use the already‑assigned SD color (derived from ACS in initializeGlobalPalette) + Color sdColor = persistentColorMap.get("SD"); + getClusterPlotPanel().addEnvelope("SD", upper, lower, sdColor); + } + + // --------------------------------------------------------------------- + // FINALIZE + // --------------------------------------------------------------------- + if (globalMin > 0) { + globalMin = 0; } getClusterPlotPanel().setGlobalMinMax(globalMin, globalMax); - getClusterPlotPanel().setDt(times[1]); // times[0] == 0; + if (times.length > 1) { + getClusterPlotPanel().setDt(times[1]); // times[0] == 0 + } getClusterPlotPanel().repaint(); } + private void redrawLegend(ClusterSpecificationPanel.ClusterSelection sel) { System.out.println("ClusterVisualizationPanel.redrawLegend() called"); getJPanelPlotLegends().removeAll(); + for (ColumnDescription cd : sel.columns) { - Color c = persistentColorMap.get(cd.getName()); - getJPanelPlotLegends().add(createLegendEntry(cd.getName(), c, sel.mode)); + String name = cd.getName(); + + Color c; + if (name.equals("SD")) { + // SD uses a translucent version of ACS color + Color cACS = persistentColorMap.get("ACS"); + c = new Color(cACS.getRed(), cACS.getGreen(), cACS.getBlue(), 80); // alpha 0–255 -> ~30% opacity + } else { + // ACS, ACO, COUNTS all use their assigned colors + c = persistentColorMap.get(name); + } + + getJPanelPlotLegends().add(createLegendEntry(name, c, sel.mode)); } getJPanelPlotLegends().revalidate(); getJPanelPlotLegends().repaint(); } + private void redrawDataTable(ClusterSpecificationPanel.ClusterSelection sel) throws ExpressionException { System.out.println("ClusterVisualizationPanel.updateDataTable() called"); getClusterDataPanel().updateData(sel); @@ -562,6 +693,13 @@ public void setSpecialityRenderer(SpecialtyTableRenderer str) { getClusterDataPanel().setSpecialityRenderer(str); } + private boolean contains(List list, String name) { + for (ColumnDescription cd : list) { + if (cd.getName().equals(name)) return true; + } + return false; + } + // crosshair coordinates and coordinates label management public void updateCrosshairCoordinates(double xVal, double yVal) { String text = formatCoord(xVal) + ", " + formatCoord(yVal); diff --git a/vcell-util/src/main/java/org/vcell/util/ColorUtil.java b/vcell-util/src/main/java/org/vcell/util/ColorUtil.java index ed70a49474..3bb8022ab7 100644 --- a/vcell-util/src/main/java/org/vcell/util/ColorUtil.java +++ b/vcell-util/src/main/java/org/vcell/util/ColorUtil.java @@ -125,10 +125,10 @@ public static int calcBrightness(int red,int grn,int blu){ } public static final Color[] TABLEAU20 = { + new Color(214,39,40), // red new Color(31,119,180), // deep blue new Color(255,127,14), // orange new Color(44,160,44), // green - new Color(214,39,40), // red new Color(148,103,189), // purple new Color(23,190,207), // teal new Color(140,86,75), // brown From bf990ea3b0c7e413592b729f3a03fe7fc018af6a Mon Sep 17 00:00:00 2001 From: Dan Vasilescu Date: Thu, 19 Mar 2026 16:53:35 -0400 Subject: [PATCH 16/31] cluster visualization panel: copy to clipboard, renderting, bug fixing --- .../java/cbit/plot/gui/ClusterDataPanel.java | 82 ++++++++++++++++--- .../java/cbit/plot/gui/ClusterPlotPanel.java | 40 ++++++--- .../ode/gui/ClusterSpecificationPanel.java | 20 ++++- 3 files changed, 117 insertions(+), 25 deletions(-) diff --git a/vcell-client/src/main/java/cbit/plot/gui/ClusterDataPanel.java b/vcell-client/src/main/java/cbit/plot/gui/ClusterDataPanel.java index 149cd7f4b8..837dd0f1fa 100644 --- a/vcell-client/src/main/java/cbit/plot/gui/ClusterDataPanel.java +++ b/vcell-client/src/main/java/cbit/plot/gui/ClusterDataPanel.java @@ -1,6 +1,11 @@ package cbit.plot.gui; +import cbit.vcell.desktop.VCellTransferable; +import cbit.vcell.math.ReservedVariable; +import cbit.vcell.parser.Expression; import cbit.vcell.parser.ExpressionException; +import cbit.vcell.parser.SymbolTableEntry; +import cbit.vcell.simdata.UiTableExporterToHDF5; import cbit.vcell.solver.ode.ODESolverResultSet; import cbit.vcell.solver.ode.gui.ClusterSpecificationPanel; import cbit.vcell.util.ColumnDescription; @@ -23,6 +28,7 @@ import java.awt.event.MouseListener; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; +import java.io.File; public class ClusterDataPanel extends JPanel { @@ -100,29 +106,32 @@ public Component getTableCellRendererComponent(JTable table, Object value, boole lbl.setToolTipText(null); return lbl; // leave ScrollTable’s default header styling intact } - + String text = ""; String unit = ""; String tooltip = ""; + ClusterSpecificationPanel.ClusterStatistic stat = ClusterSpecificationPanel.ClusterStatistic.fromString(name); if (column == 0) { unit = "seconds"; - tooltip = "Simulation time in seconds"; + text = "" + name + " [" + unit + "]"; + tooltip = "Simulation time"; } else { - switch (mode) { + switch(mode) { case COUNTS: unit = "molecules"; - tooltip = "Number of clusters of size " + name + " molecules"; + text = "" + name + " [" + unit + "]"; + tooltip = "" + "Number of clusters made of " + name + " " + unit + ""; break; case MEAN: - unit = "value"; - tooltip = "Cluster mean statistic for " + name; - break; case OVERALL: - unit = "value"; - tooltip = "Cluster overall statistic for " + name; + if(stat != null) { + unit = stat.unit(); + text = "" + stat.fullName() + " [" + unit + "]"; + tooltip = "" + stat.description() + ""; + } break; } } - lbl.setText("" + name + " [" + unit + "]"); + lbl.setText(text); lbl.setToolTipText(tooltip); return lbl; } @@ -195,6 +204,7 @@ private JPopupMenu getPopupMenu() { popupMenu.add(miCopyAll); miCopyHDF5 = new JMenuItem("Copy to HDF5"); + miCopyHDF5.setEnabled(false); // export to HDF5 code is not working miCopyHDF5.addActionListener(e -> copyCells(this,true)); popupMenu.add(miCopyHDF5); } @@ -223,11 +233,61 @@ private static synchronized void copyCells(ClusterDataPanel cdp, boolean isHDF5) throw new Exception("No table cell is selected."); } System.out.println("Copying cluster data: rows=" + rows.length + " columns=" + columns.length + " isHDF5=" + isHDF5); + boolean bHistogram = false; // means first column is time (always is for us) + String firstColName = cdp.getScrollPaneTable().getColumnName(0); + String blankCellValue = "-1"; + StringBuffer buffer = new StringBuffer(); + boolean bHasTimeColumn = false; + + if(isHDF5) { + int columnCount = cdp.getScrollPaneTable().getColumnCount(); + int rowCount = cdp.getScrollPaneTable().getRowCount(); + String[] columnNames = new String[columnCount]; + for (int i=0; i0) ){ + resolvedValues[j-(bHasTimeColumn?1:0)] = new Expression(((Double)cell).doubleValue()); + } + } + } + VCellTransferable.ResolvedValuesSelection rvs = + new VCellTransferable.ResolvedValuesSelection(tableSymbolTableEntries,null,resolvedValues,buffer.toString()); + VCellTransferable.sendToClipboard(rvs); } catch (Exception ex) { LG.error("Error copying cluster data", ex); JOptionPane.showMessageDialog(cdp, "Error copying cluster data: " + ex.getMessage(), "Copy Error", JOptionPane.ERROR_MESSAGE); diff --git a/vcell-client/src/main/java/cbit/plot/gui/ClusterPlotPanel.java b/vcell-client/src/main/java/cbit/plot/gui/ClusterPlotPanel.java index 7af6380c9e..28b40b32d7 100644 --- a/vcell-client/src/main/java/cbit/plot/gui/ClusterPlotPanel.java +++ b/vcell-client/src/main/java/cbit/plot/gui/ClusterPlotPanel.java @@ -168,7 +168,32 @@ public static String formatNumber(double v) { return String.format("%.2E", v); // 2.0E-5 } } - + public static String formatTick(double value, double step) { + double absStep = Math.abs(step); + String s; + if (absStep >= 1.0) { + s = String.format("%.0f", value); + } else if (absStep >= 0.1) { + s = String.format("%.1f", value); + } else if (absStep >= 0.01) { + s = String.format("%.2f", value); + } else if (absStep >= 0.001) { + s = String.format("%.3f", value); + } else if (absStep >= 0.0001) { + s = String.format("%.4f", value); + } else { + return String.format("%.2E", value); + } + // strip trailing zeros + while (s.contains(".") && s.endsWith("0")) { + s = s.substring(0, s.length() - 1); + } + // strip trailing decimal point + if (s.endsWith(".")) { + s = s.substring(0, s.length() - 1); + } + return s; + } private int xPixel(double t, int x0, int plotWidth, double xMaxRounded) { return x0 + (int) Math.round((t / xMaxRounded) * plotWidth); } @@ -240,15 +265,9 @@ protected void paintComponent(Graphics g) { int yPixMajor = yPixel(valueMajor, y0, plotHeight, yMaxRounded, yMinRounded); g2.drawLine(x0, yPixMajor, x1, yPixMajor); - String label = formatNumber(valueMajor); - int sw = fm.stringWidth(label); - g2.drawString(label, x0 - 10 - sw, yPixMajor + fm.getAscent() / 2); - if (i < yTicks) { double valueMid = valueMajor + yStep / 2.0; - int yPixMid = y0 - (int)Math.round( - (valueMid - yMinRounded) / yRange * plotHeight - ); + int yPixMid = y0 - (int)Math.round((valueMid - yMinRounded) / yRange * plotHeight); g2.drawLine(x0, yPixMid, x1, yPixMid); } } @@ -284,7 +303,7 @@ protected void paintComponent(Graphics g) { g2.drawLine(x0 - 5, yPixMajor, x0, yPixMajor); - String label = formatNumber(valueMajor); + String label = formatTick(valueMajor, yStep); int sw = fm.stringWidth(label); g2.drawString(label, x0 - 10 - sw, yPixMajor + fm.getAscent() / 2); @@ -296,13 +315,14 @@ protected void paintComponent(Graphics g) { } // X ticks + double xStep = xMajor[1] - xMajor[0]; for (int i = 0; i < xMajor.length; i++) { double xvMajor = xMajor[i]; int xPixMajor = x0 + (int) Math.round((xvMajor / xMaxRounded) * plotWidth); g2.drawLine(xPixMajor, y0, xPixMajor, y0 + 5); - String label = formatNumber(xvMajor); + String label = formatTick(xvMajor, xStep); int sw = fm.stringWidth(label); g2.drawString(label, xPixMajor - sw / 2, y0 + fm.getAscent() + 5); diff --git a/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterSpecificationPanel.java b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterSpecificationPanel.java index 94c6c6598e..d7f8774221 100644 --- a/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterSpecificationPanel.java +++ b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterSpecificationPanel.java @@ -64,17 +64,17 @@ public static DisplayMode fromActionCommand(String cmd) { public enum ClusterStatistic { ACS( - "Average Cluster Size", + "Avg. Cluster Size", "Average number of molecules per cluster", "molecules" ), ACO( - "Average Cluster Occupancy", - "Average number of molecules per molecule (molecule‑centric cluster size)", + "Avg. Cluster Occupancy", + "Average size of the cluster that a molecule belongs to (molecule‑centric cluster size)", "molecules" ), SD( - "Standard Deviation of Cluster Size", + "SD of Cluster Size", "Variability of cluster sizes around the average cluster size (ACS)", "molecules" ); @@ -95,6 +95,18 @@ public String description() { public String unit() { return unit; } + // like valueOf() but returns null instead of throwing exception if not found + public static ClusterStatistic fromString(String s) { + if (s == null) { + return null; + } + for (ClusterStatistic stat : ClusterStatistic.values()) { + if (stat.name().equals(s)) { + return stat; + } + } + return null; + } } public static class ClusterSelection { // used to communicate y-list selection to the ClusterVisualizationPanel From f6d4a65e8ccf4e61b0cd63c0a56aa0c1cb19d162 Mon Sep 17 00:00:00 2001 From: Dan Vasilescu Date: Mon, 23 Mar 2026 13:07:57 -0400 Subject: [PATCH 17/31] cluster plot panel: rendering y-axis ticks --- .../main/java/cbit/plot/gui/ClusterPlotPanel.java | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/vcell-client/src/main/java/cbit/plot/gui/ClusterPlotPanel.java b/vcell-client/src/main/java/cbit/plot/gui/ClusterPlotPanel.java index 28b40b32d7..32a55e41ef 100644 --- a/vcell-client/src/main/java/cbit/plot/gui/ClusterPlotPanel.java +++ b/vcell-client/src/main/java/cbit/plot/gui/ClusterPlotPanel.java @@ -154,20 +154,6 @@ private double roundUpNice(double value) { return rounded * exp; } - public static String formatNumber(double v) { - if (v == 0) return "0"; - - double abs = Math.abs(v); - if (abs >= 1) { - return String.format("%.0f", v); // 0, 10, 20, 30, 40 - } else if (abs >= 0.01) { - return String.format("%.3f", v); // 0.123 - } else if (abs >= 0.0001) { - return String.format("%.5f", v); // 0.00012 - } else { - return String.format("%.2E", v); // 2.0E-5 - } - } public static String formatTick(double value, double step) { double absStep = Math.abs(step); String s; From 9e4cc0a81c9a29f2e67524241a3ef0f47729e0ab Mon Sep 17 00:00:00 2001 From: Dan Vasilescu Date: Tue, 24 Mar 2026 14:35:03 -0400 Subject: [PATCH 18/31] molecule specification and visualization panels - the basics --- .../cbit/vcell/client/data/ODEDataViewer.java | 72 +++++++++-- .../ode/gui/ClusterSpecificationPanel.java | 11 +- .../ode/gui/ClusterVisualizationPanel.java | 1 - .../ode/gui/MoleculeSpecificationPanel.java | 114 ++++++++++++++++++ .../ode/gui/MoleculeVisualizationPanel.java | 113 +++++++++++++++++ .../simdata/LangevinSolverResultSet.java | 25 +++- 6 files changed, 316 insertions(+), 20 deletions(-) create mode 100644 vcell-client/src/main/java/cbit/vcell/solver/ode/gui/MoleculeSpecificationPanel.java create mode 100644 vcell-client/src/main/java/cbit/vcell/solver/ode/gui/MoleculeVisualizationPanel.java diff --git a/vcell-client/src/main/java/cbit/vcell/client/data/ODEDataViewer.java b/vcell-client/src/main/java/cbit/vcell/client/data/ODEDataViewer.java index 2ec7948f14..bd00950751 100644 --- a/vcell-client/src/main/java/cbit/vcell/client/data/ODEDataViewer.java +++ b/vcell-client/src/main/java/cbit/vcell/client/data/ODEDataViewer.java @@ -48,6 +48,9 @@ public class ODEDataViewer extends DataViewer { IvjEventHandler ivjEventHandler = new IvjEventHandler(); private ODESolverPlotSpecificationPanel ivjODESolverPlotSpecificationPanel1 = null; private PlotPane ivjPlotPane1 = null; + + private MoleculeSpecificationPanel moleculeSpecificationPanel = null; + private MoleculeVisualizationPanel moleculeVisualizationPanel = null; private ClusterSpecificationPanel clusterSpecificationPanel = null; private ClusterVisualizationPanel clusterVisualizationPanel = null; @@ -57,7 +60,8 @@ public class ODEDataViewer extends DataViewer { private LangevinSolverResultSet fieldLangevinSolverResultSet = null; private NFSimMolecularConfigurations nFSimMolecularConfigurations = null; private javax.swing.JPanel viewData = null; - private JPanel viewClustersPanel = null; + private JPanel viewMultiClustersPanel = null; + private JPanel viewMultiDataPanel = null; private OutputSpeciesResultsPanel outputSpeciesResultsPanel = null; private VCDataIdentifier fieldVcDataIdentifier = null; @@ -285,9 +289,13 @@ private javax.swing.JTabbedPane getJTabbedPane() { try { ivjJTabbedPane = new javax.swing.JTabbedPane(); ivjJTabbedPane.setName("JTabbedPane1"); - ivjJTabbedPane.insertTab("View Data", null, getViewData(), null, 0); + if(getSimulation() != null && hasLangevinBatchResults) { + ivjJTabbedPane.insertTab("View Data", null, getViewMultiData(), null, 0); + } else { + ivjJTabbedPane.insertTab("View Data", null, getViewMultiData(), null, 0); + } - ivjJTabbedPane.addTab(LANGEVIN_CLUSTER_RESULTS_TABNAME, getViewClusters()); + ivjJTabbedPane.addTab(LANGEVIN_CLUSTER_RESULTS_TABNAME, getViewMultiClusters()); outputSpeciesResultsPanel = new OutputSpeciesResultsPanel(this); outputSpeciesResultsPanel.addPropertyChangeListener(ivjEventHandler); @@ -301,21 +309,57 @@ private javax.swing.JTabbedPane getJTabbedPane() { return ivjJTabbedPane; } -private JPanel getViewClusters() { - if (viewClustersPanel == null) { +private JPanel getViewMultiData() { + if (viewMultiDataPanel == null) { + try { + viewMultiDataPanel = new JPanel(); + viewMultiDataPanel.setName("ViewMultiData"); + viewMultiDataPanel.setLayout(new java.awt.BorderLayout()); + viewMultiDataPanel.add(getMoleculeSpecificationPanel(), "West"); + viewMultiDataPanel.add(getMoleculeVisualizationPanel(), "Center"); + } catch (java.lang.Throwable ivjExc) { + handleException(ivjExc); + } + } + return viewMultiDataPanel; +} +public MoleculeSpecificationPanel getMoleculeSpecificationPanel() { + if (moleculeSpecificationPanel == null) { + try { + moleculeSpecificationPanel = new MoleculeSpecificationPanel(this); + moleculeSpecificationPanel.setName("MoleculeSpecificationPanel"); + } catch (java.lang.Throwable ivjExc) { + handleException(ivjExc); + } + } + return moleculeSpecificationPanel; +} +private MoleculeVisualizationPanel getMoleculeVisualizationPanel() { + if (moleculeVisualizationPanel == null) { + try { + moleculeVisualizationPanel = new MoleculeVisualizationPanel(this); + moleculeVisualizationPanel.setName("DataVisualizationPanel"); + SpecialtyTableRenderer str = new RenderDataViewerDoubleWithTooltip(); + moleculeVisualizationPanel.setSpecialityRenderer(str); + } catch (java.lang.Throwable ivjExc) { + handleException(ivjExc); + } + } + return moleculeVisualizationPanel; +} +private JPanel getViewMultiClusters() { + if (viewMultiClustersPanel == null) { try { - viewClustersPanel = new JPanel(); - viewClustersPanel.setName("ViewClusters"); - viewClustersPanel.setLayout(new java.awt.BorderLayout()); - viewClustersPanel.add(getClusterSpecificationPanel(), "West"); - viewClustersPanel.add(getClusterVisualizationPanel(), "Center"); -// viewClustersPanel = new LangevinClustersResultsPanel(this); -// viewClustersPanel.addPropertyChangeListener(ivjEventHandler); + viewMultiClustersPanel = new JPanel(); + viewMultiClustersPanel.setName("ViewMultiClusters"); + viewMultiClustersPanel.setLayout(new java.awt.BorderLayout()); + viewMultiClustersPanel.add(getClusterSpecificationPanel(), "West"); + viewMultiClustersPanel.add(getClusterVisualizationPanel(), "Center"); } catch (java.lang.Throwable ivjExc) { handleException(ivjExc); } } - return viewClustersPanel; + return viewMultiClustersPanel; } public ClusterSpecificationPanel getClusterSpecificationPanel() { if (clusterSpecificationPanel == null) { @@ -485,6 +529,8 @@ public void setVcDataIdentifier(VCDataIdentifier vcDataIdentifier) { outputSpeciesResultsPanel.refreshData(); getClusterSpecificationPanel().refreshData(); getClusterVisualizationPanel().refreshData(); + getMoleculeSpecificationPanel().refreshData(); + getMoleculeVisualizationPanel().refreshData(); } public void setOdeDataContext() { diff --git a/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterSpecificationPanel.java b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterSpecificationPanel.java index d7f8774221..1af7b6c24c 100644 --- a/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterSpecificationPanel.java +++ b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterSpecificationPanel.java @@ -125,7 +125,7 @@ class IvjEventHandler implements ActionListener, PropertyChangeListener, ListSel public void actionPerformed(ActionEvent e) { String cmd = e.getActionCommand(); if (e.getSource() instanceof JRadioButton rb && SwingUtilities.isDescendingFrom(rb, ClusterSpecificationPanel.this)) { - System.out.println("ClusterSpecificationPanel.actionPerformed() called. Source is JRadioButton: " + rb.getText()); + System.out.println(this.getClass().getName() + ".actionPerformed() called. Source is JRadioButton: " + rb.getText()); DisplayMode mode = DisplayMode.fromActionCommand(cmd); populateYAxisChoices(mode); } @@ -133,12 +133,13 @@ public void actionPerformed(ActionEvent e) { @Override public void propertyChange(PropertyChangeEvent evt) { if(evt.getSource() == ClusterSpecificationPanel.this) { - System.out.println("ClusterSpecificationPanel.IvjEventHandler.propertyChange() called"); + System.out.println(this.getClass().getName() + ".IvjEventHandler.propertyChange() called"); } } @Override public void valueChanged(ListSelectionEvent e) { if (e.getSource() == ClusterSpecificationPanel.this.getYAxisChoice() && !e.getValueIsAdjusting()) { + System.out.println(this.getClass().getName() + ".valueChanged() called. Source is YAxisChoice JList. Selected values: " + getYAxisChoice().getSelectedValuesList()); enforceAcsSdAcoRule(); @@ -251,7 +252,7 @@ public ClusterSpecificationPanel(ODEDataViewer odeDataViewer) { } private void initialize() { - System.out.println("ClusterSpecificationPanel.initialize() called"); + System.out.println(this.getClass().getSimpleName() + ".initialize() called"); setPreferredSize(new Dimension(213, 600)); setLayout(new GridBagLayout()); setSize(248, 604); @@ -437,7 +438,7 @@ private void handleException(java.lang.Throwable exception) { @Override protected void onSelectedObjectsChange(Object[] selectedObjects) { - System.out.println("ClusterSpecificationPanel.onSelectedObjectsChange() called with " + selectedObjects.length + " objects"); + System.out.println(this.getClass().getSimpleName() + ".onSelectedObjectsChange() called with " + selectedObjects.length + " objects"); } public void refreshData() { @@ -449,7 +450,7 @@ public void refreshData() { yAxisCounts.put(DisplayMode.MEAN, countColumns(langevinSolverResultSet.getClusterMean())); yAxisCounts.put(DisplayMode.OVERALL, countColumns(langevinSolverResultSet.getClusterOverall())); } - System.out.println("ClusterSpecificationPanel.refreshData() called"); + System.out.println(this.getClass().getSimpleName() + ".refreshData() called"); // find the selected radio button inside the collapsible panel and fire event as if it were just selected by mouse click // which will populate the y-axis choices based on the new data diff --git a/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterVisualizationPanel.java b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterVisualizationPanel.java index dc67c42a2c..8a1bc5b020 100644 --- a/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterVisualizationPanel.java +++ b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterVisualizationPanel.java @@ -208,7 +208,6 @@ public ClusterDataPanel getClusterDataPanel() { // actual table sh return clusterDataPanel; } - // --------------------------------------------------------------------- private JPanel getBottomRightPanel() { if (bottomRightPanel == null) { bottomRightPanel = new JPanel(); diff --git a/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/MoleculeSpecificationPanel.java b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/MoleculeSpecificationPanel.java new file mode 100644 index 0000000000..202c6a0860 --- /dev/null +++ b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/MoleculeSpecificationPanel.java @@ -0,0 +1,114 @@ +package cbit.vcell.solver.ode.gui; + +import cbit.vcell.client.data.ODEDataViewer; +import cbit.vcell.client.desktop.biomodel.DocumentEditorSubPanel; +import cbit.vcell.simdata.LangevinSolverResultSet; +import cbit.vcell.solver.SimulationModelInfo; +import cbit.vcell.solver.ode.ODESolverResultSet; +import cbit.vcell.util.ColumnDescription; +import org.vcell.util.gui.CollapsiblePanel; + +import javax.swing.*; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; +import javax.swing.event.ListSelectionEvent; +import javax.swing.event.ListSelectionListener; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; + +public class MoleculeSpecificationPanel extends DocumentEditorSubPanel { + + + public static class MoleculeSelection { // used to communicate y-list selection to the ClusterVisualizationPanel + public final java.util.List columns; + public final ODESolverResultSet resultSet; + public MoleculeSelection(java.util.List columns, ODESolverResultSet resultSet) { + this.columns = columns; + this.resultSet = resultSet; + } + } + + class IvjEventHandler implements ActionListener, PropertyChangeListener, ListSelectionListener { + + @Override + public void actionPerformed(ActionEvent e) { + if (e.getSource() instanceof JCheckBox cb && SwingUtilities.isDescendingFrom(cb, MoleculeSpecificationPanel.this)) { + System.out.println(this.getClass().getName() + ".IvjEventHandler.actionPerformed() called with " + e.getActionCommand()); + String cmd = e.getActionCommand(); + } + } + @Override + public void propertyChange(PropertyChangeEvent evt) { + if (evt.getSource() == owner.getClusterSpecificationPanel()) { + System.out.println(this.getClass().getName() + ".IvjEventHandler.propertyChange() called with " + evt.getPropertyName()); + } + } + @Override + public void valueChanged(ListSelectionEvent e) { + if (e.getSource() == MoleculeSpecificationPanel.this.getYAxisChoice() && !e.getValueIsAdjusting()) { + System.out.println(this.getClass().getName() + ".IvjEventHandler.valueChanged() called"); + } + } + } + MoleculeSpecificationPanel.IvjEventHandler ivjEventHandler = new MoleculeSpecificationPanel.IvjEventHandler(); + + // these below may go to a base class + private final ODEDataViewer owner; + LangevinSolverResultSet langevinSolverResultSet = null; + SimulationModelInfo simulationModelInfo = null; + + private CollapsiblePanel displayOptionsCollapsiblePanel = null; + private JScrollPane jScrollPaneYAxis = null; + private static final String YAxisLabelText = "Y Axis: "; + private JLabel yAxisLabel = null; + private JList yAxisChoiceList = null; + private DefaultListModel defaultListModelY = null; + + public MoleculeSpecificationPanel(ODEDataViewer owner) { + super(); + this.owner = owner; + initialize(); + } + private void initialize() { + System.out.println(this.getClass().getSimpleName() + ".initialize() called"); + // layout + + initConnections(); + } + private void initConnections() { + // listeners + } + + + + // ----------------------------------------------------------- + + private JList getYAxisChoice() { + return null; + } + + + + + + + private void handleException(java.lang.Throwable exception) { + System.out.println("--------- UNCAUGHT EXCEPTION ---------"); + exception.printStackTrace(System.out); + } + @Override + protected void onSelectedObjectsChange(Object[] selectedObjects) { + System.out.println(this.getClass().getSimpleName() + ".onSelectedObjectsChange() called with " + selectedObjects.length + " objects"); + + } + public void refreshData() { + System.out.println(this.getClass().getSimpleName() + ".refreshData() called"); + simulationModelInfo = owner.getSimulationModelInfo(); + langevinSolverResultSet = owner.getLangevinSolverResultSet(); + + } + +} diff --git a/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/MoleculeVisualizationPanel.java b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/MoleculeVisualizationPanel.java new file mode 100644 index 0000000000..0e99957af4 --- /dev/null +++ b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/MoleculeVisualizationPanel.java @@ -0,0 +1,113 @@ +package cbit.vcell.solver.ode.gui; + +import cbit.vcell.client.data.ODEDataViewer; +import cbit.vcell.client.desktop.biomodel.DocumentEditorSubPanel; +import cbit.vcell.parser.ExpressionException; +import cbit.vcell.simdata.LangevinSolverResultSet; +import cbit.vcell.solver.SimulationModelInfo; +import org.vcell.util.gui.SpecialtyTableRenderer; + +import javax.swing.*; +import javax.swing.event.ChangeListener; +import javax.swing.event.ListSelectionEvent; +import javax.swing.event.ListSelectionListener; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; + +public class MoleculeVisualizationPanel extends DocumentEditorSubPanel { + + + + + + private final ODEDataViewer owner; + LangevinSolverResultSet langevinSolverResultSet = null; + SimulationModelInfo simulationModelInfo = null; + + class IvjEventHandler implements ActionListener, PropertyChangeListener, ListSelectionListener { + @Override + public void actionPerformed(ActionEvent e) { + if (e.getSource() instanceof Component c && SwingUtilities.isDescendingFrom(c, MoleculeVisualizationPanel.this)) { + System.out.println("MoleculeVisualizationPanel.IvjEventHandler.actionPerformed() called with " + e.getActionCommand()); + // switch selection between plot panel and data panel (located in a JCardLayout) + } + } + @Override + public void propertyChange(PropertyChangeEvent evt) { // listens to changes in the MoleculeSpecificationPanel + if (evt.getSource() == owner.getMoleculeSpecificationPanel() && "MoleculeSelection".equals(evt.getPropertyName())) { + System.out.println("MoleculeVisualizationPanel.IvjEventHandler.propertyChange() called with " + evt.getPropertyName()); + // redraw everything based on the new selections + MoleculeSpecificationPanel.MoleculeSelection sel = (MoleculeSpecificationPanel.MoleculeSelection) evt.getNewValue(); + try { + redrawLegend(sel); // redraw legend (one plot, multiple curves) + redrawPlot(sel); // redraw plot (one plot, multiple curves) + redrawDataTable(sel); // redraw data table + } catch (ExpressionException e) { + throw new RuntimeException(e); + } + } + } + @Override + public void valueChanged(ListSelectionEvent e) { + if (e.getSource() instanceof Component c && SwingUtilities.isDescendingFrom(c, MoleculeVisualizationPanel.this)) { + System.out.println("MoleculeVisualizationPanel.IvjEventHandler.valueChanged() called"); + } + } + } + + public MoleculeVisualizationPanel(ODEDataViewer owner) { + super(); + this.owner = owner; + initialize(); + } + private void initialize() { + // layout + setBackground(Color.white); + + initConnections(); + } + private void initConnections() { + // listeners + + } + + + + + // ---------------------------------------------------------------------- + + + private void redrawPlot(MoleculeSpecificationPanel.MoleculeSelection sel) throws ExpressionException { + + } + private void redrawLegend(MoleculeSpecificationPanel.MoleculeSelection sel) throws ExpressionException { + + } + private void redrawDataTable(MoleculeSpecificationPanel.MoleculeSelection sel) throws ExpressionException { + + } + + private void handleException(java.lang.Throwable exception) { + System.out.println("--------- UNCAUGHT EXCEPTION ---------"); + exception.printStackTrace(System.out); + } + @Override + protected void onSelectedObjectsChange(Object[] selectedObjects) { + System.out.println("MoleculeVisualizationPanel.onSelectedObjectsChange() called with " + selectedObjects.length + " objects"); + + } + public void refreshData() { + System.out.println("MoleculeVisualizationPanel.refreshData() called"); + simulationModelInfo = owner.getSimulationModelInfo(); + langevinSolverResultSet = owner.getLangevinSolverResultSet(); + + } + public void setSpecialityRenderer(SpecialtyTableRenderer str) { +// getClusterDataPanel().setSpecialityRenderer(str); + } + + +} diff --git a/vcell-core/src/main/java/cbit/vcell/simdata/LangevinSolverResultSet.java b/vcell-core/src/main/java/cbit/vcell/simdata/LangevinSolverResultSet.java index 5496a95257..a6de94f1bf 100644 --- a/vcell-core/src/main/java/cbit/vcell/simdata/LangevinSolverResultSet.java +++ b/vcell-core/src/main/java/cbit/vcell/simdata/LangevinSolverResultSet.java @@ -2,18 +2,26 @@ import cbit.vcell.math.ODESolverResultSetColumnDescription; import cbit.vcell.parser.ExpressionException; +import cbit.vcell.solver.DataSymbolMetadata; +import cbit.vcell.solver.SimulationModelInfo; import cbit.vcell.solver.ode.ODESimData; +import cbit.vcell.units.VCUnitDefinition; import cbit.vcell.util.ColumnDescription; +import org.vcell.model.ssld.SsldUtils; import java.io.*; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; public class LangevinSolverResultSet implements Serializable { private final LangevinBatchResultSet raw; - public LangevinSolverResultSet(LangevinBatchResultSet raw) { this.raw = raw; } + public final Map metadataMap = new LinkedHashMap<>(); + // // safe getter that returns a deep copy, but I don't think we need it // public LangevinBatchResultSet getLangevinBatchResultSetSafe() { @@ -85,6 +93,7 @@ public void postProcess() { } if(isAverageDataAvailable()) { ODESimData co = getAvg(); + populateMetadata(co); checkTrivial(co); co = getMin(); checkTrivial(co); @@ -94,6 +103,20 @@ public void postProcess() { checkTrivial(co); } } + private void populateMetadata(ODESimData co) { + // from solver-generated observable (ColumnDescription.name) extract molecule, site and state names + ColumnDescription[] cds = co.getColumnDescriptions(); + for(ColumnDescription cd : cds) { + String columnName = cd.getName(); + SimulationModelInfo.ModelCategoryType filterCategory = null; // parse name to find the filter category + SsldUtils.LangevinResult lr = SsldUtils.LangevinResult.fromString(columnName); + if(lr.qualifier.equals(SsldUtils.Qualifier.NONE)) { + System.out.println("Ignoring LangevinResult token: " + columnName + ", qualifier missing"); + continue; + } + metadataMap.put(columnName, lr); + } + } private static void checkTrivial(ODESimData co) { ColumnDescription[] cds = co.getColumnDescriptions(); for(ColumnDescription columnDescription : cds) { From a6c3f49d841846fd4f5ec16d6005806afdd91c0f Mon Sep 17 00:00:00 2001 From: Dan Vasilescu Date: Tue, 24 Mar 2026 14:46:14 -0400 Subject: [PATCH 19/31] various results panels: generalized class name --- .../ode/gui/ClusterVisualizationPanel.java | 20 +++++++++---------- .../ode/gui/MoleculeVisualizationPanel.java | 10 +++++----- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterVisualizationPanel.java b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterVisualizationPanel.java index 8a1bc5b020..9ca3f42cfc 100644 --- a/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterVisualizationPanel.java +++ b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterVisualizationPanel.java @@ -104,13 +104,13 @@ public void propertyChange(PropertyChangeEvent evt) { @Override public void stateChanged(ChangeEvent e) { if (e.getSource() instanceof Component c && SwingUtilities.isDescendingFrom(c, ClusterVisualizationPanel.this)) { - System.out.println("ClusterVisualizationPanel.IvjEventHandler.stateChanged() called"); + System.out.println(this.getClass().getName() + ".stateChanged() called"); } } @Override public void valueChanged(ListSelectionEvent e) { if (e.getSource() instanceof Component c && SwingUtilities.isDescendingFrom(c, ClusterVisualizationPanel.this)) { - System.out.println("ClusterVisualizationPanel.IvjEventHandler.valueChanged() called"); + System.out.println(this.getClass().getName() + ".valueChanged() called"); } } }; @@ -188,7 +188,7 @@ private ClusterPlotPanel getClusterPlotPanel() { // actual plotting is show clusterPlotPanel.addComponentListener(new ComponentAdapter() { @Override public void componentShown(ComponentEvent e) { - System.out.println("ClusterVisualizationPanel.componentShown() called, height = " + clusterPlotPanel.getHeight()); + System.out.println(this.getClass().getSimpleName() + ".componentShown() called, height = " + clusterPlotPanel.getHeight()); } }); } catch (java.lang.Throwable ivjExc) { @@ -388,7 +388,7 @@ private void handleException(java.lang.Throwable exception) { @Override protected void onSelectedObjectsChange(Object[] selectedObjects) { - System.out.println("ClusterVisualizationPanel.onSelectedObjectsChange() called with " + selectedObjects.length + " objects"); + System.out.println(this.getClass().getSimpleName() + ".onSelectedObjectsChange() called with " + selectedObjects.length + " objects"); } public void refreshData() { @@ -402,7 +402,7 @@ public void refreshData() { } // simulationModelInfo = owner.getSimulationModelInfo(); // langevinSolverResultSet = owner.getLangevinSolverResultSet(); - System.out.println("ClusterVisualizationPanel.refreshData() called"); + System.out.println(this.getClass().getSimpleName() + ".refreshData() called"); } // --------------------------------------------------------------------- @@ -530,11 +530,11 @@ public int getIconHeight() { } private void redrawPlot(ClusterSpecificationPanel.ClusterSelection sel) throws ExpressionException { - System.out.println("ClusterVisualizationPanel.redrawPlot() called, current selection: " + sel); + System.out.println(this.getClass().getSimpleName() + ".redrawPlot() called, current selection: " + sel); if (sel != null) { - System.out.println("ClusterVisualizationPanel.redrawPlot() mode: " + sel.mode + ", columns: " + sel.columns.size() + ", resultSet: " + (sel.resultSet != null ? "present" : "null")); + System.out.println(this.getClass().getSimpleName() + ".redrawPlot() mode: " + sel.mode + ", columns: " + sel.columns.size() + ", resultSet: " + (sel.resultSet != null ? "present" : "null")); } else { - System.out.println("ClusterVisualizationPanel.redrawPlot() selection is null"); + System.out.println(this.getClass().getSimpleName() + ".redrawPlot() selection is null"); } if (sel == null || sel.resultSet == null) { @@ -662,7 +662,7 @@ private void redrawPlot(ClusterSpecificationPanel.ClusterSelection sel) throws E } private void redrawLegend(ClusterSpecificationPanel.ClusterSelection sel) { - System.out.println("ClusterVisualizationPanel.redrawLegend() called"); + System.out.println(this.getClass().getSimpleName() + ".redrawLegend() called"); getJPanelPlotLegends().removeAll(); for (ColumnDescription cd : sel.columns) { @@ -685,7 +685,7 @@ private void redrawLegend(ClusterSpecificationPanel.ClusterSelection sel) { } private void redrawDataTable(ClusterSpecificationPanel.ClusterSelection sel) throws ExpressionException { - System.out.println("ClusterVisualizationPanel.updateDataTable() called"); + System.out.println(this.getClass().getSimpleName() + ".updateDataTable() called"); getClusterDataPanel().updateData(sel); } public void setSpecialityRenderer(SpecialtyTableRenderer str) { diff --git a/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/MoleculeVisualizationPanel.java b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/MoleculeVisualizationPanel.java index 0e99957af4..8736baa93c 100644 --- a/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/MoleculeVisualizationPanel.java +++ b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/MoleculeVisualizationPanel.java @@ -31,14 +31,14 @@ class IvjEventHandler implements ActionListener, PropertyChangeListener, ListSel @Override public void actionPerformed(ActionEvent e) { if (e.getSource() instanceof Component c && SwingUtilities.isDescendingFrom(c, MoleculeVisualizationPanel.this)) { - System.out.println("MoleculeVisualizationPanel.IvjEventHandler.actionPerformed() called with " + e.getActionCommand()); + System.out.println(this.getClass().getName() + ".actionPerformed() called with " + e.getActionCommand()); // switch selection between plot panel and data panel (located in a JCardLayout) } } @Override public void propertyChange(PropertyChangeEvent evt) { // listens to changes in the MoleculeSpecificationPanel if (evt.getSource() == owner.getMoleculeSpecificationPanel() && "MoleculeSelection".equals(evt.getPropertyName())) { - System.out.println("MoleculeVisualizationPanel.IvjEventHandler.propertyChange() called with " + evt.getPropertyName()); + System.out.println(this.getClass().getName() + ".propertyChange() called with " + evt.getPropertyName()); // redraw everything based on the new selections MoleculeSpecificationPanel.MoleculeSelection sel = (MoleculeSpecificationPanel.MoleculeSelection) evt.getNewValue(); try { @@ -53,7 +53,7 @@ public void propertyChange(PropertyChangeEvent evt) { // listens to changes in @Override public void valueChanged(ListSelectionEvent e) { if (e.getSource() instanceof Component c && SwingUtilities.isDescendingFrom(c, MoleculeVisualizationPanel.this)) { - System.out.println("MoleculeVisualizationPanel.IvjEventHandler.valueChanged() called"); + System.out.println(this.getClass().getName() + ".valueChanged() called"); } } } @@ -96,11 +96,11 @@ private void handleException(java.lang.Throwable exception) { } @Override protected void onSelectedObjectsChange(Object[] selectedObjects) { - System.out.println("MoleculeVisualizationPanel.onSelectedObjectsChange() called with " + selectedObjects.length + " objects"); + System.out.println(this.getClass().getSimpleName() + ".onSelectedObjectsChange() called with " + selectedObjects.length + " objects"); } public void refreshData() { - System.out.println("MoleculeVisualizationPanel.refreshData() called"); + System.out.println(this.getClass().getSimpleName() + ".refreshData() called"); simulationModelInfo = owner.getSimulationModelInfo(); langevinSolverResultSet = owner.getLangevinSolverResultSet(); From 36e069a08e378dfa10f69709da1b8c5628731082 Mon Sep 17 00:00:00 2001 From: Dan Vasilescu Date: Tue, 24 Mar 2026 16:52:36 -0400 Subject: [PATCH 20/31] proper management and selection of tabbed panels for batch results --- .../cbit/vcell/client/data/ODEDataViewer.java | 20 ++- .../vcell/client/data/SimResultsViewer.java | 1 + .../ode/gui/MoleculeSpecificationPanel.java | 134 ++++++++++++++++-- .../cbit/vcell/simdata/ODEDataManager.java | 2 +- 4 files changed, 143 insertions(+), 14 deletions(-) diff --git a/vcell-client/src/main/java/cbit/vcell/client/data/ODEDataViewer.java b/vcell-client/src/main/java/cbit/vcell/client/data/ODEDataViewer.java index bd00950751..1ac8bc7f33 100644 --- a/vcell-client/src/main/java/cbit/vcell/client/data/ODEDataViewer.java +++ b/vcell-client/src/main/java/cbit/vcell/client/data/ODEDataViewer.java @@ -292,7 +292,7 @@ private javax.swing.JTabbedPane getJTabbedPane() { if(getSimulation() != null && hasLangevinBatchResults) { ivjJTabbedPane.insertTab("View Data", null, getViewMultiData(), null, 0); } else { - ivjJTabbedPane.insertTab("View Data", null, getViewMultiData(), null, 0); + ivjJTabbedPane.insertTab("View Data", null, getViewData(), null, 0); } ivjJTabbedPane.addTab(LANGEVIN_CLUSTER_RESULTS_TABNAME, getViewMultiClusters()); @@ -308,6 +308,14 @@ private javax.swing.JTabbedPane getJTabbedPane() { } return ivjJTabbedPane; } +public void replaceViewDataTab() { + if (getSimulation() != null && ivjJTabbedPane != null) { + int selected = ivjJTabbedPane.getSelectedIndex(); + ivjJTabbedPane.removeTabAt(0); + ivjJTabbedPane.insertTab("View Data", null, getViewMultiData(), null, 0); + ivjJTabbedPane.setSelectedIndex(selected); + } +} private JPanel getViewMultiData() { if (viewMultiDataPanel == null) { @@ -527,10 +535,12 @@ public void setVcDataIdentifier(VCDataIdentifier vcDataIdentifier) { setOdeDataContext(); firePropertyChange("vcDataIdentifier", oldValue, vcDataIdentifier); outputSpeciesResultsPanel.refreshData(); - getClusterSpecificationPanel().refreshData(); - getClusterVisualizationPanel().refreshData(); - getMoleculeSpecificationPanel().refreshData(); - getMoleculeVisualizationPanel().refreshData(); + if(hasLangevinBatchResults) { + getClusterSpecificationPanel().refreshData(); + getClusterVisualizationPanel().refreshData(); + getMoleculeSpecificationPanel().refreshData(); + getMoleculeVisualizationPanel().refreshData(); + } } public void setOdeDataContext() { diff --git a/vcell-client/src/main/java/cbit/vcell/client/data/SimResultsViewer.java b/vcell-client/src/main/java/cbit/vcell/client/data/SimResultsViewer.java index 0db1152c15..a1bb813a7b 100644 --- a/vcell-client/src/main/java/cbit/vcell/client/data/SimResultsViewer.java +++ b/vcell-client/src/main/java/cbit/vcell/client/data/SimResultsViewer.java @@ -83,6 +83,7 @@ private DataViewer createODEDataViewer() throws DataAccessException { if(getSimulation().getSimulationOwner().getMathDescription().isLangevin() && langevinSolverResultSet.isAverageDataAvailable() && langevinSolverResultSet.isClusterDataAvailable()) { odeDataViewer.setLangevinSolverResultSet(langevinSolverResultSet); odeDataViewer.hasLangevinBatchResults = true; + odeDataViewer.replaceViewDataTab(); } odeDataViewer.setOdeSolverResultSet(odesrs); odeDataViewer.setNFSimMolecularConfigurations(((ODEDataManager)dataManager).getNFSimMolecularConfigurations()); diff --git a/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/MoleculeSpecificationPanel.java b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/MoleculeSpecificationPanel.java index 202c6a0860..f4391eba8b 100644 --- a/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/MoleculeSpecificationPanel.java +++ b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/MoleculeSpecificationPanel.java @@ -21,12 +21,39 @@ public class MoleculeSpecificationPanel extends DocumentEditorSubPanel { + public enum DisplayMode { + MOLECULES("MOLECULES", "Molecules", "Display molecule counts over time"), + BOUND_SITES("BOUND_SITES", "Bound Sites", "Display number of bound sites over time"), + FREE_SITES("FREE_SITES", "Free Sites", "Display number of free sites over time"), + TOTAL_SITES("TOTAL_SITES", "Total Sites", "Display total number of sites over time"); + + private final String actionCommand; + private final String uiLabel; + private final String tooltip; + DisplayMode(String actionCommand, String uiLabel, String tooltip) { + this.actionCommand = actionCommand; + this.uiLabel = uiLabel; + this.tooltip = tooltip; + } + public String actionCommand() { return actionCommand; } + public String uiLabel() { return uiLabel; } + public String tooltip() { return tooltip; } + public static MoleculeSpecificationPanel.DisplayMode fromActionCommand(String cmd) { + for (MoleculeSpecificationPanel.DisplayMode m : values()) { + if (m.actionCommand.equals(cmd)) { + return m; + } + } + throw new IllegalArgumentException("Unknown DisplayMode: " + cmd); + } + + } - public static class MoleculeSelection { // used to communicate y-list selection to the ClusterVisualizationPanel - public final java.util.List columns; + public static class MoleculeSelection { // used to communicate y-list selection to the MoleculeVisualizationPanel + public final java.util.List columnDescriptions; public final ODESolverResultSet resultSet; - public MoleculeSelection(java.util.List columns, ODESolverResultSet resultSet) { - this.columns = columns; + public MoleculeSelection(java.util.List columnDescriptions, ODESolverResultSet resultSet) { + this.columnDescriptions = columnDescriptions; this.resultSet = resultSet; } } @@ -42,7 +69,7 @@ public void actionPerformed(ActionEvent e) { } @Override public void propertyChange(PropertyChangeEvent evt) { - if (evt.getSource() == owner.getClusterSpecificationPanel()) { + if (evt.getSource() == owner.getMoleculeSpecificationPanel()) { System.out.println(this.getClass().getName() + ".IvjEventHandler.propertyChange() called with " + evt.getPropertyName()); } } @@ -75,6 +102,48 @@ public MoleculeSpecificationPanel(ODEDataViewer owner) { private void initialize() { System.out.println(this.getClass().getSimpleName() + ".initialize() called"); // layout + setPreferredSize(new Dimension(213, 600)); + setLayout(new GridBagLayout()); + setSize(248, 604); + setMinimumSize(new Dimension(125, 300)); + + JLabel xAxisLabel = new JLabel("X Axis: "); // non-breaking space is   + GridBagConstraints gbc = new GridBagConstraints(); + gbc.anchor = GridBagConstraints.WEST; + gbc.gridx = 0; gbc.gridy = 0; + gbc.insets = new Insets(4, 4, 0, 4); + add(xAxisLabel, gbc); + + JTextField xAxisTextBox = new JTextField("time [seconds]"); + xAxisTextBox.setEnabled(false); + xAxisTextBox.setEditable(false); + gbc = new GridBagConstraints(); + gbc.gridx = 0; gbc.gridy = 1; + gbc.fill = GridBagConstraints.HORIZONTAL; + gbc.weightx = 1.0; + gbc.insets = new Insets(0, 4, 4, 4); + add(xAxisTextBox, gbc); + + gbc = new GridBagConstraints(); + gbc.anchor = GridBagConstraints.WEST; + gbc.insets = new Insets(4, 4, 0, 4); + gbc.gridx = 0; gbc.gridy = 2; + add(getYAxisLabel(), gbc); + + gbc = new GridBagConstraints(); + gbc.fill = GridBagConstraints.BOTH; + gbc.insets = new Insets(4, 4, 5, 4); + gbc.gridx = 0; + gbc.gridy = 3; + add(getDisplayOptionsPanel(), gbc); + + gbc = new GridBagConstraints(); + gbc.gridx = 0; gbc.gridy = 4; + gbc.fill = GridBagConstraints.BOTH; + gbc.weightx = 1.0; + gbc.weighty = 1.0; + gbc.insets = new Insets(0, 4, 4, 4); + add(getJScrollPaneYAxis(), gbc); initConnections(); } @@ -82,17 +151,66 @@ private void initConnections() { // listeners } + private JLabel getYAxisLabel() { + if (yAxisLabel == null) { + yAxisLabel = new JLabel(); + yAxisLabel.setName("YAxisLabel"); + String text = "" + YAxisLabelText + ""; + yAxisLabel.setText(text); + } + return yAxisLabel; + } + private CollapsiblePanel getDisplayOptionsPanel() { + if (displayOptionsCollapsiblePanel == null) { + displayOptionsCollapsiblePanel = new CollapsiblePanel("Display Options", true); + JPanel content = displayOptionsCollapsiblePanel.getContentPanel(); + content.setLayout(new GridBagLayout()); + + // TODO: add JCheckBoxes for display options here + } + return displayOptionsCollapsiblePanel; + } + private JScrollPane getJScrollPaneYAxis() { + if(jScrollPaneYAxis == null) { + jScrollPaneYAxis = new JScrollPane(); + jScrollPaneYAxis.setName("JScrollPaneYAxis"); + jScrollPaneYAxis.setViewportView(getYAxisChoice()); + // prevent collapse when list is empty + jScrollPaneYAxis.setMinimumSize(new Dimension(100, 120)); + jScrollPaneYAxis.setPreferredSize(new Dimension(100, 120)); + } + return jScrollPaneYAxis; + } - // ----------------------------------------------------------- - private JList getYAxisChoice() { - return null; + + + private javax.swing.JList getYAxisChoice() { + if ((yAxisChoiceList == null)) { + yAxisChoiceList = new JList(); + yAxisChoiceList.setName("YAxisChoice"); + yAxisChoiceList.setBounds(0, 0, 160, 120); + yAxisChoiceList.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); + yAxisChoiceList.setCellRenderer(new DefaultListCellRenderer() { + @Override + public Component getListCellRendererComponent(JList list, Object value, int index, + boolean isSelected, boolean cellHasFocus) { + JLabel label = (JLabel) super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); + if (value instanceof ColumnDescription columnDescription) { + label.setText(columnDescription.getName()); + } + return label; + } + }); + } + return yAxisChoiceList; } + // ----------------------------------------------------------- private void handleException(java.lang.Throwable exception) { diff --git a/vcell-core/src/main/java/cbit/vcell/simdata/ODEDataManager.java b/vcell-core/src/main/java/cbit/vcell/simdata/ODEDataManager.java index b406f8edaa..9c853b1234 100644 --- a/vcell-core/src/main/java/cbit/vcell/simdata/ODEDataManager.java +++ b/vcell-core/src/main/java/cbit/vcell/simdata/ODEDataManager.java @@ -188,7 +188,7 @@ private void connect() throws DataAccessException { if(langevinSolverResultSet != null) { langevinSolverResultSet.postProcess(); } - if( langevinSolverResultSet.isAverageDataAvailable()) { + if( langevinSolverResultSet != null && langevinSolverResultSet.isAverageDataAvailable()) { odeSolverResultSet = langevinSolverResultSet.getAvg(); } } From 8e19ef128f0219cca069eeb5437b039ac75c47d7 Mon Sep 17 00:00:00 2001 From: Dan Vasilescu Date: Tue, 24 Mar 2026 17:58:13 -0400 Subject: [PATCH 21/31] stubs for left and right panels --- .../cbit/vcell/client/data/SimResultsViewer.java | 10 ++++++---- .../solver/ode/gui/MoleculeSpecificationPanel.java | 12 +++++++++++- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/vcell-client/src/main/java/cbit/vcell/client/data/SimResultsViewer.java b/vcell-client/src/main/java/cbit/vcell/client/data/SimResultsViewer.java index a1bb813a7b..ad46b55daf 100644 --- a/vcell-client/src/main/java/cbit/vcell/client/data/SimResultsViewer.java +++ b/vcell-client/src/main/java/cbit/vcell/client/data/SimResultsViewer.java @@ -80,10 +80,12 @@ private DataViewer createODEDataViewer() throws DataAccessException { odeDataViewer.setSimulation(getSimulation()); ODESolverResultSet odesrs = ((ODEDataManager)dataManager).getODESolverResultSet(); LangevinSolverResultSet langevinSolverResultSet = ((ODEDataManager)dataManager).getLangevinSolverResultSet(); - if(getSimulation().getSimulationOwner().getMathDescription().isLangevin() && langevinSolverResultSet.isAverageDataAvailable() && langevinSolverResultSet.isClusterDataAvailable()) { - odeDataViewer.setLangevinSolverResultSet(langevinSolverResultSet); - odeDataViewer.hasLangevinBatchResults = true; - odeDataViewer.replaceViewDataTab(); + if(getSimulation() != null && langevinSolverResultSet != null) { + if (getSimulation().getSimulationOwner().getMathDescription().isLangevin() && langevinSolverResultSet.isAverageDataAvailable() && langevinSolverResultSet.isClusterDataAvailable()) { + odeDataViewer.setLangevinSolverResultSet(langevinSolverResultSet); + odeDataViewer.hasLangevinBatchResults = true; + odeDataViewer.replaceViewDataTab(); + } } odeDataViewer.setOdeSolverResultSet(odesrs); odeDataViewer.setNFSimMolecularConfigurations(((ODEDataManager)dataManager).getNFSimMolecularConfigurations()); diff --git a/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/MoleculeSpecificationPanel.java b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/MoleculeSpecificationPanel.java index f4391eba8b..822e8e4695 100644 --- a/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/MoleculeSpecificationPanel.java +++ b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/MoleculeSpecificationPanel.java @@ -149,6 +149,16 @@ private void initialize() { } private void initConnections() { // listeners + getDisplayOptionsPanel().addPropertyChangeListener(ivjEventHandler); + getYAxisChoice().addListSelectionListener(ivjEventHandler); + this.addPropertyChangeListener(ivjEventHandler); + getYAxisChoice().setModel(getDefaultListModelY()); + } + private DefaultListModel getDefaultListModelY() { + if (defaultListModelY == null) { + defaultListModelY = new DefaultListModel<>(); + } + return defaultListModelY; } private JLabel getYAxisLabel() { @@ -166,7 +176,7 @@ private CollapsiblePanel getDisplayOptionsPanel() { JPanel content = displayOptionsCollapsiblePanel.getContentPanel(); content.setLayout(new GridBagLayout()); - // TODO: add JCheckBoxes for display options here + } return displayOptionsCollapsiblePanel; } From ff9e446f6536237dd61c898f394a5a6ac93165ec Mon Sep 17 00:00:00 2001 From: Dan Vasilescu Date: Thu, 26 Mar 2026 18:40:07 -0400 Subject: [PATCH 22/31] Implementation for specification panels base class and molecule specification panel --- .../cbit/vcell/client/data/ODEDataViewer.java | 3 +- .../ode/gui/AbstractSpecificationPanel.java | 184 ++++++++ .../ode/gui/ClusterSpecificationPanel.java | 19 +- .../ode/gui/MoleculeSpecificationPanel.java | 421 ++++++++++++------ .../simdata/LangevinSolverResultSet.java | 9 + 5 files changed, 501 insertions(+), 135 deletions(-) create mode 100644 vcell-client/src/main/java/cbit/vcell/solver/ode/gui/AbstractSpecificationPanel.java diff --git a/vcell-client/src/main/java/cbit/vcell/client/data/ODEDataViewer.java b/vcell-client/src/main/java/cbit/vcell/client/data/ODEDataViewer.java index 1ac8bc7f33..5c15292c2a 100644 --- a/vcell-client/src/main/java/cbit/vcell/client/data/ODEDataViewer.java +++ b/vcell-client/src/main/java/cbit/vcell/client/data/ODEDataViewer.java @@ -534,12 +534,13 @@ public void setVcDataIdentifier(VCDataIdentifier vcDataIdentifier) { fieldVcDataIdentifier = vcDataIdentifier; setOdeDataContext(); firePropertyChange("vcDataIdentifier", oldValue, vcDataIdentifier); - outputSpeciesResultsPanel.refreshData(); if(hasLangevinBatchResults) { getClusterSpecificationPanel().refreshData(); getClusterVisualizationPanel().refreshData(); getMoleculeSpecificationPanel().refreshData(); getMoleculeVisualizationPanel().refreshData(); + } else { + outputSpeciesResultsPanel.refreshData(); } } diff --git a/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/AbstractSpecificationPanel.java b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/AbstractSpecificationPanel.java new file mode 100644 index 0000000000..c44d1ddb3a --- /dev/null +++ b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/AbstractSpecificationPanel.java @@ -0,0 +1,184 @@ +package cbit.vcell.solver.ode.gui; + +import cbit.vcell.client.data.ODEDataViewer; +import cbit.vcell.client.desktop.biomodel.DocumentEditorSubPanel; +import cbit.vcell.math.ODESolverResultSetColumnDescription; +import cbit.vcell.simdata.LangevinSolverResultSet; +import cbit.vcell.solver.SimulationModelInfo; +import cbit.vcell.solver.ode.ODESolverResultSet; +import cbit.vcell.util.ColumnDescription; +import org.vcell.util.gui.CollapsiblePanel; + +import java.awt.*; +import java.beans.PropertyChangeListener; +import javax.swing.*; + +@SuppressWarnings("serial") +public abstract class AbstractSpecificationPanel extends DocumentEditorSubPanel { + + // ------------------------------ + // Shared UI components + // ------------------------------ + protected CollapsiblePanel displayOptionsCollapsiblePanel = null; + + protected JScrollPane jScrollPaneYAxis = null; + protected static final String YAxisLabelText = "Y Axis: "; + protected JLabel yAxisLabel = null; + + protected JList yAxisChoiceList = null; + protected DefaultListModel defaultListModelY = null; + + // ------------------------------ + // Constructor + // ------------------------------ + public AbstractSpecificationPanel() { + super(); + initialize(); + } + + // ------------------------------ + // Initialization (shared layout) + // ------------------------------ + private void initialize() { + System.out.println(this.getClass().getSimpleName() + ".initialize() called"); + + setPreferredSize(new Dimension(213, 600)); + setLayout(new GridBagLayout()); + setSize(248, 604); + setMinimumSize(new Dimension(125, 300)); + + // X-axis label + JLabel xAxisLabel = new JLabel("X Axis: "); + GridBagConstraints gbc = new GridBagConstraints(); + gbc.anchor = GridBagConstraints.WEST; + gbc.gridx = 0; gbc.gridy = 0; + gbc.insets = new Insets(4, 4, 0, 4); + add(xAxisLabel, gbc); + + // X-axis textbox + JTextField xAxisTextBox = new JTextField("time [seconds]"); + xAxisTextBox.setEnabled(false); + xAxisTextBox.setEditable(false); + gbc = new GridBagConstraints(); + gbc.gridx = 0; gbc.gridy = 1; + gbc.fill = GridBagConstraints.HORIZONTAL; + gbc.weightx = 1.0; + gbc.insets = new Insets(0, 4, 4, 4); + add(xAxisTextBox, gbc); + + // Y-axis label + gbc = new GridBagConstraints(); + gbc.anchor = GridBagConstraints.WEST; + gbc.insets = new Insets(4, 4, 0, 4); + gbc.gridx = 0; gbc.gridy = 2; + add(getYAxisLabel(), gbc); + + // Display options panel (content added by subclasses) + gbc = new GridBagConstraints(); + gbc.fill = GridBagConstraints.BOTH; + gbc.insets = new Insets(4, 4, 5, 4); + gbc.gridx = 0; + gbc.gridy = 3; + add(getDisplayOptionsPanel(), gbc); + + // Y-axis list + gbc = new GridBagConstraints(); + gbc.gridx = 0; gbc.gridy = 4; + gbc.fill = GridBagConstraints.BOTH; + gbc.weightx = 1.0; + gbc.weighty = 1.0; + gbc.insets = new Insets(0, 4, 4, 4); + add(getJScrollPaneYAxis(), gbc); + } + + + // ------------------------------ + // Shared Y-axis label + // ------------------------------ + protected JLabel getYAxisLabel() { + if (yAxisLabel == null) { + yAxisLabel = new JLabel(); + yAxisLabel.setName("YAxisLabel"); + String text = "" + YAxisLabelText + ""; + yAxisLabel.setText(text); + } + return yAxisLabel; + } + + // ------------------------------ + // Shared Display Options container + // (subclasses populate content) + // ------------------------------ + protected CollapsiblePanel getDisplayOptionsPanel() { + if (displayOptionsCollapsiblePanel == null) { + displayOptionsCollapsiblePanel = + new CollapsiblePanel("Display Options", true); + + JPanel content = displayOptionsCollapsiblePanel.getContentPanel(); + content.setLayout(new GridBagLayout()); + } + return displayOptionsCollapsiblePanel; + } + + // ------------------------------ + // Shared Y-axis scroll pane + // ------------------------------ + protected JScrollPane getJScrollPaneYAxis() { + if (jScrollPaneYAxis == null) { + jScrollPaneYAxis = new JScrollPane(); + jScrollPaneYAxis.setName("JScrollPaneYAxis"); + jScrollPaneYAxis.setViewportView(getYAxisChoice()); + jScrollPaneYAxis.setMinimumSize(new Dimension(100, 120)); + jScrollPaneYAxis.setPreferredSize(new Dimension(100, 120)); + } + return jScrollPaneYAxis; + } + + // ------------------------------ + // Shared Y-axis list + // ------------------------------ + protected JList getYAxisChoice() { + if (yAxisChoiceList == null) { + yAxisChoiceList = new JList(); + yAxisChoiceList.setName("YAxisChoice"); + yAxisChoiceList.setBounds(0, 0, 160, 120); + yAxisChoiceList.setSelectionMode( + ListSelectionModel.MULTIPLE_INTERVAL_SELECTION + ); + yAxisChoiceList.setCellRenderer(new DefaultListCellRenderer() { + @Override + public Component getListCellRendererComponent( + JList list, Object value, int index, + boolean isSelected, boolean cellHasFocus) { + + JLabel label = (JLabel) super.getListCellRendererComponent( + list, value, index, isSelected, cellHasFocus + ); + if (value instanceof ColumnDescription cd) { + label.setText(cd.getName()); + } + return label; + } + }); + } + return yAxisChoiceList; + } + + // ------------------------------ + // Shared Y-axis model + // ------------------------------ + protected DefaultListModel getDefaultListModelY() { + if (defaultListModelY == null) { + defaultListModelY = new DefaultListModel<>(); + } + return defaultListModelY; + } + + // ------------------------------ + // Shared exception handler + // ------------------------------ + protected void handleException(Throwable exception) { + System.out.println("--------- UNCAUGHT EXCEPTION ---------"); + exception.printStackTrace(System.out); + } +} \ No newline at end of file diff --git a/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterSpecificationPanel.java b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterSpecificationPanel.java index 1af7b6c24c..b4ea4713e7 100644 --- a/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterSpecificationPanel.java +++ b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterSpecificationPanel.java @@ -248,7 +248,9 @@ private void deselect(String name) { public ClusterSpecificationPanel(ODEDataViewer odeDataViewer) { super(); this.owner = odeDataViewer; - initialize(); + initialize(); // TODO: this will go once we derive from AbstractClusterSpecificationPanel and move all + // the UI construction there, leaving only event handling and data management here + initConnections(); } private void initialize() { @@ -295,8 +297,6 @@ private void initialize() { gbc.weighty = 1.0; gbc.insets = new Insets(0, 4, 4, 4); add(getJScrollPaneYAxis(), gbc); - - initConnections(); } private CollapsiblePanel getDisplayOptionsPanel() { @@ -315,10 +315,6 @@ private CollapsiblePanel getDisplayOptionsPanel() { rbMean.setActionCommand(DisplayMode.MEAN.actionCommand()); rbOverall.setActionCommand(DisplayMode.OVERALL.actionCommand()); - rbCounts.addActionListener(ivjEventHandler); - rbMean.addActionListener(ivjEventHandler); - rbOverall.addActionListener(ivjEventHandler); - rbCounts.setToolTipText(DisplayMode.COUNTS.tooltip()); rbMean.setToolTipText(DisplayMode.MEAN.tooltip()); rbOverall.setToolTipText(DisplayMode.OVERALL.tooltip()); @@ -418,10 +414,15 @@ public Component getListCellRendererComponent(JList list, Object value, int i } private void initConnections() { - getDisplayOptionsPanel().addPropertyChangeListener(ivjEventHandler); + JPanel content = getDisplayOptionsPanel().getContentPanel(); + for (Component c : content.getComponents()) { + if (c instanceof JRadioButton rb) { + rb.addActionListener(ivjEventHandler); + } + } getYAxisChoice().addListSelectionListener(ivjEventHandler); - this.addPropertyChangeListener(ivjEventHandler); getYAxisChoice().setModel(getDefaultListModelY()); + this.addPropertyChangeListener(ivjEventHandler); } private DefaultListModel getDefaultListModelY() { diff --git a/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/MoleculeSpecificationPanel.java b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/MoleculeSpecificationPanel.java index 822e8e4695..02926db28a 100644 --- a/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/MoleculeSpecificationPanel.java +++ b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/MoleculeSpecificationPanel.java @@ -2,10 +2,12 @@ import cbit.vcell.client.data.ODEDataViewer; import cbit.vcell.client.desktop.biomodel.DocumentEditorSubPanel; +import cbit.vcell.math.ODESolverResultSetColumnDescription; import cbit.vcell.simdata.LangevinSolverResultSet; import cbit.vcell.solver.SimulationModelInfo; import cbit.vcell.solver.ode.ODESolverResultSet; import cbit.vcell.util.ColumnDescription; +import org.vcell.model.ssld.SsldUtils; import org.vcell.util.gui.CollapsiblePanel; import javax.swing.*; @@ -13,13 +15,17 @@ import javax.swing.event.ChangeListener; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; +import java.awt.Container; +import java.awt.Component; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; +import java.util.ArrayList; +import java.util.Map; -public class MoleculeSpecificationPanel extends DocumentEditorSubPanel { +public class MoleculeSpecificationPanel extends AbstractSpecificationPanel { public enum DisplayMode { MOLECULES("MOLECULES", "Molecules", "Display molecule counts over time"), @@ -38,6 +44,14 @@ public enum DisplayMode { public String actionCommand() { return actionCommand; } public String uiLabel() { return uiLabel; } public String tooltip() { return tooltip; } + public static boolean isDisplayModeActionCommand(String cmd) { + for (MoleculeSpecificationPanel.DisplayMode m : values()) { + if (m.actionCommand().equals(cmd)) { + return true; + } + } + return false; + } public static MoleculeSpecificationPanel.DisplayMode fromActionCommand(String cmd) { for (MoleculeSpecificationPanel.DisplayMode m : values()) { if (m.actionCommand.equals(cmd)) { @@ -46,15 +60,89 @@ public static MoleculeSpecificationPanel.DisplayMode fromActionCommand(String cm } throw new IllegalArgumentException("Unknown DisplayMode: " + cmd); } + } + public enum StatisticSelection { + + AVG("STAT_AVG", "Average", "Display the average value over time"), + MIN_MAX("STAT_MINMAX", "Min / Max", "Display min/max envelope over time"), + SD("STAT_SD", "Standard Deviation", "Display standard deviation around the mean"); + private final String actionCommand; + private final String uiLabel; + private final String tooltip; + + StatisticSelection(String actionCommand, String uiLabel, String tooltip) { + this.actionCommand = actionCommand; + this.uiLabel = uiLabel; + this.tooltip = tooltip; + } + + public String actionCommand() { return actionCommand; } + public String uiLabel() { return uiLabel; } + public String tooltip() { return tooltip; } + public static boolean isStatisticSelectionActionCommand(String cmd) { + for (MoleculeSpecificationPanel.StatisticSelection s : values()) { + if (s.actionCommand().equals(cmd)) { + return true; + } + } + return false; + } + public static StatisticSelection fromActionCommand(String cmd) { + for (MoleculeSpecificationPanel.StatisticSelection s : values()) { + if (s.actionCommand.equals(cmd)) { + return s; + } + } + throw new IllegalArgumentException("Unknown StatisticSelection: " + cmd); + } + } + + public static class MoleculeSelection { + public final java.util.List selectedColumns; + public final java.util.List selectedStatistics; + public final java.util.List selectedDisplayModes; + + public MoleculeSelection( + java.util.List selectedColumns, + java.util.List selectedStatistics, + java.util.List selectedDisplayModes) { + + this.selectedColumns = selectedColumns; + this.selectedStatistics = selectedStatistics; + this.selectedDisplayModes = selectedDisplayModes; + } } - public static class MoleculeSelection { // used to communicate y-list selection to the MoleculeVisualizationPanel - public final java.util.List columnDescriptions; - public final ODESolverResultSet resultSet; - public MoleculeSelection(java.util.List columnDescriptions, ODESolverResultSet resultSet) { - this.columnDescriptions = columnDescriptions; - this.resultSet = resultSet; + private static class MoleculeYAxisRenderer extends DefaultListCellRenderer { + + @Override + public Component getListCellRendererComponent(JList list, Object value, int index, + boolean isSelected, boolean cellHasFocus) { + JLabel label = (JLabel) super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); + + if (value instanceof ODESolverResultSetColumnDescription cd) { + label.setText(cd.getName()); + if (cd.isTrivial()) { + label.setForeground(Color.GRAY); + } else { + // restore normal selection / non-selection colors + label.setForeground(isSelected + ? list.getSelectionForeground() + : list.getForeground()); + } + label.setToolTipText(buildTooltip(cd)); + } + return label; + } + private String buildTooltip(ColumnDescription cd) { + StringBuilder sb = new StringBuilder(""); + sb.append("").append(cd.getName()).append(" "); + if (cd instanceof ODESolverResultSetColumnDescription mcd) { + sb.append("Solver-generated Observable"); + } + sb.append(""); + return sb.toString(); } } @@ -63,8 +151,21 @@ class IvjEventHandler implements ActionListener, PropertyChangeListener, ListSel @Override public void actionPerformed(ActionEvent e) { if (e.getSource() instanceof JCheckBox cb && SwingUtilities.isDescendingFrom(cb, MoleculeSpecificationPanel.this)) { - System.out.println(this.getClass().getName() + ".IvjEventHandler.actionPerformed() called with " + e.getActionCommand()); + boolean selected = cb.isSelected(); + System.out.println(this.getClass().getName() + ".actionPerformed() called with action command: " + e.getActionCommand() + " = " + selected); String cmd = e.getActionCommand(); + if (DisplayMode.isDisplayModeActionCommand(cmd)) { + java.util.List displayModes = getSelectedDisplayModes(); + populateYAxisChoices(displayModes); + } else if (StatisticSelection.isStatisticSelectionActionCommand(cmd)) { + MoleculeSelection sel = new MoleculeSelection( + getYAxisChoice().getSelectedValuesList(), + getSelectedStatistics(), + getSelectedDisplayModes() + ); + System.out.println("MoleculeSelection changed: " + sel.selectedColumns.size() + " columns, " + sel.selectedStatistics.size() + " statistics, " + sel.selectedDisplayModes.size() + " display modes"); + firePropertyChange("MoleculeStatisticSelectionChanged", null, sel); + } } } @Override @@ -77,6 +178,16 @@ public void propertyChange(PropertyChangeEvent evt) { public void valueChanged(ListSelectionEvent e) { if (e.getSource() == MoleculeSpecificationPanel.this.getYAxisChoice() && !e.getValueIsAdjusting()) { System.out.println(this.getClass().getName() + ".IvjEventHandler.valueChanged() called"); + if (e.getValueIsAdjusting()) { + return; + } + MoleculeSelection sel = new MoleculeSelection( + getYAxisChoice().getSelectedValuesList(), + getSelectedStatistics(), + getSelectedDisplayModes() + ); + System.out.println("MoleculeSelection changed: " + sel.selectedColumns.size() + " columns, " + sel.selectedStatistics.size() + " statistics, " + sel.selectedDisplayModes.size() + " display modes"); + firePropertyChange("MoleculeSelectionChanged", null, sel); } } } @@ -87,146 +198,193 @@ public void valueChanged(ListSelectionEvent e) { LangevinSolverResultSet langevinSolverResultSet = null; SimulationModelInfo simulationModelInfo = null; - private CollapsiblePanel displayOptionsCollapsiblePanel = null; - private JScrollPane jScrollPaneYAxis = null; - private static final String YAxisLabelText = "Y Axis: "; - private JLabel yAxisLabel = null; - private JList yAxisChoiceList = null; - private DefaultListModel defaultListModelY = null; - public MoleculeSpecificationPanel(ODEDataViewer owner) { super(); this.owner = owner; - initialize(); - } - private void initialize() { - System.out.println(this.getClass().getSimpleName() + ".initialize() called"); - // layout - setPreferredSize(new Dimension(213, 600)); - setLayout(new GridBagLayout()); - setSize(248, 604); - setMinimumSize(new Dimension(125, 300)); - - JLabel xAxisLabel = new JLabel("X Axis: "); // non-breaking space is   - GridBagConstraints gbc = new GridBagConstraints(); - gbc.anchor = GridBagConstraints.WEST; - gbc.gridx = 0; gbc.gridy = 0; - gbc.insets = new Insets(4, 4, 0, 4); - add(xAxisLabel, gbc); - - JTextField xAxisTextBox = new JTextField("time [seconds]"); - xAxisTextBox.setEnabled(false); - xAxisTextBox.setEditable(false); - gbc = new GridBagConstraints(); - gbc.gridx = 0; gbc.gridy = 1; - gbc.fill = GridBagConstraints.HORIZONTAL; - gbc.weightx = 1.0; - gbc.insets = new Insets(0, 4, 4, 4); - add(xAxisTextBox, gbc); - - gbc = new GridBagConstraints(); - gbc.anchor = GridBagConstraints.WEST; - gbc.insets = new Insets(4, 4, 0, 4); - gbc.gridx = 0; gbc.gridy = 2; - add(getYAxisLabel(), gbc); - - gbc = new GridBagConstraints(); - gbc.fill = GridBagConstraints.BOTH; - gbc.insets = new Insets(4, 4, 5, 4); - gbc.gridx = 0; - gbc.gridy = 3; - add(getDisplayOptionsPanel(), gbc); - - gbc = new GridBagConstraints(); - gbc.gridx = 0; gbc.gridy = 4; - gbc.fill = GridBagConstraints.BOTH; - gbc.weightx = 1.0; - gbc.weighty = 1.0; - gbc.insets = new Insets(0, 4, 4, 4); - add(getJScrollPaneYAxis(), gbc); - + getYAxisChoice().setCellRenderer(new MoleculeYAxisRenderer()); initConnections(); } - private void initConnections() { - // listeners - getDisplayOptionsPanel().addPropertyChangeListener(ivjEventHandler); + + protected void initConnections() { + JPanel content = getDisplayOptionsPanel().getContentPanel(); + for (Component c : content.getComponents()) { + if (c instanceof JCheckBox) { + ((JCheckBox) c).addActionListener(ivjEventHandler); // wire checkbox listeners now that ivjEventHandler is constructed + } + } getYAxisChoice().addListSelectionListener(ivjEventHandler); - this.addPropertyChangeListener(ivjEventHandler); getYAxisChoice().setModel(getDefaultListModelY()); - } - private DefaultListModel getDefaultListModelY() { - if (defaultListModelY == null) { - defaultListModelY = new DefaultListModel<>(); - } - return defaultListModelY; + this.addPropertyChangeListener(ivjEventHandler); } - private JLabel getYAxisLabel() { - if (yAxisLabel == null) { - yAxisLabel = new JLabel(); - yAxisLabel.setName("YAxisLabel"); - String text = "" + YAxisLabelText + ""; - yAxisLabel.setText(text); - } - return yAxisLabel; - } - private CollapsiblePanel getDisplayOptionsPanel() { - if (displayOptionsCollapsiblePanel == null) { - displayOptionsCollapsiblePanel = new CollapsiblePanel("Display Options", true); - JPanel content = displayOptionsCollapsiblePanel.getContentPanel(); - content.setLayout(new GridBagLayout()); + @Override + protected CollapsiblePanel getDisplayOptionsPanel() { + + CollapsiblePanel cp = super.getDisplayOptionsPanel(); + JPanel content = cp.getContentPanel(); + if (content.getComponentCount() == 0) { // Only populate once + GridBagConstraints gbc = new GridBagConstraints(); + gbc.gridx = 0; + gbc.gridy = 0; + gbc.anchor = GridBagConstraints.WEST; + gbc.insets = new Insets(2, 4, 2, 4); + + JCheckBox firstDisplayModeCheckBox = null; // GROUP 1: DisplayMode checkboxes + for (DisplayMode mode : DisplayMode.values()) { + JCheckBox cb = new JCheckBox(mode.uiLabel()); + cb.setActionCommand(mode.actionCommand()); + cb.setToolTipText(mode.tooltip()); + // can't add action listeners here, the event handler object is stil null + // will add them in initConnections() after the whole panel is constructed + if (firstDisplayModeCheckBox == null) { + firstDisplayModeCheckBox = cb; // remember first + } + content.add(cb, gbc); + gbc.gridy++; + } + if (firstDisplayModeCheckBox != null) { // Select default DisplayMode = first enum entry (MOLECULES) + firstDisplayModeCheckBox.setSelected(true); + } + gbc.insets = new Insets(8, 4, 2, 4); // Spacer + label for statistics group + JLabel statsLabel = new JLabel("Statistics:"); + content.add(statsLabel, gbc); + gbc.gridy++; + gbc.insets = new Insets(2, 16, 2, 4); + + JCheckBox firstStatisticCheckBox = null; // GROUP 2: StatisticSelection checkboxes + for (StatisticSelection stat : StatisticSelection.values()) { + JCheckBox cb = new JCheckBox(stat.uiLabel()); + cb.setActionCommand(stat.actionCommand()); + cb.setToolTipText(stat.tooltip()); + if (firstStatisticCheckBox == null) { + firstStatisticCheckBox = cb; // remember first + } + content.add(cb, gbc); + gbc.gridy++; + } + if (firstStatisticCheckBox != null) { // Select default StatisticSelection = first enum entry (AVG) + firstStatisticCheckBox.setSelected(true); + } } - return displayOptionsCollapsiblePanel; + return cp; } - private JScrollPane getJScrollPaneYAxis() { - if(jScrollPaneYAxis == null) { - jScrollPaneYAxis = new JScrollPane(); - jScrollPaneYAxis.setName("JScrollPaneYAxis"); - jScrollPaneYAxis.setViewportView(getYAxisChoice()); - // prevent collapse when list is empty - jScrollPaneYAxis.setMinimumSize(new Dimension(100, 120)); - jScrollPaneYAxis.setPreferredSize(new Dimension(100, 120)); - } - return jScrollPaneYAxis; - } - - - - - private javax.swing.JList getYAxisChoice() { - if ((yAxisChoiceList == null)) { - yAxisChoiceList = new JList(); - yAxisChoiceList.setName("YAxisChoice"); - yAxisChoiceList.setBounds(0, 0, 160, 120); - yAxisChoiceList.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); - yAxisChoiceList.setCellRenderer(new DefaultListCellRenderer() { - @Override - public Component getListCellRendererComponent(JList list, Object value, int index, - boolean isSelected, boolean cellHasFocus) { - JLabel label = (JLabel) super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); - if (value instanceof ColumnDescription columnDescription) { - label.setText(columnDescription.getName()); + private void populateYAxisChoices(java.util.List modes) { + JList list = getYAxisChoice(); + DefaultListModel model = getDefaultListModelY(); + // Remember what was selected before we touch the model + java.util.List previouslySelected = list.getSelectedValuesList(); + model.clear(); + list.setEnabled(false); + for (DisplayMode mode : modes) { + java.util.List cds = getColumnDescriptionsForMode(mode); + if (cds == null || cds.size() <= 1) { + continue; + } + for (ColumnDescription cd : cds) { + if (!"t".equals(cd.getName())) { + model.addElement(cd); + } + } + } + updateYAxisLabel(model); + if (!model.isEmpty()) { + list.setEnabled(true); + // Re-apply previous selection where possible + if (!previouslySelected.isEmpty()) { + java.util.List indicesToSelect = new ArrayList<>(); + for (int i = 0; i < model.size(); i++) { + ColumnDescription cd = model.get(i); + if (previouslySelected.contains(cd)) { + indicesToSelect.add(i); } - return label; } - }); + if (!indicesToSelect.isEmpty()) { + int[] idx = indicesToSelect.stream().mapToInt(Integer::intValue).toArray(); + list.setSelectedIndices(idx); + return; // do NOT force-select index 0 + } + } + // Fallback: nothing from previous selection exists anymore + list.setSelectedIndex(0); } - return yAxisChoiceList; } - - + private java.util.List getColumnDescriptionsForMode(DisplayMode mode) { + java.util.List list = new ArrayList<>(); + if (langevinSolverResultSet == null || langevinSolverResultSet.metadataMap == null) { + return list; + } + for (Map.Entry entry : langevinSolverResultSet.metadataMap.entrySet()) { + String columnName = entry.getKey(); + SsldUtils.LangevinResult lr = entry.getValue(); + if (lr.qualifier == SsldUtils.Qualifier.NONE) { // Qualifier.NONE is illegal for all modes + throw new IllegalArgumentException("Invalid qualifier NONE for column: " + columnName); + } + boolean include = false; + switch (mode) { + case MOLECULES: + include = (lr.site == null || lr.site.isEmpty()); // Molecules = entries with no site + break; + case BOUND_SITES: + include = (!lr.site.isEmpty() && lr.qualifier == SsldUtils.Qualifier.BOUND); + break; + case FREE_SITES: + include = (!lr.site.isEmpty() && lr.qualifier == SsldUtils.Qualifier.FREE); + break; + case TOTAL_SITES: + include = (!lr.site.isEmpty() && lr.qualifier == SsldUtils.Qualifier.TOTAL); + break; + } + if (include) { + ColumnDescription cd = langevinSolverResultSet.getColumnDescriptionByName(columnName); + if (cd != null) { + list.add(cd); + } + } + } + return list; + } // ----------------------------------------------------------- + private void updateYAxisLabel(DefaultListModel model) { + int count = (model == null) ? 0 : model.getSize(); + String text = "" + YAxisLabelText + "(" + count + " entries)"; + yAxisLabel.setText(text); + } - private void handleException(java.lang.Throwable exception) { - System.out.println("--------- UNCAUGHT EXCEPTION ---------"); - exception.printStackTrace(System.out); + public java.util.List getSelectedDisplayModes() { + java.util.List list = new ArrayList<>(); + JPanel content = getDisplayOptionsPanel().getContentPanel(); + java.awt.Component[] components = content.getComponents(); + for (Component c : components) { + if (c instanceof JCheckBox cb && cb.isSelected()) { + String actionCommand = cb.getActionCommand(); + if (DisplayMode.isDisplayModeActionCommand(actionCommand)) { + list.add(DisplayMode.fromActionCommand(actionCommand)); + } + } + } + return list; + } + public java.util.List getSelectedStatistics() { + java.util.List list = new ArrayList<>(); + JPanel content = getDisplayOptionsPanel().getContentPanel(); + java.awt.Component[] components = content.getComponents(); + for (Component c : components) { + if (c instanceof JCheckBox cb && cb.isSelected()) { + String actionCommand = cb.getActionCommand(); + if (StatisticSelection.isStatisticSelectionActionCommand(actionCommand)) { + list.add(StatisticSelection.fromActionCommand(actionCommand)); + } + } + } + return list; } + @Override protected void onSelectedObjectsChange(Object[] selectedObjects) { System.out.println(this.getClass().getSimpleName() + ".onSelectedObjectsChange() called with " + selectedObjects.length + " objects"); @@ -236,7 +394,20 @@ public void refreshData() { System.out.println(this.getClass().getSimpleName() + ".refreshData() called"); simulationModelInfo = owner.getSimulationModelInfo(); langevinSolverResultSet = owner.getLangevinSolverResultSet(); - + if(displayOptionsCollapsiblePanel == null) { // the whole panel should exist already at this point, + // lazy inilization here may be a bad idea but we'll do it anyway + System.out.println("displayOptionsCollapsiblePanel is null"); + } + JPanel content = getDisplayOptionsPanel().getContentPanel(); + for (Component c : content.getComponents()) { + if (c instanceof JCheckBox cb && cb.isSelected()) { + // we know that during the initial construction of displayOptionsCollapsiblePanel we select the + // first DisplayMode and first StatisticSelection checkboxes, so this code will fire once for sure + // and so we'll properly populate the yAxisChoiceList + ivjEventHandler.actionPerformed(new ActionEvent(cb, ActionEvent.ACTION_PERFORMED, cb.getActionCommand())); + break; + } + } } } diff --git a/vcell-core/src/main/java/cbit/vcell/simdata/LangevinSolverResultSet.java b/vcell-core/src/main/java/cbit/vcell/simdata/LangevinSolverResultSet.java index a6de94f1bf..e70b9418c0 100644 --- a/vcell-core/src/main/java/cbit/vcell/simdata/LangevinSolverResultSet.java +++ b/vcell-core/src/main/java/cbit/vcell/simdata/LangevinSolverResultSet.java @@ -51,6 +51,15 @@ public ODESimData getClusterOverall() { return raw == null ? null : raw.getOdeSimDataClusterOverall(); } + public ColumnDescription getColumnDescriptionByName(String columnName) { + if(raw == null || raw.getOdeSimDataAvg() == null) { + return null; + } + int index = raw.getOdeSimDataAvg().findColumn(columnName); + ColumnDescription cd = raw.getOdeSimDataAvg().getColumnDescriptions(index); + return cd; + } + // helper functions public boolean isAverageDataAvailable() { return getAvg() != null && From d7f9f51ce318e91dc815486d38c772bc99498b31 Mon Sep 17 00:00:00 2001 From: Dan Vasilescu Date: Mon, 30 Mar 2026 13:35:53 -0400 Subject: [PATCH 23/31] Implementation for specification panels base class and molecule specification panel --- .../ode/gui/AbstractSpecificationPanel.java | 4 +- .../ode/gui/ClusterSpecificationPanel.java | 303 ++++++++---------- .../ode/gui/MoleculeSpecificationPanel.java | 14 +- 3 files changed, 131 insertions(+), 190 deletions(-) diff --git a/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/AbstractSpecificationPanel.java b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/AbstractSpecificationPanel.java index c44d1ddb3a..808df23110 100644 --- a/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/AbstractSpecificationPanel.java +++ b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/AbstractSpecificationPanel.java @@ -111,9 +111,7 @@ protected JLabel getYAxisLabel() { // ------------------------------ protected CollapsiblePanel getDisplayOptionsPanel() { if (displayOptionsCollapsiblePanel == null) { - displayOptionsCollapsiblePanel = - new CollapsiblePanel("Display Options", true); - + displayOptionsCollapsiblePanel = new CollapsiblePanel("Display Options", true); JPanel content = displayOptionsCollapsiblePanel.getContentPanel(); content.setLayout(new GridBagLayout()); } diff --git a/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterSpecificationPanel.java b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterSpecificationPanel.java index b4ea4713e7..d7dd69a186 100644 --- a/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterSpecificationPanel.java +++ b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterSpecificationPanel.java @@ -23,7 +23,7 @@ import java.util.LinkedHashMap; import java.util.Map; -public class ClusterSpecificationPanel extends DocumentEditorSubPanel { +public class ClusterSpecificationPanel extends AbstractSpecificationPanel { public enum DisplayMode { COUNTS( @@ -120,6 +120,45 @@ public ClusterSelection(DisplayMode mode, java.util.List colu } } + private static class ClusterYAxisRenderer extends DefaultListCellRenderer { + @Override + public Component getListCellRendererComponent(JList list, Object value, int index, + boolean isSelected, boolean cellHasFocus) { + JLabel label = (JLabel) super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); + + if (value instanceof ODESolverResultSetColumnDescription cd) { + label.setText(cd.getName()); + if (cd.isTrivial()) { + label.setForeground(Color.GRAY); + } else { + label.setForeground(isSelected + ? list.getSelectionForeground() + : list.getForeground()); + } + DisplayMode mode = (DisplayMode) + ((JComponent) list).getClientProperty("ClusterDisplayMode"); + label.setToolTipText(buildTooltip(cd, mode)); + } + return label; + } + private String buildTooltip(ODESolverResultSetColumnDescription cd, DisplayMode mode) { + if (mode == null) { + return null; + } + String name = cd.getName(); + return switch (mode) { + case COUNTS -> + "Number of clusters of size " + name + + " [molecules]"; + case MEAN, OVERALL -> { + ClusterStatistic stat = ClusterStatistic.valueOf(name); + yield "" + stat.description + + " [" + stat.unit + "]"; + } + }; + } + } + class IvjEventHandler implements ActionListener, PropertyChangeListener, ListSelectionListener { @Override public void actionPerformed(ActionEvent e) { @@ -140,17 +179,13 @@ public void propertyChange(PropertyChangeEvent evt) { public void valueChanged(ListSelectionEvent e) { if (e.getSource() == ClusterSpecificationPanel.this.getYAxisChoice() && !e.getValueIsAdjusting()) { System.out.println(this.getClass().getName() + ".valueChanged() called. Source is YAxisChoice JList. Selected values: " + getYAxisChoice().getSelectedValuesList()); - enforceAcsSdAcoRule(); - // extract selected ColumnDescriptions java.util.List selected = getYAxisChoice().getSelectedValuesList(); DisplayMode mode = getCurrentDisplayMode(); ODESolverResultSet srs = getResultSetForMode(mode); - // set property to inform the list about current mode (needed for renderer) yAxisChoiceList.putClientProperty("ClusterDisplayMode", mode); - // fire the event upward firePropertyChange("ClusterSelection", null, new ClusterSelection(mode, selected, srs)); } @@ -158,36 +193,47 @@ public void valueChanged(ListSelectionEvent e) { }; private final ODEDataViewer owner; - LangevinSolverResultSet langevinSolverResultSet = null; - SimulationModelInfo simulationModelInfo = null; + private LangevinSolverResultSet langevinSolverResultSet = null; + private SimulationModelInfo simulationModelInfo = null; + private final Map yAxisCounts = new LinkedHashMap<>(); ClusterSpecificationPanel.IvjEventHandler ivjEventHandler = new ClusterSpecificationPanel.IvjEventHandler(); - private CollapsiblePanel displayOptionsCollapsiblePanel = null; - private JScrollPane jScrollPaneYAxis = null; - private static final String YAxisLabelText = "Y Axis: "; - private JLabel yAxisLabel = null; - private JList yAxisChoiceList = null; - private DefaultListModel defaultListModelY = null; + public ClusterSpecificationPanel(ODEDataViewer odeDataViewer) { + super(); + this.owner = odeDataViewer; + getYAxisChoice().setCellRenderer(new ClusterYAxisRenderer()); + initConnections(); + } + + private void initConnections() { + JPanel content = getDisplayOptionsPanel().getContentPanel(); + for (Component c : content.getComponents()) { + if (c instanceof JRadioButton rb) { + rb.addActionListener(ivjEventHandler); + } + } + getYAxisChoice().addListSelectionListener(ivjEventHandler); + getYAxisChoice().setModel(getDefaultListModelY()); + this.addPropertyChangeListener(ivjEventHandler); + } + + // -------------------------------------------------------------- private void populateYAxisChoices(DisplayMode mode) { DefaultListModel model = getDefaultListModelY(); model.clear(); getYAxisChoice().setEnabled(false); - updateYAxisLabel(mode); - ColumnDescription[] cds = getColumnDescriptionsForMode(mode); if (cds == null || cds.length <= 1) { return; } - for (ColumnDescription cd : cds) { if (!"t".equals(cd.getName())) { model.addElement(cd); } } - if (!model.isEmpty()) { getYAxisChoice().setEnabled(true); getYAxisChoice().setSelectedIndex(0); // triggers valueChanged() @@ -206,7 +252,6 @@ private void enforceAcsSdAcoRule() { return; // rule applies only in these modes } java.util.List selected = getYAxisChoice().getSelectedValuesList(); - boolean acs = contains(selected, "ACS"); boolean sd = contains(selected, "SD"); boolean aco = contains(selected, "ACO"); @@ -245,70 +290,16 @@ private void deselect(String name) { } } - public ClusterSpecificationPanel(ODEDataViewer odeDataViewer) { - super(); - this.owner = odeDataViewer; - initialize(); // TODO: this will go once we derive from AbstractClusterSpecificationPanel and move all - // the UI construction there, leaving only event handling and data management here - initConnections(); - } - - private void initialize() { - System.out.println(this.getClass().getSimpleName() + ".initialize() called"); - setPreferredSize(new Dimension(213, 600)); - setLayout(new GridBagLayout()); - setSize(248, 604); - setMinimumSize(new Dimension(125, 300)); - - JLabel xAxisLabel = new JLabel("X Axis: "); // non-breaking space is   - GridBagConstraints gbc = new GridBagConstraints(); - gbc.anchor = GridBagConstraints.WEST; - gbc.gridx = 0; gbc.gridy = 0; - gbc.insets = new Insets(4, 4, 0, 4); - add(xAxisLabel, gbc); - - JTextField xAxisTextBox = new JTextField("time [seconds]"); - xAxisTextBox.setEnabled(false); - xAxisTextBox.setEditable(false); - gbc = new GridBagConstraints(); - gbc.gridx = 0; gbc.gridy = 1; - gbc.fill = GridBagConstraints.HORIZONTAL; - gbc.weightx = 1.0; - gbc.insets = new Insets(0, 4, 4, 4); - add(xAxisTextBox, gbc); - - gbc = new GridBagConstraints(); - gbc.anchor = GridBagConstraints.WEST; - gbc.insets = new Insets(4, 4, 0, 4); - gbc.gridx = 0; gbc.gridy = 2; - add(getYAxisLabel(), gbc); - - gbc = new GridBagConstraints(); - gbc.fill = GridBagConstraints.BOTH; - gbc.insets = new Insets(4, 4, 5, 4); - gbc.gridx = 0; - gbc.gridy = 3; - add(getDisplayOptionsPanel(), gbc); - - gbc = new GridBagConstraints(); - gbc.gridx = 0; gbc.gridy = 4; - gbc.fill = GridBagConstraints.BOTH; - gbc.weightx = 1.0; - gbc.weighty = 1.0; - gbc.insets = new Insets(0, 4, 4, 4); - add(getJScrollPaneYAxis(), gbc); - } - - private CollapsiblePanel getDisplayOptionsPanel() { - if(displayOptionsCollapsiblePanel == null) { - displayOptionsCollapsiblePanel = new CollapsiblePanel("Display Options", true); - JPanel content = displayOptionsCollapsiblePanel.getContentPanel(); - content.setLayout(new GridBagLayout()); + @Override + protected CollapsiblePanel getDisplayOptionsPanel() { + CollapsiblePanel cp = super.getDisplayOptionsPanel(); + JPanel content = cp.getContentPanel(); + if (content.getComponentCount() == 0) { // Only populate once ButtonGroup group = new ButtonGroup(); - JRadioButton rbCounts = new JRadioButton(DisplayMode.COUNTS.uiLabel()); - JRadioButton rbMean = new JRadioButton(DisplayMode.MEAN.uiLabel()); + JRadioButton rbCounts = new JRadioButton(DisplayMode.COUNTS.uiLabel()); + JRadioButton rbMean = new JRadioButton(DisplayMode.MEAN.uiLabel()); JRadioButton rbOverall = new JRadioButton(DisplayMode.OVERALL.uiLabel()); rbCounts.setActionCommand(DisplayMode.COUNTS.actionCommand()); @@ -336,106 +327,63 @@ private CollapsiblePanel getDisplayOptionsPanel() { gbc.gridy = 2; content.add(rbOverall, gbc); } - return displayOptionsCollapsiblePanel; - } - - private JScrollPane getJScrollPaneYAxis() { - if(jScrollPaneYAxis == null) { - jScrollPaneYAxis = new JScrollPane(); - jScrollPaneYAxis.setName("JScrollPaneYAxis"); - jScrollPaneYAxis.setViewportView(getYAxisChoice()); - // prevent collapse when list is empty - jScrollPaneYAxis.setMinimumSize(new Dimension(100, 120)); - jScrollPaneYAxis.setPreferredSize(new Dimension(100, 120)); - } - return jScrollPaneYAxis; - } - private JLabel getYAxisLabel() { - if (yAxisLabel == null) { - yAxisLabel = new JLabel(); - yAxisLabel.setName("YAxisLabel"); - String text = "" + YAxisLabelText + ""; - yAxisLabel.setText(text); - } - return yAxisLabel; - } - private JList getYAxisChoice() { - if ((yAxisChoiceList == null)) { - yAxisChoiceList = new JList(); - yAxisChoiceList.setName("YAxisChoice"); - yAxisChoiceList.setBounds(0, 0, 160, 120); - yAxisChoiceList.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); - yAxisChoiceList.setCellRenderer(new DefaultListCellRenderer() { - @Override - public Component getListCellRendererComponent(JList list, Object value, int index, - boolean isSelected, boolean cellHasFocus) { - JLabel label = (JLabel) super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); - - if (value instanceof ODESolverResultSetColumnDescription cd) { - String name = cd.getName(); - label.setText(name); - - if (cd.isTrivial()) { // gray out trivial entries - label.setForeground(Color.GRAY); - } else { - label.setForeground(isSelected - ? list.getSelectionForeground() - : list.getForeground()); - } - - DisplayMode mode = (DisplayMode) // determine tooltip based on DisplayMode - ((JComponent) list).getClientProperty("ClusterDisplayMode"); - if (mode == null) { - label.setToolTipText(null); - return label; - } - switch (mode) { - case COUNTS: - // cluster size X molecules - label.setText(name); - label.setToolTipText( - "Number of Clusters of size: " + name + " " + "" + "[molecules] " - ); - break; - case MEAN: - case OVERALL: - ClusterStatistic stat = ClusterStatistic.valueOf(name); - label.setText(stat.fullName); - String tooltip = "" + stat.description + "" + " [" + stat.unit + "] "; - label.setToolTipText(tooltip); - break; - } - } - return label; - } - }); - } - return yAxisChoiceList; - } - - private void initConnections() { - JPanel content = getDisplayOptionsPanel().getContentPanel(); - for (Component c : content.getComponents()) { - if (c instanceof JRadioButton rb) { - rb.addActionListener(ivjEventHandler); - } - } - getYAxisChoice().addListSelectionListener(ivjEventHandler); - getYAxisChoice().setModel(getDefaultListModelY()); - this.addPropertyChangeListener(ivjEventHandler); + return cp; } - private DefaultListModel getDefaultListModelY() { - if (defaultListModelY == null) { - defaultListModelY = new DefaultListModel<>(); - } - return defaultListModelY; - } - - private void handleException(java.lang.Throwable exception) { - System.out.println("--------- UNCAUGHT EXCEPTION ---------"); - exception.printStackTrace(System.out); - } +// @Override +// protected JList getYAxisChoice() { +// if ((yAxisChoiceList == null)) { +// yAxisChoiceList = new JList(); +// yAxisChoiceList.setName("YAxisChoice"); +// yAxisChoiceList.setBounds(0, 0, 160, 120); +// yAxisChoiceList.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); +// yAxisChoiceList.setCellRenderer(new DefaultListCellRenderer() { +// @Override +// public Component getListCellRendererComponent(JList list, Object value, int index, +// boolean isSelected, boolean cellHasFocus) { +// JLabel label = (JLabel) super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); +// +// if (value instanceof ODESolverResultSetColumnDescription cd) { +// String name = cd.getName(); +// label.setText(name); +// +// if (cd.isTrivial()) { // gray out trivial entries +// label.setForeground(Color.GRAY); +// } else { +// label.setForeground(isSelected +// ? list.getSelectionForeground() +// : list.getForeground()); +// } +// +// DisplayMode mode = (DisplayMode) // determine tooltip based on DisplayMode +// ((JComponent) list).getClientProperty("ClusterDisplayMode"); +// if (mode == null) { +// label.setToolTipText(null); +// return label; +// } +// switch (mode) { +// case COUNTS: +// // cluster size X molecules +// label.setText(name); +// label.setToolTipText( +// "Number of Clusters of size: " + name + " " + "" + "[molecules] " +// ); +// break; +// case MEAN: +// case OVERALL: +// ClusterStatistic stat = ClusterStatistic.valueOf(name); +// label.setText(stat.fullName); +// String tooltip = "" + stat.description + "" + " [" + stat.unit + "] "; +// label.setToolTipText(tooltip); +// break; +// } +// } +// return label; +// } +// }); +// } +// return yAxisChoiceList; +// } @Override protected void onSelectedObjectsChange(Object[] selectedObjects) { @@ -479,6 +427,7 @@ private ODESolverResultSet getResultSetForMode(DisplayMode mode) { case OVERALL-> langevinSolverResultSet.getClusterOverall(); }; } + private ColumnDescription[] getColumnDescriptionsForMode(DisplayMode mode) { ODESolverResultSet srs = getResultSetForMode(mode); return (srs == null ? null : srs.getColumnDescriptions()); diff --git a/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/MoleculeSpecificationPanel.java b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/MoleculeSpecificationPanel.java index 02926db28a..a09469e2eb 100644 --- a/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/MoleculeSpecificationPanel.java +++ b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/MoleculeSpecificationPanel.java @@ -70,13 +70,11 @@ public enum StatisticSelection { private final String actionCommand; private final String uiLabel; private final String tooltip; - StatisticSelection(String actionCommand, String uiLabel, String tooltip) { this.actionCommand = actionCommand; this.uiLabel = uiLabel; this.tooltip = tooltip; } - public String actionCommand() { return actionCommand; } public String uiLabel() { return uiLabel; } public String tooltip() { return tooltip; } @@ -102,12 +100,10 @@ public static class MoleculeSelection { public final java.util.List selectedColumns; public final java.util.List selectedStatistics; public final java.util.List selectedDisplayModes; - public MoleculeSelection( java.util.List selectedColumns, java.util.List selectedStatistics, java.util.List selectedDisplayModes) { - this.selectedColumns = selectedColumns; this.selectedStatistics = selectedStatistics; this.selectedDisplayModes = selectedDisplayModes; @@ -115,7 +111,6 @@ public MoleculeSelection( } private static class MoleculeYAxisRenderer extends DefaultListCellRenderer { - @Override public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) { @@ -147,7 +142,6 @@ private String buildTooltip(ColumnDescription cd) { } class IvjEventHandler implements ActionListener, PropertyChangeListener, ListSelectionListener { - @Override public void actionPerformed(ActionEvent e) { if (e.getSource() instanceof JCheckBox cb && SwingUtilities.isDescendingFrom(cb, MoleculeSpecificationPanel.this)) { @@ -193,10 +187,9 @@ public void valueChanged(ListSelectionEvent e) { } MoleculeSpecificationPanel.IvjEventHandler ivjEventHandler = new MoleculeSpecificationPanel.IvjEventHandler(); - // these below may go to a base class private final ODEDataViewer owner; - LangevinSolverResultSet langevinSolverResultSet = null; - SimulationModelInfo simulationModelInfo = null; + private LangevinSolverResultSet langevinSolverResultSet = null; + private SimulationModelInfo simulationModelInfo = null; public MoleculeSpecificationPanel(ODEDataViewer owner) { super(); @@ -217,12 +210,13 @@ protected void initConnections() { this.addPropertyChangeListener(ivjEventHandler); } + // ----------------------------------------------------------- @Override protected CollapsiblePanel getDisplayOptionsPanel() { - CollapsiblePanel cp = super.getDisplayOptionsPanel(); JPanel content = cp.getContentPanel(); + if (content.getComponentCount() == 0) { // Only populate once GridBagConstraints gbc = new GridBagConstraints(); gbc.gridx = 0; From fe4ecd4e2a53c1a4dd8ef1b3db2fceea76e1f9ca Mon Sep 17 00:00:00 2001 From: Dan Vasilescu Date: Wed, 1 Apr 2026 16:21:41 -0400 Subject: [PATCH 24/31] Implementation for specification panels base class and molecule specification panel --- .../java/cbit/plot/gui/ClusterPlotPanel.java | 1 + .../ode/gui/AbstractVisualizationPanel.java | 334 ++++++++++++++++++ .../ode/gui/ClusterVisualizationPanel.java | 326 +++-------------- .../ode/gui/MoleculeVisualizationPanel.java | 4 +- 4 files changed, 379 insertions(+), 286 deletions(-) create mode 100644 vcell-client/src/main/java/cbit/vcell/solver/ode/gui/AbstractVisualizationPanel.java diff --git a/vcell-client/src/main/java/cbit/plot/gui/ClusterPlotPanel.java b/vcell-client/src/main/java/cbit/plot/gui/ClusterPlotPanel.java index 32a55e41ef..1b3e52048e 100644 --- a/vcell-client/src/main/java/cbit/plot/gui/ClusterPlotPanel.java +++ b/vcell-client/src/main/java/cbit/plot/gui/ClusterPlotPanel.java @@ -117,6 +117,7 @@ public void mouseExited(MouseEvent e) { public void setCrosshairEnabled(boolean enabled) { this.crosshairEnabled = enabled; + repaint(); } public void setCoordinateCallback(Consumer cb) { this.coordCallback = cb; diff --git a/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/AbstractVisualizationPanel.java b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/AbstractVisualizationPanel.java new file mode 100644 index 0000000000..d7692f2533 --- /dev/null +++ b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/AbstractVisualizationPanel.java @@ -0,0 +1,334 @@ +package cbit.vcell.solver.ode.gui; + +import cbit.vcell.client.desktop.biomodel.DocumentEditorSubPanel; +import org.vcell.util.gui.JToolBarToggleButton; +import org.vcell.util.gui.VCellIcons; + + +import java.awt.*; +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; +import java.util.Locale; +import javax.swing.*; +import javax.swing.border.EmptyBorder; + +public abstract class AbstractVisualizationPanel extends DocumentEditorSubPanel { + + protected class LineIcon implements Icon { + private final Color color; + public LineIcon(Color color) { + this.color = color; + } + @Override + public void paintIcon(Component c, Graphics g, int x, int y) { + Graphics2D g2 = (Graphics2D)g; + g2.setStroke(new BasicStroke(3.0f)); + g2.setPaint(color); + int midY = y + getIconHeight() / 2; + g2.drawLine(x, midY, x + getIconWidth(), midY); + } + @Override + public int getIconWidth() { return 50; } + @Override + public int getIconHeight() { + return 4; // more vertical room for a wider stroke + } + } + + protected JPanel cardPanel; + protected JPanel plotPanelContainer; + protected JPanel dataPanelContainer; + protected JPanel legendPanel; + protected JPanel legendContentPanel; + protected JPanel bottomRightPanel; + + protected JLabel bottomLabel; // info about the current simulation and # of jobs + private JLabel timeLabelBottom; // "time" under the X-axis in the plot + protected JCheckBox crosshairCheckBox; + protected JLabel crosshairCoordLabel; + + protected JToolBarToggleButton plotButton; + protected JToolBarToggleButton dataButton; + + protected CardLayout cardLayout; + + + public AbstractVisualizationPanel() { + super(); + // do NOT call initialize here !!! there are overriden calls to abstract methods that rely on subclass fields + } + + // initialize calls ovverriden methods that rely on subclass fields, so we can't call it from the constructor. + // Instead, each subclass must call initialize() after its own constructor has run and its own fields are initialized. + protected void initialize() { + setPreferredSize(new Dimension(420, 400)); + setLayout(new BorderLayout()); + setSize(513, 457); + setBackground(Color.white); + + add(getCardPanel(), BorderLayout.CENTER); + add(getBottomRightPanel(), BorderLayout.SOUTH); + add(getLegendPanel(), BorderLayout.EAST); + } + + // ------------------------- + // Abstract hooks + // ------------------------- + // Subclass must provide the plot panel (MoleculePlotPanel or ClusterPlotPanel) + protected abstract JPanel createPlotPanel(); + // Subclass must provide the data panel (MoleculeDataPanel or ClusterDataPanel) + protected abstract JPanel createDataPanel(); + // Subclass must implement crosshair enabling logic + protected abstract void setCrosshairEnabled(boolean enabled); + + // ------------------------- + // Card panel + // ------------------------- + protected JPanel getCardPanel() { + if (cardPanel == null) { + cardPanel = new JPanel(); + cardPanel.setName("CardPanel"); + cardLayout = new CardLayout(); + cardPanel.setLayout(cardLayout); + cardPanel.add(getPlotPanelContainer(), "PlotPanelContainer"); + cardPanel.add(getDataPanelContainer(), "DataPanelContainer"); + } + return cardPanel; + } + + private JPanel getPlotPanelContainer() { + if (plotPanelContainer == null) { + plotPanelContainer = new JPanel(); + plotPanelContainer.setName("PlotPanelContainer"); + plotPanelContainer.setLayout(new BorderLayout()); + plotPanelContainer.add(createPlotPanel(), BorderLayout.CENTER); // Subclass provides the actual plot panel + plotPanelContainer.add(getTimeLabelBottom(), BorderLayout.SOUTH); // Bottom label (e.g., "time") + } + return plotPanelContainer; + } + + private JLabel getTimeLabelBottom() { + if (timeLabelBottom == null) { + timeLabelBottom = new JLabel(); + timeLabelBottom.setName("TimeLabelBottom"); + timeLabelBottom.setText("time"); + timeLabelBottom.setForeground(Color.black); + timeLabelBottom.setHorizontalTextPosition(SwingConstants.CENTER); + timeLabelBottom.setHorizontalAlignment(SwingConstants.CENTER); + } + return timeLabelBottom; + } + + private JPanel getDataPanelContainer() { + if (dataPanelContainer == null) { + dataPanelContainer = new JPanel(); + dataPanelContainer.setName("DataPanelContainer"); + dataPanelContainer.setLayout(new BorderLayout()); + dataPanelContainer.add(createDataPanel(), BorderLayout.CENTER); + } + return dataPanelContainer; + } + // ------------------------- + // Legend panel + // ------------------------- + protected JPanel getLegendPanel() { + if (legendPanel == null) { + legendPanel = new JPanel(); + legendPanel.setName("LegendPanel"); + legendPanel.setLayout(new BorderLayout()); + legendPanel.add(new JLabel(" "), BorderLayout.SOUTH); // South spacer (keeps layout stable) + + JLabel labelLegendTitle = new JLabel("Plot Legend:"); // Title label with border + labelLegendTitle.setBorder(new EmptyBorder(10, 4, 10, 4)); + legendPanel.add(labelLegendTitle, BorderLayout.NORTH); + + JScrollPane scrollPane = new JScrollPane(getLegendContentPanel()); // Scrollpane containing the legend content panel + scrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); + scrollPane.setBorder(new EmptyBorder(0, 0, 0, 0)); + legendPanel.add(scrollPane, BorderLayout.CENTER); + } + return legendPanel; + } + protected JPanel getLegendContentPanel() { + if (legendContentPanel == null) { + legendContentPanel = new JPanel() { + @Override + public Dimension getPreferredSize() { + // Reserve space for vertical scrollbar from the start + Dimension d = super.getPreferredSize(); + int scrollbarWidth = UIManager.getInt("ScrollBar.width"); + return new Dimension(d.width + scrollbarWidth, d.height); + } + }; + legendContentPanel.setName("LegendContentPanel"); + legendContentPanel.setLayout(new BoxLayout(legendContentPanel, BoxLayout.Y_AXIS)); + } + return legendContentPanel; + } + // ------------------------- + // Bottom-right panel + // ------------------------- + private JPanel getBottomRightPanel() { + if (bottomRightPanel == null) { + bottomRightPanel = new JPanel(); + bottomRightPanel.setName("BottomRightPanel"); + bottomRightPanel.setLayout(new GridBagLayout()); + + GridBagConstraints gbc; + gbc = new GridBagConstraints(); + gbc.gridx = 1; + gbc.gridy = 0; + gbc.fill = GridBagConstraints.HORIZONTAL; + gbc.weightx = 1.0; + gbc.insets = new Insets(4, 4, 4, 4); + bottomRightPanel.add(getBottomLabel(), gbc); + + gbc = new GridBagConstraints(); + gbc.gridx = 2; + gbc.gridy = 0; + gbc.insets = new Insets(4, 4, 4, 2); + bottomRightPanel.add(getCrosshairCheckBox(), gbc); + + gbc = new GridBagConstraints(); + gbc.gridx = 3; + gbc.gridy = 0; + gbc.insets = new Insets(4, 2, 4, 2); + bottomRightPanel.add(getCrosshairCoordLabel(), gbc); + + gbc = new GridBagConstraints(); + gbc.gridx = 4; + gbc.gridy = 0; + gbc.insets = new Insets(4, 4, 4, 4); + bottomRightPanel.add(getPlotButton(), gbc); + + gbc = new GridBagConstraints(); + gbc.gridx = 5; + gbc.gridy = 0; + gbc.insets = new Insets(4, 4, 4, 4); + bottomRightPanel.add(getDataButton(), gbc); + } + return bottomRightPanel; + } + + protected JLabel getBottomLabel() { + if (bottomLabel == null) { + bottomLabel = new JLabel(); + bottomLabel.setName("BottomLabel"); + bottomLabel.setText(" "); + bottomLabel.setForeground(Color.blue); + bottomLabel.setPreferredSize(new Dimension(44, 20)); + bottomLabel.setMinimumSize(new Dimension(44, 20)); + bottomLabel.setFont(new Font("dialog", Font.PLAIN, 12)); + } + return bottomLabel; + } + + protected JCheckBox getCrosshairCheckBox() { + if (crosshairCheckBox == null) { + crosshairCheckBox = new JCheckBox("Show Crosshair"); + crosshairCheckBox.setName("CrosshairCheckBox"); + crosshairCheckBox.setSelected(true); // default ON + } + return crosshairCheckBox; + } + + protected JLabel getCrosshairCoordLabel() { + if (crosshairCoordLabel == null) { + crosshairCoordLabel = new JLabel(emptyCoordText); + crosshairCoordLabel.setName("CrosshairCoordLabel"); + // no fixed width — dynamic sizing will handle it but we DO want a stable height + int height = crosshairCoordLabel.getFontMetrics(crosshairCoordLabel.getFont()).getHeight(); + crosshairCoordLabel.setPreferredSize(new Dimension(1, height)); + } + return crosshairCoordLabel; + } + + protected JToolBarToggleButton getPlotButton() { + if (plotButton == null) { + plotButton = new JToolBarToggleButton(); + plotButton.setName("PlotButton"); + plotButton.setToolTipText("Show plot(s)"); + plotButton.setText(""); + plotButton.setMaximumSize(new Dimension(28, 28)); + plotButton.setMinimumSize(new Dimension(28, 28)); + plotButton.setPreferredSize(new Dimension(28, 28)); + plotButton.setActionCommand("PlotPanelContainer"); + plotButton.setSelected(true); + plotButton.setIcon(VCellIcons.dataExporterIcon); + } + return plotButton; + } + protected JToolBarToggleButton getDataButton() { + if (dataButton == null) { + dataButton = new JToolBarToggleButton(); + dataButton.setName("DataButton"); + dataButton.setToolTipText("Show data table"); + dataButton.setText(""); + dataButton.setMaximumSize(new Dimension(28, 28)); + dataButton.setMinimumSize(new Dimension(28, 28)); + dataButton.setPreferredSize(new Dimension(28, 28)); + dataButton.setActionCommand("DataPanelContainer"); + dataButton.setIcon(VCellIcons.dataSetsIcon); + } + return dataButton; + } + + protected void initConnections() { + // empty; override in the subclasses + } + + public void setVisualizationBackground(Color color) { + super.setBackground(color); + getBottomRightPanel().setBackground(color); + getBottomLabel().setBackground(color); + getCrosshairCheckBox().setBackground(color); + getCrosshairCoordLabel().setBackground(color); + getLegendPanel().setBackground(color); + getLegendContentPanel().setBackground(color); + getCardPanel().setBackground(color); + getDataPanelContainer().setBackground(color); + getPlotPanelContainer().setBackground(color); + } + + + @Override + protected void onSelectedObjectsChange(Object[] selectedObjects) { + + } + + // crosshair coordinates and coordinates label management + public void updateCrosshairCoordinates(double xVal, double yVal) { + String text = formatCoord(xVal) + ", " + formatCoord(yVal); + getCrosshairCoordLabel().setText(text); + adjustCoordLabelWidth(text); + } + public void clearCrosshairCoordinates() { + getCrosshairCoordLabel().setText(emptyCoordText); + adjustCoordLabelWidth(emptyCoordText); + } + private int lastCoordCharCount = emptyCoordText.length(); + private static final String emptyCoordText = " "; // enough spaces to reduce jitter when switching between no coord and coord + private static final DecimalFormat sci = new DecimalFormat("0.000E0", DecimalFormatSymbols.getInstance(Locale.US)); + private static final DecimalFormat fix = new DecimalFormat("0.000", DecimalFormatSymbols.getInstance(Locale.US)); + private static String formatCoord(double v) { + double av = Math.abs(v); + return (av >= 0.001) + ? fix.format(v) + : sci.format(v); + } + private void adjustCoordLabelWidth(String text) { + int charCount = text.length(); + // Only resize if the number of characters changed + if (charCount == lastCoordCharCount) { + return; + } + lastCoordCharCount = charCount; + FontMetrics fm = getCrosshairCoordLabel().getFontMetrics(getCrosshairCoordLabel().getFont()); + int charWidth = fm.charWidth('8'); // good representative width + int width = charWidth * charCount + 4; // +4 px padding + Dimension d = getCrosshairCoordLabel().getPreferredSize(); + getCrosshairCoordLabel().setPreferredSize(new Dimension(width, d.height)); + getCrosshairCoordLabel().revalidate(); // required so GridBagLayout recalculates layout + } + +} diff --git a/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterVisualizationPanel.java b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterVisualizationPanel.java index 9ca3f42cfc..73322e17e9 100644 --- a/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterVisualizationPanel.java +++ b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterVisualizationPanel.java @@ -29,8 +29,6 @@ import javax.swing.event.ChangeListener; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; -import javax.swing.table.TableCellRenderer; -import javax.swing.table.TableColumn; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; @@ -45,7 +43,7 @@ import java.util.List; -public class ClusterVisualizationPanel extends DocumentEditorSubPanel { +public class ClusterVisualizationPanel extends AbstractVisualizationPanel { ODEDataViewer owner; IvjEventHandler ivjEventHandler = new IvjEventHandler(); @@ -54,36 +52,32 @@ public class ClusterVisualizationPanel extends DocumentEditorSubPanel { private final java.util.List globalPalette = new ArrayList<>(); private int nextColorIndex = 0; - private JPanel ivjJPanel1 = null; - private JPanel ivjJPanelPlot = null; private ClusterPlotPanel clusterPlotPanel = null; // here are the plots being drawn - private JLabel ivjJLabelBottom = null; - private JPanel ivjJPanelData = null; private ClusterDataPanel clusterDataPanel = null; // here resides the data table - private JPanel bottomRightPanel = null; - private JPanel ivjJPanelLegend = null; - private JScrollPane ivjPlotLegendsScrollPane = null; - private JPanel ivjJPanelPlotLegends = null; - private JLabel bottomLabel = null; - private JCheckBox showCrosshairCheckBox = null; - private JLabel crosshairCoordLabel = null; - private JToolBarToggleButton ivjPlotButton = null; - private JToolBarToggleButton ivjDataButton = null; class IvjEventHandler implements ActionListener, PropertyChangeListener, ChangeListener, ListSelectionListener { @Override public void actionPerformed(ActionEvent e) { if (e.getSource() instanceof Component c && SwingUtilities.isDescendingFrom(c, ClusterVisualizationPanel.this)) { String cmd = e.getActionCommand(); - if (cmd.equals("JPanelPlot") || cmd.equals("JPanelData")) { - CardLayout cl = (CardLayout) ivjJPanel1.getLayout(); - cl.show(ivjJPanel1, cmd); // show the plot or the data panel - ivjPlotButton.setSelected(cmd.equals("JPanelPlot")); // update button selection state - ivjDataButton.setSelected(cmd.equals("JPanelData")); - ivjJPanelLegend.setVisible(cmd.equals("JPanelPlot")); // show legend only in plot mode - getShowCrosshairCheckBox().setVisible(cmd.equals("JPanelPlot")); // show/hide crosshair checkbox + // --- Card switching (plot <-> data) --- + if (cmd.equals("PlotPanelContainer") || cmd.equals("DataPanelContainer")) { + CardLayout cl = (CardLayout) getCardPanel().getLayout(); + cl.show(getCardPanel(), cmd); // show the plot or the data panel + getPlotButton().setSelected(cmd.equals("PlotPanelContainer")); // update button selection state + getDataButton().setSelected(cmd.equals("DataPanelContainer")); + getLegendPanel().setVisible(cmd.equals("PlotPanelContainer")); // show legend only in plot mode + getCrosshairCheckBox().setVisible(cmd.equals("PlotPanelContainer")); // show/hide crosshair checkbox only in plot mode + setCrosshairEnabled(cmd.equals("PlotPanelContainer") && getCrosshairCheckBox().isSelected()); // enable/disable crosshair logic return; } + // --- Crosshair checkbox toggled --- + if (e.getSource() == getCrosshairCheckBox()) { + boolean enabled = getCrosshairCheckBox().isSelected(); + setCrosshairEnabled(enabled); + return; + } + } } @Override @@ -119,60 +113,24 @@ public ClusterVisualizationPanel(ODEDataViewer odeDataViewer) { super(); this.owner = odeDataViewer; initialize(); - } - - private void initialize() { - setPreferredSize(new Dimension(420, 400)); - setLayout(new BorderLayout()); - setSize(513, 457); - add(getJPanel1(), "Center"); - add(getBottomRightPanel(), "South"); - add(getJPanelLegend(), "East"); - setBackground(Color.white); initConnections(); + setVisualizationBackground(Color.WHITE); } - private JPanel getJPanel1() { - if (ivjJPanel1 == null) { - ivjJPanel1 = new JPanel(); - ivjJPanel1.setName("JPanel1"); - ivjJPanel1.setLayout(new CardLayout()); - ivjJPanel1.add(getJPanelPlot(), getJPanelPlot().getName()); - ivjJPanel1.add(getJPanelData(), getJPanelData().getName()); - } - return ivjJPanel1; + // --------------------the abstract class hooks + @Override + protected JPanel createPlotPanel() { + return getClusterPlotPanel(); } - private JPanel getJPanelPlot() { - if (ivjJPanelPlot == null) { - ivjJPanelPlot = new JPanel(); - ivjJPanelPlot.setName("JPanelPlot"); - ivjJPanelPlot.setLayout(new BorderLayout()); - ivjJPanelPlot.add(getClusterPlotPanel(), "Center"); - ivjJPanelPlot.add(getJLabelBottom(), "South"); - } - return ivjJPanelPlot; + @Override + protected JPanel createDataPanel() { + return getClusterDataPanel(); } - private JPanel getJPanelData() { - if (ivjJPanelData == null) { - ivjJPanelData = new JPanel(); - ivjJPanelData.setName("JPanelData"); - ivjJPanelData.setLayout(new BorderLayout()); - ivjJPanelData.add(getClusterDataPanel(), BorderLayout.CENTER); - } - return ivjJPanelData; + @Override + protected void setCrosshairEnabled(boolean enabled) { + getClusterPlotPanel().setCrosshairEnabled(enabled); } - private JLabel getJLabelBottom() { - if (ivjJLabelBottom == null) { - ivjJLabelBottom = new JLabel(); - ivjJLabelBottom.setName("JLabelBottom"); - ivjJLabelBottom.setText("time"); - ivjJLabelBottom.setForeground(Color.black); - ivjJLabelBottom.setHorizontalTextPosition(SwingConstants.CENTER); - ivjJLabelBottom.setHorizontalAlignment(SwingConstants.CENTER); - } - return ivjJLabelBottom; - } private ClusterPlotPanel getClusterPlotPanel() { // actual plotting is shown here if (clusterPlotPanel == null) { try { @@ -208,164 +166,15 @@ public ClusterDataPanel getClusterDataPanel() { // actual table sh return clusterDataPanel; } - private JPanel getBottomRightPanel() { - if (bottomRightPanel == null) { - bottomRightPanel = new JPanel(); - bottomRightPanel.setName("JPanelBottom"); - bottomRightPanel.setLayout(new GridBagLayout()); - - GridBagConstraints gbc = new GridBagConstraints(); - gbc.gridx = 1; gbc.gridy = 0; - gbc.fill = GridBagConstraints.HORIZONTAL; - gbc.weightx = 1.0; - gbc.insets = new Insets(4, 4, 4, 4); - bottomRightPanel.add(getJBottomLabel(), gbc); - - gbc = new GridBagConstraints(); - gbc.gridx = 2; gbc.gridy = 0; - gbc.insets = new Insets(4, 4, 4, 2); - bottomRightPanel.add(getShowCrosshairCheckBox(), gbc); - - gbc = new GridBagConstraints(); - gbc.gridx = 3; gbc.gridy = 0; - gbc.insets = new Insets(4, 2, 4, 2); - bottomRightPanel.add(getCrosshairCoordLabel(), gbc); - - gbc = new GridBagConstraints(); - gbc.gridx = 4; gbc.gridy = 0; - gbc.insets = new Insets(4, 4, 4, 4); - bottomRightPanel.add(getPlotButton(), gbc); - - gbc = new GridBagConstraints(); - gbc.gridx = 5; gbc.gridy = 0; - gbc.insets = new Insets(4, 4, 4, 4); - bottomRightPanel.add(getDataButton(), gbc); - } - return bottomRightPanel; - } - private JLabel getJBottomLabel() { - if (bottomLabel == null) { - bottomLabel = new JLabel(); - bottomLabel.setName("JBottomLabel"); - bottomLabel.setText(" "); - bottomLabel.setForeground(Color.blue); - bottomLabel.setPreferredSize(new Dimension(44, 20)); - bottomLabel.setFont(new Font("dialog", 0, 12)); - bottomLabel.setMinimumSize(new Dimension(44, 20)); - } - return bottomLabel; - } - private JCheckBox getShowCrosshairCheckBox() { - if (showCrosshairCheckBox == null) { - showCrosshairCheckBox = new JCheckBox("Show Crosshair"); - showCrosshairCheckBox.setSelected(true); // default ON - - showCrosshairCheckBox.addActionListener(e -> { - boolean enabled = showCrosshairCheckBox.isSelected(); - clusterPlotPanel.setCrosshairEnabled(enabled); - clusterPlotPanel.repaint(); - }); - } - return showCrosshairCheckBox; - } - private JLabel getCrosshairCoordLabel() { - if (crosshairCoordLabel == null) { - crosshairCoordLabel = new JLabel(emptyCoordText); - // no fixed width — dynamic sizing will handle it but we DO want a stable height - int height = crosshairCoordLabel.getFontMetrics(crosshairCoordLabel.getFont()).getHeight(); - crosshairCoordLabel.setPreferredSize(new Dimension(1, height)); - } - return crosshairCoordLabel; - } - private JToolBarToggleButton getPlotButton() { - if (ivjPlotButton == null) { - ivjPlotButton = new JToolBarToggleButton(); - ivjPlotButton.setName("PlotButton"); - ivjPlotButton.setToolTipText("Show plot(s)"); - ivjPlotButton.setText(""); - ivjPlotButton.setMaximumSize(new Dimension(28, 28)); - ivjPlotButton.setActionCommand("JPanelPlot"); - ivjPlotButton.setSelected(true); - ivjPlotButton.setPreferredSize(new Dimension(28, 28)); - ivjPlotButton.setIcon(VCellIcons.dataExporterIcon); - ivjPlotButton.setMinimumSize(new Dimension(28, 28)); - } - return ivjPlotButton; - } - private JToolBarToggleButton getDataButton() { - if (ivjDataButton == null) { - ivjDataButton = new JToolBarToggleButton(); - ivjDataButton.setName("DataButton"); - ivjDataButton.setToolTipText("Show data"); - ivjDataButton.setText(""); - ivjDataButton.setMaximumSize(new Dimension(28, 28)); - ivjDataButton.setActionCommand("JPanelData"); - ivjDataButton.setIcon(VCellIcons.dataSetsIcon); - ivjDataButton.setPreferredSize(new Dimension(28, 28)); - ivjDataButton.setMinimumSize(new Dimension(28, 28)); - } - return ivjDataButton; - } - private JPanel getJPanelLegend() { - if (ivjJPanelLegend == null) { - ivjJPanelLegend = new JPanel(); - ivjJPanelLegend.setName("JPanelLegend"); - ivjJPanelLegend.setLayout(new BorderLayout()); - getJPanelLegend().add(new JLabel(" "), "South"); - JLabel labelLegendTitle = new JLabel("Plot Legend:"); - labelLegendTitle.setBorder(new EmptyBorder(10, 4, 10, 4)); - getJPanelLegend().add(labelLegendTitle, "North"); - getJPanelLegend().add(getPlotLegendsScrollPane(), "Center"); - } - return ivjJPanelLegend; - } - - private JScrollPane getPlotLegendsScrollPane() { - if (ivjPlotLegendsScrollPane == null) { - ivjPlotLegendsScrollPane = new JScrollPane(); - ivjPlotLegendsScrollPane.setName("PlotLegendsScrollPane"); - ivjPlotLegendsScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); -// ivjPlotLegendsScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED); - ivjPlotLegendsScrollPane.setBorder(new EmptyBorder(0, 0, 0, 0)); - getPlotLegendsScrollPane().setViewportView(getJPanelPlotLegends()); - } - return ivjPlotLegendsScrollPane; - } - private JPanel getJPanelPlotLegends() { - if (ivjJPanelPlotLegends == null) { - ivjJPanelPlotLegends = new JPanel() { - @Override - public Dimension getPreferredSize() { // allocate from start enough space for vertical scrollbar - Dimension d = super.getPreferredSize(); - int scrollbarWidth = UIManager.getInt("ScrollBar.width"); - return new Dimension(d.width + scrollbarWidth, d.height); - } - }; - ivjJPanelPlotLegends.setName("JPanelPlotLegends"); - ivjJPanelPlotLegends.setLayout(new BoxLayout(ivjJPanelPlotLegends, BoxLayout.Y_AXIS)); -// ivjJPanelPlotLegends.setBounds(0, 0, 72, 360); - } - return ivjJPanelPlotLegends; - } - - public void setBackground(Color color) { - super.setBackground(color); - getBottomRightPanel().setBackground(color); - getJBottomLabel().setBackground(color); - getShowCrosshairCheckBox().setBackground(color); - getCrosshairCoordLabel().setBackground(color); - getJPanelLegend().setBackground(color); - getJPanelPlotLegends().setBackground(color); - getJPanel1().setBackground(color); + public void setVisualizationBackground(Color color) { + super.setVisualizationBackground(color); getClusterPlotPanel().setBackground(color); getClusterDataPanel().setBackground(color); - getJPanelData().setBackground(color); - getJPanelPlot().setBackground(color); - } - private void initConnections() { + @Override + protected void initConnections() { initializeGlobalPalette(); // get a stable, high contrast palette // group the two buttons so only one stays selected ButtonGroup bg = new ButtonGroup(); @@ -376,6 +185,9 @@ private void initConnections() { getPlotButton().addActionListener(ivjEventHandler); getDataButton().addActionListener(ivjEventHandler); + // crosshair checkbox is plot-specific, so subclass handles it + getCrosshairCheckBox().addActionListener(ivjEventHandler); + // listen to the left panel owner.getClusterSpecificationPanel().addPropertyChangeListener(ivjEventHandler); } @@ -396,9 +208,9 @@ public void refreshData() { int jobs = owner.getSimulation().getSolverTaskDescription().getLangevinSimulationOptions().getTotalNumberOfJobs(); String name = owner.getSimulation().getName(); String str = "" + name + " [" + jobs + " job" + (jobs != 1 ? "s" : "") + "]"; - getJBottomLabel().setText(str); + getBottomLabel().setText(str); } else { - getJBottomLabel().setText(" "); + getBottomLabel().setText(" "); } // simulationModelInfo = owner.getSimulationModelInfo(); // langevinSolverResultSet = owner.getLangevinSolverResultSet(); @@ -508,26 +320,6 @@ private JComponent createLegendEntry(String name, Color color, ClusterSpecificat return p; } - public class LineIcon implements Icon { - private final Color color; - public LineIcon(Color color) { - this.color = color; - } - @Override - public void paintIcon(Component c, Graphics g, int x, int y) { - Graphics2D g2 = (Graphics2D)g; - g2.setStroke(new BasicStroke(3.0f)); - g2.setPaint(color); - int midY = y + getIconHeight() / 2; - g2.drawLine(x, midY, x + getIconWidth(), midY); - } - @Override - public int getIconWidth() { return 50; } - @Override - public int getIconHeight() { - return 4; // more vertical room for a wider stroke - } - } private void redrawPlot(ClusterSpecificationPanel.ClusterSelection sel) throws ExpressionException { System.out.println(this.getClass().getSimpleName() + ".redrawPlot() called, current selection: " + sel); @@ -663,7 +455,7 @@ private void redrawPlot(ClusterSpecificationPanel.ClusterSelection sel) throws E private void redrawLegend(ClusterSpecificationPanel.ClusterSelection sel) { System.out.println(this.getClass().getSimpleName() + ".redrawLegend() called"); - getJPanelPlotLegends().removeAll(); + getLegendContentPanel().removeAll(); for (ColumnDescription cd : sel.columns) { String name = cd.getName(); @@ -678,10 +470,10 @@ private void redrawLegend(ClusterSpecificationPanel.ClusterSelection sel) { c = persistentColorMap.get(name); } - getJPanelPlotLegends().add(createLegendEntry(name, c, sel.mode)); + getLegendContentPanel().add(createLegendEntry(name, c, sel.mode)); } - getJPanelPlotLegends().revalidate(); - getJPanelPlotLegends().repaint(); + getLegendContentPanel().revalidate(); + getLegendContentPanel().repaint(); } private void redrawDataTable(ClusterSpecificationPanel.ClusterSelection sel) throws ExpressionException { @@ -699,41 +491,7 @@ private boolean contains(List list, String name) { return false; } - // crosshair coordinates and coordinates label management - public void updateCrosshairCoordinates(double xVal, double yVal) { - String text = formatCoord(xVal) + ", " + formatCoord(yVal); - getCrosshairCoordLabel().setText(text); - adjustCoordLabelWidth(text); - } - public void clearCrosshairCoordinates() { - getCrosshairCoordLabel().setText(emptyCoordText); - adjustCoordLabelWidth(emptyCoordText); - } - private int lastCoordCharCount = emptyCoordText.length(); - private static final String emptyCoordText = " "; // enough spaces to reduce jitter when switching between no coord and coord - private static final DecimalFormat sci = new DecimalFormat("0.000E0", DecimalFormatSymbols.getInstance(Locale.US)); - private static final DecimalFormat fix = new DecimalFormat("0.000", DecimalFormatSymbols.getInstance(Locale.US)); - private static String formatCoord(double v) { - double av = Math.abs(v); - return (av >= 0.001) - ? fix.format(v) - : sci.format(v); - } - private void adjustCoordLabelWidth(String text) { - int charCount = text.length(); - // Only resize if the number of characters changed - if (charCount == lastCoordCharCount) { - return; - } - lastCoordCharCount = charCount; - FontMetrics fm = getCrosshairCoordLabel().getFontMetrics(getCrosshairCoordLabel().getFont()); - int charWidth = fm.charWidth('8'); // good representative width - int width = charWidth * charCount + 4; // +4 px padding - Dimension d = getCrosshairCoordLabel().getPreferredSize(); - getCrosshairCoordLabel().setPreferredSize(new Dimension(width, d.height)); - getCrosshairCoordLabel().revalidate(); // required so GridBagLayout recalculates layout - } - // =================================================== Evaluate JFreeChart capabilities with a simple demo === +// ----------------------------------------------------------------------------------------------------------- public static void main(String[] args) { SwingUtilities.invokeLater(() -> { diff --git a/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/MoleculeVisualizationPanel.java b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/MoleculeVisualizationPanel.java index 8736baa93c..6654cd0b9e 100644 --- a/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/MoleculeVisualizationPanel.java +++ b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/MoleculeVisualizationPanel.java @@ -63,12 +63,12 @@ public MoleculeVisualizationPanel(ODEDataViewer owner) { this.owner = owner; initialize(); } + private void initialize() { - // layout setBackground(Color.white); - initConnections(); } + private void initConnections() { // listeners From 13a9efb49e8db14d50e85d9e74c2826c1e07d4ab Mon Sep 17 00:00:00 2001 From: Dan Vasilescu Date: Tue, 7 Apr 2026 14:23:11 -0400 Subject: [PATCH 25/31] Standalone MoleculeDataPanel --- .../java/cbit/plot/gui/MoleculeDataPanel.java | 169 ++++++++++++++++++ 1 file changed, 169 insertions(+) create mode 100644 vcell-client/src/main/java/cbit/plot/gui/MoleculeDataPanel.java diff --git a/vcell-client/src/main/java/cbit/plot/gui/MoleculeDataPanel.java b/vcell-client/src/main/java/cbit/plot/gui/MoleculeDataPanel.java new file mode 100644 index 0000000000..07a71288ac --- /dev/null +++ b/vcell-client/src/main/java/cbit/plot/gui/MoleculeDataPanel.java @@ -0,0 +1,169 @@ +package cbit.plot.gui; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.vcell.util.gui.NonEditableDefaultTableModel; +import org.vcell.util.gui.ScrollTable; + +import javax.swing.*; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; +import javax.swing.table.DefaultTableCellRenderer; +import javax.swing.table.TableCellRenderer; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; + +public class MoleculeDataPanel extends JPanel { + + private static final Logger LG = LogManager.getLogger(MoleculeDataPanel.class); + + private ScrollTable scrollPaneTable; + private NonEditableDefaultTableModel nonEditableDefaultTableModel = null; + + private JPopupMenu popupMenu = null; + private JMenuItem miCopyAll = null; + private JMenuItem miCopyHDF5 = null; + private static enum CopyAction {copy,copyrow,copyall}; + + private final MoleculeDataPanel.IvjEventHandler ivjEventHandler = new MoleculeDataPanel.IvjEventHandler(); + + + + class IvjEventHandler implements ActionListener, MouseListener, PropertyChangeListener, ChangeListener { + @Override + public void mouseClicked(MouseEvent e) { + if (e.getSource() == getScrollPaneTable()) { + int row = getScrollPaneTable().rowAtPoint(e.getPoint()); + int col = getScrollPaneTable().columnAtPoint(e.getPoint()); + System.out.println("MoleculeDataPanel: clicked row=" + row + " col=" + col); + if (SwingUtilities.isRightMouseButton(e)) { + getPopupMenu().show(e.getComponent(), e.getX(), e.getY()); + } + } + } + + @Override public void mousePressed(MouseEvent e) {} + @Override public void mouseReleased(MouseEvent e) {} + @Override public void mouseEntered(MouseEvent e) {} + @Override public void mouseExited(MouseEvent e) {} + + @Override + public void actionPerformed(ActionEvent e) { + // reserved for future + } + + @Override + public void propertyChange(PropertyChangeEvent evt) { + // reserved for future dynamic formatting + } + + @Override + public void stateChanged(ChangeEvent e) { + // reserved for future slider/spinner interactions + } + } + + private class MoleculeHeaderRenderer extends DefaultTableCellRenderer { + + private final TableCellRenderer base; + + public MoleculeHeaderRenderer(TableCellRenderer base) { + this.base = base; + } + + @Override + public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, + boolean hasFocus, int row, int column) { + // First let ScrollTable’s renderer do its work + Component c = base.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); + if (!(c instanceof JLabel)) { + return c; // safety + } + return c; + } + } + + public MoleculeDataPanel() { + super(); + initialize(); + } + private void initialize() { + try { + setName("MoleculeDataPanel"); + setLayout(new java.awt.BorderLayout()); + setSize(541, 348); + add(getScrollPaneTable().getEnclosingScrollPane(), BorderLayout.CENTER); + JLabel lblNewLabel = new JLabel("To Copy table data or Export as HDF5, select rows/cells and use the right mouse button menu."); + add(lblNewLabel, BorderLayout.SOUTH); + initConnections(); +// controlKeys(); + } catch (Throwable exc) { + handleException(exc); + } + } + + private void initConnections() throws java.lang.Exception { + this.addPropertyChangeListener(ivjEventHandler); + getScrollPaneTable().addMouseListener(ivjEventHandler); + getScrollPaneTable().setModel(getNonEditableDefaultTableModel()); + getScrollPaneTable().createDefaultColumnsFromModel(); + TableCellRenderer baseHeaderRenderer = getScrollPaneTable().getTableHeader().getDefaultRenderer(); + getScrollPaneTable().getTableHeader().setDefaultRenderer(new MoleculeDataPanel.MoleculeHeaderRenderer(baseHeaderRenderer)); + + } + private void handleException(java.lang.Throwable exception) { + System.out.println("--------- UNCAUGHT EXCEPTION ---------"); + exception.printStackTrace(System.out); + } + + // ----------------------------------------------------------- + private ScrollTable getScrollPaneTable() { + if (scrollPaneTable == null) { + try { + scrollPaneTable = new ScrollTable(); + scrollPaneTable.setName("ScrollPaneTable"); + scrollPaneTable.setCellSelectionEnabled(true); + scrollPaneTable.setBounds(0, 0, 200, 200); + } catch (java.lang.Throwable ivjExc) { + handleException(ivjExc); + } + } + return scrollPaneTable; + } + + private JPopupMenu getPopupMenu() { + if (popupMenu == null) { + popupMenu = new JPopupMenu(); + + miCopyAll = new JMenuItem("Copy All"); + miCopyAll.addActionListener(e -> copyCells(this, false)); + popupMenu.add(miCopyAll); + + miCopyHDF5 = new JMenuItem("Copy to HDF5"); + miCopyHDF5.setEnabled(false); // export to HDF5 code is not working + miCopyHDF5.addActionListener(e -> copyCells(this,true)); + popupMenu.add(miCopyHDF5); + } + return popupMenu; + } + + private static synchronized void copyCells(MoleculeDataPanel cdp, boolean isHDF5) { + + } + private NonEditableDefaultTableModel getNonEditableDefaultTableModel() { + if (nonEditableDefaultTableModel == null) { + try { + nonEditableDefaultTableModel = new NonEditableDefaultTableModel(); + } catch (java.lang.Throwable ivjExc) { + handleException(ivjExc); + } + } + return nonEditableDefaultTableModel; + } + +} From e4a780e415f95c2d99322da1f7baf7de21c51436 Mon Sep 17 00:00:00 2001 From: Dan Vasilescu Date: Tue, 7 Apr 2026 19:13:16 -0400 Subject: [PATCH 26/31] Symmetrical class hierarchy for all components --- .../java/cbit/plot/gui/AbstractDataPanel.java | 201 +++++++++ .../java/cbit/plot/gui/AbstractPlotPanel.java | 419 ++++++++++++++++++ .../java/cbit/plot/gui/ClusterDataPanel.java | 351 ++++++--------- .../java/cbit/plot/gui/ClusterPlotPanel.java | 359 +-------------- .../java/cbit/plot/gui/MoleculeDataPanel.java | 292 +++++++----- .../java/cbit/plot/gui/MoleculePlotPanel.java | 16 + .../ode/gui/ClusterVisualizationPanel.java | 80 ++-- .../ode/gui/MoleculeSpecificationPanel.java | 2 +- .../ode/gui/MoleculeVisualizationPanel.java | 233 +++++++++- .../simdata/LangevinSolverResultSet.java | 6 + 10 files changed, 1234 insertions(+), 725 deletions(-) create mode 100644 vcell-client/src/main/java/cbit/plot/gui/AbstractDataPanel.java create mode 100644 vcell-client/src/main/java/cbit/plot/gui/AbstractPlotPanel.java create mode 100644 vcell-client/src/main/java/cbit/plot/gui/MoleculePlotPanel.java diff --git a/vcell-client/src/main/java/cbit/plot/gui/AbstractDataPanel.java b/vcell-client/src/main/java/cbit/plot/gui/AbstractDataPanel.java new file mode 100644 index 0000000000..0a413eb15d --- /dev/null +++ b/vcell-client/src/main/java/cbit/plot/gui/AbstractDataPanel.java @@ -0,0 +1,201 @@ +package cbit.plot.gui; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.vcell.util.gui.NonEditableDefaultTableModel; +import org.vcell.util.gui.ScrollTable; + +import javax.swing.*; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; +import javax.swing.table.DefaultTableCellRenderer; +import javax.swing.table.TableCellRenderer; +import java.awt.*; +import java.awt.event.*; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; + +public abstract class AbstractDataPanel extends JPanel { + + protected static final Logger LG = LogManager.getLogger(AbstractDataPanel.class); + + protected ScrollTable scrollPaneTable; + protected NonEditableDefaultTableModel nonEditableDefaultTableModel = null; + + protected JPopupMenu popupMenu = null; + protected JMenuItem miCopyAll = null; + protected JMenuItem miCopyHDF5 = null; + + protected enum CopyAction { copy, copyrow, copyall } + + protected final AbstractDataPanel.IvjEventHandler ivjEventHandler = new AbstractDataPanel.IvjEventHandler(); + + // ------------------------------------------------------------ + // Event handler (shared) + // ------------------------------------------------------------ + protected class IvjEventHandler implements ActionListener, MouseListener, PropertyChangeListener, ChangeListener { + + @Override + public void mouseClicked(MouseEvent e) { + if (e.getSource() == getScrollPaneTable()) { + int row = getScrollPaneTable().rowAtPoint(e.getPoint()); + int col = getScrollPaneTable().columnAtPoint(e.getPoint()); + LG.debug(getClass().getSimpleName() + ": clicked row=" + row + " col=" + col); + + if (SwingUtilities.isRightMouseButton(e)) { + getPopupMenu().show(e.getComponent(), e.getX(), e.getY()); + } + + onMouseClick(row, col, e); + } + } + + @Override public void mousePressed(MouseEvent e) {} + @Override public void mouseReleased(MouseEvent e) {} + @Override public void mouseEntered(MouseEvent e) {} + @Override public void mouseExited(MouseEvent e) {} + + @Override + public void actionPerformed(ActionEvent e) { + // Reserved for future shared actions + } + + @Override + public void propertyChange(PropertyChangeEvent evt) { + onPropertyChange(evt); + } + + @Override + public void stateChanged(ChangeEvent e) { + onStateChanged(e); + } + } + + // ------------------------------------------------------------ + // Header renderer wrapper (shared) + // ------------------------------------------------------------ + protected class GenericHeaderRenderer extends DefaultTableCellRenderer { + + private final TableCellRenderer base; + + public GenericHeaderRenderer(TableCellRenderer base) { + this.base = base; + } + + @Override + public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, + boolean hasFocus, int row, int column) { + Component c = base.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); + if (!(c instanceof JLabel)) { + return c; + } + return c; + } + } + + // ------------------------------------------------------------ + // Constructor + // ------------------------------------------------------------ + public AbstractDataPanel() { + super(); + initialize(); + } + + // ------------------------------------------------------------ + // Initialization + // ------------------------------------------------------------ + private void initialize() { + try { + setLayout(new BorderLayout()); + add(getScrollPaneTable().getEnclosingScrollPane(), BorderLayout.CENTER); + + JLabel footer = new JLabel(getFooterLabelText()); + add(footer, BorderLayout.SOUTH); + + initConnections(); + } catch (Throwable exc) { + handleException(exc); + } + } + + protected String getFooterLabelText() { + return "To Copy table data or Export as HDF5, select rows/cells and use the right mouse button menu."; + } + + protected void initConnections() throws Exception { + this.addPropertyChangeListener(ivjEventHandler); + getScrollPaneTable().addMouseListener(ivjEventHandler); + + getScrollPaneTable().setModel(getNonEditableDefaultTableModel()); + getScrollPaneTable().createDefaultColumnsFromModel(); + + TableCellRenderer baseHeaderRenderer = getScrollPaneTable().getTableHeader().getDefaultRenderer(); + getScrollPaneTable().getTableHeader().setDefaultRenderer(new GenericHeaderRenderer(baseHeaderRenderer)); + } + + protected void handleException(Throwable exception) { + System.out.println("--------- UNCAUGHT EXCEPTION ---------"); + exception.printStackTrace(System.out); + } + + // ------------------------------------------------------------ + // Lazy getters + // ------------------------------------------------------------ + protected ScrollTable getScrollPaneTable() { + if (scrollPaneTable == null) { + try { + scrollPaneTable = new ScrollTable(); + scrollPaneTable.setCellSelectionEnabled(true); + } catch (Throwable ivjExc) { + handleException(ivjExc); + } + } + return scrollPaneTable; + } + + protected NonEditableDefaultTableModel getNonEditableDefaultTableModel() { + if (nonEditableDefaultTableModel == null) { + try { + nonEditableDefaultTableModel = new NonEditableDefaultTableModel(); + } catch (Throwable ivjExc) { + handleException(ivjExc); + } + } + return nonEditableDefaultTableModel; + } + + protected JPopupMenu getPopupMenu() { + if (popupMenu == null) { + popupMenu = new JPopupMenu(); + + miCopyAll = new JMenuItem("Copy All"); + miCopyAll.addActionListener(e -> onCopyCells(false)); + popupMenu.add(miCopyAll); + + miCopyHDF5 = new JMenuItem("Copy to HDF5"); + miCopyHDF5.setEnabled(false); + miCopyHDF5.addActionListener(e -> onCopyCells(true)); + popupMenu.add(miCopyHDF5); + } + return popupMenu; + } + + // ------------------------------------------------------------ + // Hooks for subclasses + // ------------------------------------------------------------ + protected void onCopyCells(boolean isHDF5) { + // Subclasses override if needed + } + + protected void onMouseClick(int row, int col, MouseEvent e) { + // Subclasses override if needed + } + + protected void onPropertyChange(PropertyChangeEvent evt) { + // Subclasses override if needed + } + + protected void onStateChanged(ChangeEvent e) { + // Subclasses override if needed + } +} \ No newline at end of file diff --git a/vcell-client/src/main/java/cbit/plot/gui/AbstractPlotPanel.java b/vcell-client/src/main/java/cbit/plot/gui/AbstractPlotPanel.java new file mode 100644 index 0000000000..7c0597de2f --- /dev/null +++ b/vcell-client/src/main/java/cbit/plot/gui/AbstractPlotPanel.java @@ -0,0 +1,419 @@ +package cbit.plot.gui; + +import java.awt.*; +import java.awt.event.*; +import java.awt.geom.*; +import java.text.DecimalFormat; +import java.text.NumberFormat; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.KeyStroke; +import javax.swing.border.Border; + +public abstract class AbstractPlotPanel extends JPanel { + + // Insets and strokes + protected static final int LEFT_INSET = 50; + protected static final int RIGHT_INSET = 20; + protected static final int TOP_INSET = 20; + protected static final int BOTTOM_INSET = 30; + + protected static final float AXIS_STROKE = 1.0f; + protected static final float CURVE_STROKE = 1.5f; + + // Generic renderer interface + protected interface SeriesRenderer { + void draw(Graphics2D g2, + int x0, int x1, int y0, int y1, + int plotWidth, int plotHeight, + double xMaxRounded, double yMaxRounded, double yMinRounded, + double dt); + } + + // AVG renderer: polyline + protected static class AvgRenderer implements SeriesRenderer { + final double[] time; + final double[] values; + final Color color; + + AvgRenderer(double[] time, double[] values, Color color) { + this.time = time; + this.values = values; + this.color = color; + } + + @Override + public void draw(Graphics2D g2, + int x0, int x1, int y0, int y1, + int plotWidth, int plotHeight, + double xMaxRounded, double yMaxRounded, double yMinRounded, + double dt) { + + int n = values.length; + if (n < 2) return; + + int[] xs = new int[n]; + int[] ys = new int[n]; + + for (int i = 0; i < n; i++) { + double t = (time != null ? time[i] : i * dt); + xs[i] = x0 + (int)Math.round((t / xMaxRounded) * plotWidth); + double v = values[i]; + double norm = (v - yMinRounded) / (yMaxRounded - yMinRounded); + ys[i] = y0 - (int)Math.round(norm * plotHeight); + } + + g2.setColor(color); + g2.setStroke(new BasicStroke(CURVE_STROKE, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND)); + g2.drawPolyline(xs, ys, n); + } + } + + // Band renderer: min/max or sd (envelope) + protected static class BandRenderer implements SeriesRenderer { + final double[] time; + final double[] upper; + final double[] lower; + final Color fillColor; + + BandRenderer(double[] time, double[] upper, double[] lower, Color fillColor) { + this.time = time; + this.upper = upper; + this.lower = lower; + this.fillColor = fillColor; + } + + @Override + public void draw(Graphics2D g2, + int x0, int x1, int y0, int y1, + int plotWidth, int plotHeight, + double xMaxRounded, double yMaxRounded, double yMinRounded, + double dt) { + + int n = upper.length; + if (n < 2) return; + + Path2D path = new Path2D.Double(); + + double t0 = (time != null ? time[0] : 0.0); + double v0 = upper[0]; + double norm0 = (v0 - yMinRounded) / (yMaxRounded - yMinRounded); + int xStart = x0 + (int)Math.round((t0 / xMaxRounded) * plotWidth); + int yStart = y0 - (int)Math.round(norm0 * plotHeight); + path.moveTo(xStart, yStart); + + for (int i = 1; i < n; i++) { + double t = (time != null ? time[i] : i * dt); + double v = upper[i]; + double norm = (v - yMinRounded) / (yMaxRounded - yMinRounded); + int x = x0 + (int)Math.round((t / xMaxRounded) * plotWidth); + int y = y0 - (int)Math.round(norm * plotHeight); + path.lineTo(x, y); + } + + for (int i = n - 1; i >= 0; i--) { + double t = (time != null ? time[i] : i * dt); + double v = lower[i]; + double norm = (v - yMinRounded) / (yMaxRounded - yMinRounded); + int x = x0 + (int)Math.round((t / xMaxRounded) * plotWidth); + int y = y0 - (int)Math.round(norm * plotHeight); + path.lineTo(x, y); + } + + path.closePath(); + g2.setColor(fillColor); + g2.fill(path); + } + } + + // Renderers list + protected final List renderers = new ArrayList<>(); + + // Scaling state + protected double globalMin = 0; + protected double globalMax = 1; + protected double dt = 1; + + // Crosshair state + protected Integer mouseX = null; + protected Integer mouseY = null; + protected boolean crosshairEnabled = true; + protected Consumer coordCallback; + + // Cached plot area + protected int lastX0, lastX1, lastY0, lastY1; + protected double lastXMaxRounded; + protected double lastYMaxRounded; + protected double lastYMinRounded; + + public AbstractPlotPanel() { + addMouseMotionListener(new MouseMotionAdapter() { + @Override + public void mouseMoved(MouseEvent e) { + int mx = e.getX(); + int my = e.getY(); + + if (mx >= lastX0 && mx <= lastX1 && my >= lastY1 && my <= lastY0) { + mouseX = mx; + mouseY = my; + } else { + mouseX = null; + mouseY = null; + } + + if (crosshairEnabled && mouseX != null && mouseY != null) { + double fracX = (mouseX - lastX0) / (double)(lastX1 - lastX0); + double xVal = fracX * lastXMaxRounded; + + double fracY = (lastY0 - mouseY) / (double)(lastY0 - lastY1); + double yVal = lastYMinRounded + fracY * (lastYMaxRounded - lastYMinRounded); + + if (coordCallback != null) { + coordCallback.accept(new double[]{xVal, yVal}); + } + } else { + if (coordCallback != null) { + coordCallback.accept(null); + } + } + + repaint(); + } + }); + + addMouseListener(new MouseAdapter() { + @Override + public void mouseExited(MouseEvent e) { + mouseX = null; + mouseY = null; + repaint(); + } + }); + } + + // Public API + + public void setCrosshairEnabled(boolean enabled) { + this.crosshairEnabled = enabled; + repaint(); + } + + public void setCoordinateCallback(Consumer cb) { + this.coordCallback = cb; + } + + public void clear() { + renderers.clear(); + } + + public void setGlobalMinMax(double min, double max) { + this.globalMin = min; + this.globalMax = max; + } + + public void setDt(double dt) { + this.dt = dt; + } + + // High-level, stat-aware renderers + + public void addAvgRenderer(double[] time, double[] avg, Color color, String name, Object statTag) { + renderers.add(new AvgRenderer(time, avg, color)); + } + + public void addMinMaxRenderer(double[] time, double[] min, double[] max, Color color, String name, Object statTag) { + renderers.add(new BandRenderer(time, max, min, color)); + } + + public void addSDRenderer(double[] time, double[] low, double[] high, Color color, String name, Object statTag) { + renderers.add(new BandRenderer(time, high, low, color)); + } + + // Utilities + + protected double roundUpNice(double value) { + if (value <= 0) return 1; + double exp = Math.pow(10, Math.floor(Math.log10(value))); + double n = value / exp; + double rounded; + if (n <= 1) rounded = 1; + else if (n <= 2) rounded = 2; + else if (n <= 5) rounded = 5; + else rounded = 10; + return rounded * exp; + } + + public static String formatTick(double value, double step) { + double absStep = Math.abs(step); + String s; + if (absStep >= 1.0) s = String.format("%.0f", value); + else if (absStep >= 0.1) s = String.format("%.1f", value); + else if (absStep >= 0.01) s = String.format("%.2f", value); + else if (absStep >= 0.001)s = String.format("%.3f", value); + else if (absStep >= 0.0001)s = String.format("%.4f", value); + else return String.format("%.2E", value); + + while (s.contains(".") && s.endsWith("0")) { + s = s.substring(0, s.length() - 1); + } + if (s.endsWith(".")) { + s = s.substring(0, s.length() - 1); + } + return s; + } + + @Override + protected void paintComponent(Graphics g) { + super.paintComponent(g); + + Graphics2D g2 = (Graphics2D) g; + g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + + int w = getWidth(); + int h = getHeight(); + + g2.setColor(Color.white); + g2.fillRect(0, 0, w, h); + + int x0 = LEFT_INSET; + int x1 = w - RIGHT_INSET; + int y0 = h - BOTTOM_INSET; + int y1 = TOP_INSET; + + lastX0 = x0; + lastX1 = x1; + lastY0 = y0; + lastY1 = y1; + + int plotWidth = x1 - x0; + int plotHeight = y0 - y1; + if (plotWidth <= 0 || plotHeight <= 0) return; + + // Determine max length from all renderers that use arrays + int maxLength = 0; + for (SeriesRenderer r : renderers) { + if (r instanceof AvgRenderer ar) { + maxLength = Math.max(maxLength, ar.values.length); + } else if (r instanceof BandRenderer br) { + maxLength = Math.max(maxLength, br.upper.length); + } + } + if (maxLength < 2) return; + + double yMaxRounded = roundUpNice(globalMax); + double yMinRounded = (globalMin < 0) ? -roundUpNice(-globalMin) : 0; + double xMax = dt * (maxLength - 1); + double xMaxRounded = roundUpNice(xMax); + + lastXMaxRounded = xMaxRounded; + lastYMaxRounded = yMaxRounded; + lastYMinRounded = yMinRounded; + + FontMetrics fm = g2.getFontMetrics(); + + // Gridlines + g2.setColor(new Color(220, 220, 220)); + g2.setStroke(new BasicStroke(1f)); + + int yTicks = 5; + double yRange = yMaxRounded - yMinRounded; + double yStep = yRange / yTicks; + + for (int i = 0; i <= yTicks; i++) { + double valueMajor = yMinRounded + i * yStep; + double norm = (valueMajor - yMinRounded) / (yMaxRounded - yMinRounded); + int yPixMajor = y0 - (int)Math.round(norm * plotHeight); + g2.drawLine(x0, yPixMajor, x1, yPixMajor); + + if (i < yTicks) { + double valueMid = valueMajor + yStep / 2.0; + double normMid = (valueMid - yMinRounded) / (yMaxRounded - yMinRounded); + int yPixMid = y0 - (int)Math.round(normMid * plotHeight); + g2.drawLine(x0, yPixMid, x1, yPixMid); + } + } + + double[] xMajor = {0, xMaxRounded / 2, xMaxRounded}; + for (int i = 0; i < xMajor.length; i++) { + double xvMajor = xMajor[i]; + int xPixMajor = x0 + (int)Math.round((xvMajor / xMaxRounded) * plotWidth); + g2.drawLine(xPixMajor, y1, xPixMajor, y0); + + if (i < xMajor.length - 1) { + double xvMid = (xMajor[i] + xMajor[i + 1]) / 2.0; + int xPixMid = x0 + (int)Math.round((xvMid / xMaxRounded) * plotWidth); + g2.drawLine(xPixMid, y1, xPixMid, y0); + } + } + + // Axes + ticks + g2.setColor(Color.black); + g2.setStroke(new BasicStroke(AXIS_STROKE)); + + g2.drawLine(x0, y0, x1, y0); + g2.drawLine(x0, y0, x0, y1); + + for (int i = 0; i <= yTicks; i++) { + double valueMajor = i * yStep; + double norm = valueMajor / yMaxRounded; + int yPixMajor = y0 - (int)Math.round(norm * plotHeight); + + g2.drawLine(x0 - 5, yPixMajor, x0, yPixMajor); + + String label = formatTick(valueMajor, yStep); + int sw = fm.stringWidth(label); + g2.drawString(label, x0 - 10 - sw, yPixMajor + fm.getAscent() / 2); + + if (i < yTicks) { + double valueMid = (i + 0.5) * yStep; + double normMid = valueMid / yMaxRounded; + int yPixMid = y0 - (int)Math.round(normMid * plotHeight); + g2.drawLine(x0 - 3, yPixMid, x0, yPixMid); + } + } + + double xStep = xMajor[1] - xMajor[0]; + for (int i = 0; i < xMajor.length; i++) { + double xvMajor = xMajor[i]; + int xPixMajor = x0 + (int)Math.round((xvMajor / xMaxRounded) * plotWidth); + + g2.drawLine(xPixMajor, y0, xPixMajor, y0 + 5); + + String label = formatTick(xvMajor, xStep); + int sw = fm.stringWidth(label); + g2.drawString(label, xPixMajor - sw / 2, y0 + fm.getAscent() + 5); + + if (i < xMajor.length - 1) { + double xvMid = (xMajor[i] + xMajor[i + 1]) / 2.0; + int xPixMid = x0 + (int)Math.round((xvMid / xMaxRounded) * plotWidth); + g2.drawLine(xPixMid, y0, xPixMid, y0 + 3); + } + } + + // Crosshair + if (crosshairEnabled && mouseX != null && mouseY != null) { + g2.setColor(new Color(180, 180, 180)); + g2.setStroke(new BasicStroke(1f)); + g2.drawLine(mouseX, y1, mouseX, y0); + g2.drawLine(x0, mouseY, x1, mouseY); + } + + // Renderers (bands first, then lines) + for (SeriesRenderer r : renderers) { + if (r instanceof BandRenderer) { + r.draw(g2, x0, x1, y0, y1, plotWidth, plotHeight, xMaxRounded, yMaxRounded, yMinRounded, dt); + } + } + for (SeriesRenderer r : renderers) { + if (r instanceof AvgRenderer) { + r.draw(g2, x0, x1, y0, y1, plotWidth, plotHeight, xMaxRounded, yMaxRounded, yMinRounded, dt); + } + } + } +} \ No newline at end of file diff --git a/vcell-client/src/main/java/cbit/plot/gui/ClusterDataPanel.java b/vcell-client/src/main/java/cbit/plot/gui/ClusterDataPanel.java index 837dd0f1fa..5387f8197d 100644 --- a/vcell-client/src/main/java/cbit/plot/gui/ClusterDataPanel.java +++ b/vcell-client/src/main/java/cbit/plot/gui/ClusterDataPanel.java @@ -22,313 +22,251 @@ import javax.swing.table.TableCellRenderer; import javax.swing.table.TableColumn; import java.awt.*; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; import java.awt.event.MouseEvent; -import java.awt.event.MouseListener; import java.beans.PropertyChangeEvent; -import java.beans.PropertyChangeListener; import java.io.File; -public class ClusterDataPanel extends JPanel { +public class ClusterDataPanel extends AbstractDataPanel { private static final Logger LG = LogManager.getLogger(ClusterDataPanel.class); - private ScrollTable scrollPaneTable; - private NonEditableDefaultTableModel nonEditableDefaultTableModel = null; - - private JPopupMenu popupMenu = null; - private JMenuItem miCopyAll = null; - private JMenuItem miCopyHDF5 = null; - private static enum CopyAction {copy,copyrow,copyall}; - - private final IvjEventHandler ivjEventHandler = new IvjEventHandler(); - - class IvjEventHandler implements ActionListener, MouseListener, PropertyChangeListener, ChangeListener { - - @Override - public void mouseClicked(MouseEvent e) { - if (e.getSource() == getScrollPaneTable()) { - int row = getScrollPaneTable().rowAtPoint(e.getPoint()); - int col = getScrollPaneTable().columnAtPoint(e.getPoint()); - System.out.println("ClusterDataPanel: clicked row=" + row + " col=" + col); - if (SwingUtilities.isRightMouseButton(e)) { - getPopupMenu().show(e.getComponent(), e.getX(), e.getY()); - } - } - } - - @Override public void mousePressed(MouseEvent e) {} - @Override public void mouseReleased(MouseEvent e) {} - @Override public void mouseEntered(MouseEvent e) {} - @Override public void mouseExited(MouseEvent e) {} - - @Override - public void actionPerformed(ActionEvent e) { - // reserved for future - } - - @Override - public void propertyChange(PropertyChangeEvent evt) { - // reserved for future dynamic formatting - } - - @Override - public void stateChanged(ChangeEvent e) { - // reserved for future slider/spinner interactions - } - } - + // ------------------------------------------------------------------------- + // Cluster-specific header renderer + // ------------------------------------------------------------------------- private class ClusterHeaderRenderer extends DefaultTableCellRenderer { private final TableCellRenderer base; + public ClusterHeaderRenderer(TableCellRenderer base) { this.base = base; } @Override - public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, - boolean hasFocus, int row, int column) { - // First let ScrollTable’s renderer do its work - Component c = base.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); + public Component getTableCellRendererComponent( + JTable table, Object value, boolean isSelected, + boolean hasFocus, int row, int column) { + + Component c = base.getTableCellRendererComponent( + table, value, isSelected, hasFocus, row, column); + if (!(c instanceof JLabel)) { - return c; // safety + return c; } + JLabel lbl = (JLabel) c; String name = value == null ? "" : value.toString(); - // Read mode from table metadata - ClusterSpecificationPanel.DisplayMode mode = (ClusterSpecificationPanel.DisplayMode) - ((JComponent)table).getClientProperty("ClusterDisplayMode"); + ClusterSpecificationPanel.DisplayMode mode = + (ClusterSpecificationPanel.DisplayMode) + ((JComponent) table).getClientProperty("ClusterDisplayMode"); - // First-time creation: no mode yet if (mode == null) { lbl.setToolTipText(null); - return lbl; // leave ScrollTable’s default header styling intact + return lbl; } + String text = ""; String unit = ""; String tooltip = ""; - ClusterSpecificationPanel.ClusterStatistic stat = ClusterSpecificationPanel.ClusterStatistic.fromString(name); + + ClusterSpecificationPanel.ClusterStatistic stat = + ClusterSpecificationPanel.ClusterStatistic.fromString(name); + if (column == 0) { unit = "seconds"; text = "" + name + " [" + unit + "]"; tooltip = "Simulation time"; } else { - switch(mode) { + switch (mode) { case COUNTS: unit = "molecules"; text = "" + name + " [" + unit + "]"; - tooltip = "" + "Number of clusters made of " + name + " " + unit + ""; + tooltip = "Number of clusters made of " + name + " " + unit + ""; break; + case MEAN: case OVERALL: - if(stat != null) { + if (stat != null) { unit = stat.unit(); - text = "" + stat.fullName() + " [" + unit + "]"; + text = "" + stat.fullName() + + " [" + unit + "]"; tooltip = "" + stat.description() + ""; } break; } } + lbl.setText(text); lbl.setToolTipText(tooltip); return lbl; } } + // ------------------------------------------------------------------------- + // Constructor + // ------------------------------------------------------------------------- public ClusterDataPanel() { super(); - initialize(); - } - private void initialize() { - try { - setName("ClusterDataPanel"); - setLayout(new java.awt.BorderLayout()); - setSize(541, 348); - add(getScrollPaneTable().getEnclosingScrollPane(), BorderLayout.CENTER); - JLabel lblNewLabel = new JLabel("To Copy table data or Export as HDF5, select rows/cells and use the right mouse button menu."); - add(lblNewLabel, BorderLayout.SOUTH); - initConnections(); -// controlKeys(); - } catch (Throwable exc) { - handleException(exc); - } } - private void initConnections() throws java.lang.Exception { - this.addPropertyChangeListener(ivjEventHandler); - getScrollPaneTable().addMouseListener(ivjEventHandler); - getScrollPaneTable().setModel(getNonEditableDefaultTableModel()); - getScrollPaneTable().createDefaultColumnsFromModel(); - TableCellRenderer baseHeaderRenderer = getScrollPaneTable().getTableHeader().getDefaultRenderer(); - getScrollPaneTable().getTableHeader().setDefaultRenderer(new ClusterHeaderRenderer(baseHeaderRenderer)); - } - private void handleException(java.lang.Throwable exception) { - System.out.println("--------- UNCAUGHT EXCEPTION ---------"); - exception.printStackTrace(System.out); - } + // ------------------------------------------------------------------------- + // Hook up cluster-specific header renderer + // ------------------------------------------------------------------------- + @Override + protected void initConnections() throws Exception { + super.initConnections(); - // ----------------------------------------------------------- - private ScrollTable getScrollPaneTable() { - if (scrollPaneTable == null) { - try { - scrollPaneTable = new ScrollTable(); - scrollPaneTable.setName("ScrollPaneTable"); - scrollPaneTable.setCellSelectionEnabled(true); - scrollPaneTable.setBounds(0, 0, 200, 200); - } catch (java.lang.Throwable ivjExc) { - handleException(ivjExc); - } - } - return scrollPaneTable; - } + TableCellRenderer baseHeaderRenderer = + getScrollPaneTable().getTableHeader().getDefaultRenderer(); - private NonEditableDefaultTableModel getNonEditableDefaultTableModel() { - if (nonEditableDefaultTableModel == null) { - try { - nonEditableDefaultTableModel = new NonEditableDefaultTableModel(); - } catch (java.lang.Throwable ivjExc) { - handleException(ivjExc); - } - } - return nonEditableDefaultTableModel; + getScrollPaneTable().getTableHeader() + .setDefaultRenderer(new ClusterHeaderRenderer(baseHeaderRenderer)); } - private JPopupMenu getPopupMenu() { - if (popupMenu == null) { - popupMenu = new JPopupMenu(); - - miCopyAll = new JMenuItem("Copy All"); - miCopyAll.addActionListener(e -> copyCells(this, false)); - popupMenu.add(miCopyAll); - - miCopyHDF5 = new JMenuItem("Copy to HDF5"); - miCopyHDF5.setEnabled(false); // export to HDF5 code is not working - miCopyHDF5.addActionListener(e -> copyCells(this,true)); - popupMenu.add(miCopyHDF5); - } - return popupMenu; + // ------------------------------------------------------------------------- + // Override copy handler + // ------------------------------------------------------------------------- + @Override + protected void onCopyCells(boolean isHDF5) { + copyCells(isHDF5); } - // ----------------------------------------------------------------------------------------------- - private static synchronized void copyCells(ClusterDataPanel cdp, boolean isHDF5) { + // ------------------------------------------------------------------------- + // Instance version of old static copyCells() + // ------------------------------------------------------------------------- + private void copyCells(boolean isHDF5) { try { - int r = 0; - int c = 0; - int[] rows = new int[0]; - int[] columns = new int[0]; - r = cdp.getScrollPaneTable().getRowCount(); - c = cdp.getScrollPaneTable().getColumnCount(); - rows = new int[r]; - columns = new int[c]; - for (int i = 0; i < rows.length; i++){ - rows[i] = i; - } - for (int i = 0; i < columns.length; i++){ - columns[i] = i; - } - if(rows.length < 1 || columns.length < 1) - { + int r = getScrollPaneTable().getRowCount(); + int c = getScrollPaneTable().getColumnCount(); + + if (r < 1 || c < 1) { throw new Exception("No table cell is selected."); } - System.out.println("Copying cluster data: rows=" + rows.length + " columns=" + columns.length + " isHDF5=" + isHDF5); - boolean bHistogram = false; // means first column is time (always is for us) - String firstColName = cdp.getScrollPaneTable().getColumnName(0); + + int[] rows = new int[r]; + int[] columns = new int[c]; + for (int i = 0; i < r; i++) rows[i] = i; + for (int i = 0; i < c; i++) columns[i] = i; + + LG.debug("Copying cluster data: rows=" + r + " columns=" + c + " isHDF5=" + isHDF5); + + boolean bHistogram = false; String blankCellValue = "-1"; + boolean bHasTimeColumn = true; + StringBuffer buffer = new StringBuffer(); - boolean bHasTimeColumn = false; - if(isHDF5) { - int columnCount = cdp.getScrollPaneTable().getColumnCount(); - int rowCount = cdp.getScrollPaneTable().getRowCount(); + if (isHDF5) { + int columnCount = c; + int rowCount = r; + String[] columnNames = new String[columnCount]; - for (int i=0; i0) ){ - resolvedValues[j-(bHasTimeColumn?1:0)] = new Expression(((Double)cell).doubleValue()); + for (int j = 0; j < c; j++) { + Object cell = getScrollPaneTable().getValueAt(i, j); + cell = (cell != null ? cell : ""); + + buffer.append(cell.toString()); + if (j < c - 1) buffer.append("\t"); + + if (!cell.equals("") && (!bHasTimeColumn || j > 0)) { + resolvedValues[j - (bHasTimeColumn ? 1 : 0)] = + new Expression(((Double) cell).doubleValue()); } } } + VCellTransferable.ResolvedValuesSelection rvs = - new VCellTransferable.ResolvedValuesSelection(tableSymbolTableEntries,null,resolvedValues,buffer.toString()); + new VCellTransferable.ResolvedValuesSelection( + new SymbolTableEntry[c - 1], + null, + resolvedValues, + buffer.toString() + ); + VCellTransferable.sendToClipboard(rvs); + } catch (Exception ex) { LG.error("Error copying cluster data", ex); - JOptionPane.showMessageDialog(cdp, "Error copying cluster data: " + ex.getMessage(), "Copy Error", JOptionPane.ERROR_MESSAGE); + JOptionPane.showMessageDialog( + this, + "Error copying cluster data: " + ex.getMessage(), + "Copy Error", + JOptionPane.ERROR_MESSAGE + ); } } - public void setSpecialityRenderer(SpecialtyTableRenderer str) { - // TODO: write some appropriate renderer when we decide what to show in the tooltip - // use RendererViewerDoubleWithTooltip for inspiration -// getScrollPaneTable().setSpecialityRenderer(str); - } - + public void updateData(ClusterSpecificationPanel.ClusterSelection sel) + throws ExpressionException { - public void updateData(ClusterSpecificationPanel.ClusterSelection sel) throws ExpressionException { - if(sel == null) { + if (sel == null) { getScrollPaneTable().putClientProperty("ClusterDisplayMode", null); } else { getScrollPaneTable().putClientProperty("ClusterDisplayMode", sel.mode); } - if (sel == null || sel.resultSet == null || sel.columns == null || sel.columns.isEmpty()) { - getNonEditableDefaultTableModel().setDataVector(new Object[][]{}, new Object[]{"No data"}); + + if (sel == null || sel.resultSet == null || + sel.columns == null || sel.columns.isEmpty()) { + + getNonEditableDefaultTableModel() + .setDataVector(new Object[][]{}, new Object[]{"No data"}); + getScrollPaneTable().createDefaultColumnsFromModel(); revalidate(); repaint(); return; } + ODESolverResultSet srs = sel.resultSet; java.util.List columns = sel.columns; - int timeIndex = srs.findColumn("t"); + int timeIndex = srs.findColumn(ReservedVariable.TIME.getName()); double[] times = srs.extractColumn(timeIndex); int rowCount = times.length; - // column names String[] columnNames = new String[1 + columns.size()]; columnNames[0] = "time"; for (int i = 0; i < columns.size(); i++) { columnNames[i + 1] = columns.get(i).getName(); } - // data Object[][] data = new Object[rowCount][columnNames.length]; for (int r = 0; r < rowCount; r++) { int c = 0; @@ -340,10 +278,7 @@ public void updateData(ClusterSpecificationPanel.ClusterSelection sel) throws Ex } } - // update existing model getNonEditableDefaultTableModel().setDataVector(data, columnNames); - - // refresh table columns getScrollPaneTable().createDefaultColumnsFromModel(); autoSizeTableColumns(getScrollPaneTable()); @@ -351,6 +286,9 @@ public void updateData(ClusterSpecificationPanel.ClusterSelection sel) throws Ex repaint(); } + // ------------------------------------------------------------------------- + // autoSizeTableColumns() — unchanged + // ------------------------------------------------------------------------- private void autoSizeTableColumns(JTable table) { final int margin = 12; @@ -359,13 +297,11 @@ private void autoSizeTableColumns(JTable table) { int maxWidth = 0; - // header TableCellRenderer headerRenderer = table.getTableHeader().getDefaultRenderer(); Component headerComp = headerRenderer.getTableCellRendererComponent( table, column.getHeaderValue(), false, false, 0, col); maxWidth = Math.max(maxWidth, headerComp.getPreferredSize().width); - // cells for (int row = 0; row < table.getRowCount(); row++) { TableCellRenderer cellRenderer = table.getCellRenderer(row, col); Component comp = table.prepareRenderer(cellRenderer, row, col); @@ -375,4 +311,11 @@ private void autoSizeTableColumns(JTable table) { column.setPreferredWidth(maxWidth + margin); } } -} \ No newline at end of file + + public void setSpecialityRenderer(SpecialtyTableRenderer str) { + // TODO: write some appropriate renderer when we decide what to show in the tooltip + // use RendererViewerDoubleWithTooltip for inspiration +// getScrollPaneTable().setSpecialityRenderer(str); + } + +} diff --git a/vcell-client/src/main/java/cbit/plot/gui/ClusterPlotPanel.java b/vcell-client/src/main/java/cbit/plot/gui/ClusterPlotPanel.java index 1b3e52048e..b6849ac056 100644 --- a/vcell-client/src/main/java/cbit/plot/gui/ClusterPlotPanel.java +++ b/vcell-client/src/main/java/cbit/plot/gui/ClusterPlotPanel.java @@ -19,361 +19,14 @@ import org.vcell.util.gui.GeneralGuiUtils; import org.vcell.util.*; -public class ClusterPlotPanel extends JPanel { - - private static final int LEFT_INSET = 50; // insets for axes and labels - private static final int RIGHT_INSET = 20; - private static final int TOP_INSET = 20; - private static final int BOTTOM_INSET = 30; - - private static final float AXIS_STROKE = 1.0f; // stroke widths - private static final float CURVE_STROKE = 1.5f; - - private static class CurveData { - final String name; - final double[] yRaw; - final Color color; - CurveData(String name, double[] yRaw, Color color) { - this.name = name; - this.yRaw = yRaw; - this.color = color; - } - } - private static class Envelope { - final String name; - final double[] upper; - final double[] lower; - final Color fillColor; - - Envelope(String name, double[] upper, double[] lower, Color fillColor) { - this.name = name; - this.upper = upper; - this.lower = lower; - this.fillColor = fillColor; - } - } - - private final List curves = new ArrayList<>(); - private final List envelopes = new ArrayList<>(); - - private double globalMin = 0; - private double globalMax = 1; - private double dt = 1; - - private Integer mouseX = null; // mouse crosshair - private Integer mouseY = null; - private int lastX0, lastX1, lastY0, lastY1; - private boolean crosshairEnabled = true; - private Consumer coordCallback; // parent supplies this - private double lastXMaxRounded; - private double lastYMaxRounded; - private double lastYMinRounded; - +public class ClusterPlotPanel extends AbstractPlotPanel { public ClusterPlotPanel() { - - addMouseMotionListener(new MouseMotionAdapter() { - @Override - public void mouseMoved(MouseEvent e) { - int mx = e.getX(); - int my = e.getY(); - // Check if inside plot area - if (mx >= lastX0 && mx <= lastX1 && my >= lastY1 && my <= lastY0) { - mouseX = mx; - mouseY = my; - } else { - mouseX = null; - mouseY = null; - } - if (crosshairEnabled && mouseX != null && mouseY != null) { - // ---- X coordinate ---- - double fracX = (mouseX - lastX0) / (double)(lastX1 - lastX0); - double xVal = fracX * lastXMaxRounded; - // ---- Y coordinate (now using yMinRounded and yMaxRounded) ---- - double fracY = (lastY0 - mouseY) / (double)(lastY0 - lastY1); - double yVal = lastYMinRounded + fracY * (lastYMaxRounded - lastYMinRounded); - if (coordCallback != null) { - coordCallback.accept(new double[]{xVal, yVal}); - } - } else { - if (coordCallback != null) { - coordCallback.accept(null); - } - } - repaint(); - } - }); - addMouseListener(new MouseAdapter() { - @Override - public void mouseExited(MouseEvent e) { - mouseX = null; - mouseY = null; - repaint(); - } - }); - } - -// ----------------------------------------------------------------------------- - - public void setCrosshairEnabled(boolean enabled) { - this.crosshairEnabled = enabled; - repaint(); - } - public void setCoordinateCallback(Consumer cb) { - this.coordCallback = cb; - } - - public void clear() { - curves.clear(); - envelopes.clear(); - } - - public void addCurve(String name, double[] yRaw, Color color) { - curves.add(new CurveData(name, yRaw, color)); - } - public void setGlobalMinMax(double min, double max) { - this.globalMin = min; - this.globalMax = max; - } - public void addEnvelope(String name, double[] upper, double[] lower, Color fillColor) { - envelopes.add(new Envelope(name, upper, lower, fillColor)); - } - - public void setDt(double dt) { - this.dt = dt; - } - private double roundUpNice(double value) { - if (value <= 0) return 1; - - double exp = Math.pow(10, Math.floor(Math.log10(value))); - double n = value / exp; - double rounded; - if (n <= 1) rounded = 1; - else if (n <= 2) rounded = 2; - else if (n <= 5) rounded = 5; - else rounded = 10; - return rounded * exp; - } - - public static String formatTick(double value, double step) { - double absStep = Math.abs(step); - String s; - if (absStep >= 1.0) { - s = String.format("%.0f", value); - } else if (absStep >= 0.1) { - s = String.format("%.1f", value); - } else if (absStep >= 0.01) { - s = String.format("%.2f", value); - } else if (absStep >= 0.001) { - s = String.format("%.3f", value); - } else if (absStep >= 0.0001) { - s = String.format("%.4f", value); - } else { - return String.format("%.2E", value); - } - // strip trailing zeros - while (s.contains(".") && s.endsWith("0")) { - s = s.substring(0, s.length() - 1); - } - // strip trailing decimal point - if (s.endsWith(".")) { - s = s.substring(0, s.length() - 1); - } - return s; - } - private int xPixel(double t, int x0, int plotWidth, double xMaxRounded) { - return x0 + (int) Math.round((t / xMaxRounded) * plotWidth); - } - private int yPixel(double value, int y0, int plotHeight, double yMaxRounded, double yMinRounded) { - double norm = (value - yMinRounded) / (yMaxRounded - yMinRounded); - int yPix = y0 - (int)Math.round(norm * plotHeight); - return yPix; - } - - @Override - protected void paintComponent(Graphics g) { - super.paintComponent(g); - - Graphics2D g2 = (Graphics2D) g; - g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); - - int w = getWidth(); - int h = getHeight(); - g2.setColor(Color.white); - g2.fillRect(0, 0, w, h); - - int x0 = LEFT_INSET; - int x1 = w - RIGHT_INSET; - int y0 = h - BOTTOM_INSET; - int y1 = TOP_INSET; - - // store plot area for mouse listeners - lastX0 = x0; - lastX1 = x1; - lastY0 = y0; - lastY1 = y1; - - int plotWidth = x1 - x0; - int plotHeight = y0 - y1; - if (plotWidth <= 0 || plotHeight <= 0) { - return; - } - - // Determine if we have anything to draw (curves or envelopes) - int maxCurveLength = curves.stream().mapToInt(c -> c.yRaw.length).max().orElse(0); - int maxEnvelopeLength = envelopes.stream().mapToInt(e -> e.upper.length).max().orElse(0); - int maxLength = Math.max(maxCurveLength, maxEnvelopeLength); - if (maxLength < 2) { // If neither curves nor envelopes have at least 2 points, nothing to draw - return; - } - - double yMaxRounded = roundUpNice(globalMax); - double yMinRounded = (globalMin < 0) ? -roundUpNice(-globalMin) : 0; - double xMax = dt * (maxLength - 1); - double xMaxRounded = roundUpNice(xMax); - lastXMaxRounded = xMaxRounded; - lastYMaxRounded = yMaxRounded; - lastYMinRounded = yMinRounded; - FontMetrics fm = g2.getFontMetrics(); - - // ============================================================ - // GRIDLINES (major + mid) - // ============================================================ - g2.setColor(new Color(220, 220, 220)); - g2.setStroke(new BasicStroke(1f)); - - // Y gridlines - int yTicks = 5; - double yRange = yMaxRounded - yMinRounded; - double yStep = yRange / yTicks; - - for (int i = 0; i <= yTicks; i++) { - double valueMajor = yMinRounded + i * yStep; - int yPixMajor = yPixel(valueMajor, y0, plotHeight, yMaxRounded, yMinRounded); - g2.drawLine(x0, yPixMajor, x1, yPixMajor); - - if (i < yTicks) { - double valueMid = valueMajor + yStep / 2.0; - int yPixMid = y0 - (int)Math.round((valueMid - yMinRounded) / yRange * plotHeight); - g2.drawLine(x0, yPixMid, x1, yPixMid); - } - } - - // X gridlines - double[] xMajor = {0, xMaxRounded / 2, xMaxRounded}; - - for (int i = 0; i < xMajor.length; i++) { - double xvMajor = xMajor[i]; - int xPixMajor = x0 + (int) Math.round((xvMajor / xMaxRounded) * plotWidth); - g2.drawLine(xPixMajor, y1, xPixMajor, y0); - - if (i < xMajor.length - 1) { - double xvMid = (xMajor[i] + xMajor[i + 1]) / 2.0; - int xPixMid = x0 + (int) Math.round((xvMid / xMaxRounded) * plotWidth); - g2.drawLine(xPixMid, y1, xPixMid, y0); - } - } - - // ============================================================ - // AXES + TICKS - // ============================================================ - g2.setColor(Color.black); - g2.setStroke(new BasicStroke(AXIS_STROKE)); - - g2.drawLine(x0, y0, x1, y0); - g2.drawLine(x0, y0, x0, y1); - - // Y ticks - for (int i = 0; i <= yTicks; i++) { - double valueMajor = i * yStep; - int yPixMajor = y0 - (int) Math.round((valueMajor / yMaxRounded) * plotHeight); - - g2.drawLine(x0 - 5, yPixMajor, x0, yPixMajor); - - String label = formatTick(valueMajor, yStep); - int sw = fm.stringWidth(label); - g2.drawString(label, x0 - 10 - sw, yPixMajor + fm.getAscent() / 2); - - if (i < yTicks) { - double valueMid = (i + 0.5) * yStep; - int yPixMid = y0 - (int) Math.round((valueMid / yMaxRounded) * plotHeight); - g2.drawLine(x0 - 3, yPixMid, x0, yPixMid); - } - } - - // X ticks - double xStep = xMajor[1] - xMajor[0]; - for (int i = 0; i < xMajor.length; i++) { - double xvMajor = xMajor[i]; - int xPixMajor = x0 + (int) Math.round((xvMajor / xMaxRounded) * plotWidth); - - g2.drawLine(xPixMajor, y0, xPixMajor, y0 + 5); - - String label = formatTick(xvMajor, xStep); - int sw = fm.stringWidth(label); - g2.drawString(label, xPixMajor - sw / 2, y0 + fm.getAscent() + 5); - - if (i < xMajor.length - 1) { - double xvMid = (xMajor[i] + xMajor[i + 1]) / 2.0; - int xPixMid = x0 + (int) Math.round((xvMid / xMaxRounded) * plotWidth); - g2.drawLine(xPixMid, y0, xPixMid, y0 + 3); - } - } - - // ============================================================ - // SD ENVELOPES (drawn after gridlines, before curves) - // ============================================================ - for (Envelope env : envelopes) { - g2.setColor(env.fillColor); - - Path2D path = new Path2D.Double(); - int n = env.upper.length; - // Upper boundary - double t0 = 0; - path.moveTo(xPixel(t0, x0, plotWidth, xMaxRounded), yPixel(env.upper[0], y0, plotHeight, yMaxRounded, yMinRounded)); - for (int i = 1; i < n; i++) { - double t = i * dt; - path.lineTo(xPixel(t, x0, plotWidth, xMaxRounded), yPixel(env.upper[i], y0, plotHeight, yMaxRounded, yMinRounded)); - } - // Lower boundary (reverse direction) - for (int i = n - 1; i >= 0; i--) { - double t = i * dt; - path.lineTo(xPixel(t, x0, plotWidth, xMaxRounded), yPixel(env.lower[i], y0, plotHeight, yMaxRounded, yMinRounded)); - } - path.closePath(); - g2.fill(path); - } - - // ============================================================ - // CROSSHAIR (drawn after gridlines, before curves) - // ============================================================ - if (crosshairEnabled && mouseX != null && mouseY != null) { - g2.setColor(new Color(180, 180, 180)); - g2.setStroke(new BasicStroke(1f)); - g2.drawLine(mouseX, y1, mouseX, y0); - g2.drawLine(x0, mouseY, x1, mouseY); - } - - // ============================================================ - // CURVES - // ============================================================ - g2.setStroke(new BasicStroke(CURVE_STROKE, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND)); - - for (CurveData curve : curves) { - int n = curve.yRaw.length; - if (n < 2) continue; - - int[] x = new int[n]; - int[] y = new int[n]; - - for (int i = 0; i < n; i++) { - double t = i * dt; - x[i] = x0 + (int) Math.round((t / xMaxRounded) * plotWidth); - y[i] = yPixel(curve.yRaw[i], y0, plotHeight, yMaxRounded, yMinRounded); } - - g2.setColor(curve.color); - g2.drawPolyline(x, y, n); - } + super(); + // No additional initialization. + // All rendering, listeners, scaling, and crosshair logic live in AbstractPlotPanel. } + // If cluster-specific helpers are ever needed, they go here. + // For now, ClusterPlotPanel is intentionally empty. } diff --git a/vcell-client/src/main/java/cbit/plot/gui/MoleculeDataPanel.java b/vcell-client/src/main/java/cbit/plot/gui/MoleculeDataPanel.java index 07a71288ac..94bfe868c5 100644 --- a/vcell-client/src/main/java/cbit/plot/gui/MoleculeDataPanel.java +++ b/vcell-client/src/main/java/cbit/plot/gui/MoleculeDataPanel.java @@ -1,5 +1,12 @@ package cbit.plot.gui; +import cbit.vcell.math.ReservedVariable; +import cbit.vcell.parser.ExpressionException; +import cbit.vcell.simdata.LangevinSolverResultSet; +import cbit.vcell.solver.ode.ODESolverResultSet; +import cbit.vcell.solver.ode.gui.ClusterSpecificationPanel; +import cbit.vcell.solver.ode.gui.MoleculeSpecificationPanel; +import cbit.vcell.util.ColumnDescription; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.vcell.util.gui.NonEditableDefaultTableModel; @@ -10,6 +17,7 @@ import javax.swing.event.ChangeListener; import javax.swing.table.DefaultTableCellRenderer; import javax.swing.table.TableCellRenderer; +import javax.swing.table.TableColumn; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; @@ -18,152 +26,214 @@ import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; -public class MoleculeDataPanel extends JPanel { +public class MoleculeDataPanel extends AbstractDataPanel { - private static final Logger LG = LogManager.getLogger(MoleculeDataPanel.class); - - private ScrollTable scrollPaneTable; - private NonEditableDefaultTableModel nonEditableDefaultTableModel = null; - - private JPopupMenu popupMenu = null; - private JMenuItem miCopyAll = null; - private JMenuItem miCopyHDF5 = null; - private static enum CopyAction {copy,copyrow,copyall}; - - private final MoleculeDataPanel.IvjEventHandler ivjEventHandler = new MoleculeDataPanel.IvjEventHandler(); - - - - class IvjEventHandler implements ActionListener, MouseListener, PropertyChangeListener, ChangeListener { - @Override - public void mouseClicked(MouseEvent e) { - if (e.getSource() == getScrollPaneTable()) { - int row = getScrollPaneTable().rowAtPoint(e.getPoint()); - int col = getScrollPaneTable().columnAtPoint(e.getPoint()); - System.out.println("MoleculeDataPanel: clicked row=" + row + " col=" + col); - if (SwingUtilities.isRightMouseButton(e)) { - getPopupMenu().show(e.getComponent(), e.getX(), e.getY()); - } - } - } - - @Override public void mousePressed(MouseEvent e) {} - @Override public void mouseReleased(MouseEvent e) {} - @Override public void mouseEntered(MouseEvent e) {} - @Override public void mouseExited(MouseEvent e) {} - - @Override - public void actionPerformed(ActionEvent e) { - // reserved for future - } - - @Override - public void propertyChange(PropertyChangeEvent evt) { - // reserved for future dynamic formatting + public enum SubStatistic { + AVG("AVG"), + MIN("MIN"), + MAX("MAX"), + SD("SD"); + public final String uiLabel; + SubStatistic(String uiLabel) { + this.uiLabel = uiLabel; } + } - @Override - public void stateChanged(ChangeEvent e) { - // reserved for future slider/spinner interactions + public static class MoleculeColumnInfo { + public final String entityName; + public final MoleculeSpecificationPanel.StatisticSelection statistic; + public final SubStatistic subStatistic; + public MoleculeColumnInfo(String entityName, + MoleculeSpecificationPanel.StatisticSelection statistic, + SubStatistic subStatistic) { + this.entityName = entityName; + this.statistic = statistic; + this.subStatistic = subStatistic; } } - private class MoleculeHeaderRenderer extends DefaultTableCellRenderer { - private final TableCellRenderer base; + private class MoleculeTwoRowHeaderRenderer extends DefaultTableCellRenderer { - public MoleculeHeaderRenderer(TableCellRenderer base) { + private final TableCellRenderer base; + public MoleculeTwoRowHeaderRenderer(TableCellRenderer base) { this.base = base; } - @Override public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { - // First let ScrollTable’s renderer do its work Component c = base.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); if (!(c instanceof JLabel)) { - return c; // safety + return c; } - return c; + JLabel lbl = (JLabel) c; + Object id = table.getColumnModel().getColumn(column).getIdentifier(); + if (!(id instanceof MoleculeColumnInfo)) { // time column w. unit in dark red + lbl.setText("
" +"time" +"
" + + " [seconds]" + "
"); + return lbl; + } + MoleculeColumnInfo info = (MoleculeColumnInfo) id; + String statLabel = "" + info.subStatistic.uiLabel + ""; // Statistic label (bold) + String unit = " [molecules]"; // Unit in dark red + lbl.setText("
" + info.entityName + "
" + + "" + statLabel + unit + "
" + ); + return lbl; } } + // ------------------------------------------------------ + public MoleculeDataPanel() { super(); - initialize(); } - private void initialize() { - try { - setName("MoleculeDataPanel"); - setLayout(new java.awt.BorderLayout()); - setSize(541, 348); - add(getScrollPaneTable().getEnclosingScrollPane(), BorderLayout.CENTER); - JLabel lblNewLabel = new JLabel("To Copy table data or Export as HDF5, select rows/cells and use the right mouse button menu."); - add(lblNewLabel, BorderLayout.SOUTH); - initConnections(); -// controlKeys(); - } catch (Throwable exc) { - handleException(exc); - } + @Override + protected void initConnections() throws Exception { + super.initConnections(); + TableCellRenderer baseHeaderRenderer = getScrollPaneTable().getTableHeader().getDefaultRenderer(); + getScrollPaneTable().getTableHeader().setDefaultRenderer(new MoleculeTwoRowHeaderRenderer(baseHeaderRenderer)); } - private void initConnections() throws java.lang.Exception { - this.addPropertyChangeListener(ivjEventHandler); - getScrollPaneTable().addMouseListener(ivjEventHandler); - getScrollPaneTable().setModel(getNonEditableDefaultTableModel()); - getScrollPaneTable().createDefaultColumnsFromModel(); - TableCellRenderer baseHeaderRenderer = getScrollPaneTable().getTableHeader().getDefaultRenderer(); - getScrollPaneTable().getTableHeader().setDefaultRenderer(new MoleculeDataPanel.MoleculeHeaderRenderer(baseHeaderRenderer)); + // ------------------------------------------------------ - } - private void handleException(java.lang.Throwable exception) { - System.out.println("--------- UNCAUGHT EXCEPTION ---------"); - exception.printStackTrace(System.out); - } + public void updateData(MoleculeSpecificationPanel.MoleculeSelection sel, LangevinSolverResultSet lsrs) + throws ExpressionException { - // ----------------------------------------------------------- - private ScrollTable getScrollPaneTable() { - if (scrollPaneTable == null) { - try { - scrollPaneTable = new ScrollTable(); - scrollPaneTable.setName("ScrollPaneTable"); - scrollPaneTable.setCellSelectionEnabled(true); - scrollPaneTable.setBounds(0, 0, 200, 200); - } catch (java.lang.Throwable ivjExc) { - handleException(ivjExc); - } + if (sel == null) { + getScrollPaneTable().putClientProperty("MoleculeSelection", null); // may be useful for tooltip generation + } else { + getScrollPaneTable().putClientProperty("MoleculeSelection", sel); } - return scrollPaneTable; - } - private JPopupMenu getPopupMenu() { - if (popupMenu == null) { - popupMenu = new JPopupMenu(); + if (sel == null || lsrs == null || lsrs.isAverageDataAvailable() == false || // guard clause + sel.selectedColumns == null || sel.selectedColumns.isEmpty() || + sel.selectedStatistics == null || sel.selectedStatistics.isEmpty()) { - miCopyAll = new JMenuItem("Copy All"); - miCopyAll.addActionListener(e -> copyCells(this, false)); - popupMenu.add(miCopyAll); + getNonEditableDefaultTableModel().setDataVector(new Object[][]{}, new Object[]{"No data"}); + getScrollPaneTable().createDefaultColumnsFromModel(); + revalidate(); + repaint(); + return; + } - miCopyHDF5 = new JMenuItem("Copy to HDF5"); - miCopyHDF5.setEnabled(false); // export to HDF5 code is not working - miCopyHDF5.addActionListener(e -> copyCells(this,true)); - popupMenu.add(miCopyHDF5); + ODESolverResultSet avgRS = lsrs.getAvg(); + ODESolverResultSet minRS = lsrs.getMin(); + ODESolverResultSet maxRS = lsrs.getMax(); + ODESolverResultSet stdRS = lsrs.getStd(); + java.util.List selectedColumns = sel.selectedColumns; + java.util.List selectedStatistics = sel.selectedStatistics; + + int timeIndex = avgRS.findColumn(ReservedVariable.TIME.getName()); + double[] times = avgRS.extractColumn(timeIndex); + int rowCount = times.length; + + + + // --------------------------------------------------------------------- + // Build column names + // --------------------------------------------------------------------- + java.util.List colNames = new java.util.ArrayList<>(); + java.util.List metadata = new java.util.ArrayList<>(); + + colNames.add("time"); + metadata.add(null); // time has no entity/statistic + + for (ColumnDescription cd : selectedColumns) { + String base = cd.getName(); + for (MoleculeSpecificationPanel.StatisticSelection statSel : sel.selectedStatistics) { + switch (statSel) { + case AVG: + colNames.add(base + " AVG"); + metadata.add(new MoleculeColumnInfo(base, statSel, SubStatistic.AVG)); + break; + + case MIN_MAX: + colNames.add(base + " MIN"); + metadata.add(new MoleculeColumnInfo(base, statSel, SubStatistic.MIN)); + colNames.add(base + " MAX"); + metadata.add(new MoleculeColumnInfo(base, statSel, SubStatistic.MAX)); + break; + + case SD: + colNames.add(base + " SD"); + metadata.add(new MoleculeColumnInfo(base, statSel, SubStatistic.SD)); + break; + } + } } - return popupMenu; - } - private static synchronized void copyCells(MoleculeDataPanel cdp, boolean isHDF5) { + // --------------------------------------------------------------------- + // Build data matrix + // --------------------------------------------------------------------- + Object[][] data = new Object[rowCount][colNames.size()]; + + for (int r = 0; r < rowCount; r++) { + int c = 0; + data[r][c++] = times[r]; + + for (ColumnDescription cd : selectedColumns) { + String baseName = cd.getName(); + // These are guaranteed to exist because of guard clause + double[] avgCol = avgRS.extractColumn(avgRS.findColumn(baseName)); + double[] minCol = minRS.extractColumn(minRS.findColumn(baseName)); + double[] maxCol = maxRS.extractColumn(maxRS.findColumn(baseName)); + double[] stdCol = stdRS.extractColumn(stdRS.findColumn(baseName)); + for (MoleculeSpecificationPanel.StatisticSelection statSel : selectedStatistics) { + switch (statSel) { + case AVG: + data[r][c++] = avgCol[r]; + break; + case MIN_MAX: + data[r][c++] = minCol[r]; + data[r][c++] = maxCol[r]; + break; + case SD: + data[r][c++] = stdCol[r]; // raw SD value + break; + } + } + } + } + // Push to model + getNonEditableDefaultTableModel().setDataVector( + data, + colNames.toArray(new String[0]) + ); + // Rebuild columns so we can attach metadata + renderer + getScrollPaneTable().createDefaultColumnsFromModel(); + for (int i = 0; i < metadata.size(); i++) { + getScrollPaneTable().getColumnModel().getColumn(i) + .setIdentifier(metadata.get(i)); + } + autoSizeTableColumns(getScrollPaneTable()); + revalidate(); + repaint(); } - private NonEditableDefaultTableModel getNonEditableDefaultTableModel() { - if (nonEditableDefaultTableModel == null) { - try { - nonEditableDefaultTableModel = new NonEditableDefaultTableModel(); - } catch (java.lang.Throwable ivjExc) { - handleException(ivjExc); + + private void autoSizeTableColumns(JTable table) { + final int margin = 12; + + for (int col = 0; col < table.getColumnCount(); col++) { + TableColumn column = table.getColumnModel().getColumn(col); + + int maxWidth = 0; + + // header + TableCellRenderer headerRenderer = table.getTableHeader().getDefaultRenderer(); + Component headerComp = headerRenderer.getTableCellRendererComponent( + table, column.getHeaderValue(), false, false, 0, col); + maxWidth = Math.max(maxWidth, headerComp.getPreferredSize().width); + + // cells + for (int row = 0; row < table.getRowCount(); row++) { + TableCellRenderer cellRenderer = table.getCellRenderer(row, col); + Component comp = table.prepareRenderer(cellRenderer, row, col); + maxWidth = Math.max(maxWidth, comp.getPreferredSize().width); } + + column.setPreferredWidth(maxWidth + margin); } - return nonEditableDefaultTableModel; } - } diff --git a/vcell-client/src/main/java/cbit/plot/gui/MoleculePlotPanel.java b/vcell-client/src/main/java/cbit/plot/gui/MoleculePlotPanel.java new file mode 100644 index 0000000000..8a1b924c12 --- /dev/null +++ b/vcell-client/src/main/java/cbit/plot/gui/MoleculePlotPanel.java @@ -0,0 +1,16 @@ +package cbit.plot.gui; + +public class MoleculePlotPanel extends AbstractPlotPanel { + + public MoleculePlotPanel() { + super(); + // No additional initialization. + // All rendering, listeners, scaling, and crosshair logic live in AbstractPlotPanel. + } + + // If molecule-specific helpers are ever needed, they go here. + // For now, MoleculePlotPanel is intentionally empty. + + + +} diff --git a/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterVisualizationPanel.java b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterVisualizationPanel.java index 73322e17e9..2c72f0a8ae 100644 --- a/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterVisualizationPanel.java +++ b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterVisualizationPanel.java @@ -320,18 +320,17 @@ private JComponent createLegendEntry(String name, Color color, ClusterSpecificat return p; } - private void redrawPlot(ClusterSpecificationPanel.ClusterSelection sel) throws ExpressionException { - System.out.println(this.getClass().getSimpleName() + ".redrawPlot() called, current selection: " + sel); - if (sel != null) { - System.out.println(this.getClass().getSimpleName() + ".redrawPlot() mode: " + sel.mode + ", columns: " + sel.columns.size() + ", resultSet: " + (sel.resultSet != null ? "present" : "null")); - } else { - System.out.println(this.getClass().getSimpleName() + ".redrawPlot() selection is null"); - } + System.out.println(getClass().getSimpleName() + ".redrawPlot() called, current selection: " + sel); + + ClusterPlotPanel plot = getClusterPlotPanel(); + // --------------------------------------------------------------------- + // NULL CASE + // --------------------------------------------------------------------- if (sel == null || sel.resultSet == null) { - getClusterPlotPanel().clear(); - getClusterPlotPanel().repaint(); + plot.clear(); + plot.repaint(); return; } @@ -352,21 +351,16 @@ private void redrawPlot(ClusterSpecificationPanel.ClusterSelection sel) throws E for (ColumnDescription cd : columns) { String name = cd.getName(); - // SD is special: no curve, but still load its data int idx = srs.findColumn(name); - if (idx < 0) { - continue; - } + if (idx < 0) continue; double[] y = srs.extractColumn(idx); yMap.put(name, y); - // SD does not contribute its own curve, but its raw values still matter for globalMax + // SD does not draw a line, but its raw values still matter for globalMax if (!name.equals("SD")) { for (double v : y) { - if (v > globalMax) { - globalMax = v; - } + if (v > globalMax) globalMax = v; } } } @@ -399,60 +393,62 @@ private void redrawPlot(ClusterSpecificationPanel.ClusterSelection sel) throws E double upper = acs[i] + sd[i]; double lower = acs[i] - sd[i]; - if (upper > globalMax) { - globalMax = upper; - } - if (lower < globalMin) { - globalMin = lower; // may be < 0 in theory - } + if (upper > globalMax) globalMax = upper; + if (lower < globalMin) globalMin = lower; } } // --------------------------------------------------------------------- // DRAWING // --------------------------------------------------------------------- - getClusterPlotPanel().clear(); + plot.clear(); // Draw all selected curves EXCEPT SD (SD is envelope only) for (ColumnDescription cd : columns) { String name = cd.getName(); - if (name.equals("SD")) { - continue; // SD never draws a line - } + if (name.equals("SD")) continue; + double[] y = yMap.get(name); - if (y == null) { - continue; - } + if (y == null) continue; + Color c = persistentColorMap.get(name); - getClusterPlotPanel().addCurve(name, y, c); + + // Cluster curves are AVG curves in the new API + plot.addAvgRenderer(times, y, c, name, /*statTag*/ "AVG"); } // Draw SD envelope if SD is selected if (sdSelected && acs != null && sd != null) { - double[] upper = new double[acs.length]; - double[] lower = new double[acs.length]; - for (int i = 0; i < acs.length; i++) { + int n = acs.length; + double[] upper = new double[n]; + double[] lower = new double[n]; + + for (int i = 0; i < n; i++) { upper[i] = acs[i] + sd[i]; lower[i] = acs[i] - sd[i]; } - // Use the already‑assigned SD color (derived from ACS in initializeGlobalPalette) + Color sdColor = persistentColorMap.get("SD"); - getClusterPlotPanel().addEnvelope("SD", upper, lower, sdColor); + + // SD is a band renderer in the new API + plot.addSDRenderer(times, lower, upper, sdColor, "SD", /*statTag*/ "SD"); } // --------------------------------------------------------------------- // FINALIZE // --------------------------------------------------------------------- - if (globalMin > 0) { - globalMin = 0; - } - getClusterPlotPanel().setGlobalMinMax(globalMin, globalMax); + if (globalMin > 0) globalMin = 0; + + plot.setGlobalMinMax(globalMin, globalMax); + if (times.length > 1) { - getClusterPlotPanel().setDt(times[1]); // times[0] == 0 + plot.setDt(times[1]); // times[0] == 0 } - getClusterPlotPanel().repaint(); + + plot.repaint(); } + private void redrawLegend(ClusterSpecificationPanel.ClusterSelection sel) { System.out.println(this.getClass().getSimpleName() + ".redrawLegend() called"); getLegendContentPanel().removeAll(); diff --git a/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/MoleculeSpecificationPanel.java b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/MoleculeSpecificationPanel.java index a09469e2eb..3c371d63fa 100644 --- a/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/MoleculeSpecificationPanel.java +++ b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/MoleculeSpecificationPanel.java @@ -158,7 +158,7 @@ public void actionPerformed(ActionEvent e) { getSelectedDisplayModes() ); System.out.println("MoleculeSelection changed: " + sel.selectedColumns.size() + " columns, " + sel.selectedStatistics.size() + " statistics, " + sel.selectedDisplayModes.size() + " display modes"); - firePropertyChange("MoleculeStatisticSelectionChanged", null, sel); + firePropertyChange("MoleculeSelectionChanged", null, sel); } } } diff --git a/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/MoleculeVisualizationPanel.java b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/MoleculeVisualizationPanel.java index 6654cd0b9e..7ce9ecdbe7 100644 --- a/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/MoleculeVisualizationPanel.java +++ b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/MoleculeVisualizationPanel.java @@ -1,31 +1,45 @@ package cbit.vcell.solver.ode.gui; +import cbit.plot.gui.ClusterDataPanel; +import cbit.plot.gui.ClusterPlotPanel; +import cbit.plot.gui.MoleculeDataPanel; +import cbit.plot.gui.MoleculePlotPanel; import cbit.vcell.client.data.ODEDataViewer; -import cbit.vcell.client.desktop.biomodel.DocumentEditorSubPanel; import cbit.vcell.parser.ExpressionException; import cbit.vcell.simdata.LangevinSolverResultSet; import cbit.vcell.solver.SimulationModelInfo; +import cbit.vcell.solver.ode.ODESimData; +import cbit.vcell.util.ColumnDescription; +import org.vcell.util.ColorUtil; import org.vcell.util.gui.SpecialtyTableRenderer; import javax.swing.*; -import javax.swing.event.ChangeListener; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; +import java.awt.event.ComponentAdapter; +import java.awt.event.ComponentEvent; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; +import java.util.*; +import java.util.List; -public class MoleculeVisualizationPanel extends DocumentEditorSubPanel { - - - - +public class MoleculeVisualizationPanel extends AbstractVisualizationPanel { private final ODEDataViewer owner; LangevinSolverResultSet langevinSolverResultSet = null; SimulationModelInfo simulationModelInfo = null; + MoleculeVisualizationPanel.IvjEventHandler ivjEventHandler = new MoleculeVisualizationPanel.IvjEventHandler(); + + private final Map persistentColorMap = new LinkedHashMap<>(); + private final java.util.List globalPalette = new ArrayList<>(); + private int nextColorIndex = 0; + + private MoleculePlotPanel moleculePlotPanel = null; // here are the plots being drawn + private MoleculeDataPanel moleculeDataPanel = null; // here resides the data table + class IvjEventHandler implements ActionListener, PropertyChangeListener, ListSelectionListener { @Override @@ -33,14 +47,33 @@ public void actionPerformed(ActionEvent e) { if (e.getSource() instanceof Component c && SwingUtilities.isDescendingFrom(c, MoleculeVisualizationPanel.this)) { System.out.println(this.getClass().getName() + ".actionPerformed() called with " + e.getActionCommand()); // switch selection between plot panel and data panel (located in a JCardLayout) + String cmd = e.getActionCommand(); + // --- Card switching (plot <-> data) --- + if (cmd.equals("PlotPanelContainer") || cmd.equals("DataPanelContainer")) { + CardLayout cl = (CardLayout) getCardPanel().getLayout(); + cl.show(getCardPanel(), cmd); // show the plot or the data panel + getPlotButton().setSelected(cmd.equals("PlotPanelContainer")); // update button selection state + getDataButton().setSelected(cmd.equals("DataPanelContainer")); + getLegendPanel().setVisible(cmd.equals("PlotPanelContainer")); // show legend only in plot mode + getCrosshairCheckBox().setVisible(cmd.equals("PlotPanelContainer")); // show/hide crosshair checkbox only in plot mode + setCrosshairEnabled(cmd.equals("PlotPanelContainer") && getCrosshairCheckBox().isSelected()); // enable/disable crosshair logic + return; + } + // --- Crosshair checkbox toggled --- + if (e.getSource() == getCrosshairCheckBox()) { + boolean enabled = getCrosshairCheckBox().isSelected(); + setCrosshairEnabled(enabled); + return; + } } } @Override public void propertyChange(PropertyChangeEvent evt) { // listens to changes in the MoleculeSpecificationPanel - if (evt.getSource() == owner.getMoleculeSpecificationPanel() && "MoleculeSelection".equals(evt.getPropertyName())) { + if (evt.getSource() == owner.getMoleculeSpecificationPanel() && "MoleculeSelectionChanged".equals(evt.getPropertyName())) { System.out.println(this.getClass().getName() + ".propertyChange() called with " + evt.getPropertyName()); // redraw everything based on the new selections MoleculeSpecificationPanel.MoleculeSelection sel = (MoleculeSpecificationPanel.MoleculeSelection) evt.getNewValue(); + ensureColorsAssigned(sel.selectedColumns); try { redrawLegend(sel); // redraw legend (one plot, multiple curves) redrawPlot(sel); // redraw plot (one plot, multiple curves) @@ -62,15 +95,85 @@ public MoleculeVisualizationPanel(ODEDataViewer owner) { super(); this.owner = owner; initialize(); + initConnections(); + setVisualizationBackground(Color.WHITE); } - private void initialize() { - setBackground(Color.white); - initConnections(); + + @Override + protected JPanel createPlotPanel() { + return getMoleculePlotPanel(); + } + + @Override + protected JPanel createDataPanel() { + return getMoleculeDataPanel(); + } + + @Override + protected void setCrosshairEnabled(boolean enabled) { + getMoleculePlotPanel().setCrosshairEnabled(enabled); + } + + public void setVisualizationBackground(Color color) { + super.setVisualizationBackground(color); + getMoleculePlotPanel().setBackground(color); + getMoleculeDataPanel().setBackground(color); + } + + private MoleculePlotPanel getMoleculePlotPanel() { // actual plotting is shown here + if (moleculePlotPanel == null) { + try { + moleculePlotPanel = new MoleculePlotPanel(); + moleculePlotPanel.setName("ClusterPlotPanel"); + moleculePlotPanel.setCoordinateCallback(coords -> { + if (coords == null) { + clearCrosshairCoordinates(); + } else { + updateCrosshairCoordinates(coords[0], coords[1]); + } + }); + moleculePlotPanel.addComponentListener(new ComponentAdapter() { + @Override + public void componentShown(ComponentEvent e) { + System.out.println(this.getClass().getSimpleName() + ".componentShown() called, height = " + moleculePlotPanel.getHeight()); + } + }); + } catch (java.lang.Throwable ivjExc) { + handleException(ivjExc); + } + } + return moleculePlotPanel; + } + public MoleculeDataPanel getMoleculeDataPanel() { // actual table shown here + if (moleculeDataPanel == null) { + try { + moleculeDataPanel = new MoleculeDataPanel(); + } catch (java.lang.Throwable ivjExc) { + handleException(ivjExc); + } + } + return moleculeDataPanel; } - private void initConnections() { - // listeners + // --------------------------------------------------------------- + + protected void initConnections() { + initializeGlobalPalette(); // get a stable, high contrast palette + // group the two buttons so only one stays selected + ButtonGroup bg = new ButtonGroup(); + bg.add(getPlotButton()); + bg.add(getDataButton()); + + // add the shared handler + getPlotButton().addActionListener(ivjEventHandler); + getDataButton().addActionListener(ivjEventHandler); + + // crosshair checkbox is plot-specific, so subclass handles it + getCrosshairCheckBox().addActionListener(ivjEventHandler); + + // listen to the left panel + owner.getMoleculeSpecificationPanel().addPropertyChangeListener(ivjEventHandler); } @@ -81,12 +184,74 @@ private void initConnections() { private void redrawPlot(MoleculeSpecificationPanel.MoleculeSelection sel) throws ExpressionException { - +// ClusterPlotPanel plot = getMoleculePlotPanel(); +// plot.clearAllRenderers(); +// +// if (langevinSolverResultSet == null || !langevinSolverResultSet.isAverageDataAvailable()) { +// return; +// } +// +// ODESimData avgData = langevinSolverResultSet.getAvg(); +// ODESimData minData = langevinSolverResultSet.getMin(); +// ODESimData maxData = langevinSolverResultSet.getMax(); +// ODESimData stdData = langevinSolverResultSet.getStd(); +// +// int indexTime = avgData.findColumn("t"); +// double[] time = avgData.extractColumn(indexTime); +// +// for (ColumnDescription cd : sel.selectedColumns) { +// String columnName = cd.getName(); +// Color baseColor = persistentColorMap.get(columnName); +// Color mMColor = deriveMinMaxColor(baseColor); +// Color sDColor = deriveSDColor(baseColor); +// +// MoleculeSpecificationPanel.StatisticSelection ss; +// // --- AVG --- +// ss = MoleculeSpecificationPanel.StatisticSelection.AVG; +// if (sel.selectedStatistics.contains(ss)) { +// double[] avg = LangevinSolverResultSet.getSeries(avgData, columnName); +// if (avg != null) { +// plot.addAvgRenderer(time, avg, baseColor, columnName, ss); +// } +// } +// +// // --- MIN/MAX --- +// ss = MoleculeSpecificationPanel.StatisticSelection.MIN_MAX; +// if (sel.selectedStatistics.contains(ss)) { +// double[] min = LangevinSolverResultSet.getSeries(minData, columnName); +// double[] max = LangevinSolverResultSet.getSeries(maxData, columnName); +// if (min != null && max != null) { +// plot.addMinMaxRenderer(time, min, max, mMColor, columnName, ss); +// } +// } +// +// // --- SD --- +// ss = MoleculeSpecificationPanel.StatisticSelection.SD; +// if (sel.selectedStatistics.contains(ss)) { +// double[] avg = LangevinSolverResultSet.getSeries(avgData, columnName); +// double[] sd = LangevinSolverResultSet.getSeries(stdData, columnName); +// +// if (avg != null && sd != null) { +// double[] low = new double[avg.length]; +// double[] high = new double[avg.length]; +// for (int i = 0; i < avg.length; i++) { +// low[i] = avg[i] - sd[i]; +// high[i] = avg[i] + sd[i]; +// } +// plot.addSDRenderer(time, low, high, sDColor, columnName, ss); +// } +// } +// } +// plot.repaint(); } + private void redrawLegend(MoleculeSpecificationPanel.MoleculeSelection sel) throws ExpressionException { } + private void redrawDataTable(MoleculeSpecificationPanel.MoleculeSelection sel) throws ExpressionException { + System.out.println(this.getClass().getSimpleName() + ".updateDataTable() called"); + getMoleculeDataPanel().updateData(sel, langevinSolverResultSet); } @@ -105,6 +270,46 @@ public void refreshData() { langevinSolverResultSet = owner.getLangevinSolverResultSet(); } + + // ------------------------------------------------------------------------------- + + private void initializeGlobalPalette() { + // Use a curated palette from ColorUtil + globalPalette.clear(); + globalPalette.addAll(Arrays.asList(ColorUtil.TABLEAU20)); + + // Reserve ACS and ACO immediately + ensureColorsAssigned("ACS"); + ensureColorsAssigned("ACO"); + + // SD derives from ACS (does NOT consume a palette slot) + Color acsColor = persistentColorMap.get("ACS"); + Color sdColor = deriveEnvelopeColor(acsColor); + persistentColorMap.put("SD", sdColor); + } + private void ensureColorsAssigned(List columns) { + // assign colors only when needed, and keep them consistent across updates + for (ColumnDescription cd : columns) { + String name = cd.getName(); + ensureColorsAssigned(name); + } + } + private void ensureColorsAssigned(String name) { + if (!persistentColorMap.containsKey(name)) { + Color c = globalPalette.get(nextColorIndex % globalPalette.size()); + persistentColorMap.put(name, c); + nextColorIndex++; + } + } + private Color deriveEnvelopeColor(Color base) { + return new Color( + base.getRed(), + base.getGreen(), + base.getBlue(), + 80 // smaller number means lighter color (more transparent) + ); + } + public void setSpecialityRenderer(SpecialtyTableRenderer str) { // getClusterDataPanel().setSpecialityRenderer(str); } diff --git a/vcell-core/src/main/java/cbit/vcell/simdata/LangevinSolverResultSet.java b/vcell-core/src/main/java/cbit/vcell/simdata/LangevinSolverResultSet.java index e70b9418c0..d55ee3e0ef 100644 --- a/vcell-core/src/main/java/cbit/vcell/simdata/LangevinSolverResultSet.java +++ b/vcell-core/src/main/java/cbit/vcell/simdata/LangevinSolverResultSet.java @@ -154,4 +154,10 @@ private static void checkTrivial(ODESimData co) { } } + public static double[] getSeries(ODESimData data, String columnName) throws ExpressionException { + int idx = data.findColumn(columnName); + if (idx < 0) return null; + return data.extractColumn(idx); + } + } From 0723371a7b1b81ecfcf74b4e7a9525f8950b63ac Mon Sep 17 00:00:00 2001 From: Dan Vasilescu Date: Fri, 10 Apr 2026 18:20:06 -0400 Subject: [PATCH 27/31] batch simulation results visualization, alpha candidate --- .../java/cbit/plot/gui/AbstractDataPanel.java | 3 +- .../java/cbit/plot/gui/AbstractPlotPanel.java | 262 ++++++++++++----- .../java/cbit/plot/gui/MoleculeDataPanel.java | 131 ++++++++- .../cbit/vcell/client/data/ODEDataViewer.java | 6 +- .../ode/gui/AbstractVisualizationPanel.java | 2 +- .../ode/gui/ClusterSpecificationPanel.java | 33 ++- .../ode/gui/ClusterVisualizationPanel.java | 27 +- .../ode/gui/MoleculeSpecificationPanel.java | 39 ++- .../ode/gui/MoleculeVisualizationPanel.java | 274 +++++++++++++----- 9 files changed, 580 insertions(+), 197 deletions(-) diff --git a/vcell-client/src/main/java/cbit/plot/gui/AbstractDataPanel.java b/vcell-client/src/main/java/cbit/plot/gui/AbstractDataPanel.java index 0a413eb15d..76b31c5605 100644 --- a/vcell-client/src/main/java/cbit/plot/gui/AbstractDataPanel.java +++ b/vcell-client/src/main/java/cbit/plot/gui/AbstractDataPanel.java @@ -45,7 +45,6 @@ public void mouseClicked(MouseEvent e) { if (SwingUtilities.isRightMouseButton(e)) { getPopupMenu().show(e.getComponent(), e.getX(), e.getY()); } - onMouseClick(row, col, e); } } @@ -119,7 +118,7 @@ private void initialize() { } protected String getFooterLabelText() { - return "To Copy table data or Export as HDF5, select rows/cells and use the right mouse button menu."; + return "To Copy table data to the clipboard use the right mouse button menu."; } protected void initConnections() throws Exception { diff --git a/vcell-client/src/main/java/cbit/plot/gui/AbstractPlotPanel.java b/vcell-client/src/main/java/cbit/plot/gui/AbstractPlotPanel.java index 7c0597de2f..969f079473 100644 --- a/vcell-client/src/main/java/cbit/plot/gui/AbstractPlotPanel.java +++ b/vcell-client/src/main/java/cbit/plot/gui/AbstractPlotPanel.java @@ -34,6 +34,13 @@ void draw(Graphics2D g2, int plotWidth, int plotHeight, double xMaxRounded, double yMaxRounded, double yMinRounded, double dt); + /** + * Returns the closest pixel-space point to (mouseX, mouseY), + * or null if this renderer does not support snapping. + */ + default Point getClosestPoint(int mouseX, int mouseY) { + return null; // default: no snapping + } } // AVG renderer: polyline @@ -41,13 +48,17 @@ protected static class AvgRenderer implements SeriesRenderer { final double[] time; final double[] values; final Color color; + final AbstractPlotPanel parent; + + private int[] xs; + private int[] ys; - AvgRenderer(double[] time, double[] values, Color color) { + AvgRenderer(double[] time, double[] values, Color color, AbstractPlotPanel parent) { this.time = time; this.values = values; this.color = color; + this.parent = parent; } - @Override public void draw(Graphics2D g2, int x0, int x1, int y0, int y1, @@ -58,8 +69,8 @@ public void draw(Graphics2D g2, int n = values.length; if (n < 2) return; - int[] xs = new int[n]; - int[] ys = new int[n]; + xs = new int[n]; + ys = new int[n]; for (int i = 0; i < n; i++) { double t = (time != null ? time[i] : i * dt); @@ -72,6 +83,42 @@ public void draw(Graphics2D g2, g2.setColor(color); g2.setStroke(new BasicStroke(CURVE_STROKE, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND)); g2.drawPolyline(xs, ys, n); + + // Draw the polyline + g2.setColor(color); + g2.setStroke(new BasicStroke(CURVE_STROKE, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND)); + g2.drawPolyline(xs, ys, n); + + // Draw nodes if enabled + if (parent.getShowNodes()) { // parent is the AbstractPlotPanel + g2.setColor(color); + int diameter = 4; // small, unobtrusive + int radius = diameter / 2; + + for (int i = 0; i < n; i++) { + int cx = xs[i] - radius; + int cy = ys[i] - radius; + g2.fillOval(cx, cy, diameter, diameter); + } + } + } + @Override + public Point getClosestPoint(int mouseX, int mouseY) { + if (xs == null || ys == null) return null; + int bestIndex = -1; + double bestDist2 = Double.POSITIVE_INFINITY; + for (int i = 0; i < xs.length; i++) { + double dx = xs[i] - mouseX; + double dy = ys[i] - mouseY; + double d2 = dx*dx + dy*dy; + + if (d2 < bestDist2) { + bestDist2 = d2; + bestIndex = i; + } + } + if (bestIndex < 0) return null; + return new Point(xs[bestIndex], ys[bestIndex]); } } @@ -81,12 +128,14 @@ protected static class BandRenderer implements SeriesRenderer { final double[] upper; final double[] lower; final Color fillColor; + final AbstractPlotPanel parent; - BandRenderer(double[] time, double[] upper, double[] lower, Color fillColor) { + BandRenderer(double[] time, double[] upper, double[] lower, Color fillColor, AbstractPlotPanel parent) { this.time = time; this.upper = upper; this.lower = lower; this.fillColor = fillColor; + this.parent = parent; } @Override @@ -132,31 +181,36 @@ public void draw(Graphics2D g2, } } + // Renderer options list + private boolean fieldShowNodes = true; // whether to draw small circles at the data points (nodes) + private boolean fieldSnapToNodes = true; // whether the crosshair snaps to the nearest node (if false, it shows exact mouse coordinates) + private boolean fieldShowCrosshair = true; // whether to show the crosshair at all (if false, mouse coordinates are still tracked and sent to the callback, but no crosshair is drawn) + // Renderers list protected final List renderers = new ArrayList<>(); // Scaling state - protected double globalMin = 0; + protected double globalMin = 0; // on the-y axis; x-axis is always 0 to dt*(N-1) protected double globalMax = 1; protected double dt = 1; // Crosshair state - protected Integer mouseX = null; + protected Integer mouseX = null; // mouse coordinates in pixels, relative to the panel; null if mouse is outside the plot area protected Integer mouseY = null; protected boolean crosshairEnabled = true; protected Consumer coordCallback; // Cached plot area - protected int lastX0, lastX1, lastY0, lastY1; - protected double lastXMaxRounded; - protected double lastYMaxRounded; - protected double lastYMinRounded; + protected int lastX0, lastX1, lastY0, lastY1; // pixel coordinates of the plot area (insets from the panel edges) + protected double lastXMaxRounded; // in seconds + protected double lastYMaxRounded; // in molecules + protected double lastYMinRounded; // in molecules (could be negative if avg-sd<0 public AbstractPlotPanel() { addMouseMotionListener(new MouseMotionAdapter() { @Override public void mouseMoved(MouseEvent e) { - int mx = e.getX(); + int mx = e.getX(); // in pixels, relative to the panel int my = e.getY(); if (mx >= lastX0 && mx <= lastX1 && my >= lastY1 && my <= lastY0) { @@ -168,11 +222,21 @@ public void mouseMoved(MouseEvent e) { } if (crosshairEnabled && mouseX != null && mouseY != null) { - double fracX = (mouseX - lastX0) / (double)(lastX1 - lastX0); - double xVal = fracX * lastXMaxRounded; - - double fracY = (lastY0 - mouseY) / (double)(lastY0 - lastY1); - double yVal = lastYMinRounded + fracY * (lastYMaxRounded - lastYMinRounded); + mx = mouseX; + my = mouseY; + if (fieldSnapToNodes) { + Point snapped = findClosestNode(mx, my); + if (snapped != null) { + mx = snapped.x; // mx and my are now snapped to the nearest node's pixel coordinates + my = snapped.y; + mouseX = mx; // update mouseX and mouseY to the snapped coordinates for crosshair drawing + mouseY = my; + } + } + double fracX = (mx - lastX0) / (double)(lastX1 - lastX0); + double xVal = fracX * lastXMaxRounded; // mouse coord on x-axis in seconds + double fracY = (lastY0 - my) / (double)(lastY0 - lastY1); + double yVal = lastYMinRounded + fracY * (lastYMaxRounded - lastYMinRounded); // mouse coord on y-axis in molecules if (coordCallback != null) { coordCallback.accept(new double[]{xVal, yVal}); @@ -182,7 +246,6 @@ public void mouseMoved(MouseEvent e) { coordCallback.accept(null); } } - repaint(); } }); @@ -208,7 +271,7 @@ public void setCoordinateCallback(Consumer cb) { this.coordCallback = cb; } - public void clear() { + public void clearAllRenderers() { renderers.clear(); } @@ -224,15 +287,15 @@ public void setDt(double dt) { // High-level, stat-aware renderers public void addAvgRenderer(double[] time, double[] avg, Color color, String name, Object statTag) { - renderers.add(new AvgRenderer(time, avg, color)); + renderers.add(new AvgRenderer(time, avg, color, this)); } public void addMinMaxRenderer(double[] time, double[] min, double[] max, Color color, String name, Object statTag) { - renderers.add(new BandRenderer(time, max, min, color)); + renderers.add(new BandRenderer(time, max, min, color, this)); } public void addSDRenderer(double[] time, double[] low, double[] high, Color color, String name, Object statTag) { - renderers.add(new BandRenderer(time, high, low, color)); + renderers.add(new BandRenderer(time, high, low, color, this)); } // Utilities @@ -241,12 +304,11 @@ protected double roundUpNice(double value) { if (value <= 0) return 1; double exp = Math.pow(10, Math.floor(Math.log10(value))); double n = value / exp; - double rounded; - if (n <= 1) rounded = 1; - else if (n <= 2) rounded = 2; - else if (n <= 5) rounded = 5; - else rounded = 10; - return rounded * exp; + double[] steps = {1,2,3,4,5,6,7,8,9,10}; // 1–10 sequence + for (double s : steps) { + if (n <= s) return s * exp; + } + return 10 * exp; } public static String formatTick(double value, double step) { @@ -268,6 +330,36 @@ public static String formatTick(double value, double step) { return s; } + public boolean getShowNodes() { + return fieldShowNodes; + } + public boolean getSnapToNodes() { + return fieldSnapToNodes; + } + private Point findClosestNode(int mouseX, int mouseY) { // use for SnapToNodes feature + Point best = null; + double bestDist2 = Double.POSITIVE_INFINITY; + for (SeriesRenderer r : renderers) { + Point p = r.getClosestPoint(mouseX, mouseY); + if (p != null) { + double dx = p.x - mouseX; + double dy = p.y - mouseY; + double d2 = dx*dx + dy*dy; + if (d2 < bestDist2) { + bestDist2 = d2; + best = p; + } + } + } + // Snap only if within a threshold (e.g., 10px) + if (best != null && bestDist2 <= 400) { // 10px radius + return best; + } + return null; + } + + // ------------------------------------------------------------------- + @Override protected void paintComponent(Graphics g) { super.paintComponent(g); @@ -275,9 +367,8 @@ protected void paintComponent(Graphics g) { Graphics2D g2 = (Graphics2D) g; g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); - int w = getWidth(); - int h = getHeight(); - + int w = getWidth(); // width of the component (in pixels) + int h = getHeight(); // height of the component g2.setColor(Color.white); g2.fillRect(0, 0, w, h); @@ -285,8 +376,7 @@ protected void paintComponent(Graphics g) { int x1 = w - RIGHT_INSET; int y0 = h - BOTTOM_INSET; int y1 = TOP_INSET; - - lastX0 = x0; + lastX0 = x0; // in pixels lastX1 = x1; lastY0 = y0; lastY1 = y1; @@ -295,8 +385,8 @@ protected void paintComponent(Graphics g) { int plotHeight = y0 - y1; if (plotWidth <= 0 || plotHeight <= 0) return; - // Determine max length from all renderers that use arrays - int maxLength = 0; + // --- determine max length from all renderers that use arrays ----- + int maxLength = 0; // number of timepoints for (SeriesRenderer r : renderers) { if (r instanceof AvgRenderer ar) { maxLength = Math.max(maxLength, ar.values.length); @@ -306,40 +396,46 @@ protected void paintComponent(Graphics g) { } if (maxLength < 2) return; - double yMaxRounded = roundUpNice(globalMax); - double yMinRounded = (globalMin < 0) ? -roundUpNice(-globalMin) : 0; - double xMax = dt * (maxLength - 1); + // --- compute axis scaling ----------------------------------------- + double yMaxRounded = roundUpNice(globalMax); // in molecules + double yMinRounded = (globalMin < 0) ? -roundUpNice(-globalMin) : 0; // may be negative if avg-sd<0 + double xMax = dt * (maxLength - 1); // in seconds double xMaxRounded = roundUpNice(xMax); + lastXMaxRounded = xMaxRounded; // seconds + lastYMaxRounded = yMaxRounded; // molecules + lastYMinRounded = yMinRounded; // molecules - lastXMaxRounded = xMaxRounded; - lastYMaxRounded = yMaxRounded; - lastYMinRounded = yMinRounded; + // --- compute pixel location of value zero on the y-axis, to draw the horizontal axis there + double normZero = (0 - yMinRounded) / (yMaxRounded - yMinRounded); + int yZeroPix = y0 - (int)Math.round(normZero * plotHeight); FontMetrics fm = g2.getFontMetrics(); - // Gridlines + // --- grid lines ---------------------------------------------- g2.setColor(new Color(220, 220, 220)); g2.setStroke(new BasicStroke(1f)); - int yTicks = 5; - double yRange = yMaxRounded - yMinRounded; - double yStep = yRange / yTicks; - - for (int i = 0; i <= yTicks; i++) { - double valueMajor = yMinRounded + i * yStep; - double norm = (valueMajor - yMinRounded) / (yMaxRounded - yMinRounded); - int yPixMajor = y0 - (int)Math.round(norm * plotHeight); - g2.drawLine(x0, yPixMajor, x1, yPixMajor); - - if (i < yTicks) { - double valueMid = valueMajor + yStep / 2.0; + int yTicks = 5; // number of major horizontal ticks (above 0) + double yStep = yMaxRounded / yTicks; + // k runs over all integer multiples of yStep that fall inside the range + int kMin = (int)Math.floor(yMinRounded / yStep); + int kMax = (int)Math.ceil(yMaxRounded / yStep); + for (int k = kMin; k <= kMax; k++) { + double valueMajor = k * yStep; // ----- major gridline ----- + if (valueMajor >= yMinRounded - 1e-9 && valueMajor <= yMaxRounded + 1e-9) { + double normMajor = (valueMajor - yMinRounded) / (yMaxRounded - yMinRounded); + int yPixMajor = y0 - (int)Math.round(normMajor * plotHeight); + g2.drawLine(x0, yPixMajor, x1, yPixMajor); + } + double valueMid = valueMajor + yStep / 2.0; // ----- mid gridline ----- + if (valueMid >= yMinRounded && valueMid <= yMaxRounded) { double normMid = (valueMid - yMinRounded) / (yMaxRounded - yMinRounded); int yPixMid = y0 - (int)Math.round(normMid * plotHeight); g2.drawLine(x0, yPixMid, x1, yPixMid); } } - double[] xMajor = {0, xMaxRounded / 2, xMaxRounded}; + double[] xMajor = {0, xMaxRounded / 2, xMaxRounded}; // vertical grid lines for (int i = 0; i < xMajor.length; i++) { double xvMajor = xMajor[i]; int xPixMajor = x0 + (int)Math.round((xvMajor / xMaxRounded) * plotWidth); @@ -352,47 +448,57 @@ protected void paintComponent(Graphics g) { } } - // Axes + ticks + // --- draw axes ------------------------------------------------ g2.setColor(Color.black); g2.setStroke(new BasicStroke(AXIS_STROKE)); - g2.drawLine(x0, y0, x1, y0); - g2.drawLine(x0, y0, x0, y1); - - for (int i = 0; i <= yTicks; i++) { - double valueMajor = i * yStep; - double norm = valueMajor / yMaxRounded; - int yPixMajor = y0 - (int)Math.round(norm * plotHeight); + g2.drawLine(x0, yZeroPix, x1, yZeroPix); // horizontal axis, going through the "0 molecules" point + g2.drawLine(x0, y0, x0, y1); // vertical axis - g2.drawLine(x0 - 5, yPixMajor, x0, yPixMajor); + // --- ticks --------------------------------------------------- + g2.setColor(Color.black); + g2.setStroke(new BasicStroke(AXIS_STROKE)); - String label = formatTick(valueMajor, yStep); + // yStep was computed as: yStep = yMaxRounded / yTicks; + // and gridlines were drawn at k * yStep for k in [floor(min/step), ceil(max/step)] + kMin = (int)Math.floor(yMinRounded / yStep); // y-axis ticks (on the vertical axis) + kMax = (int)Math.ceil(yMaxRounded / yStep); + for (int k = kMin; k <= kMax; k++) { + double value = k * yStep; + if (value < yMinRounded - 1e-9 || value > yMaxRounded + 1e-9) { + continue; // skip values outside the rounded range (floating‑point guard) + } + // convert to pixel using the SAME normalization as gridlines and renderer + double norm = (value - yMinRounded) / (yMaxRounded - yMinRounded); + int yPix = y0 - (int)Math.round(norm * plotHeight); + g2.drawLine(x0 - 5, yPix, x0, yPix); // major tick (little horizontal line on the vertical axis) + String label = formatTick(value, yStep); // label int sw = fm.stringWidth(label); - g2.drawString(label, x0 - 10 - sw, yPixMajor + fm.getAscent() / 2); - - if (i < yTicks) { - double valueMid = (i + 0.5) * yStep; - double normMid = valueMid / yMaxRounded; - int yPixMid = y0 - (int)Math.round(normMid * plotHeight); - g2.drawLine(x0 - 3, yPixMid, x0, yPixMid); + g2.drawString(label, x0 - 10 - sw, yPix + fm.getAscent() / 2); + + if (k < kMax) { // mid tick (between this and next) + double midValue = value + yStep / 2.0; + if (midValue >= yMinRounded && midValue <= yMaxRounded) { + double normMid = (midValue - yMinRounded) / (yMaxRounded - yMinRounded); + int yPixMid = y0 - (int)Math.round(normMid * plotHeight); + g2.drawLine(x0 - 3, yPixMid, x0, yPixMid); + } } } - - double xStep = xMajor[1] - xMajor[0]; + double xStep = xMajor[1] - xMajor[0]; // x-axis ticks (on the horizontal axis) for (int i = 0; i < xMajor.length; i++) { double xvMajor = xMajor[i]; int xPixMajor = x0 + (int)Math.round((xvMajor / xMaxRounded) * plotWidth); + g2.drawLine(xPixMajor, yZeroPix, xPixMajor, yZeroPix + 5); // draw major tick on the x-axis (yZeroPix), not at y0 - g2.drawLine(xPixMajor, y0, xPixMajor, y0 + 5); - - String label = formatTick(xvMajor, xStep); + String label = formatTick(xvMajor, xStep); // label stays at the bottom int sw = fm.stringWidth(label); g2.drawString(label, xPixMajor - sw / 2, y0 + fm.getAscent() + 5); if (i < xMajor.length - 1) { double xvMid = (xMajor[i] + xMajor[i + 1]) / 2.0; int xPixMid = x0 + (int)Math.round((xvMid / xMaxRounded) * plotWidth); - g2.drawLine(xPixMid, y0, xPixMid, y0 + 3); + g2.drawLine(xPixMid, yZeroPix, xPixMid, yZeroPix + 3); // mid tick also on the x‑axis } } diff --git a/vcell-client/src/main/java/cbit/plot/gui/MoleculeDataPanel.java b/vcell-client/src/main/java/cbit/plot/gui/MoleculeDataPanel.java index 94bfe868c5..c4e552a709 100644 --- a/vcell-client/src/main/java/cbit/plot/gui/MoleculeDataPanel.java +++ b/vcell-client/src/main/java/cbit/plot/gui/MoleculeDataPanel.java @@ -1,8 +1,15 @@ package cbit.plot.gui; +import cbit.vcell.client.data.ODEDataViewer; +import cbit.vcell.desktop.VCellTransferable; import cbit.vcell.math.ReservedVariable; +import cbit.vcell.parser.Expression; import cbit.vcell.parser.ExpressionException; +import cbit.vcell.parser.SymbolTableEntry; import cbit.vcell.simdata.LangevinSolverResultSet; +import cbit.vcell.simdata.UiTableExporterToHDF5; +import cbit.vcell.solver.OutputTimeSpec; +import cbit.vcell.solver.UniformOutputTimeSpec; import cbit.vcell.solver.ode.ODESolverResultSet; import cbit.vcell.solver.ode.gui.ClusterSpecificationPanel; import cbit.vcell.solver.ode.gui.MoleculeSpecificationPanel; @@ -85,8 +92,11 @@ public Component getTableCellRendererComponent(JTable table, Object value, boole // ------------------------------------------------------ - public MoleculeDataPanel() { + ODEDataViewer owner; + + public MoleculeDataPanel(ODEDataViewer owner) { super(); + this.owner = owner; } @Override protected void initConnections() throws Exception { @@ -127,6 +137,12 @@ public void updateData(MoleculeSpecificationPanel.MoleculeSelection sel, Langevi int timeIndex = avgRS.findColumn(ReservedVariable.TIME.getName()); double[] times = avgRS.extractColumn(timeIndex); int rowCount = times.length; + OutputTimeSpec outputTimeSpec = owner.getSimulation().getSolverTaskDescription().getOutputTimeSpec(); + double dt = ((UniformOutputTimeSpec)outputTimeSpec).getOutputTimeStep(); // uniform output time step + double endingTime = owner.getSimulation().getSolverTaskDescription().getTimeBounds().getEndingTime(); + for (int i = 0; i < times.length; i++) { + times[i] = i * dt; + } @@ -236,4 +252,117 @@ private void autoSizeTableColumns(JTable table) { column.setPreferredWidth(maxWidth + margin); } } + + // ------------------------------------------------------ + + // ------------------------------------------------------------------------- + // Override copy handler + // ------------------------------------------------------------------------- + @Override + protected void onCopyCells(boolean isHDF5) { + copyCells(isHDF5); + } + + // ------------------------------------------------------------------------- + // Instance version of old static copyCells() + // ------------------------------------------------------------------------- + private void copyCells(boolean isHDF5) { + try { + int r = getScrollPaneTable().getRowCount(); + int c = getScrollPaneTable().getColumnCount(); + + if (r < 1 || c < 1) { + throw new Exception("No table cell is selected."); + } + + int[] rows = new int[r]; + int[] columns = new int[c]; + for (int i = 0; i < r; i++) rows[i] = i; + for (int i = 0; i < c; i++) columns[i] = i; + + LG.debug("Copying cluster data: rows=" + r + " columns=" + c + " isHDF5=" + isHDF5); + + boolean bHistogram = false; + String blankCellValue = "-1"; + boolean bHasTimeColumn = true; + + StringBuffer buffer = new StringBuffer(); + + if (isHDF5) { + int columnCount = c; + int rowCount = r; + + String[] columnNames = new String[columnCount]; + for (int i = 0; i < columnCount; i++) { + columnNames[i] = getScrollPaneTable().getColumnName(i); + } + + Object[][] rowColValues = new Object[rowCount][columnCount]; + for (int i = 0; i < rowCount; i++) { + for (int j = 0; j < columnCount; j++) { + rowColValues[i][j] = getScrollPaneTable().getValueAt(i, j); + } + } + + UiTableExporterToHDF5.exportTableToHDF5( + bHistogram, + blankCellValue, + columns, + rows, + "t", + "hdf5DescriptionText", + columnNames, + null, + null, + rowColValues + ); + } + + // Column headers + for (int i = 0; i < c; i++) { + buffer.append(getScrollPaneTable().getColumnName(i)); + if (i < c - 1) buffer.append("\t"); + } + + Expression[] resolvedValues = + new Expression[c - (bHasTimeColumn ? 1 : 0)]; + + // Rows + for (int i = 0; i < r; i++) { + buffer.append("\n"); + for (int j = 0; j < c; j++) { + Object cell = getScrollPaneTable().getValueAt(i, j); + cell = (cell != null ? cell : ""); + + buffer.append(cell.toString()); + if (j < c - 1) buffer.append("\t"); + + if (!cell.equals("") && (!bHasTimeColumn || j > 0)) { + resolvedValues[j - (bHasTimeColumn ? 1 : 0)] = + new Expression(((Double) cell).doubleValue()); + } + } + } + + VCellTransferable.ResolvedValuesSelection rvs = + new VCellTransferable.ResolvedValuesSelection( + new SymbolTableEntry[c - 1], + null, + resolvedValues, + buffer.toString() + ); + + VCellTransferable.sendToClipboard(rvs); + + } catch (Exception ex) { + LG.error("Error copying cluster data", ex); + JOptionPane.showMessageDialog( + this, + "Error copying cluster data: " + ex.getMessage(), + "Copy Error", + JOptionPane.ERROR_MESSAGE + ); + } + } + } diff --git a/vcell-client/src/main/java/cbit/vcell/client/data/ODEDataViewer.java b/vcell-client/src/main/java/cbit/vcell/client/data/ODEDataViewer.java index 5c15292c2a..f14e341042 100644 --- a/vcell-client/src/main/java/cbit/vcell/client/data/ODEDataViewer.java +++ b/vcell-client/src/main/java/cbit/vcell/client/data/ODEDataViewer.java @@ -535,10 +535,12 @@ public void setVcDataIdentifier(VCDataIdentifier vcDataIdentifier) { setOdeDataContext(); firePropertyChange("vcDataIdentifier", oldValue, vcDataIdentifier); if(hasLangevinBatchResults) { - getClusterSpecificationPanel().refreshData(); getClusterVisualizationPanel().refreshData(); - getMoleculeSpecificationPanel().refreshData(); getMoleculeVisualizationPanel().refreshData(); + // order is important, the visualization panels must refresh first so that necessary data will be present when + // the specification panels start sending events to the visualization panels + getClusterSpecificationPanel().refreshData(); + getMoleculeSpecificationPanel().refreshData(); } else { outputSpeciesResultsPanel.refreshData(); } diff --git a/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/AbstractVisualizationPanel.java b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/AbstractVisualizationPanel.java index d7692f2533..1e1a112411 100644 --- a/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/AbstractVisualizationPanel.java +++ b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/AbstractVisualizationPanel.java @@ -22,7 +22,7 @@ public LineIcon(Color color) { @Override public void paintIcon(Component c, Graphics g, int x, int y) { Graphics2D g2 = (Graphics2D)g; - g2.setStroke(new BasicStroke(3.0f)); + g2.setStroke(new BasicStroke(4.0f)); g2.setPaint(color); int midY = y + getIconHeight() / 2; g2.drawLine(x, midY, x + getIconWidth(), midY); diff --git a/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterSpecificationPanel.java b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterSpecificationPanel.java index d7dd69a186..e7aa99a3a7 100644 --- a/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterSpecificationPanel.java +++ b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterSpecificationPanel.java @@ -177,7 +177,10 @@ public void propertyChange(PropertyChangeEvent evt) { } @Override public void valueChanged(ListSelectionEvent e) { - if (e.getSource() == ClusterSpecificationPanel.this.getYAxisChoice() && !e.getValueIsAdjusting()) { + if (e.getSource() == ClusterSpecificationPanel.this.getYAxisChoice()) { + if(suppressEvents || e.getValueIsAdjusting()) { + return; // ignore events triggered during initialization + } System.out.println(this.getClass().getName() + ".valueChanged() called. Source is YAxisChoice JList. Selected values: " + getYAxisChoice().getSelectedValuesList()); enforceAcsSdAcoRule(); // extract selected ColumnDescriptions @@ -199,6 +202,8 @@ public void valueChanged(ListSelectionEvent e) { private final Map yAxisCounts = new LinkedHashMap<>(); ClusterSpecificationPanel.IvjEventHandler ivjEventHandler = new ClusterSpecificationPanel.IvjEventHandler(); + private boolean suppressEvents = false; // to prevent event firing during programmatic changes to the UI + public ClusterSpecificationPanel(ODEDataViewer odeDataViewer) { super(); this.owner = odeDataViewer; @@ -222,22 +227,28 @@ private void initConnections() { private void populateYAxisChoices(DisplayMode mode) { DefaultListModel model = getDefaultListModelY(); - model.clear(); - getYAxisChoice().setEnabled(false); - updateYAxisLabel(mode); - ColumnDescription[] cds = getColumnDescriptionsForMode(mode); - if (cds == null || cds.length <= 1) { - return; - } - for (ColumnDescription cd : cds) { - if (!"t".equals(cd.getName())) { - model.addElement(cd); + suppressEvents = true; // prevent firing events while we update the list model + try { + model.clear(); + getYAxisChoice().setEnabled(false); + updateYAxisLabel(mode); + ColumnDescription[] cds = getColumnDescriptionsForMode(mode); + if (cds == null || cds.length <= 1) { + return; } + for (ColumnDescription cd : cds) { + if (!"t".equals(cd.getName())) { + model.addElement(cd); + } + } + } finally { + suppressEvents = false; } if (!model.isEmpty()) { getYAxisChoice().setEnabled(true); getYAxisChoice().setSelectedIndex(0); // triggers valueChanged() } + } private void updateYAxisLabel(DisplayMode mode) { diff --git a/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterVisualizationPanel.java b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterVisualizationPanel.java index 2c72f0a8ae..9c3d5f8565 100644 --- a/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterVisualizationPanel.java +++ b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterVisualizationPanel.java @@ -77,7 +77,6 @@ public void actionPerformed(ActionEvent e) { setCrosshairEnabled(enabled); return; } - } } @Override @@ -212,11 +211,12 @@ public void refreshData() { } else { getBottomLabel().setText(" "); } + // These are not being used here, which is inconsistent with MoleculeVisualizationPanel + // Instead, we receive them indirectly via the ClusterSelection object from the ClusterSpecificationPanel // simulationModelInfo = owner.getSimulationModelInfo(); // langevinSolverResultSet = owner.getLangevinSolverResultSet(); System.out.println(this.getClass().getSimpleName() + ".refreshData() called"); } - // --------------------------------------------------------------------- private void initializeGlobalPalette() { @@ -324,12 +324,9 @@ private void redrawPlot(ClusterSpecificationPanel.ClusterSelection sel) throws E System.out.println(getClass().getSimpleName() + ".redrawPlot() called, current selection: " + sel); ClusterPlotPanel plot = getClusterPlotPanel(); + plot.clearAllRenderers(); - // --------------------------------------------------------------------- - // NULL CASE - // --------------------------------------------------------------------- if (sel == null || sel.resultSet == null) { - plot.clear(); plot.repaint(); return; } @@ -398,38 +395,27 @@ private void redrawPlot(ClusterSpecificationPanel.ClusterSelection sel) throws E } } - // --------------------------------------------------------------------- - // DRAWING - // --------------------------------------------------------------------- - plot.clear(); - - // Draw all selected curves EXCEPT SD (SD is envelope only) + // --- AVG ----------------------------------------------------- for (ColumnDescription cd : columns) { String name = cd.getName(); if (name.equals("SD")) continue; - double[] y = yMap.get(name); if (y == null) continue; - Color c = persistentColorMap.get(name); - // Cluster curves are AVG curves in the new API plot.addAvgRenderer(times, y, c, name, /*statTag*/ "AVG"); } - // Draw SD envelope if SD is selected + // --- SD ------------------------------------------------- if (sdSelected && acs != null && sd != null) { int n = acs.length; double[] upper = new double[n]; double[] lower = new double[n]; - for (int i = 0; i < n; i++) { upper[i] = acs[i] + sd[i]; lower[i] = acs[i] - sd[i]; } - Color sdColor = persistentColorMap.get("SD"); - // SD is a band renderer in the new API plot.addSDRenderer(times, lower, upper, sdColor, "SD", /*statTag*/ "SD"); } @@ -438,13 +424,10 @@ private void redrawPlot(ClusterSpecificationPanel.ClusterSelection sel) throws E // FINALIZE // --------------------------------------------------------------------- if (globalMin > 0) globalMin = 0; - plot.setGlobalMinMax(globalMin, globalMax); - if (times.length > 1) { plot.setDt(times[1]); // times[0] == 0 } - plot.repaint(); } diff --git a/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/MoleculeSpecificationPanel.java b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/MoleculeSpecificationPanel.java index 3c371d63fa..55e0d5c2fa 100644 --- a/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/MoleculeSpecificationPanel.java +++ b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/MoleculeSpecificationPanel.java @@ -170,11 +170,11 @@ public void propertyChange(PropertyChangeEvent evt) { } @Override public void valueChanged(ListSelectionEvent e) { - if (e.getSource() == MoleculeSpecificationPanel.this.getYAxisChoice() && !e.getValueIsAdjusting()) { - System.out.println(this.getClass().getName() + ".IvjEventHandler.valueChanged() called"); - if (e.getValueIsAdjusting()) { + if (e.getSource() == MoleculeSpecificationPanel.this.getYAxisChoice()) { + if (supressEvents || e.getValueIsAdjusting()) { return; } + System.out.println(this.getClass().getName() + ".IvjEventHandler.valueChanged() called"); MoleculeSelection sel = new MoleculeSelection( getYAxisChoice().getSelectedValuesList(), getSelectedStatistics(), @@ -191,6 +191,8 @@ public void valueChanged(ListSelectionEvent e) { private LangevinSolverResultSet langevinSolverResultSet = null; private SimulationModelInfo simulationModelInfo = null; + private boolean supressEvents = true; // flag to suppress events during initialization + public MoleculeSpecificationPanel(ODEDataViewer owner) { super(); this.owner = owner; @@ -270,18 +272,29 @@ private void populateYAxisChoices(java.util.List modes) { DefaultListModel model = getDefaultListModelY(); // Remember what was selected before we touch the model java.util.List previouslySelected = list.getSelectedValuesList(); - model.clear(); - list.setEnabled(false); - for (DisplayMode mode : modes) { - java.util.List cds = getColumnDescriptionsForMode(mode); - if (cds == null || cds.size() <= 1) { - continue; - } - for (ColumnDescription cd : cds) { - if (!"t".equals(cd.getName())) { - model.addElement(cd); + + // addElement() calls will fire ListDataEvents which will trigger list selection events, which will eventually + // result in multiple calls to firePropertyChange("MoleculeSelectionChanged", ...) and multiple redraws of + // the right panels (plot and data table). To avoid firing these events while we're still populating the model, + // we'll set a flag to suppress events temporarily. The event handler will check this flag and skip firing + // property changes if it's set. + supressEvents = true; + try { + model.clear(); + list.setEnabled(false); + for (DisplayMode mode : modes) { + java.util.List cds = getColumnDescriptionsForMode(mode); + if (cds == null || cds.size() <= 1) { + continue; + } + for (ColumnDescription cd : cds) { + if (!"t".equals(cd.getName())) { + model.addElement(cd); + } } } + } finally { + supressEvents = false; // re-enable events after we're done populating the model, even if an exception occurs } updateYAxisLabel(model); if (!model.isEmpty()) { diff --git a/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/MoleculeVisualizationPanel.java b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/MoleculeVisualizationPanel.java index 7ce9ecdbe7..8781b15a60 100644 --- a/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/MoleculeVisualizationPanel.java +++ b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/MoleculeVisualizationPanel.java @@ -7,13 +7,17 @@ import cbit.vcell.client.data.ODEDataViewer; import cbit.vcell.parser.ExpressionException; import cbit.vcell.simdata.LangevinSolverResultSet; +import cbit.vcell.solver.OutputTimeSpec; import cbit.vcell.solver.SimulationModelInfo; +import cbit.vcell.solver.TimeStep; +import cbit.vcell.solver.UniformOutputTimeSpec; import cbit.vcell.solver.ode.ODESimData; import cbit.vcell.util.ColumnDescription; import org.vcell.util.ColorUtil; import org.vcell.util.gui.SpecialtyTableRenderer; import javax.swing.*; +import javax.swing.border.EmptyBorder; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; import java.awt.*; @@ -28,6 +32,9 @@ public class MoleculeVisualizationPanel extends AbstractVisualizationPanel { + private static final int MINMAX_ALPHA = 90; // stronger envelope + private static final int SD_ALPHA = 60; // lighter envelope + private final ODEDataViewer owner; LangevinSolverResultSet langevinSolverResultSet = null; SimulationModelInfo simulationModelInfo = null; @@ -99,7 +106,6 @@ public MoleculeVisualizationPanel(ODEDataViewer owner) { setVisualizationBackground(Color.WHITE); } - @Override protected JPanel createPlotPanel() { return getMoleculePlotPanel(); @@ -148,7 +154,7 @@ public void componentShown(ComponentEvent e) { public MoleculeDataPanel getMoleculeDataPanel() { // actual table shown here if (moleculeDataPanel == null) { try { - moleculeDataPanel = new MoleculeDataPanel(); + moleculeDataPanel = new MoleculeDataPanel(owner); } catch (java.lang.Throwable ivjExc) { handleException(ivjExc); } @@ -174,85 +180,207 @@ protected void initConnections() { // listen to the left panel owner.getMoleculeSpecificationPanel().addPropertyChangeListener(ivjEventHandler); - } + // ---------------------------------------------------------------------- + private void redrawPlot(MoleculeSpecificationPanel.MoleculeSelection sel) throws ExpressionException { + System.out.println(getClass().getSimpleName() + ".redrawPlot() called, current selection: " + sel); + MoleculePlotPanel plot = getMoleculePlotPanel(); + plot.clearAllRenderers(); + if (sel == null || langevinSolverResultSet == null || !langevinSolverResultSet.isAverageDataAvailable()) { + plot.repaint(); + return; + } - // ---------------------------------------------------------------------- + ODESimData avgData = langevinSolverResultSet.getAvg(); + ODESimData minData = langevinSolverResultSet.getMin(); + ODESimData maxData = langevinSolverResultSet.getMax(); + ODESimData stdData = langevinSolverResultSet.getStd(); + + int indexTime = avgData.findColumn("t"); + double[] time = avgData.extractColumn(indexTime); + // time may have suffered systematic numerical precision issues, so we reconstruct it based on the + // uniform output time step and the number of rows in the data + OutputTimeSpec outputTimeSpec = owner.getSimulation().getSolverTaskDescription().getOutputTimeSpec(); + double dt = ((UniformOutputTimeSpec)outputTimeSpec).getOutputTimeStep(); // uniform output time step + double endingTime = owner.getSimulation().getSolverTaskDescription().getTimeBounds().getEndingTime(); + for (int i = 0; i < time.length; i++) { + time[i] = i * dt; + } + // ------------------------------------------------------------ + // FIRST PASS: collect all Y arrays and compute global min/max + // ------------------------------------------------------------ + double globalMin = 0.0; // same baseline as clusters + double globalMax = Double.NEGATIVE_INFINITY; + + // Store arrays so we don’t re-extract later + class Series { + double[] avg; + double[] min; + double[] max; + double[] sd; + } + Map map = new LinkedHashMap<>(); - private void redrawPlot(MoleculeSpecificationPanel.MoleculeSelection sel) throws ExpressionException { -// ClusterPlotPanel plot = getMoleculePlotPanel(); -// plot.clearAllRenderers(); -// -// if (langevinSolverResultSet == null || !langevinSolverResultSet.isAverageDataAvailable()) { -// return; -// } -// -// ODESimData avgData = langevinSolverResultSet.getAvg(); -// ODESimData minData = langevinSolverResultSet.getMin(); -// ODESimData maxData = langevinSolverResultSet.getMax(); -// ODESimData stdData = langevinSolverResultSet.getStd(); -// -// int indexTime = avgData.findColumn("t"); -// double[] time = avgData.extractColumn(indexTime); -// -// for (ColumnDescription cd : sel.selectedColumns) { -// String columnName = cd.getName(); -// Color baseColor = persistentColorMap.get(columnName); -// Color mMColor = deriveMinMaxColor(baseColor); -// Color sDColor = deriveSDColor(baseColor); -// -// MoleculeSpecificationPanel.StatisticSelection ss; -// // --- AVG --- -// ss = MoleculeSpecificationPanel.StatisticSelection.AVG; -// if (sel.selectedStatistics.contains(ss)) { -// double[] avg = LangevinSolverResultSet.getSeries(avgData, columnName); -// if (avg != null) { -// plot.addAvgRenderer(time, avg, baseColor, columnName, ss); -// } -// } -// -// // --- MIN/MAX --- -// ss = MoleculeSpecificationPanel.StatisticSelection.MIN_MAX; -// if (sel.selectedStatistics.contains(ss)) { -// double[] min = LangevinSolverResultSet.getSeries(minData, columnName); -// double[] max = LangevinSolverResultSet.getSeries(maxData, columnName); -// if (min != null && max != null) { -// plot.addMinMaxRenderer(time, min, max, mMColor, columnName, ss); -// } -// } -// -// // --- SD --- -// ss = MoleculeSpecificationPanel.StatisticSelection.SD; -// if (sel.selectedStatistics.contains(ss)) { -// double[] avg = LangevinSolverResultSet.getSeries(avgData, columnName); -// double[] sd = LangevinSolverResultSet.getSeries(stdData, columnName); -// -// if (avg != null && sd != null) { -// double[] low = new double[avg.length]; -// double[] high = new double[avg.length]; -// for (int i = 0; i < avg.length; i++) { -// low[i] = avg[i] - sd[i]; -// high[i] = avg[i] + sd[i]; -// } -// plot.addSDRenderer(time, low, high, sDColor, columnName, ss); -// } -// } -// } -// plot.repaint(); + for (ColumnDescription cd : sel.selectedColumns) { + String name = cd.getName(); + Series s = new Series(); + + s.avg = LangevinSolverResultSet.getSeries(avgData, name); + s.min = LangevinSolverResultSet.getSeries(minData, name); + s.max = LangevinSolverResultSet.getSeries(maxData, name); + s.sd = LangevinSolverResultSet.getSeries(stdData, name); + + map.put(name, s); + + // AVG contributes to globalMax + if (s.avg != null) { + for (double v : s.avg) { + if (v > globalMax) globalMax = v; + } + } + + // MIN/MAX contributes to globalMin/globalMax + if (s.min != null) { + for (double v : s.min) { + if (v < globalMin) globalMin = v; + } + } + if (s.max != null) { + for (double v : s.max) { + if (v > globalMax) globalMax = v; + } + } + + // SD envelope contributes to globalMin/globalMax + if (s.avg != null && s.sd != null) { + for (int i = 0; i < s.avg.length; i++) { + double upper = s.avg[i] + s.sd[i]; + double lower = s.avg[i] - s.sd[i]; + if (upper > globalMax) globalMax = upper; + if (lower < globalMin) globalMin = lower; + } + } + } + + // ------------------------------------------------------------ + // SECOND PASS: add renderers in correct order + // SD → MINMAX → AVG + // ------------------------------------------------------------ + for (ColumnDescription cd : sel.selectedColumns) { + String name = cd.getName(); + Series s = map.get(name); + + Color baseColor = persistentColorMap.get(name); + Color mMColor = deriveMinMaxColor(baseColor); + Color sDColor = deriveSDColor(baseColor); + + // --- SD band --- + if (sel.selectedStatistics.contains(MoleculeSpecificationPanel.StatisticSelection.SD) + && s.avg != null && s.sd != null) { + + int n = s.avg.length; + double[] low = new double[n]; + double[] high = new double[n]; + + for (int i = 0; i < n; i++) { + low[i] = s.avg[i] - s.sd[i]; + high[i] = s.avg[i] + s.sd[i]; + System.out.println(" " + s.avg[i] + " ± " + s.sd[i] + " → [" + low[i] + ", " + high[i] + "]"); + } + plot.addSDRenderer(time, low, high, sDColor, name, + MoleculeSpecificationPanel.StatisticSelection.SD); + } + + // --- MIN/MAX band --- + if (sel.selectedStatistics.contains(MoleculeSpecificationPanel.StatisticSelection.MIN_MAX) + && s.min != null && s.max != null) { + plot.addMinMaxRenderer(time, s.min, s.max, mMColor, name, + MoleculeSpecificationPanel.StatisticSelection.MIN_MAX); + } + + // --- AVG line --- + if (sel.selectedStatistics.contains(MoleculeSpecificationPanel.StatisticSelection.AVG) + && s.avg != null) { + plot.addAvgRenderer(time, s.avg, baseColor, name, + MoleculeSpecificationPanel.StatisticSelection.AVG); + } + } + + // ------------------------------------------------------------ + // FINALIZE + // ------------------------------------------------------------ + if (globalMin > 0) globalMin = 0; + plot.setGlobalMinMax(globalMin, globalMax); + + if (time.length > 1) { + plot.setDt(time[1] - time[0]); // times[0] == 0 + } + + plot.repaint(); } - private void redrawLegend(MoleculeSpecificationPanel.MoleculeSelection sel) throws ExpressionException { + public Color deriveMinMaxColor(Color base) { + return new Color( + base.getRed(), + base.getGreen(), + base.getBlue(), + MINMAX_ALPHA + ); + } + public Color deriveSDColor(Color base) { + return new Color( + base.getRed(), + base.getGreen(), + base.getBlue(), + SD_ALPHA + ); + } + + private void redrawLegend(MoleculeSpecificationPanel.MoleculeSelection sel) throws ExpressionException { + System.out.println(getClass().getSimpleName() + ".redrawLegend() called"); + JPanel legend = getLegendContentPanel(); + legend.removeAll(); + for (ColumnDescription cd : sel.selectedColumns) { + String name = cd.getName(); + Color c = persistentColorMap.get(name); // // assigned color for this molecule + if (c == null) { + // fallback or auto-assign if needed + c = Color.GRAY; + } + legend.add(createLegendEntry(name, c)); + } + legend.revalidate(); + legend.repaint(); + } + private JComponent createLegendEntry(String name, Color color) { + JPanel p = new JPanel(); + p.setLayout(new BoxLayout(p, BoxLayout.Y_AXIS)); + p.setOpaque(false); + + String unitSymbol = "molecules"; + String tooltip = "" + name + "
" + unitSymbol + ""; + JLabel line = new JLabel(new LineIcon(color)); + +// JLabel text = new JLabel("" + name + " [" + unitSymbol + "]"); + JLabel text = new JLabel("" + name + " "); + line.setBorder(new EmptyBorder(6, 0, 1, 0)); + text.setBorder(new EmptyBorder(1, 8, 6, 0)); + line.setToolTipText(tooltip); + text.setToolTipText(tooltip); + p.setToolTipText(tooltip); + p.add(line); + p.add(text); + return p; } private void redrawDataTable(MoleculeSpecificationPanel.MoleculeSelection sel) throws ExpressionException { System.out.println(this.getClass().getSimpleName() + ".updateDataTable() called"); getMoleculeDataPanel().updateData(sel, langevinSolverResultSet); - } private void handleException(java.lang.Throwable exception) { @@ -262,13 +390,25 @@ private void handleException(java.lang.Throwable exception) { @Override protected void onSelectedObjectsChange(Object[] selectedObjects) { System.out.println(this.getClass().getSimpleName() + ".onSelectedObjectsChange() called with " + selectedObjects.length + " objects"); - } + + // called directly from ODEDataViewer when a new value for vcDataIdentifier is set + // TODO: this is a little unreliable since it depends on the order of refreshData() calls in ODEDataViewer + // the right way to do it would be to add the simulationModelInfo and langevinSolverResultSet to MoleculeSelection object, + // the way it's done in ClusterVisualizationPanel, so that they are always in sync with the selections in the left panel public void refreshData() { System.out.println(this.getClass().getSimpleName() + ".refreshData() called"); simulationModelInfo = owner.getSimulationModelInfo(); langevinSolverResultSet = owner.getLangevinSolverResultSet(); + if(owner != null && owner.getSimulation() != null) { + int jobs = owner.getSimulation().getSolverTaskDescription().getLangevinSimulationOptions().getTotalNumberOfJobs(); + String name = owner.getSimulation().getName(); + String str = "" + name + " [" + jobs + " job" + (jobs != 1 ? "s" : "") + "]"; + getBottomLabel().setText(str); + } else { + getBottomLabel().setText(" "); + } } // ------------------------------------------------------------------------------- From a1f6bf03d108ba5a2cf5e280cb9e9923089445a9 Mon Sep 17 00:00:00 2001 From: Dan Vasilescu Date: Mon, 13 Apr 2026 16:26:08 -0400 Subject: [PATCH 28/31] replaces system.out with logger.debug --- .../java/cbit/plot/gui/AbstractDataPanel.java | 5 +- .../java/cbit/plot/gui/AbstractPlotPanel.java | 6 +++ .../java/cbit/plot/gui/ClusterDataPanel.java | 2 +- .../java/cbit/plot/gui/MoleculeDataPanel.java | 3 ++ .../ode/gui/AbstractSpecificationPanel.java | 7 ++- .../ode/gui/AbstractVisualizationPanel.java | 4 ++ .../ode/gui/ClusterSpecificationPanel.java | 16 ++++-- .../ode/gui/ClusterVisualizationPanel.java | 26 ++++----- .../ode/gui/MoleculeSpecificationPanel.java | 24 ++++++--- .../ode/gui/MoleculeVisualizationPanel.java | 54 +++++++++---------- .../simdata/LangevinSolverResultSet.java | 8 ++- 11 files changed, 94 insertions(+), 61 deletions(-) diff --git a/vcell-client/src/main/java/cbit/plot/gui/AbstractDataPanel.java b/vcell-client/src/main/java/cbit/plot/gui/AbstractDataPanel.java index 76b31c5605..99f9235782 100644 --- a/vcell-client/src/main/java/cbit/plot/gui/AbstractDataPanel.java +++ b/vcell-client/src/main/java/cbit/plot/gui/AbstractDataPanel.java @@ -17,7 +17,7 @@ public abstract class AbstractDataPanel extends JPanel { - protected static final Logger LG = LogManager.getLogger(AbstractDataPanel.class); + protected static final Logger lg = LogManager.getLogger(AbstractDataPanel.class); protected ScrollTable scrollPaneTable; protected NonEditableDefaultTableModel nonEditableDefaultTableModel = null; @@ -40,7 +40,7 @@ public void mouseClicked(MouseEvent e) { if (e.getSource() == getScrollPaneTable()) { int row = getScrollPaneTable().rowAtPoint(e.getPoint()); int col = getScrollPaneTable().columnAtPoint(e.getPoint()); - LG.debug(getClass().getSimpleName() + ": clicked row=" + row + " col=" + col); + lg.debug(getClass().getSimpleName() + ": clicked row=" + row + " col=" + col); if (SwingUtilities.isRightMouseButton(e)) { getPopupMenu().show(e.getComponent(), e.getX(), e.getY()); @@ -133,6 +133,7 @@ protected void initConnections() throws Exception { } protected void handleException(Throwable exception) { + lg.error("Uncaught exception in " + getClass().getSimpleName(), exception); System.out.println("--------- UNCAUGHT EXCEPTION ---------"); exception.printStackTrace(System.out); } diff --git a/vcell-client/src/main/java/cbit/plot/gui/AbstractPlotPanel.java b/vcell-client/src/main/java/cbit/plot/gui/AbstractPlotPanel.java index 969f079473..5d94e38535 100644 --- a/vcell-client/src/main/java/cbit/plot/gui/AbstractPlotPanel.java +++ b/vcell-client/src/main/java/cbit/plot/gui/AbstractPlotPanel.java @@ -1,5 +1,9 @@ package cbit.plot.gui; +import cbit.vcell.solver.ode.gui.MoleculeVisualizationPanel; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + import java.awt.*; import java.awt.event.*; import java.awt.geom.*; @@ -18,6 +22,8 @@ public abstract class AbstractPlotPanel extends JPanel { + private static final Logger lg = LogManager.getLogger(AbstractPlotPanel.class); + // Insets and strokes protected static final int LEFT_INSET = 50; protected static final int RIGHT_INSET = 20; diff --git a/vcell-client/src/main/java/cbit/plot/gui/ClusterDataPanel.java b/vcell-client/src/main/java/cbit/plot/gui/ClusterDataPanel.java index 5387f8197d..9f1f8f9dd2 100644 --- a/vcell-client/src/main/java/cbit/plot/gui/ClusterDataPanel.java +++ b/vcell-client/src/main/java/cbit/plot/gui/ClusterDataPanel.java @@ -28,7 +28,7 @@ public class ClusterDataPanel extends AbstractDataPanel { - private static final Logger LG = LogManager.getLogger(ClusterDataPanel.class); + private static final Logger lg = LogManager.getLogger(ClusterDataPanel.class); // ------------------------------------------------------------------------- // Cluster-specific header renderer diff --git a/vcell-client/src/main/java/cbit/plot/gui/MoleculeDataPanel.java b/vcell-client/src/main/java/cbit/plot/gui/MoleculeDataPanel.java index c4e552a709..dd00db5d36 100644 --- a/vcell-client/src/main/java/cbit/plot/gui/MoleculeDataPanel.java +++ b/vcell-client/src/main/java/cbit/plot/gui/MoleculeDataPanel.java @@ -13,6 +13,7 @@ import cbit.vcell.solver.ode.ODESolverResultSet; import cbit.vcell.solver.ode.gui.ClusterSpecificationPanel; import cbit.vcell.solver.ode.gui.MoleculeSpecificationPanel; +import cbit.vcell.solver.ode.gui.MoleculeVisualizationPanel; import cbit.vcell.util.ColumnDescription; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -35,6 +36,8 @@ public class MoleculeDataPanel extends AbstractDataPanel { + private static final Logger lg = LogManager.getLogger(MoleculeDataPanel.class); + public enum SubStatistic { AVG("AVG"), MIN("MIN"), diff --git a/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/AbstractSpecificationPanel.java b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/AbstractSpecificationPanel.java index 808df23110..bb38fecf0a 100644 --- a/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/AbstractSpecificationPanel.java +++ b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/AbstractSpecificationPanel.java @@ -7,6 +7,8 @@ import cbit.vcell.solver.SimulationModelInfo; import cbit.vcell.solver.ode.ODESolverResultSet; import cbit.vcell.util.ColumnDescription; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.vcell.util.gui.CollapsiblePanel; import java.awt.*; @@ -16,6 +18,8 @@ @SuppressWarnings("serial") public abstract class AbstractSpecificationPanel extends DocumentEditorSubPanel { + private static final Logger lg = LogManager.getLogger(AbstractSpecificationPanel.class); + // ------------------------------ // Shared UI components // ------------------------------ @@ -40,7 +44,7 @@ public AbstractSpecificationPanel() { // Initialization (shared layout) // ------------------------------ private void initialize() { - System.out.println(this.getClass().getSimpleName() + ".initialize() called"); + lg.debug("initialize() called"); setPreferredSize(new Dimension(213, 600)); setLayout(new GridBagLayout()); @@ -176,6 +180,7 @@ protected DefaultListModel getDefaultListModelY() { // Shared exception handler // ------------------------------ protected void handleException(Throwable exception) { + lg.error("Uncaught exception in AbstractSpecificationPanel", exception); System.out.println("--------- UNCAUGHT EXCEPTION ---------"); exception.printStackTrace(System.out); } diff --git a/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/AbstractVisualizationPanel.java b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/AbstractVisualizationPanel.java index 1e1a112411..d556f702cb 100644 --- a/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/AbstractVisualizationPanel.java +++ b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/AbstractVisualizationPanel.java @@ -1,6 +1,8 @@ package cbit.vcell.solver.ode.gui; import cbit.vcell.client.desktop.biomodel.DocumentEditorSubPanel; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.vcell.util.gui.JToolBarToggleButton; import org.vcell.util.gui.VCellIcons; @@ -14,6 +16,8 @@ public abstract class AbstractVisualizationPanel extends DocumentEditorSubPanel { + private static final Logger lg = LogManager.getLogger(AbstractVisualizationPanel.class); + protected class LineIcon implements Icon { private final Color color; public LineIcon(Color color) { diff --git a/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterSpecificationPanel.java b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterSpecificationPanel.java index e7aa99a3a7..87c17b801f 100644 --- a/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterSpecificationPanel.java +++ b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterSpecificationPanel.java @@ -7,6 +7,8 @@ import cbit.vcell.solver.SimulationModelInfo; import cbit.vcell.solver.ode.ODESolverResultSet; import cbit.vcell.util.ColumnDescription; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.vcell.util.gui.CollapsiblePanel; import javax.swing.*; @@ -25,6 +27,8 @@ public class ClusterSpecificationPanel extends AbstractSpecificationPanel { + private static final Logger lg = LogManager.getLogger(ClusterSpecificationPanel.class); + public enum DisplayMode { COUNTS( "COUNTS", @@ -164,7 +168,7 @@ class IvjEventHandler implements ActionListener, PropertyChangeListener, ListSel public void actionPerformed(ActionEvent e) { String cmd = e.getActionCommand(); if (e.getSource() instanceof JRadioButton rb && SwingUtilities.isDescendingFrom(rb, ClusterSpecificationPanel.this)) { - System.out.println(this.getClass().getName() + ".actionPerformed() called. Source is JRadioButton: " + rb.getText()); + lg.debug("actionPerformed() called. Source is JRadioButton: {}", rb.getText()); DisplayMode mode = DisplayMode.fromActionCommand(cmd); populateYAxisChoices(mode); } @@ -172,7 +176,8 @@ public void actionPerformed(ActionEvent e) { @Override public void propertyChange(PropertyChangeEvent evt) { if(evt.getSource() == ClusterSpecificationPanel.this) { - System.out.println(this.getClass().getName() + ".IvjEventHandler.propertyChange() called"); + lg.debug("propertyChange() called. Source is ClusterSpecificationPanel. Property name: {}, old value: {}, new value: {}", + evt.getPropertyName(), evt.getOldValue(), evt.getNewValue()); } } @Override @@ -181,7 +186,7 @@ public void valueChanged(ListSelectionEvent e) { if(suppressEvents || e.getValueIsAdjusting()) { return; // ignore events triggered during initialization } - System.out.println(this.getClass().getName() + ".valueChanged() called. Source is YAxisChoice JList. Selected values: " + getYAxisChoice().getSelectedValuesList()); + lg.debug("valueChanged() called. Source is YAxisChoice JList. Selected values: {}", getYAxisChoice().getSelectedValuesList()); enforceAcsSdAcoRule(); // extract selected ColumnDescriptions java.util.List selected = getYAxisChoice().getSelectedValuesList(); @@ -398,7 +403,7 @@ protected CollapsiblePanel getDisplayOptionsPanel() { @Override protected void onSelectedObjectsChange(Object[] selectedObjects) { - System.out.println(this.getClass().getSimpleName() + ".onSelectedObjectsChange() called with " + selectedObjects.length + " objects"); + lg.debug("onSelectedObjectsChange() called. Number of selected objects: {}", selectedObjects.length); } public void refreshData() { @@ -410,7 +415,8 @@ public void refreshData() { yAxisCounts.put(DisplayMode.MEAN, countColumns(langevinSolverResultSet.getClusterMean())); yAxisCounts.put(DisplayMode.OVERALL, countColumns(langevinSolverResultSet.getClusterOverall())); } - System.out.println(this.getClass().getSimpleName() + ".refreshData() called"); + lg.debug("refreshData() called. SimulationModelInfo: {}, LangevinSolverResultSet: {}, YAxisCounts: {}", + simulationModelInfo, langevinSolverResultSet, yAxisCounts); // find the selected radio button inside the collapsible panel and fire event as if it were just selected by mouse click // which will populate the y-axis choices based on the new data diff --git a/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterVisualizationPanel.java b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterVisualizationPanel.java index 9c3d5f8565..51d97a983a 100644 --- a/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterVisualizationPanel.java +++ b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterVisualizationPanel.java @@ -1,6 +1,8 @@ package cbit.vcell.solver.ode.gui; import cbit.plot.gui.ClusterDataPanel; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.jfree.chart.ChartFactory; import org.jfree.chart.ChartPanel; import org.jfree.chart.JFreeChart; @@ -45,6 +47,8 @@ public class ClusterVisualizationPanel extends AbstractVisualizationPanel { + private static final Logger lg = LogManager.getLogger(ClusterVisualizationPanel.class); + ODEDataViewer owner; IvjEventHandler ivjEventHandler = new IvjEventHandler(); @@ -97,13 +101,13 @@ public void propertyChange(PropertyChangeEvent evt) { @Override public void stateChanged(ChangeEvent e) { if (e.getSource() instanceof Component c && SwingUtilities.isDescendingFrom(c, ClusterVisualizationPanel.this)) { - System.out.println(this.getClass().getName() + ".stateChanged() called"); + lg.debug("stateChanged() called, source = " + e.getSource().getClass().getSimpleName()); } } @Override public void valueChanged(ListSelectionEvent e) { if (e.getSource() instanceof Component c && SwingUtilities.isDescendingFrom(c, ClusterVisualizationPanel.this)) { - System.out.println(this.getClass().getName() + ".valueChanged() called"); + lg.debug("valueChanged() called, source = " + e.getSource().getClass().getSimpleName()); } } }; @@ -145,7 +149,7 @@ private ClusterPlotPanel getClusterPlotPanel() { // actual plotting is show clusterPlotPanel.addComponentListener(new ComponentAdapter() { @Override public void componentShown(ComponentEvent e) { - System.out.println(this.getClass().getSimpleName() + ".componentShown() called, height = " + clusterPlotPanel.getHeight()); + lg.debug("componentShown() called, height = " + clusterPlotPanel.getHeight()); } }); } catch (java.lang.Throwable ivjExc) { @@ -193,13 +197,14 @@ protected void initConnections() { private void handleException(java.lang.Throwable exception) { + lg.error("Uncaught exception in " + this.getClass().getSimpleName(), exception); System.out.println("--------- UNCAUGHT EXCEPTION ---------"); exception.printStackTrace(System.out); } @Override protected void onSelectedObjectsChange(Object[] selectedObjects) { - System.out.println(this.getClass().getSimpleName() + ".onSelectedObjectsChange() called with " + selectedObjects.length + " objects"); + lg.debug("onSelectedObjectsChange() called with " + selectedObjects.length + " objects"); } public void refreshData() { @@ -215,7 +220,7 @@ public void refreshData() { // Instead, we receive them indirectly via the ClusterSelection object from the ClusterSpecificationPanel // simulationModelInfo = owner.getSimulationModelInfo(); // langevinSolverResultSet = owner.getLangevinSolverResultSet(); - System.out.println(this.getClass().getSimpleName() + ".refreshData() called"); + lg.debug("refreshData() called, simulation = " + owner.getSimulation()); } // --------------------------------------------------------------------- @@ -321,7 +326,7 @@ private JComponent createLegendEntry(String name, Color color, ClusterSpecificat } private void redrawPlot(ClusterSpecificationPanel.ClusterSelection sel) throws ExpressionException { - System.out.println(getClass().getSimpleName() + ".redrawPlot() called, current selection: " + sel); + lg.debug("redrawPlot() called, current selection: " + sel); ClusterPlotPanel plot = getClusterPlotPanel(); plot.clearAllRenderers(); @@ -330,10 +335,8 @@ private void redrawPlot(ClusterSpecificationPanel.ClusterSelection sel) throws E plot.repaint(); return; } - List columns = sel.columns; ODESolverResultSet srs = sel.resultSet; - int indexTime = srs.findColumn("t"); double[] times = srs.extractColumn(indexTime); @@ -433,12 +436,12 @@ private void redrawPlot(ClusterSpecificationPanel.ClusterSelection sel) throws E private void redrawLegend(ClusterSpecificationPanel.ClusterSelection sel) { - System.out.println(this.getClass().getSimpleName() + ".redrawLegend() called"); + lg.debug("redrawLegend() called, current selection: " + sel); + getLegendContentPanel().removeAll(); for (ColumnDescription cd : sel.columns) { String name = cd.getName(); - Color c; if (name.equals("SD")) { // SD uses a translucent version of ACS color @@ -448,7 +451,6 @@ private void redrawLegend(ClusterSpecificationPanel.ClusterSelection sel) { // ACS, ACO, COUNTS all use their assigned colors c = persistentColorMap.get(name); } - getLegendContentPanel().add(createLegendEntry(name, c, sel.mode)); } getLegendContentPanel().revalidate(); @@ -456,7 +458,7 @@ private void redrawLegend(ClusterSpecificationPanel.ClusterSelection sel) { } private void redrawDataTable(ClusterSpecificationPanel.ClusterSelection sel) throws ExpressionException { - System.out.println(this.getClass().getSimpleName() + ".updateDataTable() called"); + lg.debug("redrawDataTable() called, current selection: " + sel); getClusterDataPanel().updateData(sel); } public void setSpecialityRenderer(SpecialtyTableRenderer str) { diff --git a/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/MoleculeSpecificationPanel.java b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/MoleculeSpecificationPanel.java index 55e0d5c2fa..d608c6141c 100644 --- a/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/MoleculeSpecificationPanel.java +++ b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/MoleculeSpecificationPanel.java @@ -7,6 +7,8 @@ import cbit.vcell.solver.SimulationModelInfo; import cbit.vcell.solver.ode.ODESolverResultSet; import cbit.vcell.util.ColumnDescription; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.vcell.model.ssld.SsldUtils; import org.vcell.util.gui.CollapsiblePanel; @@ -27,6 +29,8 @@ public class MoleculeSpecificationPanel extends AbstractSpecificationPanel { + private static final Logger lg = LogManager.getLogger(MoleculeSpecificationPanel.class); + public enum DisplayMode { MOLECULES("MOLECULES", "Molecules", "Display molecule counts over time"), BOUND_SITES("BOUND_SITES", "Bound Sites", "Display number of bound sites over time"), @@ -146,8 +150,10 @@ class IvjEventHandler implements ActionListener, PropertyChangeListener, ListSel public void actionPerformed(ActionEvent e) { if (e.getSource() instanceof JCheckBox cb && SwingUtilities.isDescendingFrom(cb, MoleculeSpecificationPanel.this)) { boolean selected = cb.isSelected(); - System.out.println(this.getClass().getName() + ".actionPerformed() called with action command: " + e.getActionCommand() + " = " + selected); String cmd = e.getActionCommand(); +// System.out.println(this.getClass().getName() + ".actionPerformed() called with action command: " + e.getActionCommand() + " = " + selected); + lg.debug("actionPerformed() called with action command: {} = {}", cmd, selected); + if (DisplayMode.isDisplayModeActionCommand(cmd)) { java.util.List displayModes = getSelectedDisplayModes(); populateYAxisChoices(displayModes); @@ -157,7 +163,8 @@ public void actionPerformed(ActionEvent e) { getSelectedStatistics(), getSelectedDisplayModes() ); - System.out.println("MoleculeSelection changed: " + sel.selectedColumns.size() + " columns, " + sel.selectedStatistics.size() + " statistics, " + sel.selectedDisplayModes.size() + " display modes"); + lg.debug("MoleculeSelection changed: {} columns, {} statistics, {} display modes", + sel.selectedColumns.size(), sel.selectedStatistics.size(), sel.selectedDisplayModes.size()); firePropertyChange("MoleculeSelectionChanged", null, sel); } } @@ -165,7 +172,7 @@ public void actionPerformed(ActionEvent e) { @Override public void propertyChange(PropertyChangeEvent evt) { if (evt.getSource() == owner.getMoleculeSpecificationPanel()) { - System.out.println(this.getClass().getName() + ".IvjEventHandler.propertyChange() called with " + evt.getPropertyName()); + lg.debug("propertyChange() called with property name: {}", evt.getPropertyName()); } } @Override @@ -174,13 +181,14 @@ public void valueChanged(ListSelectionEvent e) { if (supressEvents || e.getValueIsAdjusting()) { return; } - System.out.println(this.getClass().getName() + ".IvjEventHandler.valueChanged() called"); + lg.debug("valueChanged() called with selected indices: {}", getYAxisChoice().getSelectedIndices()); MoleculeSelection sel = new MoleculeSelection( getYAxisChoice().getSelectedValuesList(), getSelectedStatistics(), getSelectedDisplayModes() ); - System.out.println("MoleculeSelection changed: " + sel.selectedColumns.size() + " columns, " + sel.selectedStatistics.size() + " statistics, " + sel.selectedDisplayModes.size() + " display modes"); + lg.debug("MoleculeSelection changed: {} columns, {} statistics, {} display modes", + sel.selectedColumns.size(), sel.selectedStatistics.size(), sel.selectedDisplayModes.size()); firePropertyChange("MoleculeSelectionChanged", null, sel); } } @@ -394,16 +402,16 @@ public java.util.List getSelectedStatistics() { @Override protected void onSelectedObjectsChange(Object[] selectedObjects) { - System.out.println(this.getClass().getSimpleName() + ".onSelectedObjectsChange() called with " + selectedObjects.length + " objects"); + lg.debug("onSelectedObjectsChange() called with {} objects", selectedObjects.length); } public void refreshData() { - System.out.println(this.getClass().getSimpleName() + ".refreshData() called"); + lg.debug("refreshData() called"); simulationModelInfo = owner.getSimulationModelInfo(); langevinSolverResultSet = owner.getLangevinSolverResultSet(); if(displayOptionsCollapsiblePanel == null) { // the whole panel should exist already at this point, // lazy inilization here may be a bad idea but we'll do it anyway - System.out.println("displayOptionsCollapsiblePanel is null"); + lg.warn("displayOptionsCollapsiblePanel is null during refreshData()"); } JPanel content = getDisplayOptionsPanel().getContentPanel(); for (Component c : content.getComponents()) { diff --git a/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/MoleculeVisualizationPanel.java b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/MoleculeVisualizationPanel.java index 8781b15a60..22dd6b25fb 100644 --- a/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/MoleculeVisualizationPanel.java +++ b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/MoleculeVisualizationPanel.java @@ -13,6 +13,8 @@ import cbit.vcell.solver.UniformOutputTimeSpec; import cbit.vcell.solver.ode.ODESimData; import cbit.vcell.util.ColumnDescription; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.vcell.util.ColorUtil; import org.vcell.util.gui.SpecialtyTableRenderer; @@ -32,6 +34,8 @@ public class MoleculeVisualizationPanel extends AbstractVisualizationPanel { + private static final Logger lg = LogManager.getLogger(MoleculeVisualizationPanel.class); + private static final int MINMAX_ALPHA = 90; // stronger envelope private static final int SD_ALPHA = 60; // lighter envelope @@ -47,12 +51,12 @@ public class MoleculeVisualizationPanel extends AbstractVisualizationPanel { private MoleculePlotPanel moleculePlotPanel = null; // here are the plots being drawn private MoleculeDataPanel moleculeDataPanel = null; // here resides the data table - class IvjEventHandler implements ActionListener, PropertyChangeListener, ListSelectionListener { @Override public void actionPerformed(ActionEvent e) { if (e.getSource() instanceof Component c && SwingUtilities.isDescendingFrom(c, MoleculeVisualizationPanel.this)) { - System.out.println(this.getClass().getName() + ".actionPerformed() called with " + e.getActionCommand()); +// System.out.println(this.getClass().getName() + ".actionPerformed() called with " + e.getActionCommand()); + lg.debug("actionPerformed() called with command: " + e.getActionCommand()); // switch selection between plot panel and data panel (located in a JCardLayout) String cmd = e.getActionCommand(); // --- Card switching (plot <-> data) --- @@ -77,7 +81,7 @@ public void actionPerformed(ActionEvent e) { @Override public void propertyChange(PropertyChangeEvent evt) { // listens to changes in the MoleculeSpecificationPanel if (evt.getSource() == owner.getMoleculeSpecificationPanel() && "MoleculeSelectionChanged".equals(evt.getPropertyName())) { - System.out.println(this.getClass().getName() + ".propertyChange() called with " + evt.getPropertyName()); + lg.debug("propertyChange() called with property: " + evt.getPropertyName()); // redraw everything based on the new selections MoleculeSpecificationPanel.MoleculeSelection sel = (MoleculeSpecificationPanel.MoleculeSelection) evt.getNewValue(); ensureColorsAssigned(sel.selectedColumns); @@ -93,7 +97,7 @@ public void propertyChange(PropertyChangeEvent evt) { // listens to changes in @Override public void valueChanged(ListSelectionEvent e) { if (e.getSource() instanceof Component c && SwingUtilities.isDescendingFrom(c, MoleculeVisualizationPanel.this)) { - System.out.println(this.getClass().getName() + ".valueChanged() called"); + lg.debug("valueChanged() called"); } } } @@ -142,7 +146,8 @@ private MoleculePlotPanel getMoleculePlotPanel() { // actual plotting is sh moleculePlotPanel.addComponentListener(new ComponentAdapter() { @Override public void componentShown(ComponentEvent e) { - System.out.println(this.getClass().getSimpleName() + ".componentShown() called, height = " + moleculePlotPanel.getHeight()); +// System.out.println(this.getClass().getSimpleName() + ".componentShown() called, height = " + moleculePlotPanel.getHeight()); + lg.debug("componentShown() called, height = " + moleculePlotPanel.getHeight()); } }); } catch (java.lang.Throwable ivjExc) { @@ -185,7 +190,7 @@ protected void initConnections() { // ---------------------------------------------------------------------- private void redrawPlot(MoleculeSpecificationPanel.MoleculeSelection sel) throws ExpressionException { - System.out.println(getClass().getSimpleName() + ".redrawPlot() called, current selection: " + sel); + lg.debug("redrawPlot() called, current selection: " + sel); MoleculePlotPanel plot = getMoleculePlotPanel(); plot.clearAllRenderers(); @@ -236,15 +241,12 @@ class Series { map.put(name, s); - // AVG contributes to globalMax - if (s.avg != null) { + if (s.avg != null) { // AVG contributes to globalMax for (double v : s.avg) { if (v > globalMax) globalMax = v; } } - - // MIN/MAX contributes to globalMin/globalMax - if (s.min != null) { + if (s.min != null) { // MIN/MAX contributes to globalMin/globalMax for (double v : s.min) { if (v < globalMin) globalMin = v; } @@ -254,9 +256,7 @@ class Series { if (v > globalMax) globalMax = v; } } - - // SD envelope contributes to globalMin/globalMax - if (s.avg != null && s.sd != null) { + if (s.avg != null && s.sd != null) { // SD envelope contributes to globalMin/globalMax for (int i = 0; i < s.avg.length; i++) { double upper = s.avg[i] + s.sd[i]; double lower = s.avg[i] - s.sd[i]; @@ -281,32 +281,26 @@ class Series { // --- SD band --- if (sel.selectedStatistics.contains(MoleculeSpecificationPanel.StatisticSelection.SD) && s.avg != null && s.sd != null) { - int n = s.avg.length; double[] low = new double[n]; double[] high = new double[n]; - for (int i = 0; i < n; i++) { low[i] = s.avg[i] - s.sd[i]; high[i] = s.avg[i] + s.sd[i]; - System.out.println(" " + s.avg[i] + " ± " + s.sd[i] + " → [" + low[i] + ", " + high[i] + "]"); } - plot.addSDRenderer(time, low, high, sDColor, name, - MoleculeSpecificationPanel.StatisticSelection.SD); + plot.addSDRenderer(time, low, high, sDColor, name, MoleculeSpecificationPanel.StatisticSelection.SD); } // --- MIN/MAX band --- if (sel.selectedStatistics.contains(MoleculeSpecificationPanel.StatisticSelection.MIN_MAX) && s.min != null && s.max != null) { - plot.addMinMaxRenderer(time, s.min, s.max, mMColor, name, - MoleculeSpecificationPanel.StatisticSelection.MIN_MAX); + plot.addMinMaxRenderer(time, s.min, s.max, mMColor, name, MoleculeSpecificationPanel.StatisticSelection.MIN_MAX); } // --- AVG line --- if (sel.selectedStatistics.contains(MoleculeSpecificationPanel.StatisticSelection.AVG) && s.avg != null) { - plot.addAvgRenderer(time, s.avg, baseColor, name, - MoleculeSpecificationPanel.StatisticSelection.AVG); + plot.addAvgRenderer(time, s.avg, baseColor, name, MoleculeSpecificationPanel.StatisticSelection.AVG); } } @@ -315,11 +309,9 @@ class Series { // ------------------------------------------------------------ if (globalMin > 0) globalMin = 0; plot.setGlobalMinMax(globalMin, globalMax); - if (time.length > 1) { plot.setDt(time[1] - time[0]); // times[0] == 0 } - plot.repaint(); } @@ -342,7 +334,7 @@ public Color deriveSDColor(Color base) { } private void redrawLegend(MoleculeSpecificationPanel.MoleculeSelection sel) throws ExpressionException { - System.out.println(getClass().getSimpleName() + ".redrawLegend() called"); + lg.debug("redrawLegend() called with selection: " + sel); JPanel legend = getLegendContentPanel(); legend.removeAll(); for (ColumnDescription cd : sel.selectedColumns) { @@ -379,17 +371,19 @@ private JComponent createLegendEntry(String name, Color color) { } private void redrawDataTable(MoleculeSpecificationPanel.MoleculeSelection sel) throws ExpressionException { - System.out.println(this.getClass().getSimpleName() + ".updateDataTable() called"); + lg.debug("redrawDataTable() called with selection: " + sel); getMoleculeDataPanel().updateData(sel, langevinSolverResultSet); } private void handleException(java.lang.Throwable exception) { + lg.error("Uncaught exception: " + exception.getMessage(), exception); System.out.println("--------- UNCAUGHT EXCEPTION ---------"); exception.printStackTrace(System.out); } + @Override protected void onSelectedObjectsChange(Object[] selectedObjects) { - System.out.println(this.getClass().getSimpleName() + ".onSelectedObjectsChange() called with " + selectedObjects.length + " objects"); + lg.debug("onSelectedObjectsChange() called with " + selectedObjects.length + " objects"); } // called directly from ODEDataViewer when a new value for vcDataIdentifier is set @@ -397,7 +391,7 @@ protected void onSelectedObjectsChange(Object[] selectedObjects) { // the right way to do it would be to add the simulationModelInfo and langevinSolverResultSet to MoleculeSelection object, // the way it's done in ClusterVisualizationPanel, so that they are always in sync with the selections in the left panel public void refreshData() { - System.out.println(this.getClass().getSimpleName() + ".refreshData() called"); + lg.debug("refreshData() called"); simulationModelInfo = owner.getSimulationModelInfo(); langevinSolverResultSet = owner.getLangevinSolverResultSet(); @@ -451,7 +445,7 @@ private Color deriveEnvelopeColor(Color base) { } public void setSpecialityRenderer(SpecialtyTableRenderer str) { -// getClusterDataPanel().setSpecialityRenderer(str); +// getMoleculeDataPanel().setSpecialityRenderer(str); } diff --git a/vcell-core/src/main/java/cbit/vcell/simdata/LangevinSolverResultSet.java b/vcell-core/src/main/java/cbit/vcell/simdata/LangevinSolverResultSet.java index d55ee3e0ef..ec0ac99a0f 100644 --- a/vcell-core/src/main/java/cbit/vcell/simdata/LangevinSolverResultSet.java +++ b/vcell-core/src/main/java/cbit/vcell/simdata/LangevinSolverResultSet.java @@ -7,6 +7,8 @@ import cbit.vcell.solver.ode.ODESimData; import cbit.vcell.units.VCUnitDefinition; import cbit.vcell.util.ColumnDescription; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.vcell.model.ssld.SsldUtils; import java.io.*; @@ -16,6 +18,8 @@ public class LangevinSolverResultSet implements Serializable { + private static final Logger lg = LogManager.getLogger(LangevinSolverResultSet.class); + private final LangevinBatchResultSet raw; public LangevinSolverResultSet(LangevinBatchResultSet raw) { this.raw = raw; @@ -120,7 +124,7 @@ private void populateMetadata(ODESimData co) { SimulationModelInfo.ModelCategoryType filterCategory = null; // parse name to find the filter category SsldUtils.LangevinResult lr = SsldUtils.LangevinResult.fromString(columnName); if(lr.qualifier.equals(SsldUtils.Qualifier.NONE)) { - System.out.println("Ignoring LangevinResult token: " + columnName + ", qualifier missing"); + lg.warn("Ignoring LangevinResult token: " + columnName + ", qualifier missing"); continue; } metadataMap.put(columnName, lr); @@ -135,7 +139,7 @@ private static void checkTrivial(ODESimData co) { try { data = co.extractColumn(index); } catch (ExpressionException e) { - System.out.println("Failed to extract column: " + e.getMessage()); + lg.warn("Failed to extract column: " + e.getMessage()); continue; } if(data == null || data.length == 0) { From ca32b38ab80c1518ae1cf8759732dd1acd3c5775 Mon Sep 17 00:00:00 2001 From: Dan Vasilescu Date: Mon, 13 Apr 2026 16:46:23 -0400 Subject: [PATCH 29/31] replaces system.out with logger.debug --- .../java/org/vcell/model/ssld/SsldUtils.java | 23 ++++++------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/vcell-core/src/main/java/org/vcell/model/ssld/SsldUtils.java b/vcell-core/src/main/java/org/vcell/model/ssld/SsldUtils.java index 6794b2a6ae..0e67ea7990 100644 --- a/vcell-core/src/main/java/org/vcell/model/ssld/SsldUtils.java +++ b/vcell-core/src/main/java/org/vcell/model/ssld/SsldUtils.java @@ -28,6 +28,8 @@ public class SsldUtils { + private static final Logger lg = LogManager.getLogger(SsldUtils.class); + public enum Qualifier { FREE, BOUND, TOTAL, NONE; public static Qualifier fromPrefix(String s) { @@ -40,8 +42,6 @@ public static Qualifier fromPrefix(String s) { } } - private static Logger lg = LogManager.getLogger(SsldUtils.class); - // result entry from langevin output public static class LangevinResult { public final Qualifier qualifier; @@ -56,34 +56,27 @@ public static LangevinResult fromString(String str) { // Step 1: split only on DOUBLE underscore String[] parts = str.split("__"); - String first = parts[0]; // Step 2: find first underscore int idx = first.indexOf('_'); - Qualifier qualifier = Qualifier.NONE; String molecule; - if (idx > 0) { String prefix = first.substring(0, idx); qualifier = Qualifier.fromPrefix(prefix); - if (qualifier != Qualifier.NONE) { - // Valid qualifier found + // valid qualifier found molecule = first.substring(idx + 1); } else { - // Not a valid qualifier → whole thing is molecule - molecule = first; + molecule = first; // not a valid qualifier -> whole thing is molecule } } else { - // No underscore at all → no qualifier - molecule = first; + molecule = first; // no underscore at all -> no qualifier } String site = ""; String state = ""; - if (parts.length >= 2) { site = parts[1]; } @@ -93,7 +86,6 @@ public static LangevinResult fromString(String str) { if (parts.length > 3) { throw new RuntimeException("Too many '__' sections in: " + str); } - return new LangevinResult(qualifier, molecule, site, state); } @@ -1075,7 +1067,6 @@ private void adjustTransitionReactionSites(TransitionReaction ssldReaction, Mapp // the other end of the bond (the molecule and site of the transition product) } else { transitionMoleculeIndexInSpeciesPattern = 0; - mtpConditionReactant = null; mcpConditionReactant = null; mtpConditionProduct = null; @@ -1104,7 +1095,6 @@ private void adjustTransitionReactionSites(TransitionReaction ssldReaction, Mapp mcpConditionReactant.setBond(SpeciesPattern.generateBond(mtpTransitionReactant, mcpTransitionReactant)); mcpTransitionReactant.setBond(SpeciesPattern.generateBond(mtpConditionReactant, mcpConditionReactant)); } - SpeciesPattern spProduct = m.getProductPatternOneFromMolecule(ssldTransitionMolecule).getSpeciesPattern(); MolecularTypePattern mtpTransitionProduct = spProduct.getMolecularTypePatterns(ssldTransitionMolecule.getName()).get(transitionMoleculeIndexInSpeciesPattern); MolecularComponentPattern mcpTransitionProduct = mtpTransitionProduct.getMolecularComponentPattern(ssldSiteType.getName()); @@ -1112,7 +1102,8 @@ private void adjustTransitionReactionSites(TransitionReaction ssldReaction, Mapp ComponentStateDefinition csdFinal = mcpTransitionProduct.getMolecularComponent().getComponentStateDefinition(ssldFinalStateName); ComponentStatePattern cspTransitionProduct = new ComponentStatePattern(csdFinal); mcpTransitionProduct.setComponentStatePattern(cspTransitionProduct); - System.out.println(" " + ssldReaction.getCondition()); + lg.debug(" " + ssldReaction.getCondition()); + if(ssldReaction.getCondition().equals(TransitionReaction.FREE_CONDITION)) { mcpTransitionProduct.setBondType(MolecularComponentPattern.BondType.None); } else if(ssldReaction.getCondition().equals(TransitionReaction.BOUND_CONDITION)) { From 178039e07d2d4d70163262f68d5e76c2196f43b7 Mon Sep 17 00:00:00 2001 From: Dan Vasilescu Date: Tue, 14 Apr 2026 13:45:33 -0400 Subject: [PATCH 30/31] typos --- .../src/main/java/cbit/plot/gui/ClusterDataPanel.java | 4 ++-- .../src/main/java/cbit/plot/gui/MoleculeDataPanel.java | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/vcell-client/src/main/java/cbit/plot/gui/ClusterDataPanel.java b/vcell-client/src/main/java/cbit/plot/gui/ClusterDataPanel.java index 9f1f8f9dd2..2e9a791610 100644 --- a/vcell-client/src/main/java/cbit/plot/gui/ClusterDataPanel.java +++ b/vcell-client/src/main/java/cbit/plot/gui/ClusterDataPanel.java @@ -148,7 +148,7 @@ private void copyCells(boolean isHDF5) { for (int i = 0; i < r; i++) rows[i] = i; for (int i = 0; i < c; i++) columns[i] = i; - LG.debug("Copying cluster data: rows=" + r + " columns=" + c + " isHDF5=" + isHDF5); + lg.debug("Copying cluster data: rows=" + r + " columns=" + c + " isHDF5=" + isHDF5); boolean bHistogram = false; String blankCellValue = "-1"; @@ -223,7 +223,7 @@ private void copyCells(boolean isHDF5) { VCellTransferable.sendToClipboard(rvs); } catch (Exception ex) { - LG.error("Error copying cluster data", ex); + lg.error("Error copying cluster data", ex); JOptionPane.showMessageDialog( this, "Error copying cluster data: " + ex.getMessage(), diff --git a/vcell-client/src/main/java/cbit/plot/gui/MoleculeDataPanel.java b/vcell-client/src/main/java/cbit/plot/gui/MoleculeDataPanel.java index dd00db5d36..1f550dc476 100644 --- a/vcell-client/src/main/java/cbit/plot/gui/MoleculeDataPanel.java +++ b/vcell-client/src/main/java/cbit/plot/gui/MoleculeDataPanel.java @@ -283,7 +283,7 @@ private void copyCells(boolean isHDF5) { for (int i = 0; i < r; i++) rows[i] = i; for (int i = 0; i < c; i++) columns[i] = i; - LG.debug("Copying cluster data: rows=" + r + " columns=" + c + " isHDF5=" + isHDF5); + lg.debug("Copying cluster data: rows=" + r + " columns=" + c + " isHDF5=" + isHDF5); boolean bHistogram = false; String blankCellValue = "-1"; @@ -358,7 +358,7 @@ private void copyCells(boolean isHDF5) { VCellTransferable.sendToClipboard(rvs); } catch (Exception ex) { - LG.error("Error copying cluster data", ex); + lg.error("Error copying cluster data", ex); JOptionPane.showMessageDialog( this, "Error copying cluster data: " + ex.getMessage(), From eb6529d687b89ac9658e486e6ae0e84ef2239977 Mon Sep 17 00:00:00 2001 From: Dan Vasilescu Date: Tue, 14 Apr 2026 14:37:56 -0400 Subject: [PATCH 31/31] Eliminating all references to JFreeChart --- vcell-client/pom.xml | 6 - .../java/cbit/plot/gui/AbstractPlotPanel.java | 11 + .../ode/gui/ClusterVisualizationPanel.java | 252 ------------------ 3 files changed, 11 insertions(+), 258 deletions(-) diff --git a/vcell-client/pom.xml b/vcell-client/pom.xml index 051b3ede55..238ddacbcb 100644 --- a/vcell-client/pom.xml +++ b/vcell-client/pom.xml @@ -104,12 +104,6 @@ 10.0.5
- - org.jfree - jfreechart - 1.5.5 - - org.junit.jupiter junit-jupiter diff --git a/vcell-client/src/main/java/cbit/plot/gui/AbstractPlotPanel.java b/vcell-client/src/main/java/cbit/plot/gui/AbstractPlotPanel.java index 5d94e38535..da4adcaadb 100644 --- a/vcell-client/src/main/java/cbit/plot/gui/AbstractPlotPanel.java +++ b/vcell-client/src/main/java/cbit/plot/gui/AbstractPlotPanel.java @@ -364,6 +364,17 @@ private Point findClosestNode(int mouseX, int mouseY) { // use for SnapToNod return null; } + private static Shape createDiamondShape(int size) { + Path2D.Double p = new Path2D.Double(); + p.moveTo(0, -size); + p.lineTo(size, 0); + p.lineTo(0, size); + p.lineTo(-size, 0); + p.closePath(); + return p; + } + + // ------------------------------------------------------------------- @Override diff --git a/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterVisualizationPanel.java b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterVisualizationPanel.java index 51d97a983a..1d597935b8 100644 --- a/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterVisualizationPanel.java +++ b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterVisualizationPanel.java @@ -3,16 +3,6 @@ import cbit.plot.gui.ClusterDataPanel; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.jfree.chart.ChartFactory; -import org.jfree.chart.ChartPanel; -import org.jfree.chart.JFreeChart; -import org.jfree.chart.plot.PlotOrientation; -import org.jfree.chart.plot.XYPlot; -import org.jfree.chart.renderer.xy.XYDifferenceRenderer; -import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer; -import org.jfree.chart.ui.RectangleEdge; -import org.jfree.data.xy.XYSeries; -import org.jfree.data.xy.XYSeriesCollection; import cbit.plot.gui.ClusterPlotPanel; import cbit.vcell.client.data.ODEDataViewer; @@ -472,246 +462,4 @@ private boolean contains(List list, String name) { return false; } -// ----------------------------------------------------------------------------------------------------------- - - public static void main(String[] args) { - SwingUtilities.invokeLater(() -> { - - Random rand = new Random(); - - int n = 50; - double xMin = 0.0; - double xMax = 7.0; - double dx = (xMax - xMin) / (n - 1); - - double xLower = 0.0; // these are the limits for the x values of the axis - double xUpper = 7.5; - - Color sinColor = new Color(31, 119, 180); // blue - Color tanColor = new Color(255, 127, 14); // orange - - // --- SIN series --- - XYSeries sinMin = new XYSeries("sin-min"); - XYSeries sinMax = new XYSeries("sin-max"); - XYSeries sinMain = new XYSeries("sin"); - XYSeries sinStd = new XYSeries("sin-std"); - - for (int i = 0; i < n; i++) { - double x = xMin + i * dx; - double y = Math.sin(x); - - double delta = 0.2 + rand.nextDouble() * 0.2; - double yMin = y - delta; - double yMax = y + delta; - - double std = 0.08 + rand.nextDouble() * 0.08; - - sinMin.add(x, yMin); - sinMax.add(x, yMax); - sinMain.add(x, y); - sinStd.add(x, y + std); - } - - // --- TAN series --- - XYSeries tanMin = new XYSeries("tan-min"); - XYSeries tanMax = new XYSeries("tan-max"); - XYSeries tanMain = new XYSeries("tan"); - XYSeries tanStd = new XYSeries("tan-std"); - - for (int i = 0; i < n; i++) { - double x = xMin + i * dx; - double y = Math.tan(x); - - if (y > 3) y = 3; - if (y < -3) y = -3; - - double delta = 0.3 + rand.nextDouble() * 0.3; - double yMin = y - delta; - double yMax = y + delta; - - double std = 0.2 + rand.nextDouble() * 0.2; - - tanMin.add(x, yMin); - tanMax.add(x, yMax); - tanMain.add(x, y); - tanStd.add(x, y + std); - } - - double globalMin = Double.POSITIVE_INFINITY; - double globalMax = Double.NEGATIVE_INFINITY; - for (int i = 0; i < n; i++) { - globalMin = Math.min(globalMin, sinMin.getY(i).doubleValue()); - globalMin = Math.min(globalMin, tanMin.getY(i).doubleValue()); - globalMax = Math.max(globalMax, sinMax.getY(i).doubleValue()); - globalMax = Math.max(globalMax, tanMax.getY(i).doubleValue()); - } - // Add padding - double pad = 0.1 * (globalMax - globalMin); - globalMin -= pad; - globalMax += pad; - - // --- Datasets --- - - // Dataset 0: sin min/max (for band) - XYSeriesCollection sinMinMaxDataset = new XYSeriesCollection(); - sinMinMaxDataset.addSeries(sinMax); // upper - sinMinMaxDataset.addSeries(sinMin); // lower - - // Dataset 1: tan min/max (for band) - XYSeriesCollection tanMinMaxDataset = new XYSeriesCollection(); - tanMinMaxDataset.addSeries(tanMax); // upper - tanMinMaxDataset.addSeries(tanMin); // lower - - // Dataset 2: main curves - XYSeriesCollection mainDataset = new XYSeriesCollection(); - mainDataset.addSeries(sinMain); - mainDataset.addSeries(tanMain); - - // Dataset 3: std diamonds - XYSeriesCollection stdDataset = new XYSeriesCollection(); - stdDataset.addSeries(sinStd); - stdDataset.addSeries(tanStd); - - // --- Chart skeleton --- - JFreeChart chart = ChartFactory.createXYLineChart( - "Min/Max Bands + STD Demo", - "x", - "y", - null, - PlotOrientation.VERTICAL, - true, - true, - false - ); - XYPlot plot = chart.getXYPlot(); - - // Transparent backgrounds - chart.setBackgroundPaint(Color.WHITE); - plot.setBackgroundPaint(Color.WHITE); - plot.setOutlinePaint(null); - plot.setDomainGridlinePaint(new Color(180, 180, 180)); // very light - plot.setRangeGridlinePaint(new Color(180, 180, 180)); - - plot.getDomainAxis().setAutoRange(false); // lock the axis so that they never resize - plot.getRangeAxis().setAutoRange(false); - plot.getDomainAxis().setRange(xLower, xUpper); - plot.getRangeAxis().setRange(globalMin, globalMax); - - // --- Legend to the right - chart.getLegend().setPosition(RectangleEdge.RIGHT); - chart.getLegend().setBackgroundPaint(Color.WHITE); -// chart.getLegend().setFrame(BlockBorder.NONE); - - // --- Renderer 0: sin band --- - XYDifferenceRenderer sinBandRenderer = new XYDifferenceRenderer(); - Color sinBandColor = new Color(sinColor.getRed(), sinColor.getGreen(), sinColor.getBlue(), 40); - sinBandRenderer.setPositivePaint(sinBandColor); - sinBandRenderer.setNegativePaint(sinBandColor); - sinBandRenderer.setSeriesStroke(0, new BasicStroke(0f)); - sinBandRenderer.setSeriesStroke(1, new BasicStroke(0f)); -// sinBandRenderer.setOutlinePaint(null); - sinBandRenderer.setSeriesVisibleInLegend(0, false); // hide sin-max legend entries - sinBandRenderer.setSeriesVisibleInLegend(1, false); - - plot.setDataset(0, sinMinMaxDataset); - plot.setRenderer(0, sinBandRenderer); - - // --- Renderer 1: tan band --- - XYDifferenceRenderer tanBandRenderer = new XYDifferenceRenderer(); - Color tanBandColor = new Color(tanColor.getRed(), tanColor.getGreen(), tanColor.getBlue(), 40); - tanBandRenderer.setPositivePaint(tanBandColor); - tanBandRenderer.setNegativePaint(tanBandColor); - tanBandRenderer.setSeriesStroke(0, new BasicStroke(0f)); - tanBandRenderer.setSeriesStroke(1, new BasicStroke(0f)); -// tanBandRenderer.setOutlinePaint(null); - tanBandRenderer.setSeriesVisibleInLegend(0, false); - tanBandRenderer.setSeriesVisibleInLegend(1, false); - - plot.setDataset(1, tanMinMaxDataset); - plot.setRenderer(1, tanBandRenderer); - - // --- Renderer 2: main curves --- - XYLineAndShapeRenderer lineRenderer = new XYLineAndShapeRenderer(true, false); - lineRenderer.setSeriesPaint(0, sinColor); - lineRenderer.setSeriesPaint(1, tanColor); - lineRenderer.setSeriesStroke(0, new BasicStroke(2f)); - lineRenderer.setSeriesStroke(1, new BasicStroke(2f)); - lineRenderer.setSeriesVisibleInLegend(0, true); // keep the main curves visible in Legend - lineRenderer.setSeriesVisibleInLegend(1, true); - - plot.setDataset(2, mainDataset); - plot.setRenderer(2, lineRenderer); - - // --- Renderer 3: STD diamonds --- - XYLineAndShapeRenderer stdRenderer = new XYLineAndShapeRenderer(false, true); - Shape diamond = createDiamondShape(4); - - stdRenderer.setSeriesShape(0, diamond); - stdRenderer.setSeriesShape(1, diamond); - stdRenderer.setSeriesPaint(0, sinColor.darker()); - stdRenderer.setSeriesPaint(1, tanColor.darker()); - stdRenderer.setSeriesVisibleInLegend(0, false); - stdRenderer.setSeriesVisibleInLegend(1, false); - - plot.setDataset(3, stdDataset); - plot.setRenderer(3, stdRenderer); - - // --- ChartPanel --- - ChartPanel panel = new ChartPanel(chart); - panel.setOpaque(true); // must be opaque for white to show - panel.setBackground(Color.WHITE); - - // --- checkboxes to control display - JPanel controls = new JPanel(); - JCheckBox cbAvg = new JCheckBox("Averages", true); - JCheckBox cbMinMax = new JCheckBox("Min-Max", false); - JCheckBox cbStd = new JCheckBox("STD", false); - controls.add(cbAvg); - controls.add(cbMinMax); - controls.add(cbStd); - - // Averages (dataset 2) - cbAvg.addActionListener(e -> { - boolean on = cbAvg.isSelected(); - plot.getRenderer(2).setSeriesVisible(0, on); - plot.getRenderer(2).setSeriesVisible(1, on); - lineRenderer.setSeriesVisibleInLegend(0, true); // force legend visible - lineRenderer.setSeriesVisibleInLegend(1, true); - }); - - // Min-Max (datasets 0 and 1) - cbMinMax.addActionListener(e -> { - boolean on = cbMinMax.isSelected(); - plot.setRenderer(0, on ? sinBandRenderer : null); - plot.setRenderer(1, on ? tanBandRenderer : null); - }); - - // STD (dataset 3) - cbStd.addActionListener(e -> { - boolean on = cbStd.isSelected(); - stdRenderer.setSeriesVisible(0, on); - stdRenderer.setSeriesVisible(1, on); - }); - - // --- Frame --- - JFrame frame = new JFrame("JFreeChart Min/Max/STD Demo"); - frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); - frame.add(panel, BorderLayout.CENTER); - frame.add(controls, BorderLayout.SOUTH); - frame.setSize(900, 600); - frame.setLocationRelativeTo(null); - frame.setVisible(true); - }); - } - - private static Shape createDiamondShape(int size) { - Path2D.Double p = new Path2D.Double(); - p.moveTo(0, -size); - p.lineTo(size, 0); - p.lineTo(0, size); - p.lineTo(-size, 0); - p.closePath(); - return p; - } - }