/* * 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.BasicStroke; import java.awt.Color; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; import java.util.Locale; import javax.swing.Icon; import javax.swing.JComponent; import javax.swing.JTextField; import org.jfree.chart.ChartFactory; import org.jfree.chart.JFreeChart; import org.jfree.chart.axis.DateAxis; import org.jfree.chart.axis.NumberAxis; import org.jfree.chart.axis.SymbolAxis; import org.jfree.chart.axis.ValueAxis; import org.jfree.chart.block.BlockBorder; import org.jfree.chart.plot.PlotOrientation; import org.jfree.chart.plot.XYPlot; import org.jfree.chart.renderer.xy.DeviationRenderer; import org.jfree.chart.title.LegendTitle; import org.jfree.data.Range; import org.jfree.data.xy.XYDataset; import org.jfree.data.xy.YIntervalSeries; import org.jfree.data.xy.YIntervalSeriesCollection; import org.jfree.ui.HorizontalAlignment; import org.jfree.ui.RectangleEdge; import org.jfree.ui.RectangleInsets; import com.rapidminer.datatable.DataTable; import com.rapidminer.datatable.DataTableRow; import com.rapidminer.gui.MainFrame; import com.rapidminer.gui.plotter.PlotterConfigurationModel; import com.rapidminer.gui.plotter.RangeablePlotterAdapter; import com.rapidminer.operator.ports.InputPort; import com.rapidminer.parameter.ParameterType; import com.rapidminer.parameter.ParameterTypeDouble; import com.rapidminer.tools.LogService; import com.rapidminer.tools.ParameterService; import com.rapidminer.tools.Tools; import com.rapidminer.tools.math.MathFunctions; /** * This is the series chart plotter. * * @author Ingo Mierswa, Sebastian Land */ public class SeriesChartPlotter extends RangeablePlotterAdapter { private static final long serialVersionUID = -8763693366081949249L; private static final String PARAMETER_MARKER = "marker_at"; private static final String VALUEAXIS_LABEL = "value"; private static final String SERIESINDEX_LABEL = "index"; private JTextField limitField = new JTextField(); /** The currently used data table object. */ private transient DataTable dataTable; /** The data set used for the plotter. */ private YIntervalSeriesCollection dataset = null; /** The column which is used for the values. */ private boolean[] columns; /** The axis values for the upper and lower bounds. */ private int[] axis = new int[] { -1, -1, -1 }; private boolean useLimit = false; private double limit = 0; private static final int MIN = 0; private static final int MAX = 1; private static final int INDEX = 2; /** Indicates if bounds are plotted. */ private boolean plotBounds = false; private int boundsSeriesIndex = 1; private List<Integer> plotIndexToColumnIndexMap = new ArrayList<Integer>(); public SeriesChartPlotter(final PlotterConfigurationModel settings) { super(settings); setBackground(Color.white); limitField.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { try { double value = Double.parseDouble(limitField.getText()); settings.setParameterAsDouble(PARAMETER_MARKER, value); } catch (NumberFormatException ex) { useLimit = false; } } }); } public SeriesChartPlotter(PlotterConfigurationModel settings, DataTable dataTable) { this(settings); setDataTable(dataTable); } private JFreeChart createChart(XYDataset dataset, boolean createLegend) { // create the chart... JFreeChart chart = ChartFactory.createXYLineChart(null, // chart title null, // x axis label null, // y axis label dataset, // data PlotOrientation.VERTICAL, createLegend, // include legend true, // tooltips false // urls ); chart.setBackgroundPaint(Color.white); // get a reference to the plot for further customization... XYPlot plot = (XYPlot) chart.getPlot(); plot.setBackgroundPaint(Color.WHITE); plot.setAxisOffset(new RectangleInsets(5.0, 5.0, 5.0, 5.0)); plot.setDomainGridlinePaint(Color.LIGHT_GRAY); plot.setRangeGridlinePaint(Color.LIGHT_GRAY); DeviationRenderer renderer = new DeviationRenderer(true, false); // colors if (dataset.getSeriesCount() == 1) { renderer.setSeriesStroke(0, new BasicStroke(1.5f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND)); renderer.setSeriesPaint(0, getColorProvider().getPointColor(1.0d)); } else { // special case needed for avoiding devision by zero for (int i = 0; i < dataset.getSeriesCount(); i++) { renderer.setSeriesStroke(i, new BasicStroke(1.5f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND)); renderer.setSeriesPaint(i, getColorProvider().getPointColor(1.0d - i / (double) (dataset.getSeriesCount() - 1))); } } // background for bounds if (plotBounds) { float[] dashArray = new float[] { 7, 14 }; renderer.setSeriesStroke(boundsSeriesIndex, new BasicStroke(1.5f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, 1.0f, dashArray, 0)); renderer.setSeriesPaint(boundsSeriesIndex, Color.GRAY.brighter()); renderer.setSeriesFillPaint(boundsSeriesIndex, Color.GRAY); } // alpha renderer.setAlpha(0.25f); plot.setRenderer(renderer); NumberAxis xAxis = (NumberAxis) plot.getDomainAxis(); if (axis[INDEX] < 0) { xAxis.setStandardTickUnits(NumberAxis.createIntegerTickUnits(Locale.US)); xAxis.setLabel(SERIESINDEX_LABEL); Range range = getRangeForName(SERIESINDEX_LABEL); if (range == null) { xAxis.setAutoRange(true); xAxis.setAutoRangeStickyZero(false); xAxis.setAutoRangeIncludesZero(false); } else { xAxis.setRange(range, true, false); } } else { xAxis.setLabel(dataTable.getColumnName(axis[INDEX])); Range range = getRangeForDimension(axis[INDEX]); if (range == null) { xAxis.setAutoRange(true); xAxis.setAutoRangeStickyZero(false); xAxis.setAutoRangeIncludesZero(false); } else { xAxis.setRange(range, true, false); } } xAxis.setLabelFont(LABEL_FONT_BOLD); xAxis.setTickLabelFont(LABEL_FONT); xAxis.setVerticalTickLabels(isLabelRotating()); NumberAxis yAxis = (NumberAxis) plot.getRangeAxis(); yAxis.setLabel(VALUEAXIS_LABEL); yAxis.setStandardTickUnits(NumberAxis.createStandardTickUnits(Locale.US)); setYAxisRange(yAxis); yAxis.setLabelFont(LABEL_FONT_BOLD); yAxis.setTickLabelFont(LABEL_FONT); return chart; } /** Returns a line icon depending on the index. */ @Override public Icon getIcon(int index) { return null; } @Override public void dataTableSet() { this.dataTable = getDataTable(); columns = new boolean[dataTable.getNumberOfColumns()]; updatePlotter(); } @Override public int getValuePlotSelectionType() { return MULTIPLE_SELECTION; } @Override public void setPlotColumn(int index, boolean plot) { if (index >= 0 && index < columns.length) this.columns[index] = plot; updatePlotter(); } @Override public boolean getPlotColumn(int index) { return this.columns[index]; } @Override public String getPlotName() { return "Plot Series"; } @Override public int getNumberOfAxes() { return axis.length; } @Override public String getAxisName(int index) { switch (index) { case MIN: return "Lower Bound"; case MAX: return "Upper Bound"; case INDEX: return "Index Dimension"; default: return "none"; } } @Override public List<ParameterType> getAdditionalParameterKeys(InputPort inputPort) { List<ParameterType> types = super.getAdditionalParameterKeys(inputPort); types.add(new ParameterTypeDouble(PARAMETER_MARKER, "Defines a horizontal line as a reference to the plot.", Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY, true)); return types; } @Override public void setAdditionalParameter(String key, String value) { super.setAdditionalParameter(key, value); if (PARAMETER_MARKER.equals(key)) { try { double newLimit = Double.parseDouble(value); useLimit = true; if (limit != newLimit) { limitField.setText(limit + ""); limit = newLimit; updatePlotter(); } } catch (NumberFormatException e) { if (useLimit) { useLimit = false; updatePlotter(); } } } } @Override public int getAxis(int index) { return axis[index]; } @Override public void setAxis(int index, int dimension) { if (axis[index] != dimension) { axis[index] = dimension; updatePlotter(); } } private int prepareData() { synchronized (dataTable) { this.dataset = new YIntervalSeriesCollection(); this.plotBounds = false; this.plotIndexToColumnIndexMap.clear(); // series int columnCount = 0; for (int c = 0; c < dataTable.getNumberOfColumns(); c++) { if (getPlotColumn(c)) { if (dataTable.isNumerical(c)) { YIntervalSeries series = new YIntervalSeries(this.dataTable.getColumnName(c)); Iterator<DataTableRow> i = dataTable.iterator(); int index = 0; while (i.hasNext()) { DataTableRow row = i.next(); double value = row.getValue(c); if (axis[INDEX] >= 0 && !dataTable.isNominal(axis[INDEX])) { double indexValue = row.getValue(axis[INDEX]); series.add(indexValue, value, value, value); } else { series.add(index++, value, value, value); } } dataset.addSeries(series); plotIndexToColumnIndexMap.add(c); columnCount++; } } } // Lower and upper bound if (getAxis(MIN) > -1 && getAxis(MAX) > -1) { if (dataTable.isNumerical(getAxis(MIN)) && dataTable.isNumerical(getAxis(MAX))) { YIntervalSeries series = new YIntervalSeries("Bounds"); Iterator<DataTableRow> i = dataTable.iterator(); int index = 0; while (i.hasNext()) { DataTableRow row = i.next(); double lowerValue = row.getValue(getAxis(0)); double upperValue = row.getValue(getAxis(1)); if (lowerValue > upperValue) { double dummy = lowerValue; lowerValue = upperValue; upperValue = dummy; } double mean = (upperValue - lowerValue) / 2.0d + lowerValue; if (axis[INDEX] >= 0 && !dataTable.isNominal(axis[INDEX])) { double indexValue = row.getValue(axis[INDEX]); series.add(indexValue, mean, lowerValue, upperValue); } else { series.add(index++, mean, lowerValue, upperValue); } } dataset.addSeries(series); this.plotBounds = true; this.boundsSeriesIndex = dataset.getSeriesCount() - 1; } } // limit if (useLimit) { YIntervalSeries series = new YIntervalSeries("Limit"); int index = 0; for (DataTableRow row : dataTable) { if (!dataTable.isNominal(axis[INDEX])) { double indexValue = row.getValue(axis[INDEX]); series.add(indexValue, limit, limit, limit); } else { series.add(index++, limit, limit, limit); } } dataset.addSeries(series); } return columnCount; } } private void setYAxisRange(NumberAxis axis) { Range range = getRangeForName(VALUEAXIS_LABEL); if (range == null) { for (int c = 0; c < this.dataTable.getNumberOfColumns(); c++) { if (this.columns[c] || c == getAxis(0) || c == getAxis(1)) { if (range == null) range = getRangeForDimension(c); else { Range newRange = getRangeForDimension(c); if (newRange != null) range = new Range(MathFunctions.robustMin(range.getLowerBound(), newRange.getLowerBound()), MathFunctions.robustMax(range.getUpperBound(), newRange.getUpperBound())); } } } } if (range != null) axis.setRange(range); else { axis.setAutoRange(true); axis.setAutoRangeStickyZero(false); axis.setAutoRangeIncludesZero(false); } } @Override public JComponent getOptionsComponent(int index) { if (index == 0) return limitField; else if (index == 1) return getRotateLabelComponent(); return null; } @Override protected void updatePlotter() { int categoryCount = prepareData(); String maxClassesProperty = ParameterService.getParameterValue(MainFrame.PROPERTY_RAPIDMINER_GUI_PLOTTER_COLORS_CLASSLIMIT); int maxClasses = 20; try { if (maxClassesProperty != null) maxClasses = Integer.parseInt(maxClassesProperty); } catch (NumberFormatException e) { LogService.getGlobal().log("Series plotter: cannot parse property 'rapidminer.gui.plotter.colors.classlimit', using maximal 20 different classes.", LogService.WARNING); } boolean createLegend = categoryCount > 0 && categoryCount < maxClasses; JFreeChart chart = createChart(this.dataset, createLegend); // set the background color for the chart... chart.setBackgroundPaint(Color.white); // domain axis if (axis[INDEX] >= 0) { if (!dataTable.isNominal(axis[INDEX])) { if (dataTable.isDate(axis[INDEX]) || dataTable.isDateTime(axis[INDEX])) { DateAxis domainAxis = new DateAxis(dataTable.getColumnName(axis[INDEX])); domainAxis.setTimeZone(Tools.getPreferredTimeZone()); chart.getXYPlot().setDomainAxis(domainAxis); if (getRangeForDimension(axis[INDEX]) != null) domainAxis.setRange(getRangeForDimension(axis[INDEX])); domainAxis.setLabelFont(LABEL_FONT_BOLD); domainAxis.setTickLabelFont(LABEL_FONT); domainAxis.setVerticalTickLabels(isLabelRotating()); } } else { LinkedHashSet<String> values = new LinkedHashSet<String>(); for (DataTableRow row : dataTable) { String stringValue = dataTable.mapIndex(axis[INDEX], (int) row.getValue(axis[INDEX])); if (stringValue.length() > 40) stringValue = stringValue.substring(0, 40); values.add(stringValue); } ValueAxis categoryAxis = new SymbolAxis(dataTable.getColumnName(axis[INDEX]), values.toArray(new String[values.size()])); categoryAxis.setLabelFont(LABEL_FONT_BOLD); categoryAxis.setTickLabelFont(LABEL_FONT); categoryAxis.setVerticalTickLabels(isLabelRotating()); chart.getXYPlot().setDomainAxis(categoryAxis); } } // 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); } AbstractChartPanel panel = getPlotterPanel(); if (panel == null) { panel = createPanel(chart); } else { panel.setChart(chart); } // ATTENTION: WITHOUT THIS WE GET SEVERE MEMORY LEAKS!!! panel.getChartRenderingInfo().setEntityCollection(null); } @Override public String getPlotterName() { return PlotterConfigurationModel.SERIES_PLOT; } @Override public Collection<String> resolveXAxis(int axisIndex) { if (axis[INDEX] != -1) return Collections.singletonList(dataTable.getColumnName(axis[INDEX])); else return Collections.singletonList(SERIESINDEX_LABEL); } @Override public Collection<String> resolveYAxis(int axisIndex) { Collection<String> names = new LinkedList<String>(); for (int i = 0; i < columns.length; i++) { if (columns[i]) names.add(dataTable.getColumnName(i)); } return names; } }