/*
* 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.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.Collection;
import java.util.Date;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.SortedSet;
import java.util.TreeSet;
import javax.swing.BorderFactory;
import javax.swing.Icon;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JScrollPane;
import javax.swing.ListSelectionModel;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import org.jfree.chart.ChartPanel;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.block.BlockBorder;
import org.jfree.chart.plot.PiePlot;
import org.jfree.chart.title.LegendTitle;
import org.jfree.data.general.DefaultPieDataset;
import org.jfree.data.general.PieDataset;
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.gui.plotter.PlotterConfigurationModel;
import com.rapidminer.gui.plotter.PlotterConfigurationModel.PlotterSettingsChangedListener;
import com.rapidminer.gui.plotter.PlotterPanel;
import com.rapidminer.gui.plotter.settings.ListeningJCheckBox;
import com.rapidminer.gui.plotter.settings.ListeningJComboBox;
import com.rapidminer.gui.plotter.settings.ListeningJSlider;
import com.rapidminer.gui.plotter.settings.ListeningListSelectionModel;
import com.rapidminer.gui.tools.ExtendedJList;
import com.rapidminer.gui.tools.ExtendedJScrollPane;
import com.rapidminer.gui.tools.ExtendedListModel;
import com.rapidminer.operator.ports.InputPort;
import com.rapidminer.parameter.ParameterType;
import com.rapidminer.parameter.ParameterTypeBoolean;
import com.rapidminer.parameter.ParameterTypeCategory;
import com.rapidminer.parameter.ParameterTypeEnumeration;
import com.rapidminer.parameter.ParameterTypeInt;
import com.rapidminer.parameter.ParameterTypeString;
import com.rapidminer.tools.LogService;
import com.rapidminer.tools.ParameterService;
import com.rapidminer.tools.Tools;
import com.rapidminer.tools.math.function.aggregation.AbstractAggregationFunction;
import com.rapidminer.tools.math.function.aggregation.AggregationFunction;
import com.rapidminer.tools.math.function.aggregation.AverageFunction;
/**
* This is the main pie chart plotter.
*
* @author Ingo Mierswa
*
*/
public abstract class AbstractPieChartPlotter extends PlotterAdapter {
public static final String PARAMETERS_AGGREGATION = "aggregation";
public static final String PARAMETERS_USE_DISTINCT = "use_distinct";
public static final String PARAMETERS_EXPLOSION_GROUPS = "explosion_groups";
public static final String PARAMETERS_EXPLOSION_AMOUNT = "explosion_amount";
private static final long serialVersionUID = 8750708105082707503L;
/** The maximal number of printable categories. */
private static final int MAX_CATEGORIES = 50;
/** The currently used data table object. */
private DataTable dataTable;
/** The pie data set. */
private DefaultPieDataset pieDataSet = new DefaultPieDataset();
/** The column which is used for the piece names (or group-by statements). */
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 ListeningJCheckBox useDistinct;
/** The used aggregation function. */
private ListeningJComboBox aggregationFunction = null;
/** Indicates if absolute values should be used. */
private boolean absoluteFlag = false;
/** The currently selected groups for explosion. */
private String[] explodingGroups = new String[0];
/** This list hold all groups of the selected grouping column which should be selected for explosion. */
private ExtendedJList explodingGroupList;
private ListeningListSelectionModel explodingGroupListSelectionModel;
/** The slider for the amount of explosion. */
private ListeningJSlider explodingSlider;
/** The currently selected amount of explosion. */
private double explodingAmount = 0.0d;
private String selectedAggregationFunction;
private boolean useDistinctFlag = false;
private ChartPanel panel = new ChartPanel(null);
public AbstractPieChartPlotter(final PlotterConfigurationModel settings) {
super(settings);
setBackground(Color.white);
useDistinct = new ListeningJCheckBox("_" + PARAMETERS_USE_DISTINCT, "Use Only Distinct", false);
useDistinct.setToolTipText("Indicates if only distinct values should be used for aggregation functions.");
useDistinct.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
settings.setParameterAsBoolean(PARAMETERS_USE_DISTINCT, useDistinct.isSelected());
}
});
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 ListeningJComboBox(settings, "_" + PARAMETERS_AGGREGATION, allFunctions);
aggregationFunction.setToolTipText("Select the type of the aggregation function which should be used for grouped values.");
aggregationFunction.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
settings.setParameterAsString(PARAMETERS_AGGREGATION, aggregationFunction.getSelectedItem().toString());
}
});
explodingGroupList = new ExtendedJList(new ExtendedListModel(), 200);
explodingGroupListSelectionModel = new ListeningListSelectionModel("_" + PARAMETERS_EXPLOSION_GROUPS, explodingGroupList);
explodingGroupList.setSelectionModel(explodingGroupListSelectionModel);
explodingGroupList.addListSelectionListener(new ListSelectionListener() {
@Override
public void valueChanged(ListSelectionEvent e) {
if (!e.getValueIsAdjusting()) {
Object[] values = explodingGroupList.getSelectedValues();
List<String> list = new LinkedList<String>();
for (Object object : values) {
list.add((String) object);
}
String result = ParameterTypeEnumeration.transformEnumeration2String(list);
settings.setParameterAsString(PARAMETERS_EXPLOSION_GROUPS, result);
}
}
});
explodingGroupList.setForeground(Color.BLACK);
explodingGroupList.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
explodingGroupList.setBorder(BorderFactory.createLoweredBevelBorder());
explodingGroupList.setCellRenderer(new PlotterPanel.LineStyleCellRenderer(this));
updateGroups();
explodingSlider = new ListeningJSlider("_" + PARAMETERS_EXPLOSION_AMOUNT, 0, 100, 0);
explodingSlider.addChangeListener(new ChangeListener() {
@Override
public void stateChanged(ChangeEvent e) {
settings.setParameterAsInt(PARAMETERS_EXPLOSION_AMOUNT, explodingSlider.getValue());
}
});
}
public AbstractPieChartPlotter(PlotterConfigurationModel settings, DataTable dataTable) {
this(settings);
setDataTable(dataTable);
}
public abstract JFreeChart createChart(PieDataset pieDataSet, boolean createLegend);
public abstract boolean isSupportingExplosion();
@Override
public void setDataTable(DataTable dataTable) {
super.setDataTable(dataTable);
this.dataTable = dataTable;
groupByColumn = -1;
legendByColumn = -1;
valueColumn = -1;
absoluteFlag = false;
explodingGroups = new String[0];
explodingAmount = 0.0d;
updatePlotter();
}
@Override
public void setAbsolute(boolean absolute) {
this.absoluteFlag = absolute;
updatePlotter();
}
@Override
public boolean isSupportingAbsoluteValues() {
return true;
}
@Override
public void setPlotColumn(int index, boolean plot) {
if (plot)
this.valueColumn = index;
else
this.valueColumn = -1;
updatePlotter();
}
@Override
public boolean getPlotColumn(int index) {
return valueColumn == index;
}
@Override
public Icon getIcon(int index) {
return null;
}
@Override
public String getPlotName() {
return "Value Column";
}
@Override
public int getNumberOfAxes() {
return 2;
}
@Override
public void setAxis(int index, int dimension) {
if (index == 0) {
groupByColumn = dimension;
updateGroups();
} else if (index == 1) {
legendByColumn = dimension;
}
updatePlotter();
}
@Override
public int getAxis(int index) {
if (index == 0)
return groupByColumn;
else if (index == 1)
return legendByColumn;
else
return -1;
}
@Override
public String getAxisName(int index) {
if (index == 0)
return "Group-By Column";
else if (index == 1)
return "Legend Column";
else
return "Unknown";
}
private void updateGroups() {
SortedSet<String> groups = new TreeSet<String>();
if (groupByColumn >= 0) {
synchronized (dataTable) {
if (dataTable.isDate(groupByColumn)) {
for (int i = 0; i < dataTable.getNumberOfRows(); i++) {
DataTableRow row = dataTable.getRow(i);
groups.add(Tools.formatDate(new Date((long) row.getValue(groupByColumn))));
}
} else if (dataTable.isTime(groupByColumn)) {
for (int i = 0; i < dataTable.getNumberOfRows(); i++) {
DataTableRow row = dataTable.getRow(i);
groups.add(Tools.formatTime(new Date((long) row.getValue(groupByColumn))));
}
} else if (dataTable.isDateTime(groupByColumn)) {
for (int i = 0; i < dataTable.getNumberOfRows(); i++) {
DataTableRow row = dataTable.getRow(i);
groups.add(Tools.formatDateTime(new Date((long) row.getValue(groupByColumn))));
}
} else if (dataTable.isNominal(groupByColumn)) {
for (int i = 0; i < dataTable.getNumberOfRows(); i++) {
DataTableRow row = dataTable.getRow(i);
groups.add(dataTable.mapIndex(groupByColumn, (int) row.getValue(groupByColumn)));
}
} else {
for (int i = 0; i < dataTable.getNumberOfRows(); i++) {
DataTableRow row = dataTable.getRow(i);
groups.add(Tools.formatIntegerIfPossible(row.getValue(groupByColumn)));
}
}
}
}
if (groups.size() > 0) {
ExtendedListModel model = new ExtendedListModel();
for (String group : groups) {
model.addElement(group, "Select group '" + group + "' for explosion.");
}
this.explodingGroupList.setModel(model);
} else {
ExtendedListModel model = new ExtendedListModel();
model.addElement("Specify 'Group By' first...");
this.explodingGroupList.setModel(model);
}
}
private int prepareData() {
synchronized (dataTable) {
AggregationFunction aggregation = null;
if (selectedAggregationFunction != null && !selectedAggregationFunction.equals("none")) {
try {
aggregation = AbstractAggregationFunction.createAggregationFunction(selectedAggregationFunction);
} catch (Exception e) {
LogService.getGlobal().logWarning("Cannot instantiate aggregation function '" + selectedAggregationFunction + "', using 'average' as default.");
aggregation = new AverageFunction();
}
}
Iterator<DataTableRow> i = this.dataTable.iterator();
Map<String, Collection<Double>> categoryValues = new LinkedHashMap<String, Collection<Double>>();
pieDataSet.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 (absoluteFlag)
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 (useDistinctFlag) {
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 (legendByColumn >= 0) {
pieDataSet.setValue(name, value);
} else {
pieDataSet.setValue(name + " (" + Tools.formatIntegerIfPossible(value) + ")", value);
}
}
} 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 (legendByColumn >= 0) {
pieDataSet.setValue(name, value);
} else {
pieDataSet.setValue(name + " (" + Tools.formatIntegerIfPossible(value) + ")", value);
}
}
}
}
}
return categoryValues.size();
}
}
@Override
public JComponent getPlotter() {
return panel;
}
public void updatePlotter() {
int categoryCount = prepareData();
String maxClassesProperty = ParameterService.getParameterValue(MainFrame.PROPERTY_RAPIDMINER_GUI_PLOTTER_LEGEND_CLASSLIMIT);
int maxClasses = 20;
try {
if (maxClassesProperty != null)
maxClasses = Integer.parseInt(maxClassesProperty);
} catch (NumberFormatException e) {
LogService.getGlobal().log("Pie 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(pieDataSet, createLegend);
// set the background color for the chart...
chart.setBackgroundPaint(Color.white);
PiePlot plot = (PiePlot) chart.getPlot();
plot.setBackgroundPaint(Color.WHITE);
plot.setSectionOutlinesVisible(true);
plot.setShadowPaint(new Color(104, 104, 104, 100));
int size = pieDataSet.getKeys().size();
for (int i = 0; i < size; i++) {
Comparable key = pieDataSet.getKey(i);
plot.setSectionPaint(key, getColorProvider().getPointColor(i / (double) (size - 1)));
boolean explode = false;
for (String explosionGroup : explodingGroups) {
if (key.toString().startsWith(explosionGroup) || explosionGroup.startsWith(key.toString())) {
explode = true;
break;
}
}
if (explode) {
plot.setExplodePercent(key, this.explodingAmount);
}
}
plot.setLabelFont(LABEL_FONT);
plot.setNoDataMessage("No data available");
plot.setCircular(false);
plot.setLabelGap(0.02);
plot.setOutlinePaint(Color.WHITE);
// 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);
}
if (panel instanceof AbstractChartPanel) {
panel.setChart(chart);
} else {
panel = new AbstractChartPanel(chart, getWidth(), getHeight() - MARGIN);
final ChartPanelShiftController controller = new ChartPanelShiftController(panel);
panel.addMouseListener(controller);
panel.addMouseMotionListener(controller);
}
// ATTENTION: WITHOUT THIS WE GET SEVERE MEMORY LEAKS!!!
panel.getChartRenderingInfo().setEntityCollection(null);
} else {
LogService.getGlobal().logNote("Too many columns (" + categoryCount + "), this chart is only able to plot up to " + MAX_CATEGORIES + " different categories.");
}
}
@Override
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;
case 3:
if (isSupportingExplosion()) {
label = new JLabel("Explosion Groups");
label.setToolTipText("Select the groups which should explode, i.e. which should be located outside of the chart to the specified extent.");
return label;
} else {
return null;
}
case 4:
if (isSupportingExplosion()) {
explodingGroupList.setToolTipText("Select the groups which should explode, i.e. which should be located outside of the chart to the specified extent.");
JScrollPane pane = new ExtendedJScrollPane(explodingGroupList);
return pane;
} else {
return null;
}
case 5:
if (isSupportingExplosion()) {
label = new JLabel("Explosion Amount");
label.setToolTipText("Select the amount of explosion for the selected groups.");
return label;
} else {
return null;
}
case 6:
if (isSupportingExplosion()) {
explodingSlider.setToolTipText("Select the amount of explosion for the selected groups.");
return explodingSlider;
}
}
return null;
}
@Override
public List<ParameterType> getAdditionalParameterKeys(InputPort inputPort) {
List<ParameterType> types = super.getAdditionalParameterKeys(inputPort);
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);
types.add(new ParameterTypeCategory(PARAMETERS_AGGREGATION, "The function used for aggregating the values grouped by the specified column.", allFunctions, 0));
types.add(new ParameterTypeBoolean(PARAMETERS_USE_DISTINCT, "Indicates if only distinct values should be regarded for aggregation.", false));
if (isSupportingExplosion()) {
types.add(new ParameterTypeString(PARAMETERS_EXPLOSION_GROUPS, "A comma separated list of groups which should be exploded, i.e. moved out from the center of the plot.", true));
types.add(new ParameterTypeInt(PARAMETERS_EXPLOSION_AMOUNT, "The percentage of explosion for the selected groups.", 0, 100, 0));
}
return types;
}
/** The default implementation does nothing. */
@Override
public void setAdditionalParameter(String key, String value) {
super.setAdditionalParameter(key, value);
if (key.equals(PARAMETERS_AGGREGATION)) {
selectedAggregationFunction = value;
updatePlotter();
} else if (key.equals(PARAMETERS_USE_DISTINCT)) {
useDistinctFlag = Boolean.parseBoolean(value);
updatePlotter();
} else if (key.equals(PARAMETERS_EXPLOSION_GROUPS)) {
String[] newGroups = new String[0];
if (value != null) {
newGroups = value.split(",");
}
for (int i = 0; i < newGroups.length; i++) {
newGroups[i] = newGroups[i].trim();
}
this.explodingGroups = newGroups;
updatePlotter();
} else if (key.equals(PARAMETERS_EXPLOSION_AMOUNT)) {
explodingAmount = Double.parseDouble(value) / 50d;
updatePlotter();
} else if (key.equals(PARAMETERS_USE_DISTINCT)) {
this.useDistinctFlag = Boolean.parseBoolean(value);
updatePlotter();
}
}
@Override
public List<PlotterSettingsChangedListener> getListeningObjects() {
List<PlotterSettingsChangedListener> listeningObjects = super.getListeningObjects();
listeningObjects.add(explodingGroupListSelectionModel);
listeningObjects.add(useDistinct);
listeningObjects.add(explodingSlider);
listeningObjects.add(aggregationFunction);
return listeningObjects;
}
}