/*
Copyright 2013 by Sean Luke and George Mason University
Licensed under the Academic Free License version 3.0
See the file "LICENSE" for more information
*/
package sim.util.media.chart;
import java.awt.*;
import java.util.*;
import java.awt.event.*;
import javax.swing.*;
import sim.util.gui.*;
// From JFreeChart
import org.jfree.chart.*;
import org.jfree.chart.plot.*;
import org.jfree.data.general.*;
import org.jfree.data.statistics.*;
import org.jfree.chart.renderer.xy.*;
import org.jfree.data.category.*;
import org.jfree.chart.title.*;
import org.jfree.ui.*;
// from iText (www.lowagie.com/iText/)
import com.lowagie.text.*;
import com.lowagie.text.pdf.*;
/* // looks like we'll have to move to these soon
import com.itextpdf.text.*;
import com.itextpdf.text.pdf.*;
*/
/** A ChartGenerator for Pie Charts. */
public class PieChartGenerator extends ChartGenerator
{
public void removeSeries(int index)
{
super.removeSeries(index);
update();
}
public void moveSeries(int index, boolean up)
{
super.moveSeries(index, up);
update();
}
/** The total number of unique groups permitted in the generator, to keep from overwhelming JFreeChart. */
public static final int MAXIMUM_PIE_CHART_ITEMS = 20;
final DefaultCategoryDataset emptyDataset = new DefaultCategoryDataset();
public Dataset getSeriesDataset() { return ((MultiplePiePlot)(chart.getPlot())).getDataset(); }
public void setSeriesDataset(Dataset obj)
{
// here we will interrupt things if they're too big
if (((CategoryDataset)obj).getRowCount() > MAXIMUM_PIE_CHART_ITEMS)
{
((MultiplePiePlot)(chart.getPlot())).setDataset(emptyDataset);
setInvalidChartTitle("[[ Dataset has too many items. ]]");
}
else
{
((MultiplePiePlot)(chart.getPlot())).setDataset((DefaultCategoryDataset)obj);
if (invalidChartTitle != null)
setInvalidChartTitle(null);
}
}
public int getProspectiveSeriesCount(Object[] objs)
{
HashMap map = convertIntoAmountsAndLabels(objs);
String[] labels = revisedLabels(map);
return labels.length;
}
public int getSeriesCount()
{
SeriesAttributes[] sa = getSeriesAttributes();
return sa.length; // we do this instead of returning the columns in the dataset because hidden series don't have columns (stupid JFreeChart)
}
protected void buildChart()
{
DefaultCategoryDataset dataset = new DefaultCategoryDataset();
chart = ChartFactory.createMultiplePieChart("Untitled Chart", dataset,
org.jfree.util.TableOrder.BY_COLUMN, false, true, false);
chart.setAntiAlias(true);
//chartPanel = new ScrollableChartPanel(chart, true);
chartPanel = buildChartPanel(chart);
//chartHolder.getViewport().setView(chartPanel);
setChartPanel(chartPanel);
JFreeChart baseChart = (JFreeChart) ((MultiplePiePlot)(chart.getPlot())).getPieChart();
PiePlot base = (PiePlot) (baseChart.getPlot());
base.setIgnoreZeroValues(true);
base.setLabelOutlinePaint(java.awt.Color.WHITE);
base.setLabelShadowPaint(java.awt.Color.WHITE);
base.setMaximumLabelWidth(0.25); // allow bigger labels by a bit (this will make the chart smaller)
base.setInteriorGap(0.000); // allow stretch to compensate for the bigger label width
base.setLabelBackgroundPaint(java.awt.Color.WHITE);
base.setOutlinePaint(null);
base.setBackgroundPaint(null);
base.setShadowPaint(null);
base.setSimpleLabels(false); // I think they're false anyway
// change the look of the series title to be smaller
StandardChartTheme theme = new StandardChartTheme("Hi");
TextTitle title = new TextTitle("Whatever", theme.getLargeFont());
title.setPaint(theme.getAxisLabelPaint());
title.setPosition(RectangleEdge.BOTTOM);
baseChart.setTitle(title);
// this must come last because the chart must exist for us to set its dataset
setSeriesDataset(dataset);
}
protected void update()
{
// We have to rebuild the dataset from scratch (deleting and replacing it) because JFreeChart's
// piechart facility doesn't have a way to move series. Just like the histogram system: stupid stupid stupid.
SeriesAttributes[] sa = getSeriesAttributes();
DefaultCategoryDataset dataset = new DefaultCategoryDataset();
for(int i=0; i < sa.length; i++)
if (sa[i].isPlotVisible())
{
PieChartSeriesAttributes attributes = (PieChartSeriesAttributes)(sa[i]);
Object[] elements = attributes.getElements();
double[] values = null;
String[] labels = null;
if (elements != null)
{
HashMap map = convertIntoAmountsAndLabels(elements);
labels = revisedLabels(map);
values = amounts(map, labels);
}
else
{
values = attributes.getValues();
labels = attributes.getLabels();
}
UniqueString seriesName = new UniqueString(attributes.getSeriesName());
for(int j = 0; j < values.length; j++)
dataset.addValue(values[j], labels[j], seriesName); // ugh
}
setSeriesDataset(dataset);
}
public PieChartGenerator()
{
// buildChart is called by super() first
}
protected PieChartSeriesAttributes buildNewAttributes(String name, SeriesChangeListener stopper)
{
return new PieChartSeriesAttributes(this, name, getSeriesCount(), stopper);
}
/** Adds a series, plus a (possibly null) SeriesChangeListener which will receive a <i>single</i>
event if/when the series is deleted from the chart by the user. Returns the series attributes. */
public SeriesAttributes addSeries(double[] amounts, String[] labels, String name, SeriesChangeListener stopper)
{
int i = getSeriesCount();
// need to have added the dataset BEFORE calling this since it'll try to change the name of the series
PieChartSeriesAttributes csa = buildNewAttributes(name, stopper);
// set information
csa.setValues((double[])(amounts.clone()));
csa.setLabels((String[])(labels.clone()));
seriesAttributes.add(csa);
revalidate(); // display the new series panel
update();
// won't update properly unless I force it here by letting all the existing scheduled events to go through. Dumb design. :-(
SwingUtilities.invokeLater(new Runnable() { public void run() { update(); } });
return csa;
}
/** Adds a series, plus a (possibly null) SeriesChangeListener which will receive a <i>single</i>
event if/when the series is deleted from the chart by the user. Returns the series attributes. */
public SeriesAttributes addSeries(Object[] objs, String name, SeriesChangeListener stopper)
{
int i = getSeriesCount();
// need to have added the dataset BEFORE calling this since it'll try to change the name of the series
PieChartSeriesAttributes csa = buildNewAttributes(name, stopper);
// set information
csa.setElements((Object[])(objs.clone()));
seriesAttributes.add(csa);
revalidate(); // display the new series panel
update();
// won't update properly unless I force it here by letting all the existing scheduled events to go through. Dumb design. :-(
SwingUtilities.invokeLater(new Runnable() { public void run() { update(); } });
return csa;
}
/** Adds a series, plus a (possibly null) SeriesChangeListener which will receive a <i>single</i>
event if/when the series is deleted from the chart by the user. Returns the series attributes. */
public SeriesAttributes addSeries(Collection objs, String name, SeriesChangeListener stopper)
{
//int i = getSeriesCount();
// need to have added the dataset BEFORE calling this since it'll try to change the name of the series
PieChartSeriesAttributes csa = buildNewAttributes(name, stopper);
// set information
csa.setElements(new ArrayList(objs));
seriesAttributes.add(csa);
revalidate(); // display the new series panel
update();
// won't update properly unless I force it here by letting all the existing scheduled events to go through. Dumb design. :-(
SwingUtilities.invokeLater(new Runnable() { public void run() { update(); } });
return csa;
}
// Takes objects and produces an object->count mapping
HashMap convertIntoAmountsAndLabels(Object[] objs)
{
// Total the amounts
HashMap map = new HashMap();
for(int i = 0; i < objs.length; i++)
{
String label = "null";
if (objs[i] != null)
label = objs[i].toString();
if (map.containsKey(label))
map.put(label,
new Double(((Double)(map.get(label))).doubleValue() + 1));
else
map.put(label, new Double(1));
}
return map;
}
// Sorts labels from the mapping. We may get rid of this later perhaps.
String[] revisedLabels(HashMap map)
{
// Sort labels
String[] labels = new String[map.size()];
labels = (String[])(map.keySet().toArray(labels));
Arrays.sort(labels);
return labels;
}
// Returns the counts from the mapping, in the same order as the labels
double[] amounts(HashMap map, String[] revisedLabels)
{
// Extract amounts
double[] amounts = new double[map.size()];
for(int i = 0; i < amounts.length; i++)
amounts[i] = ((Double)(map.get(revisedLabels[i]))).doubleValue();
return amounts;
}
public void updateSeries(int index, Collection objs)
{
if (index < 0) // this happens when we're a dead chart but the inspector doesn't know
return;
if (index >= getNumSeriesAttributes()) // this can happen when we close a window if we use the Histogram in a display
return;
PieChartSeriesAttributes hsa = (PieChartSeriesAttributes)(getSeriesAttribute(index));
hsa.setElements(new ArrayList(objs));
}
public void updateSeries(int index, Object[] objs)
{
if (index < 0) // this happens when we're a dead chart but the inspector doesn't know
return;
if (index >= getNumSeriesAttributes()) // this can happen when we close a window if we use the Histogram in a display
return;
PieChartSeriesAttributes hsa = (PieChartSeriesAttributes)(getSeriesAttribute(index));
hsa.setElements((Object[])(objs.clone()));
}
public void updateSeries(int index, double[] amounts, String[] labels)
{
if (index < 0) // this happens when we're a dead chart but the inspector doesn't know
return;
if (index >= getNumSeriesAttributes()) // this can happen when we close a window if we use the Histogram in a display
return;
PieChartSeriesAttributes hsa = (PieChartSeriesAttributes)(getSeriesAttribute(index));
hsa.setValues((double[])(amounts.clone()));
hsa.setLabels((String[])(labels.clone()));
}
}