/* * RapidMiner * * Copyright (C) 2001-2008 by Rapid-I and the contributors * * Complete list of developers available at our web site: * * http://rapid-i.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.plotter; import java.awt.Color; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.geom.Rectangle2D; import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Random; import javax.swing.DefaultListModel; import javax.swing.JComboBox; import javax.swing.JComponent; import javax.swing.JLabel; import javax.swing.JList; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; import com.rapidminer.datatable.DataTable; import com.rapidminer.datatable.DataTableRow; import com.rapidminer.gui.plotter.conditions.ColumnsPlotterCondition; import com.rapidminer.gui.plotter.conditions.PlotterCondition; import com.rapidminer.gui.tools.ExtendedJScrollPane; import com.rapidminer.tools.LogService; import com.rapidminer.tools.math.MathFunctions; /** * A Radial coordinate Visualization, hence the name RadViz. * The spring paradigm for displaying high-dimensional data * has been quite successful. M lines radially eminate from the center of * the circle and terminate at the perimeter in special special endpoints * called dimensional anchors (DA). One end of a * spring is attached to each DA. The other end of each spring is attached to a * data point. The spring constant K_j has the value of the j-th coordinate of * the data point. The data point values are typically locally normalized. * Each data point is then displayed at the position that * produces a spring force sum of 0. If all m coordinates have the same value * the data point lies exactly in the center of the circle independently of the * actual values. If the point is a unit vector point it lies exaclty at the fixed * point on the edge of the circle, where the spring for that dimension is fixed. * Many points can map to the same position. This mapping represents a non-linear * transformation of the data that preserves certain symmetries. * * @author Daniel Hakenjos, Ingo Mierswa * @version $Id: RadVizPlotter.java,v 1.5 2008/05/09 19:22:51 ingomierswa Exp $ */ public class RadVizPlotter extends PlotterAdapter { private static final long serialVersionUID = 199188198448229742L; private static final int MAX_NUMBER_OF_COLUMNS = 1000; /** Indicates the initial zoom factor. */ private static final int ZOOM_FACTOR = 50; /** Indicates which type of column mapping should be used. */ private static final String[] COLUMN_MAPPING_TYPES = { "ordered", "weights", "random" }; /** Indicates a ordered column mapping. */ private static final int ORDERED = 0; /** Indicates a ordered column mapping. */ private static final int WEIGHTS = 1; /** Indicates a ordered column mapping. */ private static final int RANDOM = 2; /** The list of all plotter points. */ protected List<PlotterPoint> plotterPoints = new LinkedList<PlotterPoint>(); /** The currently used data table. */ protected transient DataTable dataTable; /** Maps the axes to the data table columns. */ protected int[] columnMapping; /** The maximum column weight (if weights are available in data table). */ protected double maxWeight = Double.NaN; /** The vector directions of the axes of the rad viz. */ protected double[] anchorVectorX, anchorVectorY; /** The angles between the axes. */ private double[] angles; /** The column which should be used to colorize the data points. */ protected int colorColumn = -1; /** The minimum value of the color column. */ private double minColor; /** The maximum value of the color column. */ private double maxColor; /** Selection of column mapping. */ private JComboBox columnMappingSelection; /** The list of columns which should not be used as dimension anchors. */ protected JList ignoreList; /** The currently selected type of column mapping. Default is ORDERED. */ private int columnMappingType = ORDERED; /** The scaling factor for point plotting, usually 1. */ protected double scale = 1; /** Currently used random seed for random ordering. */ private long orderRandomSeed = 2001; /** The random number generator for random seeds. */ private Random randomSeedRandom = new Random(); /** Creates a new RadViz plotter. */ public RadVizPlotter() { super(); setBackground(Color.white); this.columnMappingSelection = new JComboBox(COLUMN_MAPPING_TYPES); columnMappingSelection.setToolTipText("Indicates the type of column mapping (reordering)."); this.columnMappingSelection.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { setColumnMapping(columnMappingSelection.getSelectedIndex()); } }); this.ignoreList = new JList(new DefaultListModel()); this.ignoreList.setToolTipText("The selected columns will not be used as dimension anchors."); ignoreList.addListSelectionListener(new ListSelectionListener() { public void valueChanged(ListSelectionEvent e) { repaint(); } }); } /** Creates a new RadViz plotter from the given data table. */ public RadVizPlotter(DataTable dataTable) { this(); setDataTable(dataTable); } public PlotterCondition getPlotterCondition() { return new ColumnsPlotterCondition(MAX_NUMBER_OF_COLUMNS); } public void setDataTable(DataTable dataTable) { super.setDataTable(dataTable); this.dataTable = dataTable; // ignore list DefaultListModel ignoreModel = (DefaultListModel)ignoreList.getModel(); ignoreModel.clear(); for (int i = 0; i < this.dataTable.getNumberOfColumns(); i++) { if (i == colorColumn) continue; ignoreModel.addElement(this.dataTable.getColumnName(i)); } this.maxWeight = getMaxWeight(dataTable); repaint(); } public void setPlotColumn(int index, boolean plot) { if (plot) this.colorColumn = index; else this.colorColumn = -1; // ignore list DefaultListModel ignoreModel = (DefaultListModel)ignoreList.getModel(); ignoreModel.clear(); for (int i = 0; i < this.dataTable.getNumberOfColumns(); i++) { if (i == this.colorColumn) continue; ignoreModel.addElement(this.dataTable.getColumnName(i)); } repaint(); } public boolean getPlotColumn(int index) { return colorColumn == index; } public String getPlotName() { return "Color"; } public JComponent getOptionsComponent(int index) { if (index == 0) { JLabel label = new JLabel("Column mapping:"); label.setToolTipText("Indicates the type of column mapping (reordering)."); return label; } else if (index == 1) { return columnMappingSelection; } else if (index == 2) { JLabel label = new JLabel("Ignore columns:"); label.setToolTipText("The selected columns will not be used as dimension anchors."); return label; } else if (index == 3) { return new ExtendedJScrollPane(ignoreList); } else { return null; } } public boolean canHandleZooming() { return true; } public void setZooming(int zooming) { this.scale = zooming / (double)ZOOM_FACTOR; repaint(); } public int getInitialZoomFactor() { return ZOOM_FACTOR; } // =============================================================== private void setColumnMapping(int mapping) { this.columnMappingType = mapping; if (mapping == RANDOM) { this.orderRandomSeed = randomSeedRandom.nextLong(); } repaint(); } protected boolean shouldIgnoreColumn(int column) { return shouldIgnoreColumn(this.dataTable.getColumnName(this.columnMapping[column])); } protected boolean shouldIgnoreColumn(String column) { Object[] ignoredColumns = ignoreList.getSelectedValues(); for (int i = 0; i < ignoredColumns.length; i++) { if (ignoredColumns[i].equals(column)) { return true; } } return false; } /** Creates the column mapping. */ private void calculateColumnMapping() { this.columnMapping = new int[this.dataTable.getNumberOfColumns()]; for (int i = 0; i < this.columnMapping.length; i++) { this.columnMapping[i] = i; } switch (columnMappingType) { case ORDERED: // does nothing break; case WEIGHTS: if (this.dataTable.isSupportingColumnWeights()) { this.columnMapping = new int[this.dataTable.getNumberOfColumns()]; List<WeightIndex> indices = new LinkedList<WeightIndex>(); for (int i = 0; i < this.dataTable.getNumberOfColumns(); i++) { if ((colorColumn != i) && (!shouldIgnoreColumn(i))) indices.add(new WeightIndex(i, Math.abs(this.dataTable.getColumnWeight(i)))); else indices.add(new WeightIndex(i, 0.0d)); } Collections.sort(indices); Iterator<WeightIndex> w = indices.iterator(); int counter = 0; while (w.hasNext()) { this.columnMapping[counter++] = w.next().getIndex(); } } else { LogService.getGlobal().log("Cannot use weight based ordering since no column weights are given.", LogService.WARNING); } break; case RANDOM: this.columnMapping = new int[this.dataTable.getNumberOfColumns()]; List<Integer> indices = new ArrayList<Integer>(); for (int i = 0; i < this.columnMapping.length; i++) { this.columnMapping[i] = i; if ((colorColumn != i) && (!shouldIgnoreColumn(i))) indices.add(i); } Random random = new Random(orderRandomSeed); for (int i = 0; i < this.columnMapping.length; i++) { if ((colorColumn != i) && (!shouldIgnoreColumn(i))) { int other = indices.get(random.nextInt(indices.size())); int dummy = this.columnMapping[i]; this.columnMapping[i] = this.columnMapping[other]; this.columnMapping[other] = dummy; } } break; default: break; } } /** * Calculates the sample points in the RadViz. */ protected void calculateSamplePoints() { plotterPoints.clear(); // color min and max this.minColor = Double.POSITIVE_INFINITY; this.maxColor = Double.NEGATIVE_INFINITY; if (colorColumn >= 0) { Iterator<DataTableRow> sample = this.dataTable.iterator(); while (sample.hasNext()) { DataTableRow row = sample.next(); double color = row.getValue(colorColumn); this.minColor = MathFunctions.robustMin(minColor, color); this.maxColor = MathFunctions.robustMax(maxColor, color); } } Iterator<DataTableRow> sample = this.dataTable.iterator(); while (sample.hasNext()) { DataTableRow row = sample.next(); double xPos = 0.0d; double yPos = 0.0d; // calculate sum and fetch color double sum = 0.0d; for (int d = 0; d < this.dataTable.getNumberOfColumns(); d++) { if ((d != colorColumn) && (!shouldIgnoreColumn(d))) { sum += row.getValue(columnMapping[d]); } } // calculate w double[] w = new double[this.dataTable.getNumberOfColumns()]; for (int d = 0; d < this.dataTable.getNumberOfColumns(); d++) { if ((d == colorColumn) || (shouldIgnoreColumn(d))) continue; w[d] = row.getValue(columnMapping[d]) / sum; } // calculate u, i.e. the x and y pos in rad viz space for (int d = 0; d < this.dataTable.getNumberOfColumns(); d++) { if ((d == colorColumn) || (shouldIgnoreColumn(d))) continue; xPos += w[d] * anchorVectorX[d]; yPos += w[d] * anchorVectorY[d]; } double color = 1.0d; Color borderColor = Color.BLACK; if (colorColumn >= 0) { color = getPointColorValue(this.dataTable, row, colorColumn, this.minColor, this.maxColor); borderColor = getPointBorderColor(this.dataTable, row, colorColumn); } plotterPoints.add(new PlotterPoint(xPos, yPos, color, borderColor)); } } /** * Calculate the attribute vectors. * */ protected void calculateAttributeVectors() { anchorVectorX = new double[this.dataTable.getNumberOfColumns()]; anchorVectorY = new double[this.dataTable.getNumberOfColumns()]; for (int i = 0; i < this.dataTable.getNumberOfColumns(); i++) { if ((i == colorColumn) || (shouldIgnoreColumn(i))) continue; double angle = angles[i]; double x = 0.0f, y = 0.0f; if ((int)angle / 90 == 0) { x = sin(angle); y = sin(90.0f - angle); } else if ((int)angle / 90 == 1) { angle = angle - 90.0f; x = sin(90.0f - angle); y = sin(angle); y = -y; } else if ((int)angle / 90 == 2) { angle = angle - 180.0f; x = sin(angle); y = sin(90.0f - angle); x = -x; y = -y; } else if ((int)angle / 90 == 3) { angle = angle - 270.0f; x = sin(90.0f - angle); y = sin(angle); x = -x; } anchorVectorX[i] = x; anchorVectorY[i] = y; } } private void calculateAngles() { int numberOfColumns = this.dataTable.getNumberOfColumns(); if (colorColumn >= 0) numberOfColumns--; int[] ignoredColumns = ignoreList.getSelectedIndices(); numberOfColumns -= ignoredColumns.length; double totalAngle = 360.0d; double delta = totalAngle / numberOfColumns; double angle = 0.0d; angles = new double[this.dataTable.getNumberOfColumns()]; for (int i = 0; i < angles.length; i++) { if ((i == colorColumn) || (shouldIgnoreColumn(i))) continue; angles[i] = angle; angle += delta; } } public void paintComponent(Graphics g) { super.paintComponent(g); // the calculation of the column mapping must be done before the method paintPlotter // since this method can be performed for all types of dimension anchored plotters calculateColumnMapping(); paintPlotter(g); } protected void paintPlotter(Graphics graphics) { Graphics2D g = (Graphics2D)graphics.create(); calculateAngles(); calculateAttributeVectors(); calculateSamplePoints(); int width = getWidth(); int height = getHeight(); int midX = width / 2; int midY = height / 2; double radius = (Math.min(width, height) - 4 * MARGIN) / 2.0d; // draw the circle g.setColor(GRID_COLOR); g.drawOval((int)(midX - radius), (int)(midY - radius), (int)(2.0d * radius), (int)(2.0d * radius)); for (int i = 0; i < this.dataTable.getNumberOfColumns(); i++) { if ((i == colorColumn) || (shouldIgnoreColumn(i))) continue; int endX = (int)(midX + anchorVectorX[i] * radius); int endY = (int)(midY - anchorVectorY[i] * radius); g.drawLine(midX, midY, endX, endY); } // draw axis-labels g.setFont(LABEL_FONT); for (int i = 0; i < this.dataTable.getNumberOfColumns(); i++) { if ((i == colorColumn) || (shouldIgnoreColumn(i))) continue; double x = midX + anchorVectorX[i] * radius; double y = midY - anchorVectorY[i] * radius; // calculate offsets according to angle and string bounds Rectangle2D stringBounds = LABEL_FONT.getStringBounds(this.dataTable.getColumnName(columnMapping[i]), g.getFontRenderContext()); if ((angles[i] >= 0) && (angles[i] <= 90)) { x += (anchorVectorX[i] * 5); y -= (anchorVectorY[i] * 5); } else if ((angles[i] >= 90) && (angles[i] < 180)) { x += (anchorVectorX[i] * 10); y -= (anchorVectorY[i] * 10); } else if ((angles[i] >= 180) && (angles[i] < 270)) { x += (anchorVectorX[i] * 15) - stringBounds.getWidth(); y -= (anchorVectorY[i] * 15); } else if ((angles[i] >= 270) && (angles[i] < 360)) { x += (anchorVectorX[i] * 10) - stringBounds.getWidth(); y -= (anchorVectorY[i] * 10); } if (this.dataTable.isSupportingColumnWeights()) { Rectangle2D weightRect = new Rectangle2D.Double(x - 2, y - stringBounds.getHeight(), stringBounds.getWidth() + 2, stringBounds.getHeight() + 3); g.setColor(getWeightColor(this.dataTable.getColumnWeight(columnMapping[i]), maxWeight)); g.fill(weightRect); } g.setColor(GRID_COLOR); g.drawString(this.dataTable.getColumnName(columnMapping[i]), (int)x, (int)y); } // draw the points Iterator<PlotterPoint> i = plotterPoints.iterator(); while (i.hasNext()) { drawPoint(g, i.next(), midX, midY, radius); } // legend if ((colorColumn != -1) && (plotterPoints.size() > 0)) { drawLegend(g, dataTable, colorColumn); } } /** * Draw a data point. */ protected void drawPoint(Graphics2D g, PlotterPoint point, int midX, int midY, double radius) { int x = midX; int y = midY; x += (int)(point.getX() * radius * scale); y -= (int)(point.getY() * radius * scale); Color pointColor = Color.red; if (colorColumn != -1) { pointColor = getPointColor(point.getColor()); } drawPoint(g, x, y, pointColor, point.getBorderColor()); } /** * Gets the sinus of the angle. * * @param angle */ private double sin(double angle) { while (angle >= 180.0d) { angle -= 180.0d; } double value = (angle / 180.0d * Math.PI); return Math.sin(value); } }