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/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..99f9235782
--- /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 to the clipboard 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) {
+ lg.error("Uncaught exception in " + getClass().getSimpleName(), 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..da4adcaadb
--- /dev/null
+++ b/vcell-client/src/main/java/cbit/plot/gui/AbstractPlotPanel.java
@@ -0,0 +1,542 @@
+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.*;
+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 {
+
+ 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;
+ 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);
+ /**
+ * 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
+ 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, 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,
+ int plotWidth, int plotHeight,
+ double xMaxRounded, double yMaxRounded, double yMinRounded,
+ double dt) {
+
+ int n = values.length;
+ if (n < 2) return;
+
+ xs = new int[n];
+ 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);
+
+ // 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]);
+ }
+ }
+
+ // 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;
+ final AbstractPlotPanel parent;
+
+ 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
+ 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);
+ }
+ }
+
+ // 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; // 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; // 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; // 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(); // in pixels, relative to the panel
+ 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) {
+ 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});
+ }
+ } 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 clearAllRenderers() {
+ 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, this));
+ }
+
+ public void addMinMaxRenderer(double[] time, double[] min, double[] max, Color color, String name, Object statTag) {
+ 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, this));
+ }
+
+ // 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[] 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) {
+ 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;
+ }
+
+ 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;
+ }
+
+ 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
+ protected void paintComponent(Graphics g) {
+ super.paintComponent(g);
+
+ Graphics2D g2 = (Graphics2D) g;
+ g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
+
+ 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);
+
+ int x0 = LEFT_INSET;
+ int x1 = w - RIGHT_INSET;
+ int y0 = h - BOTTOM_INSET;
+ int y1 = TOP_INSET;
+ lastX0 = x0; // in pixels
+ 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; // number of timepoints
+ 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;
+
+ // --- 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
+
+ // --- 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();
+
+ // --- grid lines ----------------------------------------------
+ g2.setColor(new Color(220, 220, 220));
+ g2.setStroke(new BasicStroke(1f));
+
+ 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}; // vertical grid lines
+ 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);
+ }
+ }
+
+ // --- draw axes ------------------------------------------------
+ g2.setColor(Color.black);
+ g2.setStroke(new BasicStroke(AXIS_STROKE));
+
+ g2.drawLine(x0, yZeroPix, x1, yZeroPix); // horizontal axis, going through the "0 molecules" point
+ g2.drawLine(x0, y0, x0, y1); // vertical axis
+
+ // --- ticks ---------------------------------------------------
+ g2.setColor(Color.black);
+ g2.setStroke(new BasicStroke(AXIS_STROKE));
+
+ // 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, 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]; // 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
+
+ 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, yZeroPix, xPixMid, yZeroPix + 3); // mid tick also on the x‑axis
+ }
+ }
+
+ // 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
new file mode 100644
index 0000000000..2e9a791610
--- /dev/null
+++ b/vcell-client/src/main/java/cbit/plot/gui/ClusterDataPanel.java
@@ -0,0 +1,321 @@
+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;
+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.MouseEvent;
+import java.beans.PropertyChangeEvent;
+import java.io.File;
+
+public class ClusterDataPanel extends AbstractDataPanel {
+
+ private static final Logger lg = LogManager.getLogger(ClusterDataPanel.class);
+
+ // -------------------------------------------------------------------------
+ // 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) {
+
+ Component c = base.getTableCellRendererComponent(
+ table, value, isSelected, hasFocus, row, column);
+
+ if (!(c instanceof JLabel)) {
+ return c;
+ }
+
+ JLabel lbl = (JLabel) c;
+ String name = value == null ? "" : value.toString();
+
+ ClusterSpecificationPanel.DisplayMode mode =
+ (ClusterSpecificationPanel.DisplayMode)
+ ((JComponent) table).getClientProperty("ClusterDisplayMode");
+
+ if (mode == null) {
+ lbl.setToolTipText(null);
+ return lbl;
+ }
+
+ String text = "";
+ String unit = "";
+ String tooltip = "";
+
+ ClusterSpecificationPanel.ClusterStatistic stat =
+ ClusterSpecificationPanel.ClusterStatistic.fromString(name);
+
+ if (column == 0) {
+ unit = "seconds";
+ text = "" + name + " [" + unit + "]";
+ tooltip = "Simulation time";
+ } else {
+ switch (mode) {
+ case COUNTS:
+ unit = "molecules";
+ text = "" + name + " [" + unit + "]";
+ tooltip = "Number of clusters made of " + name + " " + unit + "";
+ break;
+
+ case MEAN:
+ case OVERALL:
+ if (stat != null) {
+ unit = stat.unit();
+ text = "" + stat.fullName() +
+ " [" + unit + "]";
+ tooltip = "" + stat.description() + "";
+ }
+ break;
+ }
+ }
+
+ lbl.setText(text);
+ lbl.setToolTipText(tooltip);
+ return lbl;
+ }
+ }
+
+ // -------------------------------------------------------------------------
+ // Constructor
+ // -------------------------------------------------------------------------
+ public ClusterDataPanel() {
+ super();
+ }
+
+ // -------------------------------------------------------------------------
+ // Hook up cluster-specific header renderer
+ // -------------------------------------------------------------------------
+ @Override
+ protected void initConnections() throws Exception {
+ super.initConnections();
+
+ TableCellRenderer baseHeaderRenderer =
+ getScrollPaneTable().getTableHeader().getDefaultRenderer();
+
+ getScrollPaneTable().getTableHeader()
+ .setDefaultRenderer(new ClusterHeaderRenderer(baseHeaderRenderer));
+ }
+
+ // -------------------------------------------------------------------------
+ // 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
+ );
+ }
+ }
+
+ 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(ReservedVariable.TIME.getName());
+ double[] times = srs.extractColumn(timeIndex);
+ int rowCount = times.length;
+
+ 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();
+ }
+
+ 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];
+ }
+ }
+
+ getNonEditableDefaultTableModel().setDataVector(data, columnNames);
+ getScrollPaneTable().createDefaultColumnsFromModel();
+ autoSizeTableColumns(getScrollPaneTable());
+
+ revalidate();
+ repaint();
+ }
+
+ // -------------------------------------------------------------------------
+ // autoSizeTableColumns() — unchanged
+ // -------------------------------------------------------------------------
+ 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;
+
+ TableCellRenderer headerRenderer = table.getTableHeader().getDefaultRenderer();
+ Component headerComp = headerRenderer.getTableCellRendererComponent(
+ table, column.getHeaderValue(), false, false, 0, col);
+ maxWidth = Math.max(maxWidth, headerComp.getPreferredSize().width);
+
+ 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);
+ }
+ }
+
+ 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
new file mode 100644
index 0000000000..b6849ac056
--- /dev/null
+++ b/vcell-client/src/main/java/cbit/plot/gui/ClusterPlotPanel.java
@@ -0,0 +1,32 @@
+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;
+
+import org.vcell.util.gui.GeneralGuiUtils;
+import org.vcell.util.*;
+
+public class ClusterPlotPanel extends AbstractPlotPanel {
+
+ public ClusterPlotPanel() {
+ 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
new file mode 100644
index 0000000000..1f550dc476
--- /dev/null
+++ b/vcell-client/src/main/java/cbit/plot/gui/MoleculeDataPanel.java
@@ -0,0 +1,371 @@
+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;
+import cbit.vcell.solver.ode.gui.MoleculeVisualizationPanel;
+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 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 MoleculeDataPanel extends AbstractDataPanel {
+
+ private static final Logger lg = LogManager.getLogger(MoleculeDataPanel.class);
+
+ public enum SubStatistic {
+ AVG("AVG"),
+ MIN("MIN"),
+ MAX("MAX"),
+ SD("SD");
+ public final String uiLabel;
+ SubStatistic(String uiLabel) {
+ this.uiLabel = uiLabel;
+ }
+ }
+
+ 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 MoleculeTwoRowHeaderRenderer extends DefaultTableCellRenderer {
+
+ 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) {
+ Component c = base.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
+ if (!(c instanceof JLabel)) {
+ 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;
+ }
+ }
+
+ // ------------------------------------------------------
+
+ ODEDataViewer owner;
+
+ public MoleculeDataPanel(ODEDataViewer owner) {
+ super();
+ this.owner = owner;
+ }
+ @Override
+ protected void initConnections() throws Exception {
+ super.initConnections();
+ TableCellRenderer baseHeaderRenderer = getScrollPaneTable().getTableHeader().getDefaultRenderer();
+ getScrollPaneTable().getTableHeader().setDefaultRenderer(new MoleculeTwoRowHeaderRenderer(baseHeaderRenderer));
+ }
+
+ // ------------------------------------------------------
+
+ public void updateData(MoleculeSpecificationPanel.MoleculeSelection sel, LangevinSolverResultSet lsrs)
+ throws ExpressionException {
+
+ if (sel == null) {
+ getScrollPaneTable().putClientProperty("MoleculeSelection", null); // may be useful for tooltip generation
+ } else {
+ getScrollPaneTable().putClientProperty("MoleculeSelection", sel);
+ }
+
+ if (sel == null || lsrs == null || lsrs.isAverageDataAvailable() == false || // guard clause
+ sel.selectedColumns == null || sel.selectedColumns.isEmpty() ||
+ sel.selectedStatistics == null || sel.selectedStatistics.isEmpty()) {
+
+ getNonEditableDefaultTableModel().setDataVector(new Object[][]{}, new Object[]{"No data"});
+ getScrollPaneTable().createDefaultColumnsFromModel();
+ revalidate();
+ repaint();
+ return;
+ }
+
+ 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;
+ 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;
+ }
+
+
+
+ // ---------------------------------------------------------------------
+ // 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;
+ }
+ }
+ }
+
+ // ---------------------------------------------------------------------
+ // 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 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);
+ }
+ }
+
+ // ------------------------------------------------------
+
+ // -------------------------------------------------------------------------
+ // 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/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/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/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/client/data/ODEDataViewer.java b/vcell-client/src/main/java/cbit/vcell/client/data/ODEDataViewer.java
index a267f7208a..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
@@ -9,15 +9,15 @@
*/
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;
+import cbit.vcell.simdata.LangevinSolverResultSet;
+import cbit.vcell.solver.ode.gui.*;
import org.vcell.solver.nfsim.NFSimMolecularConfigurations;
import org.vcell.util.document.VCDataIdentifier;
import org.vcell.util.gui.DialogUtils;
@@ -37,10 +37,7 @@
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;
import cbit.vcell.util.ColumnDescription;
/**
* Insert the type's description here.
@@ -51,14 +48,25 @@ 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;
+
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 javax.swing.JPanel viewData = null;
+ private JPanel viewMultiClustersPanel = null;
+ private JPanel viewMultiDataPanel = 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 +225,9 @@ public ODESolverPlotSpecificationPanel getODESolverPlotSpecificationPanel1() {
public ODESolverResultSet getOdeSolverResultSet() {
return fieldOdeSolverResultSet;
}
+public LangevinSolverResultSet getLangevinSolverResultSet() {
+ return fieldLangevinSolverResultSet;
+}
public NFSimMolecularConfigurations getNFSimMolecularConfigurations() {
return nFSimMolecularConfigurations;
}
@@ -265,7 +276,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 {
}
}
@@ -276,10 +289,18 @@ 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, getViewData(), null, 0);
+ }
+
+ ivjJTabbedPane.addTab(LANGEVIN_CLUSTER_RESULTS_TABNAME, getViewMultiClusters());
+
outputSpeciesResultsPanel = new OutputSpeciesResultsPanel(this);
outputSpeciesResultsPanel.addPropertyChangeListener(ivjEventHandler);
ivjJTabbedPane.addTab(OUTPUT_SPECIES_TABNAME, outputSpeciesResultsPanel);
+
ivjJTabbedPane.addChangeListener(mainTabChangeListener);
} catch (java.lang.Throwable ivjExc) {
handleException(ivjExc);
@@ -287,23 +308,105 @@ private javax.swing.JTabbedPane getJTabbedPane() {
}
return ivjJTabbedPane;
}
-/**
- * Return the ViewData property value.
- * @return javax.swing.JPanel
- */
-private javax.swing.JPanel getViewData() {
- if (ivjViewData == null) {
+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) {
+ 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 {
+ 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 viewMultiClustersPanel;
+}
+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");
+ SpecialtyTableRenderer str = new RenderDataViewerDoubleWithTooltip();
+ clusterVisualizationPanel.setSpecialityRenderer(str);
+ } catch (java.lang.Throwable ivjExc) {
+ handleException(ivjExc);
+ }
+ }
+ return clusterVisualizationPanel;
+}
+
+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;
}
@@ -390,6 +493,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;
@@ -426,14 +534,28 @@ public void setVcDataIdentifier(VCDataIdentifier vcDataIdentifier) {
fieldVcDataIdentifier = vcDataIdentifier;
setOdeDataContext();
firePropertyChange("vcDataIdentifier", oldValue, vcDataIdentifier);
- outputSpeciesResultsPanel.refreshData();
+ if(hasLangevinBatchResults) {
+ getClusterVisualizationPanel().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();
+ }
}
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..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
@@ -79,6 +79,14 @@ private DataViewer createODEDataViewer() throws DataAccessException {
odeDataViewer = new ODEDataViewer();
odeDataViewer.setSimulation(getSimulation());
ODESolverResultSet odesrs = ((ODEDataManager)dataManager).getODESolverResultSet();
+ LangevinSolverResultSet langevinSolverResultSet = ((ODEDataManager)dataManager).getLangevinSolverResultSet();
+ 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());
odeDataViewer.setVcDataIdentifier(dataManager.getVCDataIdentifier());
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..bb38fecf0a
--- /dev/null
+++ b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/AbstractSpecificationPanel.java
@@ -0,0 +1,187 @@
+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.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.vcell.util.gui.CollapsiblePanel;
+
+import java.awt.*;
+import java.beans.PropertyChangeListener;
+import javax.swing.*;
+
+@SuppressWarnings("serial")
+public abstract class AbstractSpecificationPanel extends DocumentEditorSubPanel {
+
+ private static final Logger lg = LogManager.getLogger(AbstractSpecificationPanel.class);
+
+ // ------------------------------
+ // 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() {
+ lg.debug("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) {
+ lg.error("Uncaught exception in AbstractSpecificationPanel", 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/AbstractVisualizationPanel.java b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/AbstractVisualizationPanel.java
new file mode 100644
index 0000000000..d556f702cb
--- /dev/null
+++ b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/AbstractVisualizationPanel.java
@@ -0,0 +1,338 @@
+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;
+
+
+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 {
+
+ private static final Logger lg = LogManager.getLogger(AbstractVisualizationPanel.class);
+
+ 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(4.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/ClusterSpecificationPanel.java b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterSpecificationPanel.java
new file mode 100644
index 0000000000..87c17b801f
--- /dev/null
+++ b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterSpecificationPanel.java
@@ -0,0 +1,461 @@
+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.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+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;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+public class ClusterSpecificationPanel extends AbstractSpecificationPanel {
+
+ private static final Logger lg = LogManager.getLogger(ClusterSpecificationPanel.class);
+
+ 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(
+ "Avg. Cluster Size",
+ "Average number of molecules per cluster",
+ "molecules"
+ ),
+ ACO(
+ "Avg. Cluster Occupancy",
+ "Average size of the cluster that a molecule belongs to (molecule‑centric cluster size)",
+ "molecules"
+ ),
+ SD(
+ "SD 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;
+ }
+ // 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
+ 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;
+ }
+ }
+
+ 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) {
+ String cmd = e.getActionCommand();
+ if (e.getSource() instanceof JRadioButton rb && SwingUtilities.isDescendingFrom(rb, ClusterSpecificationPanel.this)) {
+ lg.debug("actionPerformed() called. Source is JRadioButton: {}", rb.getText());
+ DisplayMode mode = DisplayMode.fromActionCommand(cmd);
+ populateYAxisChoices(mode);
+ }
+ }
+ @Override
+ public void propertyChange(PropertyChangeEvent evt) {
+ if(evt.getSource() == ClusterSpecificationPanel.this) {
+ lg.debug("propertyChange() called. Source is ClusterSpecificationPanel. Property name: {}, old value: {}, new value: {}",
+ evt.getPropertyName(), evt.getOldValue(), evt.getNewValue());
+ }
+ }
+ @Override
+ public void valueChanged(ListSelectionEvent e) {
+ if (e.getSource() == ClusterSpecificationPanel.this.getYAxisChoice()) {
+ if(suppressEvents || e.getValueIsAdjusting()) {
+ return; // ignore events triggered during initialization
+ }
+ lg.debug("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));
+ }
+ }
+ };
+
+ private final ODEDataViewer owner;
+ private LangevinSolverResultSet langevinSolverResultSet = null;
+ private SimulationModelInfo simulationModelInfo = null;
+
+ 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;
+ 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();
+ 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) {
+ int count = yAxisCounts.getOrDefault(mode, 0);
+ String text = "" + YAxisLabelText + "(" + count + " entries)";
+ 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;
+ }
+ }
+ }
+
+ @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 rbOverall = new JRadioButton(DisplayMode.OVERALL.uiLabel());
+
+ rbCounts.setActionCommand(DisplayMode.COUNTS.actionCommand());
+ rbMean.setActionCommand(DisplayMode.MEAN.actionCommand());
+ rbOverall.setActionCommand(DisplayMode.OVERALL.actionCommand());
+
+ rbCounts.setToolTipText(DisplayMode.COUNTS.tooltip());
+ rbMean.setToolTipText(DisplayMode.MEAN.tooltip());
+ rbOverall.setToolTipText(DisplayMode.OVERALL.tooltip());
+
+ group.add(rbCounts);
+ group.add(rbMean);
+ group.add(rbOverall);
+
+ rbCounts.setSelected(true); // select the first option by default, which will populate the y-axis choices
+
+ 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 cp;
+ }
+
+// @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) {
+ lg.debug("onSelectedObjectsChange() called. Number of selected objects: {}", selectedObjects.length);
+ }
+
+ 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()));
+ }
+ 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
+ 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;
+ }
+ }
+ }
+ 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
+ }
+ 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
new file mode 100644
index 0000000000..1d597935b8
--- /dev/null
+++ b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterVisualizationPanel.java
@@ -0,0 +1,465 @@
+package cbit.vcell.solver.ode.gui;
+
+import cbit.plot.gui.ClusterDataPanel;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import cbit.plot.gui.ClusterPlotPanel;
+import cbit.vcell.client.data.ODEDataViewer;
+import cbit.vcell.client.desktop.biomodel.DocumentEditorSubPanel;
+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;
+import org.vcell.util.gui.SpecialtyTableRenderer;
+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.awt.event.ComponentAdapter;
+import java.awt.event.ComponentEvent;
+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;
+
+
+public class ClusterVisualizationPanel extends AbstractVisualizationPanel {
+
+ private static final Logger lg = LogManager.getLogger(ClusterVisualizationPanel.class);
+
+ 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 ClusterPlotPanel clusterPlotPanel = null; // here are the plots being drawn
+ private ClusterDataPanel clusterDataPanel = null; // here resides the data table
+
+ 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();
+ // --- 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) {
+ if (evt.getSource() == owner.getClusterSpecificationPanel() && "ClusterSelection".equals(evt.getPropertyName())) {
+ 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);
+ }
+ return;
+ }
+ }
+ @Override
+ public void stateChanged(ChangeEvent e) {
+ if (e.getSource() instanceof Component c && SwingUtilities.isDescendingFrom(c, ClusterVisualizationPanel.this)) {
+ 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)) {
+ lg.debug("valueChanged() called, source = " + e.getSource().getClass().getSimpleName());
+ }
+ }
+ };
+
+ public ClusterVisualizationPanel(ODEDataViewer odeDataViewer) {
+ super();
+ this.owner = odeDataViewer;
+ initialize();
+ initConnections();
+ setVisualizationBackground(Color.WHITE);
+ }
+
+ // --------------------the abstract class hooks
+ @Override
+ protected JPanel createPlotPanel() {
+ return getClusterPlotPanel();
+ }
+ @Override
+ protected JPanel createDataPanel() {
+ return getClusterDataPanel();
+ }
+ @Override
+ protected void setCrosshairEnabled(boolean enabled) {
+ getClusterPlotPanel().setCrosshairEnabled(enabled);
+ }
+
+ private ClusterPlotPanel getClusterPlotPanel() { // actual plotting is shown here
+ if (clusterPlotPanel == null) {
+ 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) {
+ lg.debug("componentShown() called, height = " + clusterPlotPanel.getHeight());
+ }
+ });
+ } catch (java.lang.Throwable ivjExc) {
+ handleException(ivjExc);
+ }
+ }
+ return clusterPlotPanel;
+ }
+ public ClusterDataPanel getClusterDataPanel() { // actual table shown here
+ if (clusterDataPanel == null) {
+ try {
+ clusterDataPanel = new ClusterDataPanel();
+ } catch (java.lang.Throwable ivjExc) {
+ handleException(ivjExc);
+ }
+ }
+ return clusterDataPanel;
+ }
+
+
+ public void setVisualizationBackground(Color color) {
+ super.setVisualizationBackground(color);
+ getClusterPlotPanel().setBackground(color);
+ getClusterDataPanel().setBackground(color);
+ }
+
+ @Override
+ 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.getClusterSpecificationPanel().addPropertyChangeListener(ivjEventHandler);
+ }
+
+
+ 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) {
+ lg.debug("onSelectedObjectsChange() called with " + selectedObjects.length + " objects");
+ }
+
+ 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" : "") + "]";
+ getBottomLabel().setText(str);
+ } 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();
+ lg.debug("refreshData() called, simulation = " + owner.getSimulation());
+ }
+ // ---------------------------------------------------------------------
+
+ 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)
+ );
+ }
+ /*
+ 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");
+ p.setLayout(new BoxLayout(p, BoxLayout.Y_AXIS));
+ p.setOpaque(false);
+
+ 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;
+ }
+
+ // 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.setToolTipText(tooltip);
+ text.setToolTipText(tooltip);
+ p.setToolTipText(tooltip);
+ p.add(line);
+ p.add(text);
+ return p;
+ }
+
+ private void redrawPlot(ClusterSpecificationPanel.ClusterSelection sel) throws ExpressionException {
+ lg.debug("redrawPlot() called, current selection: " + sel);
+
+ ClusterPlotPanel plot = getClusterPlotPanel();
+ plot.clearAllRenderers();
+
+ if (sel == null || sel.resultSet == null) {
+ plot.repaint();
+ return;
+ }
+ List columns = sel.columns;
+ ODESolverResultSet srs = sel.resultSet;
+ int indexTime = srs.findColumn("t");
+ double[] times = srs.extractColumn(indexTime);
+
+ // ---------------------------------------------------------------------
+ // 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();
+
+ int idx = srs.findColumn(name);
+ if (idx < 0) continue;
+
+ double[] y = srs.extractColumn(idx);
+ yMap.put(name, y);
+
+ // 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;
+ }
+ }
+ }
+
+ // ---------------------------------------------------------------------
+ // 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;
+ }
+ }
+
+ // --- 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");
+ }
+
+ // --- 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");
+ }
+
+ // ---------------------------------------------------------------------
+ // FINALIZE
+ // ---------------------------------------------------------------------
+ if (globalMin > 0) globalMin = 0;
+ plot.setGlobalMinMax(globalMin, globalMax);
+ if (times.length > 1) {
+ plot.setDt(times[1]); // times[0] == 0
+ }
+ plot.repaint();
+ }
+
+
+ private void redrawLegend(ClusterSpecificationPanel.ClusterSelection sel) {
+ 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
+ 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);
+ }
+ getLegendContentPanel().add(createLegendEntry(name, c, sel.mode));
+ }
+ getLegendContentPanel().revalidate();
+ getLegendContentPanel().repaint();
+ }
+
+ private void redrawDataTable(ClusterSpecificationPanel.ClusterSelection sel) throws ExpressionException {
+ lg.debug("redrawDataTable() called, current selection: " + sel);
+ getClusterDataPanel().updateData(sel);
+ }
+ 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;
+ }
+
+}
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..d52b29a746
--- /dev/null
+++ b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/LangevinClustersResultsPanel.java
@@ -0,0 +1,516 @@
+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;
+import javax.swing.event.ListSelectionListener;
+import java.awt.*;
+import java.awt.event.ActionEvent;
+import java.beans.PropertyChangeEvent;
+import java.beans.PropertyChangeListener;
+
+@Deprecated
+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;
+
+ 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) {
+ 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());
+ 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 LangevinClustersResultsPanel(ODEDataViewer odeDataViewer) {
+ super();
+ this.owner = odeDataViewer;
+ initialize();
+ }
+
+ 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);
+
+ initConnectionsLeft();
+ }
+ 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");
+
+ 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(javax.swing.ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
+ }
+ return yAxisChoiceList;
+ }
+
+ private void initConnectionsLeft() {
+
+ 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");
+ 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) {
+ System.out.println("LangevinClustersResultsPanel.onSelectedObjectsChange() called with " + selectedObjects.length + " objects");
+ }
+
+ public void refreshData() {
+ 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;
+ }
+ }
+
+ }
+
+
+}
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..d608c6141c
--- /dev/null
+++ b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/MoleculeSpecificationPanel.java
@@ -0,0 +1,428 @@
+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.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.vcell.model.ssld.SsldUtils;
+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.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 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"),
+ 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 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)) {
+ return m;
+ }
+ }
+ 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;
+ }
+ }
+
+ 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();
+ }
+ }
+
+ class IvjEventHandler implements ActionListener, PropertyChangeListener, ListSelectionListener {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ if (e.getSource() instanceof JCheckBox cb && SwingUtilities.isDescendingFrom(cb, MoleculeSpecificationPanel.this)) {
+ boolean selected = cb.isSelected();
+ 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);
+ } else if (StatisticSelection.isStatisticSelectionActionCommand(cmd)) {
+ MoleculeSelection sel = new MoleculeSelection(
+ getYAxisChoice().getSelectedValuesList(),
+ getSelectedStatistics(),
+ getSelectedDisplayModes()
+ );
+ lg.debug("MoleculeSelection changed: {} columns, {} statistics, {} display modes",
+ sel.selectedColumns.size(), sel.selectedStatistics.size(), sel.selectedDisplayModes.size());
+ firePropertyChange("MoleculeSelectionChanged", null, sel);
+ }
+ }
+ }
+ @Override
+ public void propertyChange(PropertyChangeEvent evt) {
+ if (evt.getSource() == owner.getMoleculeSpecificationPanel()) {
+ lg.debug("propertyChange() called with property name: {}", evt.getPropertyName());
+ }
+ }
+ @Override
+ public void valueChanged(ListSelectionEvent e) {
+ if (e.getSource() == MoleculeSpecificationPanel.this.getYAxisChoice()) {
+ if (supressEvents || e.getValueIsAdjusting()) {
+ return;
+ }
+ lg.debug("valueChanged() called with selected indices: {}", getYAxisChoice().getSelectedIndices());
+ MoleculeSelection sel = new MoleculeSelection(
+ getYAxisChoice().getSelectedValuesList(),
+ getSelectedStatistics(),
+ getSelectedDisplayModes()
+ );
+ lg.debug("MoleculeSelection changed: {} columns, {} statistics, {} display modes",
+ sel.selectedColumns.size(), sel.selectedStatistics.size(), sel.selectedDisplayModes.size());
+ firePropertyChange("MoleculeSelectionChanged", null, sel);
+ }
+ }
+ }
+ MoleculeSpecificationPanel.IvjEventHandler ivjEventHandler = new MoleculeSpecificationPanel.IvjEventHandler();
+
+ private final ODEDataViewer owner;
+ 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;
+ getYAxisChoice().setCellRenderer(new MoleculeYAxisRenderer());
+ initConnections();
+ }
+
+ 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);
+ getYAxisChoice().setModel(getDefaultListModelY());
+ 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;
+ 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 cp;
+ }
+
+ 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();
+
+ // 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()) {
+ 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);
+ }
+ }
+ 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);
+ }
+ }
+
+ 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);
+ }
+
+ 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) {
+ lg.debug("onSelectedObjectsChange() called with {} objects", selectedObjects.length);
+
+ }
+ public void refreshData() {
+ 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
+ lg.warn("displayOptionsCollapsiblePanel is null during refreshData()");
+ }
+ 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-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..22dd6b25fb
--- /dev/null
+++ b/vcell-client/src/main/java/cbit/vcell/solver/ode/gui/MoleculeVisualizationPanel.java
@@ -0,0 +1,452 @@
+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.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.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+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.*;
+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 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
+
+ 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
+ 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());
+ 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) ---
+ 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() && "MoleculeSelectionChanged".equals(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);
+ 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)) {
+ lg.debug("valueChanged() called");
+ }
+ }
+ }
+
+ public MoleculeVisualizationPanel(ODEDataViewer owner) {
+ super();
+ this.owner = owner;
+ initialize();
+ initConnections();
+ setVisualizationBackground(Color.WHITE);
+ }
+
+ @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());
+ lg.debug("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(owner);
+ } catch (java.lang.Throwable ivjExc) {
+ handleException(ivjExc);
+ }
+ }
+ return moleculeDataPanel;
+ }
+
+ // ---------------------------------------------------------------
+
+ 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);
+ }
+
+ // ----------------------------------------------------------------------
+
+ private void redrawPlot(MoleculeSpecificationPanel.MoleculeSelection sel) throws ExpressionException {
+ lg.debug("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<>();
+
+ 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);
+
+ if (s.avg != null) { // AVG contributes to globalMax
+ for (double v : s.avg) {
+ if (v > globalMax) globalMax = v;
+ }
+ }
+ if (s.min != null) { // MIN/MAX contributes to globalMin/globalMax
+ for (double v : s.min) {
+ if (v < globalMin) globalMin = v;
+ }
+ }
+ if (s.max != null) {
+ for (double v : s.max) {
+ if (v > globalMax) globalMax = v;
+ }
+ }
+ 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];
+ 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];
+ }
+ 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();
+ }
+
+ 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 {
+ lg.debug("redrawLegend() called with selection: " + sel);
+ 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 {
+ 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) {
+ lg.debug("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() {
+ lg.debug("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(" ");
+ }
+ }
+
+ // -------------------------------------------------------------------------------
+
+ 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) {
+// getMoleculeDataPanel().setSpecialityRenderer(str);
+ }
+
+
+}
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)
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..ec0ac99a0f 100644
--- a/vcell-core/src/main/java/cbit/vcell/simdata/LangevinSolverResultSet.java
+++ b/vcell-core/src/main/java/cbit/vcell/simdata/LangevinSolverResultSet.java
@@ -1,16 +1,31 @@
package cbit.vcell.simdata;
+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.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+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;
+ private static final Logger lg = LogManager.getLogger(LangevinSolverResultSet.class);
+ 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() {
@@ -40,6 +55,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 &&
@@ -71,4 +95,73 @@ 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();
+ populateMetadata(co);
+ checkTrivial(co);
+ co = getMin();
+ checkTrivial(co);
+ co = getMax();
+ checkTrivial(co);
+ co = getStd();
+ 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)) {
+ lg.warn("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) {
+ if (columnDescription instanceof ODESolverResultSetColumnDescription cd) {
+ double[] data = null;
+ int index = co.findColumn(cd.getName());
+ try {
+ data = co.extractColumn(index);
+ } catch (ExpressionException e) {
+ lg.warn("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);
+ }
+ }
+ }
+
+ public static double[] getSeries(ODESimData data, String columnName) throws ExpressionException {
+ int idx = data.findColumn(columnName);
+ if (idx < 0) return null;
+ return data.extractColumn(idx);
+ }
+
}
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..9c853b1234 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.
@@ -182,7 +185,10 @@ private void connect() throws DataAccessException {
nFSimMolecularConfigurations = getVCDataManager().getNFSimMolecularConfigurations(getVCDataIdentifier());
LangevinBatchResultSet raw = getVCDataManager().getLangevinBatchResultSet(getVCDataIdentifier());
langevinSolverResultSet = new LangevinSolverResultSet(raw); // may be null
- if( langevinSolverResultSet.isAverageDataAvailable()) {
+ if(langevinSolverResultSet != null) {
+ langevinSolverResultSet.postProcess();
+ }
+ if( langevinSolverResultSet != null && langevinSolverResultSet.isAverageDataAvailable()) {
odeSolverResultSet = langevinSolverResultSet.getAvg();
}
}
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)) {
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..3bb8022ab7 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(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(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(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(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
+ };
+
}