1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38 package ffx.numerics.clustering.visualization;
39
40 import ffx.numerics.clustering.Cluster;
41
42 import javax.swing.JPanel;
43 import java.awt.BasicStroke;
44 import java.awt.Color;
45 import java.awt.Graphics;
46 import java.awt.Graphics2D;
47 import java.awt.RenderingHints;
48 import java.awt.geom.Rectangle2D;
49 import java.io.Serial;
50
51
52
53
54
55
56
57
58
59 public class DendrogramPanel extends JPanel {
60
61 @Serial
62 private static final long serialVersionUID = 1L;
63
64 private final static BasicStroke SOLID_STROKE =
65 new BasicStroke(1.0f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_ROUND);
66
67 private Cluster model;
68 private ClusterComponent component;
69 private Color lineColor = Color.BLACK;
70 private boolean showDistanceValues = false;
71 private boolean showScale = true;
72 private int borderTop = 20;
73 private int borderLeft = 20;
74 private int borderRight = 20;
75 private int borderBottom = 20;
76 private int scalePadding = 10;
77 private int scaleTickLength = 4;
78 private int scaleTickLabelPadding = 4;
79 private double scaleValueInterval = 0;
80 private int scaleValueDecimals = 0;
81
82 private double xModelOrigin = 0.0;
83 private double yModelOrigin = 0.0;
84 private double wModel = 0.0;
85 private double hModel = 0.0;
86
87
88
89
90
91
92 public boolean isShowDistanceValues() {
93 return showDistanceValues;
94 }
95
96
97
98
99
100
101 public void setShowDistances(boolean showDistanceValues) {
102 this.showDistanceValues = showDistanceValues;
103 }
104
105
106
107
108
109
110 public boolean isShowScale() {
111 return showScale;
112 }
113
114
115
116
117
118
119 public void setShowScale(boolean showScale) {
120 this.showScale = showScale;
121 }
122
123
124
125
126
127
128 public int getScalePadding() {
129 return scalePadding;
130 }
131
132
133
134
135
136
137 public void setScalePadding(int scalePadding) {
138 this.scalePadding = scalePadding;
139 }
140
141 public int getScaleTickLength() {
142 return scaleTickLength;
143 }
144
145 public void setScaleTickLength(int scaleTickLength) {
146 this.scaleTickLength = scaleTickLength;
147 }
148
149 public double getScaleValueInterval() {
150 return scaleValueInterval;
151 }
152
153 public void setScaleValueInterval(double scaleTickInterval) {
154 this.scaleValueInterval = scaleTickInterval;
155 }
156
157 public int getScaleValueDecimals() {
158 return scaleValueDecimals;
159 }
160
161 public void setScaleValueDecimals(int scaleValueDecimals) {
162 this.scaleValueDecimals = scaleValueDecimals;
163 }
164
165 public int getBorderTop() {
166 return borderTop;
167 }
168
169 public void setBorderTop(int borderTop) {
170 this.borderTop = borderTop;
171 }
172
173 public int getBorderLeft() {
174 return borderLeft;
175 }
176
177 public void setBorderLeft(int borderLeft) {
178 this.borderLeft = borderLeft;
179 }
180
181 public int getBorderRight() {
182 return borderRight;
183 }
184
185 public void setBorderRight(int borderRight) {
186 this.borderRight = borderRight;
187 }
188
189 public int getBorderBottom() {
190 return borderBottom;
191 }
192
193 public void setBorderBottom(int borderBottom) {
194 this.borderBottom = borderBottom;
195 }
196
197 public Color getLineColor() {
198 return lineColor;
199 }
200
201 public void setLineColor(Color lineColor) {
202 this.lineColor = lineColor;
203 }
204
205 public Cluster getModel() {
206 return model;
207 }
208
209 public void setModel(Cluster model) {
210 this.model = model;
211 component = createComponent(model);
212 updateModelMetrics();
213 }
214
215 private void updateModelMetrics() {
216 double minX = component.getRectMinX();
217 double maxX = component.getRectMaxX();
218 double minY = component.getRectMinY();
219 double maxY = component.getRectMaxY();
220
221 xModelOrigin = minX;
222 yModelOrigin = minY;
223 wModel = maxX - minX;
224 hModel = maxY - minY;
225 }
226
227 private ClusterComponent createComponent(Cluster cluster, VCoord initCoord, double clusterHeight) {
228
229 ClusterComponent comp = null;
230 if (cluster != null) {
231 comp = new ClusterComponent(cluster, cluster.isLeaf(), initCoord);
232 double leafHeight = clusterHeight / cluster.countLeafs();
233 double yChild = initCoord.y() - (clusterHeight / 2);
234 double distance = cluster.getDistanceValue() == null ? 0 : cluster.getDistanceValue();
235 for (Cluster child : cluster.getChildren()) {
236 int childLeafCount = child.countLeafs();
237 double childHeight = childLeafCount * leafHeight;
238 double childDistance = child.getDistanceValue() == null ? 0 : child.getDistanceValue();
239 VCoord childInitCoord = new VCoord(
240 initCoord.x() + (distance - childDistance),
241 yChild + childHeight / 2.0);
242 yChild += childHeight;
243
244
245 ClusterComponent childComp = createComponent(child, childInitCoord, childHeight);
246
247 childComp.setLinkPoint(initCoord);
248 comp.getChildren().add(childComp);
249 }
250 }
251 return comp;
252
253 }
254
255 private ClusterComponent createComponent(Cluster model) {
256
257 double virtualModelHeight = 1;
258 VCoord initCoord = new VCoord(0, virtualModelHeight / 2);
259
260 ClusterComponent comp = createComponent(model, initCoord, virtualModelHeight);
261 comp.setLinkPoint(initCoord);
262 return comp;
263 }
264
265 @Override
266 public void paint(Graphics g) {
267 super.paint(g);
268 Graphics2D g2 = (Graphics2D) g;
269 g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
270 g2.setColor(lineColor);
271 g2.setStroke(SOLID_STROKE);
272
273 int wDisplay = getWidth() - borderLeft - borderRight;
274 int hDisplay = getHeight() - borderTop - borderBottom;
275 int xDisplayOrigin = borderLeft;
276 int yDisplayOrigin = borderBottom;
277
278 if (component != null) {
279
280 int nameGutterWidth = component.getMaxNameWidth(g2, false) + component.getNamePadding();
281 wDisplay -= nameGutterWidth;
282
283 if (showScale) {
284 Rectangle2D rect = g2.getFontMetrics().getStringBounds("0", g2);
285 int scaleHeight = (int) rect.getHeight() + scalePadding + scaleTickLength + scaleTickLabelPadding;
286 hDisplay -= scaleHeight;
287 yDisplayOrigin += scaleHeight;
288 }
289
290
291 double xFactor = wDisplay / wModel;
292 double yFactor = hDisplay / hModel;
293 int xOffset = (int) (xDisplayOrigin - xModelOrigin * xFactor);
294 int yOffset = (int) (yDisplayOrigin - yModelOrigin * yFactor);
295 component.paint(g2, xOffset, yOffset, xFactor, yFactor, showDistanceValues);
296
297 if (showScale) {
298 int x1 = xDisplayOrigin;
299 int y1 = yDisplayOrigin - scalePadding;
300 int x2 = x1 + wDisplay;
301 int y2 = y1;
302 g2.drawLine(x1, y1, x2, y2);
303
304 double totalDistance = component.getCluster().getTotalDistance();
305 double xModelInterval;
306 if (scaleValueInterval <= 0) {
307 xModelInterval = totalDistance / 10.0;
308 } else {
309 xModelInterval = scaleValueInterval;
310 }
311
312 int xTick = xDisplayOrigin + wDisplay;
313 y1 = yDisplayOrigin - scalePadding;
314 y2 = yDisplayOrigin - scalePadding - scaleTickLength;
315 double distanceValue = 0;
316 double xDisplayInterval = xModelInterval * xFactor;
317 while (xTick >= xDisplayOrigin) {
318 g2.drawLine(xTick, y1, xTick, y2);
319
320 String distanceValueStr = String.format("%." + scaleValueDecimals + "f", distanceValue);
321 Rectangle2D rect = g2.getFontMetrics().getStringBounds(distanceValueStr, g2);
322 g2.drawString(distanceValueStr, (int) (xTick - (rect.getWidth() / 2)), y2 - scaleTickLabelPadding);
323 xTick -= xDisplayInterval;
324 distanceValue += xModelInterval;
325 }
326
327 }
328 } else {
329
330
331 String str = "No data";
332 Rectangle2D rect = g2.getFontMetrics().getStringBounds(str, g2);
333 int xt = (int) (wDisplay / 2.0 - rect.getWidth() / 2.0);
334 int yt = (int) (hDisplay / 2.0 - rect.getHeight() / 2.0);
335 g2.drawString(str, xt, yt);
336 }
337 }
338 }