Skip to content

Commit 4bc2155

Browse files
committed
Hovering over legend, better color balance.
1 parent eae5281 commit 4bc2155

5 files changed

Lines changed: 160 additions & 64 deletions

File tree

vcell-client/src/main/java/cbit/plot/gui/AbstractPlotPanel.java

Lines changed: 104 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import java.awt.*;
1010
import java.awt.event.*;
1111
import java.util.ArrayList;
12+
import java.util.Collections;
1213
import java.util.List;
1314
import java.util.function.Consumer;
1415

@@ -45,9 +46,11 @@ public class RendererOptions {
4546
private boolean showBubbleSolid = false; // whether to draw bubbles as solid circles (instead of empty circles)
4647
private boolean showBubbleFading = true; // whether to fade bubbles (with alpha) based on their size
4748
private boolean showBubbleAsEmptyCircles = false; // whether to draw bubbles as empty circles
49+
boolean showOnlySelectedSeries = true; // whether to hide the gaps in the multi-selection entities
4850
}
51+
4952
// Renderer options
50-
private final RendererOptions options = new RendererOptions();
53+
final RendererOptions options = new RendererOptions();
5154
protected JDialog optionsDialog = null;
5255

5356
// Insets and strokes
@@ -60,21 +63,23 @@ public class RendererOptions {
6063
protected static final float CURVE_STROKE = 1.5f; // stroke for the main curve (avg line); bands will be filled, not stroked
6164
protected static final int DIMMED_LINE_ALPHA = 20; // for dimming non-hovered series (0–255)
6265
protected static final int DIMMED_BAND_ALPHA = 0; // for dimming non-hovered bands (0–255)
66+
protected static final int DIMMED_SOLID_BUBBLE_ALPHA = 30; // for dimming bubble histograms
67+
protected static final int DIMMED_FADING_BUBBLE_ALPHA = 2;
6368

6469
// Renderers list
6570
protected final List<SeriesRenderer> renderers = new ArrayList<>();
6671

6772
// Scaling state
6873
protected double globalMin = 0; // on the-y axis; x-axis is always 0 to dt*(N-1)
6974
protected double globalMax = 1;
70-
protected double dt = 1;
75+
protected double dt = 1; // time step between consecutive points in seconds
7176

7277
// Crosshair state
7378
protected Integer mouseX = null; // mouse coordinates in pixels, relative to the panel; null if mouse is outside the plot area
7479
protected Integer mouseY = null;
7580
protected Integer snappedX = null; // we are snapped here (or null)
7681
protected Integer snappedY = null;
77-
private BubbleHit lastBubbleHit; // used for snapping to bubbles and showing tooltip info about the bubble we snapped to
82+
// private BubbleHit lastBubbleHit; // used for snapping to bubbles and showing tooltip info about the bubble we snapped to
7883
protected Consumer<double[]> coordCallback;
7984

8085
// Cached plot area
@@ -100,7 +105,7 @@ public void mouseMoved(MouseEvent e) {
100105
mouseY = null;
101106
snappedX = null;
102107
snappedY = null;
103-
lastBubbleHit = null;
108+
// lastBubbleHit = null; // don't seem to need it
104109
if (coordCallback != null) {
105110
coordCallback.accept(null);
106111
}
@@ -151,7 +156,7 @@ public void mouseMoved(MouseEvent e) {
151156
if (isSnapToNodes()) {
152157
hit = findClosestBubble(mx, my);
153158
}
154-
lastBubbleHit = hit;
159+
// lastBubbleHit = hit;
155160
if (hit != null) {
156161
// highlight bubble
157162
snappedX = hit.px;
@@ -180,7 +185,7 @@ public void mouseExited(MouseEvent e) {
180185
mouseY = null;
181186
snappedX = null;
182187
snappedY = null;
183-
lastBubbleHit = null;
188+
// lastBubbleHit = null;
184189
if (coordCallback != null) {
185190
coordCallback.accept(null);
186191
}
@@ -362,6 +367,10 @@ public void setShowBubbleSolid(boolean v) {
362367
}
363368
repaint();
364369
}
370+
public void setShowOnlySelectedSeries(boolean v) {
371+
this.options.showOnlySelectedSeries = v;
372+
repaint();
373+
}
365374

366375
public boolean isShowAvgAsStep() { return options.showAvgAsStep; }
367376
public boolean isShowBandAsStep() { return options.showBandAsStep; }
@@ -387,6 +396,9 @@ public boolean isShowBubbleFading() {
387396
public boolean isShowBubbleAsEmptyCircles() {
388397
return options.showBubbleAsEmptyCircles;
389398
}
399+
public boolean isShowOnlySelectedSeries() {
400+
return options.showOnlySelectedSeries;
401+
}
390402

391403
private Point findClosestNode(int mouseX, int mouseY) { // use for SnapToNodes feature
392404
Point best = null;
@@ -432,6 +444,16 @@ private BubbleHit findClosestBubble(int mouseX, int mouseY) {
432444
return best;
433445
}
434446

447+
int getOrdinalIndexForClusterSize(int cs) {
448+
int idx = 0;
449+
for (SeriesRenderer r : renderers) {
450+
if (r instanceof BubbleRenderer bub) {
451+
if (bub.getClusterSize() == cs) return idx;
452+
idx++;
453+
}
454+
}
455+
return 0; // fallback
456+
}
435457

436458
public void setHoveredSeriesName(String name) {
437459
this.hoveredSeriesName = name;
@@ -504,22 +526,42 @@ protected void paintComponent(Graphics g) {
504526
return;
505527
}
506528

529+
// --- collect selected cluster sizes (needed for bubble mode only)
530+
List<Integer> selectedSizes = null;
531+
if (hasBubble && options.showOnlySelectedSeries) {
532+
selectedSizes = new ArrayList<>();
533+
for (SeriesRenderer r : renderers) {
534+
if (r instanceof BubbleRenderer bub) {
535+
selectedSizes.add(bub.getClusterSize());
536+
}
537+
}
538+
Collections.sort(selectedSizes);
539+
}
540+
507541
// --- compute axis scaling -----------------------------------------
508542
double yMaxRounded;
509543
double yMinRounded;
510544
if(!hasBubble) {
511545
yMaxRounded = roundUpNice(globalMax); // in molecules
512546
yMinRounded = (globalMin < 0) ? -roundUpNice(-globalMin) : 0; // may be negative if avg-sd<0
513547
} else {
514-
// bubble mode: Y-axis = cluster size (0,1,2,...)
515-
int maxCluster = Integer.MIN_VALUE;
516-
for (SeriesRenderer r : renderers) {
517-
if (r instanceof BubbleRenderer bub) {
518-
maxCluster = Math.max(maxCluster, bub.getClusterSize());
548+
if (!options.showOnlySelectedSeries) {
549+
// bubble mode showing all between 0 and max selected cluster size
550+
// Y-axis = cluster size (0,1,2,...)
551+
int maxCluster = Integer.MIN_VALUE;
552+
for (SeriesRenderer r : renderers) {
553+
if (r instanceof BubbleRenderer bub) {
554+
maxCluster = Math.max(maxCluster, bub.getClusterSize());
555+
}
519556
}
557+
yMinRounded = 0; // Y-axis always starts at 0
558+
yMaxRounded = maxCluster + 0.5; // top padding: +0.5 so the top cluster size is centered
559+
} else {
560+
// compressed mode: only selected sizes
561+
int count = selectedSizes.size();
562+
yMinRounded = 0;
563+
yMaxRounded = count + 0.5; // 0 baseline + count rows above
520564
}
521-
yMinRounded = 0; // Y-axis always starts at 0
522-
yMaxRounded = maxCluster + 0.5; // top padding: +0.5 so the top cluster size is centered
523565
}
524566
double xMax = dt * (maxLength - 1); // in seconds
525567
double xMaxRounded = roundUpNice(xMax);
@@ -563,12 +605,22 @@ protected void paintComponent(Graphics g) {
563605
}
564606
}
565607
} else {
566-
// bubble mode: grid at every integer cluster size
567-
int maxCluster = (int)Math.floor(yMaxRounded - 0.5);
568-
for (int cs = 0; cs <= maxCluster; cs++) {
569-
double norm = (cs - yMinRounded) / (yMaxRounded - yMinRounded);
570-
int yPix = y0 - (int)Math.round(norm * plotHeight);
571-
g2.drawLine(x0, yPix, x1, yPix);
608+
if (!options.showOnlySelectedSeries) {
609+
// bubble all mode: grid at every integer cluster size
610+
int maxCluster = (int)Math.floor(yMaxRounded - 0.5);
611+
for (int cs = 0; cs <= maxCluster; cs++) {
612+
double norm = (cs - yMinRounded) / (yMaxRounded - yMinRounded);
613+
int yPix = y0 - (int)Math.round(norm * plotHeight);
614+
g2.drawLine(x0, yPix, x1, yPix);
615+
}
616+
} else {
617+
// bubble compressed mode: only selected cluster sizes
618+
for (int i = 0; i < selectedSizes.size(); i++) {
619+
int row = i+1; // row 0 is for baseline (horizontal axis), then row 1 for first selected size, etc.
620+
double norm = (row - yMinRounded) / (yMaxRounded - yMinRounded);
621+
int yPix = y0 - (int)Math.round(norm * plotHeight);
622+
g2.drawLine(x0, yPix, x1, yPix);
623+
}
572624
}
573625
}
574626

@@ -626,14 +678,29 @@ protected void paintComponent(Graphics g) {
626678
}
627679
}
628680
} else {
629-
int maxCluster = (int)Math.floor(yMaxRounded - 0.5);
630-
for (int cs = 0; cs <= maxCluster; cs++) {
631-
double norm = (cs - yMinRounded) / (yMaxRounded - yMinRounded);
632-
int yPix = y0 - (int)Math.round(norm * plotHeight);
633-
g2.drawLine(x0 - 5, yPix, x0, yPix); // major tick
634-
String label = Integer.toString(cs); // label
635-
int sw = fm.stringWidth(label);
636-
g2.drawString(label, x0 - 10 - sw, yPix + fm.getAscent() / 2);
681+
if (!options.showOnlySelectedSeries) {
682+
// show all cluster sizes on the y-axis, from 0 to the max one selected
683+
int maxCluster = (int)Math.floor(yMaxRounded - 0.5);
684+
for (int cs = 0; cs <= maxCluster; cs++) {
685+
double norm = (cs - yMinRounded) / (yMaxRounded - yMinRounded);
686+
int yPix = y0 - (int)Math.round(norm * plotHeight);
687+
g2.drawLine(x0 - 5, yPix, x0, yPix);
688+
String label = Integer.toString(cs);
689+
int sw = fm.stringWidth(label);
690+
g2.drawString(label, x0 - 10 - sw, yPix + fm.getAscent() / 2);
691+
}
692+
} else {
693+
// compressed mode
694+
for (int i = 0; i < selectedSizes.size(); i++) {
695+
int cs = selectedSizes.get(i);
696+
int row = i+1; // row 0 is for baseline (horizontal axis), then row 1 for first selected size, etc.
697+
double norm = (row - yMinRounded) / (yMaxRounded - yMinRounded);
698+
int yPix = y0 - (int)Math.round(norm * plotHeight);
699+
g2.drawLine(x0 - 5, yPix, x0, yPix);
700+
String label = Integer.toString(cs);
701+
int sw = fm.stringWidth(label);
702+
g2.drawString(label, x0 - 10 - sw, yPix + fm.getAscent() / 2);
703+
}
637704
}
638705
}
639706
double xStep = xMajor[1] - xMajor[0]; // x-axis ticks (on the horizontal axis)
@@ -661,15 +728,6 @@ protected void paintComponent(Graphics g) {
661728
g2.drawLine(x0, mouseY, x1, mouseY);
662729
}
663730

664-
// Highlight snapped point (if any)
665-
if (snappedX != null && snappedY != null) {
666-
Graphics2D g3 = (Graphics2D) g2.create();
667-
g3.setColor(new Color(255, 80, 0)); // bright red-ish
668-
int r = 4; // radius of highlight
669-
g3.fillOval(snappedX - r, snappedY - r, 2*r, 2*r);
670-
g3.dispose();
671-
}
672-
673731
// Renderers (bands first, then bubbles, then lines)
674732
for (SeriesRenderer r : renderers) {
675733
if (r instanceof BandRenderer || r instanceof BarRenderer) {
@@ -686,5 +744,15 @@ protected void paintComponent(Graphics g) {
686744
r.draw(g2, x0, x1, y0, y1, plotWidth, plotHeight, xMaxRounded, yMaxRounded, yMinRounded, dt);
687745
}
688746
}
747+
748+
// Highlight snapped point (if any)
749+
if (snappedX != null && snappedY != null) {
750+
Graphics2D g3 = (Graphics2D) g2.create();
751+
g3.setColor(new Color(255, 80, 0)); // bright red-ish
752+
int r = 3; // radius of highlight
753+
g3.fillOval(snappedX - r, snappedY - r, 2*r, 2*r);
754+
g3.dispose();
755+
}
756+
689757
}
690758
}

vcell-client/src/main/java/cbit/plot/gui/BubbleOptionsPanel.java

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,13 @@ public BubbleOptionsPanel(AbstractPlotPanel plot) {
1212

1313
int row = 0;
1414

15-
// // --- Show nodes ---
16-
// JCheckBox showNodes = new JCheckBox("Show nodes", plot.getShowNodes());
17-
// gbc.gridx = 0;
18-
// gbc.gridy = row;
19-
// gbc.insets = new Insets(4, 6, 2, 0);
20-
// add(showNodes, gbc);
21-
// showNodes.addActionListener(e -> plot.setShowNodes(showNodes.isSelected()));
15+
// --- showOnlySelectedSeries ---
16+
JCheckBox showOnlySelected = new JCheckBox("Only Selected", plot.isShowOnlySelectedSeries());
17+
gbc.gridx = 0;
18+
gbc.gridy = row;
19+
gbc.insets = new Insets(4, 6, 2, 0);
20+
add(showOnlySelected, gbc);
21+
showOnlySelected.addActionListener(e -> plot.setShowOnlySelectedSeries(showOnlySelected.isSelected()));
2222

2323
// --- Snap to nodes ---
2424
JCheckBox snapToNodes = new JCheckBox("Snap to nodes", plot.isSnapToNodes());

vcell-client/src/main/java/cbit/plot/gui/PlotRenderers.java

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -418,15 +418,24 @@ public void draw(Graphics2D g2,
418418
// 1. Determine the vertical pixel position for this cluster size
419419
// In bubble mode, Y-axis = cluster size (a discrete category).
420420
// All bubbles for this series lie on one horizontal line.
421-
double yNorm = (clusterSize - yMinRounded) / (yMaxRounded - yMinRounded);
422-
int yPix = y0 - (int) Math.round(yNorm * plotHeight);
421+
double yNorm;
422+
if (!parent.options.showOnlySelectedSeries) {
423+
// original behavior
424+
yNorm = (clusterSize - yMinRounded) / (yMaxRounded - yMinRounded);
425+
} else {
426+
// compressed mode: map clusterSize → ordinal index
427+
int index = parent.getOrdinalIndexForClusterSize(clusterSize);
428+
int row = index + 1; // shift up, keep 0 empty
429+
yNorm = (row - yMinRounded) / (yMaxRounded - yMinRounded);
430+
}
431+
int yPix = y0 - (int)Math.round(yNorm * plotHeight);
423432

424433
// 2. Bubble size scaling
425434
// Diameter encodes the count (or mass).
426435
// Scaling is GLOBAL across all cluster sizes, not per-series.
427436
// This ensures correct proportionality (e.g., 15.4 vs 0.05).
428437
final double minDiam = 4.0; // smallest visible bubble
429-
final double maxDiam = 30.0; // largest bubble
438+
final double maxDiam = 36.0; // largest bubble
430439
double globalMax;
431440
if (parent instanceof ClusterPlotPanel) {
432441
globalMax = ((ClusterPlotPanel) parent).getMaxClusterOverall();
@@ -475,18 +484,16 @@ public void draw(Graphics2D g2,
475484
c = color;
476485
}
477486

478-
// 5. Hover dimming
479-
// If another series is hovered, dim this one.
480-
// if (parent.hoveredSeriesName != null &&
481-
// !parent.hoveredSeriesName.equals(seriesName)) {
482-
// c = new Color(c.getRed(), c.getGreen(), c.getBlue(), DIMMED_LINE_ALPHA);
483-
// }
484-
487+
// 5. Hover dimming - if another series is hovered, dim this one.
488+
if (parent.hoveredSeriesName != null && !parent.hoveredSeriesName.equals(seriesName)) {
489+
c = new Color(c.getRed(), c.getGreen(), c.getBlue(), DIMMED_SOLID_BUBBLE_ALPHA);
490+
if(parent.isShowBubbleFading()) {
491+
c = new Color(c.getRed(), c.getGreen(), c.getBlue(), DIMMED_FADING_BUBBLE_ALPHA);
492+
}
493+
}
485494
g2.setColor(c);
486495

487-
// ---------------------------------------------------------------------
488496
// 6. Draw bubbles (mutually exclusive modes)
489-
// ---------------------------------------------------------------------
490497
if (parent.isShowBubbleAsEmptyCircles()) { // --- Contour Bubble ---
491498
for (int i = 0; i < n; i++) {
492499
int d = diameters[i];
@@ -523,10 +530,14 @@ private void drawFadingBubble(Graphics2D g2, int xCenter, int yCenter, int d, Co
523530
float centerY = yCenter;
524531
float radius = r;
525532
// fading colors
526-
Color edge = new Color(base.getRed(), base.getGreen(), base.getBlue(), 60);
527-
Color mid = new Color(base.getRed(), base.getGreen(), base.getBlue(), 128);
533+
Color edge = new Color(base.getRed(), base.getGreen(), base.getBlue(), 40); // 60
534+
Color mid = new Color(base.getRed(), base.getGreen(), base.getBlue(), 160); // 128
535+
if (parent.hoveredSeriesName != null && !parent.hoveredSeriesName.equals(seriesName)) {
536+
edge = new Color(base.getRed(), base.getGreen(), base.getBlue(), 10);
537+
mid = new Color(base.getRed(), base.getGreen(), base.getBlue(), 40);
538+
}
528539
// abrupt drop near center, slow fade to edge
529-
float[] dist = {0.0f, 0.55f, 1.0f};
540+
float[] dist = {0.0f, 0.35f, 1.0f}; // 0.0f, 0.55f, 1.0f
530541
Color[] cols = {base, mid, edge};
531542
RadialGradientPaint paint = new RadialGradientPaint(
532543
new Point2D.Float(centerX, centerY),

vcell-client/src/main/java/cbit/vcell/solver/ode/gui/ClusterVisualizationPanel.java

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,7 @@
2020
import javax.swing.event.ListSelectionEvent;
2121
import javax.swing.event.ListSelectionListener;
2222
import java.awt.*;
23-
import java.awt.event.ActionEvent;
24-
import java.awt.event.ActionListener;
25-
import java.awt.event.ComponentAdapter;
26-
import java.awt.event.ComponentEvent;
23+
import java.awt.event.*;
2724
import java.beans.PropertyChangeEvent;
2825
import java.beans.PropertyChangeListener;
2926
import java.util.*;
@@ -310,6 +307,22 @@ private JComponent createLegendEntry(String name, Color color, ClusterSpecificat
310307
p.setToolTipText(tooltip);
311308
p.add(line);
312309
p.add(text);
310+
311+
// hover behavior - install the same listener on p, line, and text so that hovering anywhere on the legend entry triggers it
312+
MouseAdapter hover = new MouseAdapter() {
313+
@Override
314+
public void mouseEntered(MouseEvent e) {
315+
getClusterPlotPanel().setHoveredSeriesName(name);
316+
}
317+
@Override
318+
public void mouseExited(MouseEvent e) {
319+
// As soon as we leave line/text, we are no longer "over an entity"
320+
getClusterPlotPanel().setHoveredSeriesName(null);
321+
}
322+
};
323+
line.addMouseListener(hover);
324+
text.addMouseListener(hover);
325+
313326
return p;
314327
}
315328

0 commit comments

Comments
 (0)