/*
* 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.charts;
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.Collection;
import java.util.Date;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.TreeSet;
import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JLabel;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.block.BlockBorder;
import org.jfree.chart.title.LegendTitle;
import org.jfree.data.category.CategoryDataset;
import org.jfree.data.category.DefaultCategoryDataset;
import org.jfree.ui.HorizontalAlignment;
import org.jfree.ui.RectangleEdge;
import com.rapidminer.datatable.DataTable;
import com.rapidminer.datatable.DataTableRow;
import com.rapidminer.gui.MainFrame;
import com.rapidminer.gui.plotter.PlotterAdapter;
import com.rapidminer.tools.LogService;
import com.rapidminer.tools.Tools;
import com.rapidminer.tools.math.function.AbstractAggregationFunction;
import com.rapidminer.tools.math.function.AggregationFunction;
import com.rapidminer.tools.math.function.AverageFunction;
/**
* This is the main bar chart plotter. The plotter is also capable
* to produce average aggregations based on an additional group-by
* attribute.
*
* @author Ingo Mierswa
* @version $Id: AbstractBarChartPlotter.java,v 1.13 2008/08/20 11:09:50 tobiasmalbrecht Exp $
*
*/
public abstract class AbstractBarChartPlotter extends PlotterAdapter {
private static final long serialVersionUID = 1208210421840512091L;
/** The maximal number of printable categories. */
private static final int MAX_CATEGORIES = 200;
/** The currently used data table object. */
private DataTable dataTable;
/** The pie data set. */
private DefaultCategoryDataset categoryDataSet = new DefaultCategoryDataset();
/** The column which is used for the group by attribute. */
private int groupByColumn = -1;
/** The column which is used for the legend. */
private int legendByColumn = -1;
/** The column which is used for the values. */
private int valueColumn = -1;
/** Indicates if only distinct values should be used for aggregation functions. */
private JCheckBox useDistinct = new JCheckBox("Use Only Distinct", false);
/** The used aggregation function. */
private JComboBox aggregationFunction = null;
/** Indicates if absolute values should be used. */
private boolean absolute = false;
public AbstractBarChartPlotter() {
super();
setBackground(Color.white);
useDistinct.setToolTipText("Indicates if only distinct values should be used for aggregation functions.");
useDistinct.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
repaint();
}
});
String[] allFunctions = new String[AbstractAggregationFunction.KNOWN_AGGREGATION_FUNCTION_NAMES.length + 1];
allFunctions[0] = "none";
System.arraycopy(AbstractAggregationFunction.KNOWN_AGGREGATION_FUNCTION_NAMES, 0, allFunctions, 1, AbstractAggregationFunction.KNOWN_AGGREGATION_FUNCTION_NAMES.length);
aggregationFunction = new JComboBox(allFunctions);
aggregationFunction.setToolTipText("Select the type of the aggregation function which should be used for grouped values.");
aggregationFunction.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
repaint();
}
});
}
public AbstractBarChartPlotter(DataTable dataTable) {
this();
setDataTable(dataTable);
}
public abstract JFreeChart createChart(CategoryDataset categoryDataSet, String groupByName, String valueName, boolean createLegend);
public void setDataTable(DataTable dataTable) {
super.setDataTable(dataTable);
this.dataTable = dataTable;
repaint();
}
public void setAbsolute(boolean absolute) {
this.absolute = absolute;
repaint();
}
public boolean isSupportingAbsoluteValues() {
return true;
}
public void setPlotColumn(int index, boolean plot) {
if (plot)
this.valueColumn = index;
else
this.valueColumn = -1;
repaint();
}
public boolean getPlotColumn(int index) {
return valueColumn == index;
}
public String getPlotName() { return "Value Column"; }
public int getNumberOfAxes() {
return 2;
}
public void setAxis(int index, int dimension) {
if (index == 0)
groupByColumn = dimension;
else if (index == 1)
legendByColumn = dimension;
repaint();
}
public int getAxis(int index) {
if (index == 0)
return groupByColumn;
else if (index == 1)
return legendByColumn;
else
return -1;
}
public String getAxisName(int index) {
if (index == 0)
return "Group-By Column";
else if (index == 1)
return "Legend Column";
else
return "Unknown";
}
private int prepareData() {
synchronized (dataTable) {
AggregationFunction aggregation = null;
if (aggregationFunction.getSelectedIndex() > 0) {
try {
aggregation = AbstractAggregationFunction.createAggregationFunction(AbstractAggregationFunction.KNOWN_AGGREGATION_FUNCTION_NAMES[aggregationFunction.getSelectedIndex() - 1]);
} catch (Exception e) {
LogService.getGlobal().logWarning("Cannot instantiate aggregation function '" + aggregationFunction.getSelectedItem() + "', using 'average' as default.");
aggregation = new AverageFunction();
}
}
Iterator<DataTableRow> i = this.dataTable.iterator();
Map<String,Collection<Double>> categoryValues = new LinkedHashMap<String, Collection<Double>>();
categoryDataSet.clear();
if ((groupByColumn >= 0) && (dataTable.isNumerical(groupByColumn)))
return 0;
while (i.hasNext()) {
DataTableRow row = i.next();
double value = Double.NaN;
if (valueColumn >= 0) {
value = row.getValue(valueColumn);
}
if (!Double.isNaN(value)) {
if (absolute)
value = Math.abs(value);
// name
String valueString = null;
if (dataTable.isDate(valueColumn)) {
valueString = Tools.formatDate(new Date((long)value));
} else if (dataTable.isTime(valueColumn)) {
valueString = Tools.formatTime(new Date((long)value));
} else if (dataTable.isDateTime(valueColumn)) {
valueString = Tools.formatDateTime(new Date((long)value));
} else if (dataTable.isNominal(valueColumn)) {
valueString = dataTable.mapIndex(valueColumn, (int)value);
} else {
valueString = Tools.formatIntegerIfPossible(value);
}
String legendName = valueString + "";
if (legendByColumn >= 0) {
double nameValue = row.getValue(legendByColumn);
if (dataTable.isDate(legendByColumn)) {
legendName = Tools.formatDate(new Date((long)nameValue));
} else if (dataTable.isTime(legendByColumn)) {
legendName = Tools.formatTime(new Date((long)nameValue));
} else if (dataTable.isDateTime(legendByColumn)) {
legendName = Tools.formatDateTime(new Date((long)nameValue));
} else if (dataTable.isNominal(legendByColumn)) {
legendName = dataTable.mapIndex(legendByColumn, (int)nameValue) + " (" + valueString + ")";
} else {
legendName = Tools.formatIntegerIfPossible(nameValue) + " (" + valueString + ")";
}
}
String groupByName = legendName;
if (groupByColumn >= 0) {
double nameValue = row.getValue(groupByColumn);
if (dataTable.isDate(groupByColumn)) {
groupByName = Tools.formatDate(new Date((long)nameValue));
} else if (dataTable.isTime(groupByColumn)) {
groupByName = Tools.formatTime(new Date((long)nameValue));
} else if (dataTable.isDateTime(groupByColumn)) {
groupByName = Tools.formatDateTime(new Date((long)nameValue));
} else if (dataTable.isNominal(groupByColumn)) {
groupByName = dataTable.mapIndex(groupByColumn, (int)nameValue);
} else {
groupByName = Tools.formatIntegerIfPossible(nameValue) + "";
}
}
// increment values
Collection<Double> values = categoryValues.get(groupByName);
if (values == null) {
if (useDistinct.isSelected()) {
values = new TreeSet<Double>();
} else {
values = new LinkedList<Double>();
}
categoryValues.put(groupByName, values);
}
values.add(value);
}
}
// calculate aggregation and set values
if (valueColumn >= 0) {
if (aggregation != null) {
Iterator<Map.Entry<String, Collection<Double>>> c = categoryValues.entrySet().iterator();
while (c.hasNext()) {
Map.Entry<String, Collection<Double>> entry = c.next();
String name = entry.getKey();
Collection<Double> values = entry.getValue();
double[] valueArray = new double[values.size()];
Iterator<Double> v = values.iterator();
int valueIndex = 0;
while (v.hasNext()) {
valueArray[valueIndex++] = v.next();
}
double value = aggregation.calculate(valueArray);
if (groupByColumn < 0) {
categoryDataSet.setValue(value, dataTable.getColumnName(valueColumn), name);
} else {
categoryDataSet.setValue(value, name, dataTable.getColumnName(valueColumn));
}
}
} else {
Iterator<Map.Entry<String, Collection<Double>>> c = categoryValues.entrySet().iterator();
while (c.hasNext()) {
Map.Entry<String, Collection<Double>> entry = c.next();
String name = entry.getKey();
Collection<Double> values = entry.getValue();
Iterator<Double> v = values.iterator();
while (v.hasNext()) {
double value = v.next();
if (groupByColumn < 0) {
categoryDataSet.setValue(value, dataTable.getColumnName(valueColumn), name);
} else {
categoryDataSet.setValue(value, name, dataTable.getColumnName(valueColumn));
}
}
}
}
}
return categoryValues.size();
}
}
public void paintComponent(Graphics g) {
super.paintComponent(g);
paintBarChart(g);
}
public void paintBarChart(Graphics graphics) {
int categoryCount = prepareData();
String groupByName = groupByColumn >= 0 ? dataTable.getColumnName(groupByColumn) : null;
String valueName = valueColumn >= 0 ? dataTable.getColumnName(valueColumn) : null;
String maxClassesProperty = System.getProperty(MainFrame.PROPERTY_RAPIDMINER_GUI_PLOTTER_COLORS_CLASSLIMIT);
int maxClasses = 20;
try {
if (maxClassesProperty != null)
maxClasses = Integer.parseInt(maxClassesProperty);
} catch (NumberFormatException e) {
LogService.getGlobal().log("Bar Chart plotter: cannot parse property 'rapidminer.gui.plotter.colors.classlimit', using maximal 20 different classes.", LogService.WARNING);
}
boolean createLegend = categoryCount > 0 && categoryCount < maxClasses;
if (categoryCount <= MAX_CATEGORIES) {
JFreeChart chart = createChart(categoryDataSet, groupByName, valueName, createLegend);
// set the background color for the chart...
chart.setBackgroundPaint(Color.white);
// legend settings
LegendTitle legend = chart.getLegend();
if (legend != null) {
legend.setPosition(RectangleEdge.TOP);
legend.setFrame(BlockBorder.NONE);
legend.setHorizontalAlignment(HorizontalAlignment.LEFT);
}
Rectangle2D drawRect = new Rectangle2D.Double(0, 0, getWidth(), getHeight());
chart.draw((Graphics2D)graphics, drawRect);
} else {
graphics.drawString("Too many columns (" + categoryCount + "), this chart is only able to plot up to " + MAX_CATEGORIES + " different categories", MARGIN, MARGIN);
}
}
public JComponent getOptionsComponent(int index) {
switch (index) {
case 0:
JLabel label = new JLabel("Aggregation");
label.setToolTipText("Select the type of the aggregation function which should be used for grouped values.");
return label;
case 1:
return aggregationFunction;
case 2:
return useDistinct;
}
return null;
}
/** The default implementation delivers an empty set. */
public Collection<String> getAdditionalParameterKeys() {
Collection<String> result = new LinkedList<String>();
result.add("aggregation");
result.add("use_distinct");
return result;
}
/** The default implementation does nothing. */
public void setAdditionalParameter(String key, String value) {
if ("aggregation".equals(key)) {
this.aggregationFunction.setSelectedItem(value);
} else if ("use_distinct".equals(key)) {
boolean distinct = Tools.booleanValue(value, false);
this.useDistinct.setSelected(distinct);
}
}
}