/* Copyright 2006 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 javax.swing.event.*; import sim.util.gui.*; // From JFreeChart import org.jfree.chart.*; import org.jfree.chart.axis.*; import org.jfree.chart.event.*; import org.jfree.chart.plot.*; import org.jfree.data.statistics.*; import org.jfree.data.general.*; import org.jfree.chart.title.*; import org.jfree.data.xy.*; import org.jfree.chart.renderer.category.*; import org.jfree.data.*; import org.jfree.data.category.*; import org.jfree.chart.labels.*; // 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.*; */ /** BoxPlotGenerator is a ChartGenerator which displays a BoxPlot using the JFreeChart library. The generator uses the HistoramDataset as its dataset, which holds BoxPlot elements consisting of a name, an array of doubles (the samples), and an integer (the number of bins). representing a time series displayed on the chart. You add series to the generator with the <tt>addSeries</tt> method. <p>BoxPlotChartGenerator creates attributes components in the form of BoxPlotAttributes, which work with the generator to properly update the chart to reflect changes the user has made to its display. */ public class BoxPlotGenerator extends ChartGenerator { /** The global attributes range axis field. */ PropertyField yLabel; /** The global attributes domain axis field. */ PropertyField xLabel; /** The global attributes logarithmic range axis check box. */ JCheckBox yLog; JCheckBox mean; JCheckBox median; NumberTextField maximumWidthField; public void setMaximumWidth(double value) { maximumWidthField.setValue(maximumWidthField.newValue(value)); } public double getMaximumWidth() { return maximumWidthField.getValue(); } public void setYAxisLogScaled(boolean isLogScaled){yLog.setSelected(isLogScaled);} public boolean isYAxisLogScaled(){return yLog.isSelected();} public void setMeanShown(boolean val){mean.setSelected(val);} public boolean isMeanShown(){return mean.isSelected();} public void setMedianShown(boolean val){median.setSelected(val);} public boolean isMedianShown(){return median.isSelected();} /** Returns the name of the Y Axis label. */ public String getYAxisLabel() { return ((CategoryPlot)(chart.getPlot())).getRangeAxis().getLabel(); } /** Returns the name of the X Axis label. */ public String getXAxisLabel() { return ((CategoryPlot)(chart.getPlot())).getDomainAxis().getLabel(); } public Dataset getSeriesDataset() { return ((CategoryPlot)(chart.getPlot())).getDataset(); } public void setSeriesDataset(Dataset obj) { ((CategoryPlot)(chart.getPlot())).setDataset((DefaultBoxAndWhiskerCategoryDataset)obj); if (invalidChartTitle != null) setInvalidChartTitle(null); } public int getSeriesCount() { DefaultBoxAndWhiskerCategoryDataset dataset = (DefaultBoxAndWhiskerCategoryDataset)(getSeriesDataset()); return dataset.getRowCount(); } public void removeSeries(int index) { super.removeSeries(index); update(); } public void moveSeries(int index, boolean up) { super.moveSeries(index, up); update(); } protected void buildChart() { DefaultBoxAndWhiskerCategoryDataset dataset = new DefaultBoxAndWhiskerCategoryDataset(); // we build the chart manually rather than using ChartFactory // because we need to customize the getDataRange method below CategoryAxis categoryAxis = new CategoryAxis(""); NumberAxis valueAxis = new NumberAxis("Untitled Y Axis"); valueAxis.setAutoRangeIncludesZero(false); BoxAndWhiskerRenderer renderer = new BoxAndWhiskerRenderer(); renderer.setBaseToolTipGenerator(new BoxAndWhiskerToolTipGenerator()); CategoryPlot plot = new CategoryPlot(dataset, categoryAxis, valueAxis, renderer) { // Customizing this method in order to provide a bit of // vertical buffer. Otherwise the bar chart box gets drawn // slightly off-chart, which looks really bad. public Range getDataRange(ValueAxis axis) { Range range = super.getDataRange(axis); if (range == null) return null; final double EXTRA_PERCENTAGE = 0.02; return Range.expand(range, EXTRA_PERCENTAGE, EXTRA_PERCENTAGE); } }; chart = new JFreeChart("Untitled Chart", JFreeChart.DEFAULT_TITLE_FONT, plot, false); ChartFactory.getChartTheme().apply(chart); chart.setAntiAlias(true); chartPanel = buildChartPanel(chart); setChartPanel(chartPanel); // this must come last because the chart must exist for us to set its dataset setSeriesDataset(dataset); } ArrayList buildList(double[] vals) { ArrayList list = new ArrayList(); for(int i = 0; i < vals.length; i++) list.add(new Double(vals[i])); return list; } protected void update() { // We have to rebuild the dataset from scratch (deleting and replacing it) because JFreeChart's // BoxPlot facility doesn't have a way to remove or move elements. Stupid stupid stupid. SeriesAttributes[] sa = getSeriesAttributes(); DefaultBoxAndWhiskerCategoryDataset dataset = new DefaultBoxAndWhiskerCategoryDataset(); for(int i=0; i < sa.length; i++) { BoxPlotSeriesAttributes attributes = (BoxPlotSeriesAttributes)(sa[i]); double[][] values = attributes.getValues(); String[] labels = attributes.getLabels(); //UniqueString series = new UniqueString(attributes.getSeriesName()); String series = attributes.getSeriesName(); for(int j = 0; j < values.length; j++) { dataset.add(buildList(values[j]), series, labels[j]); } } ((BoxAndWhiskerRenderer)(((CategoryPlot)(chart.getPlot())).getRenderer())).setMaximumBarWidth(getMaximumWidth()); setSeriesDataset(dataset); } public SeriesAttributes addSeries(double[] vals, String name, SeriesChangeListener stopper) { double[][] vvals = new double[1][]; vvals[0] = vals; return addSeries(vvals, name, 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. */ SeriesAttributes addSeries(double[][] vals, String name, SeriesChangeListener stopper) { if (vals == null || vals.length == 0) vals = new double[0][0]; int i = getSeriesCount(); // need to have added the dataset BEFORE calling this since it'll try to change the name of the series BoxPlotSeriesAttributes csa = new BoxPlotSeriesAttributes(this, name, i, vals, stopper); 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; } public SeriesAttributes addSeries(double[][] vals, String[] labels, String name, SeriesChangeListener stopper) { if (vals == null || vals.length == 0) vals = new double[0][0]; int i = getSeriesCount(); // need to have added the dataset BEFORE calling this since it'll try to change the name of the series BoxPlotSeriesAttributes csa = new BoxPlotSeriesAttributes(this, name, i, vals, labels, stopper); 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; } /** Sets the name of the Y Axis label. */ public void setYAxisLabel(String val) { CategoryPlot xyplot = (CategoryPlot)(chart.getPlot()); xyplot.getRangeAxis().setLabel(val); xyplot.axisChanged(new AxisChangeEvent(xyplot.getRangeAxis())); yLabel.setValue(val); } /** Sets the name of the X Axis label. */ public void setXAxisLabel(String val) { CategoryPlot xyplot = (CategoryPlot)(chart.getPlot()); xyplot.getDomainAxis().setLabel(val); xyplot.axisChanged(new AxisChangeEvent(xyplot.getDomainAxis())); xLabel.setValue(val); } public void updateSeries(int index, double[] vals) { double[][] vvals = new double[1][]; vvals[0] = vals; updateSeries(index, vvals); } public void updateSeries(int index, double[][] vals) { 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 BoxPlot in a display return; if (vals == null || vals.length == 0) vals = new double[0][0]; BoxPlotSeriesAttributes hsa = (BoxPlotSeriesAttributes)(getSeriesAttribute(index)); hsa.setValues(vals); hsa.setLabels(null); } public void updateSeries(int index, double[][] vals, 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 BoxPlot in a display return; if (vals == null || vals.length == 0) vals = new double[0][0]; if (labels == null || labels.length == 0) labels = new String[0]; if (vals.length != labels.length) // uh oh return; BoxPlotSeriesAttributes hsa = (BoxPlotSeriesAttributes)(getSeriesAttribute(index)); hsa.setValues(vals); hsa.setLabels(labels); } protected void buildGlobalAttributes(LabelledList list) { // create the chart ((CategoryPlot)(chart.getPlot())).setRangeGridlinesVisible(false); ((CategoryPlot)(chart.getPlot())).setRangeGridlinePaint(new Color(200,200,200)); xLabel = new PropertyField() { public String newValue(String newValue) { setXAxisLabel(newValue); getChartPanel().repaint(); return newValue; } }; xLabel.setValue(getXAxisLabel()); list.add(new JLabel("X Label"), xLabel); yLabel = new PropertyField() { public String newValue(String newValue) { setYAxisLabel(newValue); getChartPanel().repaint(); return newValue; } }; yLabel.setValue(getYAxisLabel()); list.add(new JLabel("Y Label"), yLabel); yLog = new JCheckBox(); yLog.addChangeListener(new ChangeListener(){ public void stateChanged(ChangeEvent e) { if(yLog.isSelected()) { LogarithmicAxis logAxis = new LogarithmicAxis(yLabel.getValue()); logAxis.setStrictValuesFlag(false); ((CategoryPlot)(chart.getPlot())).setRangeAxis(logAxis); } else ((CategoryPlot)(chart.getPlot())).setRangeAxis(new NumberAxis(yLabel.getValue())); } }); list.add(new JLabel("Y Log Axis"), yLog); final JCheckBox ygridlines = new JCheckBox(); ygridlines.setSelected(false); ItemListener il = new ItemListener() { public void itemStateChanged(ItemEvent e) { if (e.getStateChange() == ItemEvent.SELECTED) { ((CategoryPlot)(chart.getPlot())).setRangeGridlinesVisible(true); } else { ((CategoryPlot)(chart.getPlot())).setRangeGridlinesVisible(false); } } }; ygridlines.addItemListener(il); // JFreeChart's Box Plots look awful when wide because the mean // circle is based on the width of the bar to the exclusion of all // else. So I've restricted the width to be no more than 0.4, and 0.1 // is the suggested default. final double INITIAL_WIDTH = 0.1; final double MAXIMUM_RATIONAL_WIDTH = 0.4; maximumWidthField = new NumberTextField(INITIAL_WIDTH, 2.0, 0) { public double newValue(double newValue) { if (newValue <= 0.0 || newValue > MAXIMUM_RATIONAL_WIDTH) newValue = currentValue; ((BoxAndWhiskerRenderer)(((CategoryPlot)(chart.getPlot())).getRenderer())).setMaximumBarWidth(newValue); //update(); return newValue; } }; list.addLabelled("Max Width",maximumWidthField); Box box = Box.createHorizontalBox(); box.add(new JLabel(" Y")); box.add(ygridlines); box.add(Box.createGlue()); list.add(new JLabel("Y Grid Lines"), ygridlines); mean = new JCheckBox(); mean.setSelected(true); il = new ItemListener() { public void itemStateChanged(ItemEvent e) { BoxAndWhiskerRenderer renderer = ((BoxAndWhiskerRenderer)((CategoryPlot)(chart.getPlot())).getRenderer()); renderer.setMeanVisible(mean.isSelected()); } }; mean.addItemListener(il); median = new JCheckBox(); median.setSelected(true); il = new ItemListener() { public void itemStateChanged(ItemEvent e) { BoxAndWhiskerRenderer renderer = ((BoxAndWhiskerRenderer)((CategoryPlot)(chart.getPlot())).getRenderer()); renderer.setMedianVisible(median.isSelected()); } }; median.addItemListener(il); list.add(new JLabel("Mean"), mean); list.add(new JLabel("Median"), median); final JCheckBox horizontal = new JCheckBox(); horizontal.setSelected(false); il = new ItemListener() { public void itemStateChanged(ItemEvent e) { CategoryPlot plot = (CategoryPlot)(chart.getPlot()); if (e.getStateChange() == ItemEvent.SELECTED) { plot.setOrientation(PlotOrientation.HORIZONTAL); } else { plot.setOrientation(PlotOrientation.VERTICAL); } //updateGridLines(); } }; horizontal.addItemListener(il); list.add(new JLabel("Horizontal"), horizontal); final JCheckBox whiskersUseFillColorButton = new JCheckBox(); whiskersUseFillColorButton.setSelected(false); whiskersUseFillColorButton.addChangeListener(new ChangeListener(){ public void stateChanged(ChangeEvent e) { BoxAndWhiskerRenderer renderer = ((BoxAndWhiskerRenderer)((CategoryPlot)(chart.getPlot())).getRenderer()); renderer.setUseOutlinePaintForWhiskers(!whiskersUseFillColorButton.isSelected()); } }); box = Box.createHorizontalBox(); box.add(new JLabel(" Colored")); box.add(whiskersUseFillColorButton); box.add(Box.createGlue()); list.add(new JLabel("Whiskers"), box); } }