View Javadoc
1   // ******************************************************************************
2   //
3   // Title:       Force Field X.
4   // Description: Force Field X - Software for Molecular Biophysics.
5   // Copyright:   Copyright (c) Michael J. Schnieders 2001-2025.
6   //
7   // This file is part of Force Field X.
8   //
9   // Force Field X is free software; you can redistribute it and/or modify it
10  // under the terms of the GNU General Public License version 3 as published by
11  // the Free Software Foundation.
12  //
13  // Force Field X is distributed in the hope that it will be useful, but WITHOUT
14  // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
15  // FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
16  // details.
17  //
18  // You should have received a copy of the GNU General Public License along with
19  // Force Field X; if not, write to the Free Software Foundation, Inc., 59 Temple
20  // Place, Suite 330, Boston, MA 02111-1307 USA
21  //
22  // Linking this library statically or dynamically with other modules is making a
23  // combined work based on this library. Thus, the terms and conditions of the
24  // GNU General Public License cover the whole combination.
25  //
26  // As a special exception, the copyright holders of this library give you
27  // permission to link this library with independent modules to produce an
28  // executable, regardless of the license terms of these independent modules, and
29  // to copy and distribute the resulting executable under terms of your choice,
30  // provided that you also meet, for each linked independent module, the terms
31  // and conditions of the license of that module. An independent module is a
32  // module which is not derived from or based on this library. If you modify this
33  // library, you may extend this exception to your version of the library, but
34  // you are not obligated to do so. If you do not wish to do so, delete this
35  // exception statement from your version.
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   * Swing panel responsible for laying out and drawing a dendrogram for a
53   * hierarchical Cluster model, including optional scale and distance labels.
54   *
55   * @author Lars Behnke, 2013
56   * @author Michael J. Schnieders
57   * @since 1.0
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     * Returns whether linkage distance values should be rendered on the dendrogram.
89     *
90     * @return true if distance values are shown
91     */
92    public boolean isShowDistanceValues() {
93      return showDistanceValues;
94    }
95  
96    /**
97     * Sets whether linkage distance values should be rendered on the dendrogram.
98     *
99     * @param showDistanceValues true to show distance values
100    */
101   public void setShowDistances(boolean showDistanceValues) {
102     this.showDistanceValues = showDistanceValues;
103   }
104 
105   /**
106    * Returns whether the X-axis scale should be drawn below the dendrogram.
107    *
108    * @return true if the scale is shown
109    */
110   public boolean isShowScale() {
111     return showScale;
112   }
113 
114   /**
115    * Sets whether the X-axis scale should be drawn below the dendrogram.
116    *
117    * @param showScale true to show the scale
118    */
119   public void setShowScale(boolean showScale) {
120     this.showScale = showScale;
121   }
122 
123   /**
124    * Gets the padding (pixels) between the dendrogram and the scale axis.
125    *
126    * @return padding in pixels
127    */
128   public int getScalePadding() {
129     return scalePadding;
130   }
131 
132   /**
133    * Sets the padding (pixels) between the dendrogram and the scale axis.
134    *
135    * @param scalePadding padding in pixels
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         /* Traverse cluster node tree */
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       /* Calculate conversion factor and offset for display */
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       /* No data available */
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 }