/**
* Copyright (C) 2001-2017 by RapidMiner and the contributors
*
* Complete list of developers available at our web site:
*
* http://rapidminer.com
*
* This program is free software: you can redistribute it and/or modify it under the terms of the
* GNU Affero General Public License as published by the Free Software Foundation, either version 3
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
* even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License along with this program.
* If not, see http://www.gnu.org/licenses/.
*/
package com.rapidminer.gui.viewer;
import com.rapidminer.gui.actions.export.AbstractPrintableIOObjectPanel;
import com.rapidminer.gui.tools.SwingTools;
import com.rapidminer.operator.clustering.HierarchicalClusterModel;
import com.rapidminer.operator.clustering.HierarchicalClusterNode;
import com.rapidminer.report.Renderable;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Graphics;
/**
* Plots a dendrogram of a given cluster model. The nodes in the model must have different, non-NaN
* distance values for this operator to work.
*
* @author Sebastian Land, Michael Wurst
*
*/
public class DendrogramPlotter extends AbstractPrintableIOObjectPanel implements Renderable {
private static final long serialVersionUID = 2892192060246909733L;
private static final int MARGIN = 10;
private static final int MIN_HEIGHT = 2;
private static final int MIN_WIDTH = 5;
private HierarchicalClusterModel hcm;
private int numObjects;
private double maxDistance;
private double minDistance;
private int maxX;
private int maxY;
private int count;
private Color color = SwingTools.DARKEST_BLUE;
public DendrogramPlotter(HierarchicalClusterModel hcm) {
super(hcm, "dendogram_view");
this.hcm = hcm;
numObjects = hcm.getRootNode().getNumberOfExamplesInSubtree();
minDistance = Double.POSITIVE_INFINITY;
maxDistance = Double.NEGATIVE_INFINITY;
findMinMaxDistance(hcm.getRootNode());
}
private void findMinMaxDistance(HierarchicalClusterNode node) {
double distance = node.getDistance();
maxDistance = Math.max(maxDistance, distance);
minDistance = Math.min(minDistance, distance);
for (HierarchicalClusterNode subNode : node.getSubNodes()) {
findMinMaxDistance(subNode);
}
}
private void drawLine(int x1, int y1, int x2, int y2, Graphics g) {
g.setColor(color);
g.drawLine(x1, y1, x2, y2);
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
if ((minDistance == maxDistance) || (Double.isNaN(minDistance)) || (Double.isInfinite(minDistance))
|| (Double.isNaN(maxDistance)) || (Double.isInfinite(maxDistance))) {
g.drawString("Dendrogram not available for this cluster model. Use an agglomerative clusterer.", MARGIN,
MARGIN + 15);
return;
}
this.maxX = getWidth() - 2 * MARGIN;
this.maxY = getHeight() - 2 * MARGIN;
g.setColor(Color.WHITE);
g.fillRect(0, 0, getWidth(), getHeight());
Graphics translated = g.create();
translated.translate(MARGIN, MARGIN);
count = 0;
paintRecursively(hcm.getRootNode(), hcm.getRootNode().getDistance(), translated);
}
private int weightToYPos(double weight) {
return (int) Math.round(maxY * (((maxDistance - weight) - minDistance) / ((maxDistance - minDistance))));
}
private int countToXPos(int count) {
return (int) Math.round((((double) count) / ((double) numObjects)) * (maxX));
}
private int paintRecursively(HierarchicalClusterNode node, double baseDistance, Graphics g) {
int leftPos = -1;
int rightPos = -1;
// doing recursive descent
for (HierarchicalClusterNode subNode : node.getSubNodes()) {
if ((subNode.getNumberOfSubNodes() > 0) || (subNode.getNumberOfExamplesInSubtree() > 1)) {
int currentPos = paintRecursively(subNode, node.getDistance(), g);
if (leftPos == -1) {
leftPos = currentPos;
}
rightPos = currentPos;
}
}
// drawing vertical cluster lines of one elemental clusters
for (HierarchicalClusterNode subNode : node.getSubNodes()) {
if ((subNode.getNumberOfExamplesInSubtree() == 1) && (subNode.getNumberOfSubNodes() == 0)) {
int currentPos = countToXPos(count);
drawLine(currentPos, weightToYPos(node.getDistance()), currentPos, weightToYPos(minDistance), g);
if (leftPos == -1) {
leftPos = currentPos;
}
rightPos = currentPos;
count++;
}
}
int middlePos = (rightPos + leftPos) / 2;
// painting vertical connections of merged clusters to next cluster
drawLine(middlePos, weightToYPos(baseDistance), middlePos, weightToYPos(node.getDistance()), g);
// painting horizontal connections of merged clusters
drawLine(leftPos, weightToYPos(node.getDistance()), rightPos, weightToYPos(node.getDistance()), g);
return middlePos;
}
@Override
public void prepareRendering() {}
@Override
public void finishRendering() {}
@Override
public int getRenderHeight(int preferredHeight) {
int height = getHeight();
if (height < 1) {
height = preferredHeight;
}
return height;
}
@Override
public int getRenderWidth(int preferredWidth) {
int width = getWidth();
if (width < 1) {
width = preferredWidth;
}
return width;
}
@Override
public void render(Graphics graphics, int width, int height) {
setSize(width, height);
paint(graphics);
}
@Override
public Dimension getPreferredSize() {
int width = MIN_WIDTH * numObjects;
int height = MIN_HEIGHT * numObjects;
Dimension current = super.getPreferredSize();
width = Math.max(width, current.width);
height = Math.max(height, current.height);
return new Dimension(width, height);
}
@Override
public Component getExportComponent() {
return this;
}
}