/* * RapidMiner * * Copyright (C) 2001-2011 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.charts; import java.awt.Color; import java.awt.Font; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Random; import javax.swing.JComponent; import org.jfree.chart.ChartFactory; import org.jfree.chart.ChartMouseEvent; import org.jfree.chart.ChartMouseListener; import org.jfree.chart.JFreeChart; import org.jfree.chart.axis.DateAxis; import org.jfree.chart.axis.LogAxis; import org.jfree.chart.axis.NumberAxis; import org.jfree.chart.axis.SymbolAxis; import org.jfree.chart.block.BlockBorder; import org.jfree.chart.block.BlockContainer; import org.jfree.chart.block.BlockResult; import org.jfree.chart.block.BorderArrangement; import org.jfree.chart.block.LabelBlock; import org.jfree.chart.entity.XYItemEntity; import org.jfree.chart.labels.XYToolTipGenerator; import org.jfree.chart.plot.PlotOrientation; import org.jfree.chart.plot.XYPlot; import org.jfree.chart.renderer.xy.AbstractXYItemRenderer; import org.jfree.chart.title.LegendTitle; import org.jfree.data.Range; import org.jfree.data.xy.DefaultXYDataset; import org.jfree.data.xy.DefaultXYZDataset; import org.jfree.data.xy.XYDataset; import org.jfree.ui.HorizontalAlignment; import org.jfree.ui.RectangleEdge; import com.rapidminer.ObjectVisualizer; import com.rapidminer.datatable.DataTable; import com.rapidminer.datatable.DataTableRow; import com.rapidminer.gui.plotter.PlotterConfigurationModel; import com.rapidminer.gui.plotter.RangeablePlotterAdapter; import com.rapidminer.tools.ObjectVisualizerService; import com.rapidminer.tools.Tools; import com.rapidminer.tools.math.MathFunctions; /** * This is the abstract superclass for scatter plotter based on JFreeChart. * * @author Ingo Mierswa */ public abstract class Abstract2DChartPlotter extends RangeablePlotterAdapter { private static final long serialVersionUID = 4568273282283350833L; public static class SeriesAndItem { private int series; private int item; public SeriesAndItem(int series, int item) { this.series = series; this.item = item; } @Override public int hashCode() { return Integer.valueOf(series).hashCode() ^ Integer.valueOf(item).hashCode(); } @Override public boolean equals(Object o) { if (!(o instanceof SeriesAndItem)) { return false; } SeriesAndItem s = (SeriesAndItem)o; return this.series == s.series && this.item == s.item; } } /** The axis names. */ private static final String[] axisNames = { "x-Axis", "y-Axis" }; private static final int X_AXIS = 0; private static final int Y_AXIS = 1; private static final int COLOR_AXIS = 2; /** The currently used data table object. */ protected transient DataTable dataTable; private XYDataset dataSet = new DefaultXYDataset(); /** The columns which are used for the axes. */ private int[] axis = new int[] { -1, -1 }; /** The column which is used for the color. */ private int colorColumn = -1; private int jitterAmount = 0; private boolean[] logScales = new boolean[] { false, false }; private boolean plotColumnsLogScale = false; private double minColor; private double maxColor; private boolean nominal = true; private Map<SeriesAndItem, String> idMap = new HashMap<SeriesAndItem, String>(); public Abstract2DChartPlotter(final PlotterConfigurationModel settings) { super(settings); setBackground(Color.white); } public Abstract2DChartPlotter(PlotterConfigurationModel settings, DataTable dataTable) { this(settings); setDataTable(dataTable); } /** Subclasses have to implement this method. */ public abstract AbstractXYItemRenderer getItemRenderer(boolean nominal, int size, double minColor, double maxColor); /** Returns true. */ @Override public boolean canHandleJitter() { return true; } /** Sets the level of jitter and initiates a repaint. */ @Override public void setJitter(int jitter) { this.jitterAmount = jitter; updatePlotter(); } /** Returns true if a log scale for this column is supported. Returns true for the x- and y-axis. */ @Override public boolean isSupportingLogScale(int axis) { if ((axis == X_AXIS) || (axis == Y_AXIS)) return true; else return false; } /** Returns true. */ @Override public boolean isSupportingLogScaleForPlotColumns() { return true; } /** Sets if the given axis should be plotted with log scale. */ @Override public void setLogScale(int axis, boolean logScale) { logScales[axis] = logScale; updatePlotter(); } /** Sets if the given axis should be plotted with log scale. */ @Override public void setLogScaleForPlotColumns(boolean logScale) { plotColumnsLogScale = logScale; updatePlotter(); } @Override public void dataTableSet() { this.dataTable = getDataTable(); updatePlotter(); } @Override public void setPlotColumn(int index, boolean plot) { if (plot) this.colorColumn = index; else this.colorColumn = -1; updatePlotter(); } @Override public boolean getPlotColumn(int index) { return colorColumn == index; } @Override public String getPlotName() { return "Color Column"; } @Override public int getNumberOfAxes() { return axisNames.length; } @Override public void setAxis(int index, int dimension) { axis[index] = dimension; updatePlotter(); } @Override public int getAxis(int index) { return axis[index]; } @Override public String getAxisName(int index) { return axisNames[index]; } private void prepareData() { idMap.clear(); if ((colorColumn < 0) || (dataTable.isNominal(colorColumn))) { prepareNominalData(); } else { prepareNumericalData(); } } private void prepareNumericalData() { this.nominal = false; dataSet = new DefaultXYZDataset(); if ((axis[X_AXIS] >= 0) && (axis[Y_AXIS] >= 0)) { this.minColor = Double.POSITIVE_INFINITY; this.maxColor = Double.NEGATIVE_INFINITY; List<double[]> dataList = new LinkedList<double[]>(); List<String> idList = new LinkedList<String>(); synchronized (dataTable) { Iterator<DataTableRow> i = this.dataTable.iterator(); while (i.hasNext()) { DataTableRow row = i.next(); double xValue = Double.NaN; if (axis[X_AXIS] >= 0) { xValue = row.getValue(axis[X_AXIS]); } double yValue = Double.NaN; if (axis[Y_AXIS] >= 0) { yValue = row.getValue(axis[Y_AXIS]); } double colorValue = Double.NaN; if (colorColumn >= 0) { colorValue = row.getValue(colorColumn); } if (plotColumnsLogScale) { if (Tools.isLessEqual(colorValue, 0.0d)) { colorValue = 0; } else { colorValue = Math.log10(colorValue); } } // TM: removed check // if (!Double.isNaN(xValue) && !Double.isNaN(yValue)) { double[] data = new double[3]; data[X_AXIS] = xValue; data[Y_AXIS] = yValue; data[COLOR_AXIS] = colorValue; if (!Double.isNaN(colorValue)) { this.minColor = Math.min(this.minColor, colorValue); this.maxColor = Math.max(this.maxColor, colorValue); } dataList.add(data); idList.add(row.getId()); // } } } double[][] data = new double[3][dataList.size()]; double minX = Double.POSITIVE_INFINITY; double maxX = Double.NEGATIVE_INFINITY; double minY = Double.POSITIVE_INFINITY; double maxY = Double.NEGATIVE_INFINITY; int index = 0; for (double[] d : dataList) { data[X_AXIS][index] = d[X_AXIS]; data[Y_AXIS][index] = d[Y_AXIS]; data[COLOR_AXIS][index] = d[COLOR_AXIS]; minX = MathFunctions.robustMin(minX, d[X_AXIS]); maxX = MathFunctions.robustMax(maxX, d[X_AXIS]); minY = MathFunctions.robustMin(minY, d[Y_AXIS]); maxY = MathFunctions.robustMax(maxY, d[Y_AXIS]); index++; } // jittering if (this.jitterAmount > 0) { Random jitterRandom = new Random(2001); double oldXRange = maxX - minX; double oldYRange = maxY - minY; for (int i = 0; i < dataList.size(); i++) { if (Double.isInfinite(oldXRange) || Double.isNaN(oldXRange)) oldXRange = 0; if (Double.isInfinite(oldYRange) || Double.isNaN(oldYRange)) oldYRange = 0; double pertX = oldXRange * (jitterAmount / 200.0d) * jitterRandom.nextGaussian(); double pertY = oldYRange * (jitterAmount / 200.0d) * jitterRandom.nextGaussian(); data[X_AXIS][i] += pertX; data[Y_AXIS][i] += pertY; } } // add data ((DefaultXYZDataset)dataSet).addSeries("All", data); // id handling int idCounter = 0; for (String id : idList) { idMap.put(new SeriesAndItem(0, idCounter++), id); } } } protected String getId(int series, int index) { return idMap.get(new SeriesAndItem(series, index)); } private void prepareNominalData() { this.nominal = true; dataSet = new DefaultXYDataset(); if ((axis[X_AXIS] >= 0) && (axis[Y_AXIS] >= 0)) { Map<String, List<double[]>> dataCollection = new LinkedHashMap<String, List<double[]>>(); Map<String, List<String>> idCollection = new LinkedHashMap<String, List<String>>(); synchronized (dataTable) { if (colorColumn >= 0) { for (int v = 0; v < dataTable.getNumberOfValues(colorColumn); v++) { dataCollection.put(dataTable.mapIndex(colorColumn, v), new LinkedList<double[]>()); idCollection.put(dataTable.mapIndex(colorColumn, v), new LinkedList<String>()); } } Iterator<DataTableRow> i = this.dataTable.iterator(); int index = 0; while (i.hasNext()) { DataTableRow row = i.next(); double xValue = row.getValue(axis[X_AXIS]); double yValue = row.getValue(axis[Y_AXIS]); double colorValue = Double.NaN; if (colorColumn >= 0) { colorValue = row.getValue(colorColumn); } // TM: removed check // if (!Double.isNaN(xValue) && !Double.isNaN(yValue)) { addPoint(dataCollection, idCollection, row.getId(), xValue, yValue, colorValue); // } index++; } } double minX = Double.POSITIVE_INFINITY; double maxX = Double.NEGATIVE_INFINITY; double minY = Double.POSITIVE_INFINITY; double maxY = Double.NEGATIVE_INFINITY; Iterator<Map.Entry<String,List<double[]>>> i = dataCollection.entrySet().iterator(); while (i.hasNext()) { Map.Entry<String,List<double[]>> entry = i.next(); List<double[]> dataList = entry.getValue(); Iterator<double[]> j = dataList.iterator(); while (j.hasNext()) { double[] current = j.next(); minX = MathFunctions.robustMin(minX, current[X_AXIS]); maxX = MathFunctions.robustMax(maxX, current[X_AXIS]); minY = MathFunctions.robustMin(minY, current[Y_AXIS]); maxY = MathFunctions.robustMax(maxY, current[Y_AXIS]); } } Random jitterRandom = new Random(2001); double oldXRange = maxX - minX; double oldYRange = maxY - minY; if (Double.isInfinite(oldXRange) || Double.isNaN(oldXRange)) oldXRange = 0; if (Double.isInfinite(oldYRange) || Double.isNaN(oldYRange)) oldYRange = 0; i = dataCollection.entrySet().iterator(); while (i.hasNext()) { Map.Entry<String,List<double[]>> entry = i.next(); String seriesName = entry.getKey(); List<double[]> dataList = entry.getValue(); double[][] data = new double[2][dataList.size()]; int listCounter = 0; Iterator<double[]> j = dataList.iterator(); while (j.hasNext()) { double[] current = j.next(); data[X_AXIS][listCounter] = current[X_AXIS]; data[Y_AXIS][listCounter] = current[Y_AXIS]; if (this.jitterAmount > 0) { double pertX = oldXRange * (jitterAmount / 200.0d) * jitterRandom.nextGaussian(); double pertY = oldYRange * (jitterAmount / 200.0d) * jitterRandom.nextGaussian(); data[X_AXIS][listCounter] += pertX; data[Y_AXIS][listCounter] += pertY; } listCounter++; } ((DefaultXYDataset)dataSet).addSeries(seriesName, data); } int seriesCounter = 0; Iterator<List<String>> v = idCollection.values().iterator(); while (v.hasNext()) { List<String> idList = v.next(); int itemCounter = 0; Iterator<String> j = idList.iterator(); while (j.hasNext()) { idMap.put(new SeriesAndItem(seriesCounter, itemCounter++), j.next()); } seriesCounter++; } } } private void addPoint(Map<String,List<double[]>> dataCollection, Map<String,List<String>> idCollection, String id, double x, double y, double color) { List<double[]> dataList = null; List<String> idList = null; if (Double.isNaN(color)) { dataList = dataCollection.get("Unknown"); if (dataList == null) { dataList = new LinkedList<double[]>(); dataCollection.put("Unknown", dataList); } idList = idCollection.get("Unknown"); if (idList == null) { idList = new LinkedList<String>(); idCollection.put("Unknown", idList); } } else { String name = color + ""; if (dataTable.isNominal(colorColumn)) { name = dataTable.mapIndex(colorColumn, (int)color); } else if (dataTable.isDate(colorColumn)) { name = Tools.formatDate(new Date((long)color)); } else if (dataTable.isTime(colorColumn)) { name = Tools.formatTime(new Date((long)color)); } else if (dataTable.isDateTime(colorColumn)) { name = Tools.formatDateTime(new Date((long)color)); } dataList = dataCollection.get(name); if (dataList == null) { dataList = new LinkedList<double[]>(); dataCollection.put(name, dataList); } idList = idCollection.get(name); if (idList == null) { idList = new LinkedList<String>(); idCollection.put(name, idList); } } dataList.add(new double[] { x, y }); idList.add(id); } @Override public void updatePlotter() { prepareData(); JFreeChart chart; if ((axis[X_AXIS] >= 0) && (axis[Y_AXIS] >= 0)) { if (nominal) { int size = dataSet.getSeriesCount(); chart = ChartFactory.createScatterPlot( null, // chart title null, // domain axis label null, // range axis label dataSet, // data PlotOrientation.VERTICAL, // orientation colorColumn >= 0 && size < 100 ? true : false, // include legend true, // tooltips false // URLs ); // renderer settings try { chart.getXYPlot().setRenderer(getItemRenderer(nominal, size, this.minColor, this.maxColor)); } catch (Exception e) { // do nothing } // legend settings LegendTitle legend = chart.getLegend(); if (legend != null) { legend.setPosition(RectangleEdge.TOP); legend.setFrame(BlockBorder.NONE); legend.setHorizontalAlignment(HorizontalAlignment.LEFT); legend.setItemFont(LABEL_FONT); BlockContainer wrapper = new BlockContainer(new BorderArrangement()); LabelBlock title = new LabelBlock(getDataTable().getColumnName(colorColumn), new Font("SansSerif", Font.BOLD, 12)); title.setPadding(0, 5, 5, 5); wrapper.add(title, RectangleEdge.LEFT); BlockContainer items = legend.getItemContainer(); wrapper.add(items, RectangleEdge.RIGHT); legend.setWrapper(wrapper); } } else { chart = ChartFactory.createScatterPlot( null, // chart title null, // domain axis label null, // range axis label dataSet, // data PlotOrientation.VERTICAL, // orientation false, // include legend true, // tooltips false // URLs ); // renderer settings try { chart.getXYPlot().setRenderer(getItemRenderer(nominal, -1, minColor, maxColor)); } catch (Exception e) { // do nothing } LegendTitle legendTitle = new LegendTitle(chart.getXYPlot().getRenderer()) { private static final long serialVersionUID = 1288380309936848376L; @Override public Object draw(java.awt.Graphics2D g2, java.awt.geom.Rectangle2D area, java.lang.Object params) { if (dataTable.isDate(colorColumn) || dataTable.isTime(colorColumn) || dataTable.isDateTime(colorColumn)) { drawSimpleDateLegend(g2, (int)(area.getCenterX() - 170), (int)(area.getCenterY() + 7), dataTable, colorColumn, minColor, maxColor); return new BlockResult(); } else { final String minColorString = Tools.formatNumber(minColor); final String maxColorString = Tools.formatNumber(maxColor); drawSimpleNumericalLegend(g2, (int)(area.getCenterX() - 75), (int)(area.getCenterY() + 7), getDataTable().getColumnName(colorColumn), minColorString, maxColorString); return new BlockResult(); } } @Override public void draw(java.awt.Graphics2D g2, java.awt.geom.Rectangle2D area) { draw(g2, area, null); } }; BlockContainer wrapper = new BlockContainer(new BorderArrangement()); LabelBlock title = new LabelBlock(getDataTable().getColumnName(colorColumn), new Font("SansSerif", Font.BOLD, 12)); title.setPadding(0, 5, 5, 5); wrapper.add(title, RectangleEdge.LEFT); BlockContainer items = legendTitle.getItemContainer(); wrapper.add(items, RectangleEdge.RIGHT); legendTitle.setWrapper(wrapper); chart.addLegend(legendTitle); } } else { chart = ChartFactory.createScatterPlot( null, // chart title null, // domain axis label null, // range axis label dataSet, // data PlotOrientation.VERTICAL, // orientation false, // include legend true, // tooltips false // URLs ); } // GENERAL CHART SETTINGS // set the background colors for the chart... chart.setBackgroundPaint(Color.WHITE); chart.getPlot().setBackgroundPaint(Color.WHITE); chart.setAntiAlias(false); // general plot settings XYPlot plot = chart.getXYPlot(); plot.setDomainGridlinePaint(Color.LIGHT_GRAY); plot.setRangeGridlinePaint(Color.LIGHT_GRAY); // domain axis if (axis[X_AXIS] >= 0) { if (dataTable.isNominal(axis[X_AXIS])) { String[] values = new String[dataTable.getNumberOfValues(axis[X_AXIS])]; for (int i = 0; i < values.length; i++) { values[i] = dataTable.mapIndex(axis[X_AXIS], i); } plot.setDomainAxis(new SymbolAxis(dataTable.getColumnName(axis[X_AXIS]), values)); } else if ((dataTable.isDate(axis[X_AXIS])) || (dataTable.isDateTime(axis[X_AXIS]))) { DateAxis domainAxis = new DateAxis(dataTable.getColumnName(axis[X_AXIS])); domainAxis.setTimeZone(Tools.getPreferredTimeZone()); plot.setDomainAxis(domainAxis); } else { if (logScales[X_AXIS]) { LogAxis domainAxis = new LogAxis(dataTable.getColumnName(axis[X_AXIS])); domainAxis.setStandardTickUnits(NumberAxis.createStandardTickUnits(Locale.US)); plot.setDomainAxis(domainAxis); } else { NumberAxis domainAxis = new NumberAxis(dataTable.getColumnName(axis[X_AXIS])); domainAxis.setAutoRangeStickyZero(false); domainAxis.setAutoRangeIncludesZero(false); plot.setDomainAxis(domainAxis); } } } if (axis[X_AXIS] >= 0) { Range range = getRangeForDimension(axis[X_AXIS]); if (range != null) { plot.getDomainAxis().setRange(range, true, false); } else { plot.getDomainAxis().setAutoRange(true); } } plot.getDomainAxis().setLabelFont(LABEL_FONT_BOLD); plot.getDomainAxis().setTickLabelFont(LABEL_FONT); // rotate labels if (isLabelRotating()) { plot.getDomainAxis().setTickLabelsVisible(true); plot.getDomainAxis().setVerticalTickLabels(true); } // range axis if (axis[Y_AXIS] >= 0) { if (dataTable.isNominal(axis[Y_AXIS])) { String[] values = new String[dataTable.getNumberOfValues(axis[Y_AXIS])]; for (int i = 0; i < values.length; i++) { values[i] = dataTable.mapIndex(axis[Y_AXIS], i); } plot.setRangeAxis(new SymbolAxis(dataTable.getColumnName(axis[Y_AXIS]), values)); } else if ((dataTable.isDate(axis[Y_AXIS])) || (dataTable.isDateTime(axis[Y_AXIS]))) { DateAxis rangeAxis = new DateAxis(dataTable.getColumnName(axis[Y_AXIS])); rangeAxis.setTimeZone(Tools.getPreferredTimeZone()); plot.setRangeAxis(rangeAxis); } else { if (logScales[Y_AXIS]) { LogAxis rangeAxis = new LogAxis(dataTable.getColumnName(axis[Y_AXIS])); rangeAxis.setStandardTickUnits(NumberAxis.createStandardTickUnits(Locale.US)); plot.setRangeAxis(rangeAxis); } else { NumberAxis rangeAxis = new NumberAxis(dataTable.getColumnName(axis[Y_AXIS])); rangeAxis.setAutoRangeStickyZero(false); rangeAxis.setAutoRangeIncludesZero(false); plot.setRangeAxis(rangeAxis); } } } plot.getRangeAxis().setLabelFont(LABEL_FONT_BOLD); plot.getRangeAxis().setTickLabelFont(LABEL_FONT); if (axis[Y_AXIS] >= 0) { Range range = getRangeForDimension(axis[Y_AXIS]); if (range != null) { plot.getRangeAxis().setRange(range, true, false); } else { plot.getRangeAxis().setAutoRange(true); } } // Chart Panel Settings AbstractChartPanel panel = getPlotterPanel(); if (panel == null) { panel = createPanel(chart); // react to mouse clicks panel.addChartMouseListener(new ChartMouseListener() { public void chartMouseClicked(ChartMouseEvent e) { if (e.getTrigger().getClickCount() > 1) { if (e.getEntity() instanceof XYItemEntity) { XYItemEntity entity = (XYItemEntity)e.getEntity(); if (entity != null) { String id = idMap.get(new SeriesAndItem(entity.getSeriesIndex(), entity.getItem())); if (id != null) { ObjectVisualizer visualizer = ObjectVisualizerService.getVisualizerForObject(dataTable); visualizer.startVisualization(id); } } } } } public void chartMouseMoved(ChartMouseEvent e) {} }); } else { panel.setChart(chart); } // tooltips class CustomXYToolTipGenerator implements XYToolTipGenerator { public CustomXYToolTipGenerator() {} private String formatValue(int axis, double value) { if (dataTable.isNominal(axis)) { // TODO add mapping of value to nominal value return Tools.formatIntegerIfPossible(value); } else if (dataTable.isNumerical(axis)) { return Tools.formatIntegerIfPossible(value); } else if (dataTable.isDate(axis)) { return Tools.formatDate(new Date((long) value)); } else if (dataTable.isTime(axis)) { return Tools.formatTime(new Date((long) value)); } else if (dataTable.isDateTime(axis)) { return Tools.formatDateTime(new Date((long) value)); } return "?"; } public String generateToolTip(XYDataset dataset, int row, int column) { String id = idMap.get(new SeriesAndItem(row, column)); if (id != null) { return "<html><b>Id: " + id + "</b> (" + dataset.getSeriesKey(row) + ", " + formatValue(axis[X_AXIS], dataset.getXValue(row, column)) + ", " + formatValue(axis[Y_AXIS], dataset.getYValue(row, column)) + ")</html>"; } else { return "<html>(" + dataset.getSeriesKey(row) + ", " + formatValue(axis[X_AXIS], dataset.getXValue(row, column)) + ", " + formatValue(axis[Y_AXIS], dataset.getYValue(row, column)) + ")</html>"; } } } for (int i = 0; i < dataSet.getSeriesCount(); i++) { plot.getRenderer().setSeriesToolTipGenerator(i, new CustomXYToolTipGenerator()); } } @Override public JComponent getOptionsComponent(int index) { if (index == 0) { return getRotateLabelComponent(); } else { return null; } } @Override public Collection<String> resolveXAxis(int axisIndex) { if (axis[X_AXIS] != -1) return Collections.singletonList(dataTable.getColumnName(axis[X_AXIS])); else return Collections.emptyList(); } @Override public Collection<String> resolveYAxis(int axisIndex) { if (axis[Y_AXIS] != -1) return Collections.singletonList(dataTable.getColumnName(axis[Y_AXIS])); else return Collections.emptyList(); } }