/* * 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.Component; import java.awt.Font; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.geom.AffineTransform; import java.awt.geom.Line2D; import java.awt.geom.Rectangle2D; import java.text.DecimalFormat; import java.util.Date; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import javax.swing.Icon; import javax.swing.JCheckBox; import javax.swing.JComponent; import javax.swing.JLabel; import javax.swing.JSlider; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import com.rapidminer.datatable.DataTable; import com.rapidminer.datatable.DataTableRow; import com.rapidminer.tools.Tools; import com.rapidminer.tools.math.MathFunctions; /** The distribution plotter can be used to plot distributions (histograms) of * the selected dimension. * * @author Ingo Mierswa * @version $Id: DistributionPlotter.java,v 2.12 2006/03/27 13:21:58 ingomierswa * Exp $ */ public class HistogramPlotter extends PlotterAdapter { private static final long serialVersionUID = 5447178172542465015L; public static final int MIN_BIN_NUMBER = 2; public static final int MAX_BIN_NUMBER = 100; public static final int DEFAULT_BIN_NUMBER = 40; private static Icon[] RECTANGLE_STYLE_ICONS; static { RECTANGLE_STYLE_ICONS = new RectangleStyleIcon[RectangleStyle.NUMBER_OF_STYLES]; for (int i = 0; i < RECTANGLE_STYLE_ICONS.length; i++) { RECTANGLE_STYLE_ICONS[i] = new RectangleStyleIcon(i); } } /** * Defines the icon which is plotted before the attribute in the selection * list (legend or key). */ private static class RectangleStyleIcon implements Icon { private RectangleStyle style; private RectangleStyleIcon(int index) { this.style = new RectangleStyle(index); } public int getIconWidth() { return 16; } public int getIconHeight() { return 16; } public void paintIcon(Component c, Graphics g, int x, int y) { style.set((Graphics2D) g); g.fillRect(x, y, 16, 16); g.setColor(Color.black); g.drawRect(x, y, 16, 16); } } private static final Font SCALED_LABEL_FONT = LABEL_FONT.deriveFont(AffineTransform.getScaleInstance(1, -1)); private static final int LABEL_MARGIN_X = 50; private static final int LABEL_MARGIN_Y = 15; protected transient DataTable dataTable; protected double minX, maxX, minY, maxY, xTicSize, yTicSize; /** Indicates which columns will be plotted. */ private boolean[] columns = new boolean[0]; protected Map<Integer,Bins> allPlots = new HashMap<Integer,Bins>(); protected int binNumber = DEFAULT_BIN_NUMBER; protected boolean drawLegend = true; private String key = null; protected int currentXPlotterColumn = -1; private int jitterAmount = 0; private boolean logScale = false; private boolean absolute = false; public HistogramPlotter() { super(); setBackground(Color.white); } public HistogramPlotter(DataTable dataTable) { this(); setDataTable(dataTable); } public void setDataTable(DataTable dataTable) { super.setDataTable(dataTable); this.dataTable = dataTable; this.columns = new boolean[this.dataTable.getNumberOfColumns()]; repaint(); } public Bins getBins(int plotColumn) { return allPlots.get(plotColumn); } public Icon getIcon(int index) { return RECTANGLE_STYLE_ICONS[index % RECTANGLE_STYLE_ICONS.length]; } public JComponent getOptionsComponent(int index) { if (index == 0) { JLabel label = new JLabel("Number of bins:"); label.setToolTipText("Set the number of bins which should be displayed."); return label; } else if (index == 1) { final JSlider binNumberSlider = new JSlider(MIN_BIN_NUMBER, MAX_BIN_NUMBER, DEFAULT_BIN_NUMBER); binNumberSlider.setMajorTickSpacing(MAX_BIN_NUMBER - MIN_BIN_NUMBER); binNumberSlider.setMinorTickSpacing((MAX_BIN_NUMBER - MIN_BIN_NUMBER) / 10); binNumberSlider.setPaintTicks(true); binNumberSlider.setPaintLabels(true); binNumberSlider.setToolTipText("Set the number of bins which should be displayed."); binNumberSlider.addChangeListener(new ChangeListener() { public void stateChanged(ChangeEvent e) { if (!binNumberSlider.getValueIsAdjusting()) setBinNumber(binNumberSlider.getValue()); } }); return binNumberSlider; } else if (index == 2) { final JCheckBox logScaleBox = new JCheckBox("Log Scale"); logScaleBox.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { setLogScale(logScaleBox.isSelected()); } }); return logScaleBox; } else { return null; } } public void setLogScale(boolean logScale) { this.logScale = logScale; repaint(); } public boolean isLogScale() { return this.logScale; } public void setAbsolute(boolean absolute) { this.absolute = absolute; repaint(); } public boolean isSupportingAbsoluteValues() { return true; } /** Indicates how many bins should be used for the distribution plot. */ public void setBinNumber(int binNumber) { this.binNumber = binNumber; repaint(); } public void setPlotColumn(int index, boolean plot) { columns[index] = plot; repaint(); } public boolean getPlotColumn(int index) { return columns[index]; } public int getValuePlotSelectionType() { return MULTIPLE_SELECTION; } protected int getNumberOfPlots() { int counter = 0; for (int i = 0; i < columns.length; i++) { if (columns[i]) counter++; } return counter; } public void setDrawLegend(boolean drawLegend) { this.drawLegend = drawLegend; } public void setKey(String key) { this.key = key; } /** Returns true. */ public boolean canHandleJitter() { return true; } /** Sets the level of jitter and initiates a repaint. */ public void setJitter(int jitter) { this.jitterAmount = jitter; repaint(); } public void prepareData() { this.minX = Double.POSITIVE_INFINITY; this.maxX = Double.NEGATIVE_INFINITY; this.minY = Double.POSITIVE_INFINITY; this.maxY = Double.NEGATIVE_INFINITY; allPlots.clear(); if (getNumberOfPlots() == 0) { return; } this.currentXPlotterColumn = -1; synchronized (dataTable) { // init double[] minX = new double[dataTable.getNumberOfColumns()]; double[] maxX = new double[dataTable.getNumberOfColumns()]; for (int d = 0; d < minX.length; d++) { minX[d] = Double.POSITIVE_INFINITY; maxX[d] = Double.NEGATIVE_INFINITY; } // calculate min and max for all plots Iterator<DataTableRow> i = dataTable.iterator(); while (i.hasNext()) { DataTableRow row = i.next(); for (int d = 0; d < row.getNumberOfValues(); d++) { if (getPlotColumn(d)) { currentXPlotterColumn = d; double value = row.getValue(d); if (absolute) value = Math.abs(value); if (!Double.isNaN(value)) { minX[d] = MathFunctions.robustMin(value, minX[d]); this.minX = MathFunctions.robustMin(value, this.minX); maxX[d] = MathFunctions.robustMax(value, maxX[d]); this.maxX = MathFunctions.robustMax(value, this.maxX); } } } } // create bins and add to bin map for (int d = 0; d < minX.length; d++) { if (getPlotColumn(d)) { Bins bins = new Bins(d, minX[d], maxX[d], this.binNumber); allPlots.put(d, bins); } } // fill with data points i = dataTable.iterator(); while (i.hasNext()) { DataTableRow row = i.next(); for (int d = 0; d < row.getNumberOfValues(); d++) { if (getPlotColumn(d)) { Bins bins = allPlots.get(d); double value = row.getValue(d); if (absolute) value = Math.abs(value); bins.addPoint(value); this.maxY = Math.max(bins.getMaxCounter(), this.maxY); } } } } // rescale counters for logscale? if (isLogScale()) { this.maxY = 0.0d; for (Bins bins : allPlots.values()) { for (Bin bin : bins) { double counter = bin.getCounter(); if (counter > 1.0d) { double newValue = Math.log10(counter); bin.setCounter(newValue); this.maxY = Math.max(newValue, this.maxY); } else if (counter == 1) { // hack in order to prevent 1 counters from vanishing since log(1) = 0 bin.setCounter(0.2d); this.maxY = Math.max(0.2d, this.maxY); } } } } this.minY = 0; if (dataTable.getNumberOfRows() == 0) { minX = 0; maxX = 1; minY = 0; maxY = 1; } if (minX == maxX) { minX -= 0.5; maxX += 0.5; } if (minY == maxY) { minY -= 0.5; maxY += 0.5; } xTicSize = getTicSize(dataTable, currentXPlotterColumn, minX, maxX); yTicSize = getNumericalTicSize(minY, maxY); minX = Math.floor(minX / xTicSize) * xTicSize; maxX = Math.ceil(maxX / xTicSize) * xTicSize; minY = Math.floor(minY / yTicSize) * yTicSize; maxY = Math.ceil(maxY / yTicSize) * yTicSize; } private void drawBins(Graphics2D g, double dx, double dy, double sx, double sy) { if (allPlots.size() == 0) return; Iterator<Bins> b = allPlots.values().iterator(); int offset = 0; while (b.hasNext()) { Bins bins = b.next(); if (bins.getMaxCounter() > 0) { Iterator i = bins.getIterator(); while (i.hasNext()) { Bin bin = (Bin) i.next(); Rectangle2D.Double rectangle = new Rectangle2D.Double((bin.getLeft() + dx) * sx + offset, dy * sy, (bin.getRight() - bin.getLeft()) * sx, bin.getCounter() * sy); bins.getRectangleStyle().set(g); g.fill(rectangle); g.setColor(Color.black); g.draw(rectangle); } } offset += this.jitterAmount; } } private void drawGrid(Graphics2D g, double dx, double dy, double sx, double sy) { DecimalFormat format = new DecimalFormat("0.00E0"); g.setFont(SCALED_LABEL_FONT); int numberOfXTics = (int)Math.ceil((maxX - minX) / xTicSize) + 1; for (int i = 0; i < numberOfXTics; i++) { drawVerticalTic(g, i, format, dx, dy, sx, sy); } int numberOfYTics = (int)Math.ceil((maxY - minY) / yTicSize) + 1; for (int i = 0; i < numberOfYTics; i++) { drawHorizontalTic(g, i, format, dx, dy, sx, sy); } } private void drawVerticalTic(Graphics2D g, int ticNumber, DecimalFormat format, double dx, double dy, double sx, double sy) { double x = ticNumber * xTicSize + minX; g.setColor(GRID_COLOR); g.draw(new Line2D.Double((x + dx) * sx, (minY + dy) * sy, (x + dx) * sx, (maxY + dy) * sy)); g.setColor(Color.black); String label = null; if ((getNumberOfPlots(dataTable) == 1) && (dataTable.isNominal(currentXPlotterColumn))) { int index = (int)Math.round(x); if ((index >= 0) && (index < dataTable.getNumberOfValues(currentXPlotterColumn))) label = dataTable.mapIndex(currentXPlotterColumn, index); } else if ((getNumberOfPlots(dataTable) == 1) && (dataTable.isDate(currentXPlotterColumn))) { long index = (long)Math.round(x); label = Tools.formatDate(new Date(index)); } else if ((getNumberOfPlots(dataTable) == 1) && (dataTable.isTime(currentXPlotterColumn))) { long index = (long)Math.round(x); label = Tools.formatTime(new Date(index)); } else if ((getNumberOfPlots(dataTable) == 1) && (dataTable.isDateTime(currentXPlotterColumn))) { long index = (long)Math.round(x); label = Tools.formatDateTime(new Date(index)); } else { label = format.format(x); } if (label != null) { Rectangle2D stringBounds = SCALED_LABEL_FONT.getStringBounds(label, g.getFontRenderContext()); g.drawString(label, (float) ((x + dx) * sx - stringBounds.getWidth() / 2), (float) ((minY + dy) * sy + stringBounds.getHeight())); } } private void drawHorizontalTic(Graphics2D g, int ticNumber, DecimalFormat format, double dx, double dy, double sx, double sy) { double y = ticNumber * yTicSize + minY; g.setColor(GRID_COLOR); g.draw(new Line2D.Double((minX + dx) * sx, (y + dy) * sy, (maxX + dx) * sx, (y + dy) * sy)); g.setColor(Color.black); String label = format.format(y) + " "; Rectangle2D stringBounds = SCALED_LABEL_FONT.getStringBounds(label, g.getFontRenderContext()); g.drawString(label, (float) ((minX + dx) * sx - stringBounds.getWidth()), (float) ((y + dy) * sy - stringBounds.getHeight() / 2 - stringBounds.getY())); } private void drawBins(Graphics2D g, int pixWidth, int pixHeight) { double sx = 0.0d; double sy = 0.0d; sx = ((double) pixWidth - LABEL_MARGIN_X) / (maxX - minX); sy = ((double) pixHeight - LABEL_MARGIN_Y) / (maxY - minY); Graphics2D coordinateSpace = (Graphics2D) g.create(); if (drawLegend) coordinateSpace.translate(LABEL_MARGIN_X, LABEL_MARGIN_Y); else coordinateSpace.translate(LABEL_MARGIN_X / 2.0d, 3); if (Double.isNaN(sx) || Double.isNaN(sy)) { coordinateSpace.scale(1, -1); coordinateSpace.drawString("No data points available (yet).", 0, -20); coordinateSpace.drawString("Zooming out with a right click might help.", 0, 0); } else { if (drawLegend) drawGrid(coordinateSpace, -minX, -minY, sx, sy); drawBins(coordinateSpace, -minX, -minY, sx, sy); } coordinateSpace.dispose(); } public void paintComponent(Graphics graphics) { super.paintComponent(graphics); int pixWidth = getWidth() - 2 * MARGIN; int pixHeight = getHeight() - 2 * MARGIN; Graphics2D translated = (Graphics2D) graphics.create(); translated.translate(MARGIN, MARGIN); paintHistogram(translated, pixWidth, pixHeight); } public void paintHistogram(Graphics graphics, int pixWidth, int pixHeight) { Graphics2D g = (Graphics2D) graphics; if (key != null) { Rectangle2D stringBounds = LABEL_FONT.getStringBounds(key, g.getFontRenderContext()); int xPos = (int)(pixWidth / 2.0d - stringBounds.getWidth() / 2.0d); int yPos = 16; g.setColor(Color.black); g.drawString(key, xPos, yPos); } Graphics2D scaled = (Graphics2D) g.create(); scaled.translate(0, pixHeight + 1); prepareData(); if (allPlots.size() == 0) { scaled.drawString("No plots selected.", 20, -20); } else { scaled.scale(1, -1); g.setColor(Color.black); drawBins(scaled, pixWidth, pixHeight); } scaled.dispose(); } }