Hi
now I implemented a data container for my stacked bar charts with time series, here is how it looks like at the moment:
But unfortunately there are some open questions:
1. Is there a more elegant way of doing this ingeneral? Or are there some data container already which met my requirements better?
2. Why are the bar charts so smal, how can I have them wider?
3. The tooltips show only the complete amount of the series, not the value I put into the series, this has todo with the renderer, I guess. But I've no idea how and where to change it.
Here is some code, no further changes would be neccessary in the existing JFreeChart 0.9.20:
The Example Class:
Code: Select all
package org.jfree.chart.demo;
import java.text.DecimalFormat;
import java.text.SimpleDateFormat;
import org.jfree.chart.ChartPanel;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.axis.DateAxis;
import org.jfree.chart.axis.DateTickMarkPosition;
import org.jfree.chart.axis.NumberAxis;
import org.jfree.chart.axis.ValueAxis;
import org.jfree.chart.labels.StandardXYToolTipGenerator;
import org.jfree.chart.plot.DatasetRenderingOrder;
import org.jfree.chart.plot.XYPlot;
import org.jfree.chart.renderer.StandardXYItemRenderer;
import org.jfree.chart.renderer.XYBarRenderer;
import org.jfree.chart.renderer.XYItemRenderer;
import org.jfree.data.DefaultTableStackedTimeSeriesDataset;
import org.jfree.data.IntervalXYDataset;
import org.jfree.data.XYDataset;
import org.jfree.data.time.Day;
import org.jfree.data.time.StackedBarTimeSeries;
import org.jfree.data.time.TimeSeries;
import org.jfree.data.time.TimeSeriesCollection;
import org.jfree.date.SerialDate;
import org.jfree.ui.ApplicationFrame;
import org.jfree.ui.RefineryUtilities;
/**
* A demonstration application showing a time series chart overlaid with a vertical StackedTimeSeries bar chart.
*
* @author Heinrich G&tzger
*/
public class OverlaidXYPlotDemo3 extends ApplicationFrame {
/**
* Constructs a new demonstration application.
*
* @param title the frame title.
*/
public OverlaidXYPlotDemo3(String title) {
super(title);
JFreeChart chart = createOverlaidChart();
ChartPanel panel = new ChartPanel(chart, true, true, true, true, true);
panel.setPreferredSize(new java.awt.Dimension(500, 270));
setContentPane(panel);
}
/**
* Creates an overlaid chart.
*
* @return The chart.
*/
private JFreeChart createOverlaidChart() {
// create plot ...
final IntervalXYDataset data1 = createDataset1();
final XYItemRenderer renderer1 = new XYBarRenderer(0.2);
renderer1.setToolTipGenerator(
new StandardXYToolTipGenerator(
StandardXYToolTipGenerator.DEFAULT_TOOL_TIP_FORMAT,
new SimpleDateFormat("d-MMM-yyyy"), new DecimalFormat("0.00")
)
);
final DateAxis domainAxis = new DateAxis("Date");
domainAxis.setTickMarkPosition(DateTickMarkPosition.MIDDLE);
final ValueAxis rangeAxis = new NumberAxis("Value");
final XYPlot plot = new XYPlot(data1, domainAxis, rangeAxis, renderer1);
// add a second dataset and renderer...
final XYDataset data2 = createDataset2();
final XYItemRenderer renderer2 = new StandardXYItemRenderer();
renderer2.setToolTipGenerator(
new StandardXYToolTipGenerator(
StandardXYToolTipGenerator.DEFAULT_TOOL_TIP_FORMAT,
new SimpleDateFormat("d-MMM-yyyy"), new DecimalFormat("0.00")
)
);
plot.setDataset(1, data2);
plot.setRenderer(1, renderer2);
plot.setDatasetRenderingOrder(DatasetRenderingOrder.FORWARD);
// return a new chart containing the overlaid plot...
return new JFreeChart("Overlaid Plot Example 3", JFreeChart.DEFAULT_TITLE_FONT, plot, true);
}
/**
* Creates a sample dataset.
*
* @return The dataset.
*/
private IntervalXYDataset createDataset1() {
DefaultTableStackedTimeSeriesDataset ts = new DefaultTableStackedTimeSeriesDataset();
StackedBarTimeSeries s1 = new StackedBarTimeSeries("Series 1", Day.class);
StackedBarTimeSeries s2 = new StackedBarTimeSeries("Series 2", Day.class);
StackedBarTimeSeries s3 = new StackedBarTimeSeries("Series 3", Day.class);
StackedBarTimeSeries s4 = new StackedBarTimeSeries("Series 4", Day.class);
s1.add(new Day(3, SerialDate.MARCH, 2002), 20);
s1.add(new Day(7, SerialDate.MARCH, 2002), 10);
// s1.add(new Day(14, SerialDate.MARCH, 2002), 30);
s2.add(new Day(3, SerialDate.MARCH, 2002), 20);
// s2.add(new Day(7, SerialDate.MARCH, 2002), 10);
s2.add(new Day(14, SerialDate.MARCH, 2002), 30);
// s3.add(new Day(3, SerialDate.MARCH, 2002), 20);
s3.add(new Day(7, SerialDate.MARCH, 2002), 10);
s3.add(new Day(14, SerialDate.MARCH, 2002), 30);
// s4.add(new Day(3, SerialDate.MARCH, 2002), 20);
s4.add(new Day(7, SerialDate.MARCH, 2002), 10);
// s4.add(new Day(14, SerialDate.MARCH, 2002), 30);
ts.addSeries(s1);
ts.addSeries(s2);
ts.addSeries(s3);
ts.addSeries(s4);
return ts;
}
/**
* Creates a sample dataset.
*
* @return The dataset.
*/
private XYDataset createDataset2() {
// create dataset 2...
TimeSeries series2 = new TimeSeries("Series 5", Day.class);
series2.add(new Day(3, SerialDate.MARCH, 2002), 168.2);
series2.add(new Day(4, SerialDate.MARCH, 2002), 196.3);
series2.add(new Day(5, SerialDate.MARCH, 2002), 182.5);
series2.add(new Day(6, SerialDate.MARCH, 2002), 153.3);
series2.add(new Day(7, SerialDate.MARCH, 2002), 135.0);
series2.add(new Day(8, SerialDate.MARCH, 2002), 126.3);
series2.add(new Day(9, SerialDate.MARCH, 2002), 139.2);
series2.add(new Day(10, SerialDate.MARCH, 2002), 119.2);
series2.add(new Day(11, SerialDate.MARCH, 2002), 169.9);
series2.add(new Day(12, SerialDate.MARCH, 2002), 178.2);
series2.add(new Day(13, SerialDate.MARCH, 2002), 164.3);
series2.add(new Day(14, SerialDate.MARCH, 2002), 179.6);
series2.add(new Day(15, SerialDate.MARCH, 2002), 185.7);
series2.add(new Day(16, SerialDate.MARCH, 2002), 195.9);
TimeSeriesCollection tsc = new TimeSeriesCollection(series2);
return tsc;
}
/**
* Starting point for the demonstration application.
*
* @param args ignored.
*/
public static void main(String[] args) {
OverlaidXYPlotDemo3 demo = new OverlaidXYPlotDemo3("Overlaid TimeSeriesPlot Demo 3");
demo.pack();
RefineryUtilities.centerFrameOnScreen(demo);
demo.setVisible(true);
}
}
The StackedBarTimeSeries is just to have a differnet series class:
Code: Select all
package org.jfree.data.time;
/**
* Represents a sequence of zero or more data items in the form (period, value).
*/
public class StackedBarTimeSeries extends TimeSeries {
/**
* Creates a new (empty) time series. By default, a daily time series is
* created. Use one of the other constructors if you require a different time
* period.
* @param name the series name (<code>null</code> not permitted).
*/
public StackedBarTimeSeries(final String name) {
super(name, DEFAULT_DOMAIN_DESCRIPTION, DEFAULT_RANGE_DESCRIPTION,
Day.class);
}
/**
* Creates a new (empty) time series.
* @param name the series name (<code>null</code> not permitted).
* @param timePeriodClass the type of time period (<code>null</code> not
* permitted).
*/
public StackedBarTimeSeries(final String name, final Class timePeriodClass) {
super(name, DEFAULT_DOMAIN_DESCRIPTION, DEFAULT_RANGE_DESCRIPTION,
timePeriodClass);
}
/**
* Creates a new time series that contains no data.
* <P>
* Descriptions can be specified for the domain and range. One situation
* where this is helpful is when generating a chart for the time series -
* axis labels can be taken from the domain and range description.
* @param name the name of the series (<code>null</code> not permitted).
* @param domain the domain description (<code>null</code> permitted).
* @param range the range description (<code>null</code> permitted).
* @param timePeriodClass the type of time period (<code>null</code> not
* permitted).
*/
public StackedBarTimeSeries(final String name, final String domain,
final String range, final Class timePeriodClass) {
super(name, domain, range, timePeriodClass);
}
}
The interface for the Table of TimeSeries ....
which is basicly a copy of
Code: Select all
package org.jfree.data;
/**
* A dataset containing one or more time series containing (x, y) data items, where all series
* in the dataset share the same set of x-values (points of time). This is a restricted form of the
* {@link XYDataset} interface (which allows independent x-values between series). This is used
* primarily by the {@link org.jfree.chart.renderer.StackedXYAreaRenderer}.
*/
public interface TableTimeSeriesDataset extends XYDataset {
/**
* Returns the number of items every series.
*
* @return the item count.
*/
public int getItemCount();
}
.... and it's implementation
which is basicly a copy of DefaultTableXYDataset with adjustment to StackedBarTimeSeries and the method updateYPoints:
Code: Select all
package org.jfree.data;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import org.jfree.data.time.RegularTimePeriod;
import org.jfree.data.time.StackedBarTimeSeries;
import org.jfree.data.time.TimeSeriesDataItem;
import org.jfree.util.ObjectUtils;
/**
* An {@link XYDataset} where every series shares the same point-of-time-values (required for
* generating stacked time bar charts).
*
* @author Heinrich G&tzger
*/
public class DefaultTableStackedTimeSeriesDataset extends AbstractIntervalXYDataset
implements TableTimeSeriesDataset, IntervalXYDataset, DomainInfo {
/** Storage for the data. */
private List data = null;
/** Storage for the x values. */
private HashSet xPoints = null;
/** A flag that controls whether or not events are propogated. */
private boolean propagateEvents = true;
private boolean autoPrune = false;
private IntervalXYDelegate intervalDelegate;
/**
* Creates a new empty dataset.
*/
public DefaultTableStackedTimeSeriesDataset() {
this(false);
}
/**
* Creates a new empty dataset.
*
* @param autoPrune a flag that controls whether or not x-values are removed whenever the
* corresponding y-values are all <code>null</code>.
*/
public DefaultTableStackedTimeSeriesDataset(boolean autoPrune) {
this.autoPrune = autoPrune;
this.data = new ArrayList();
this.xPoints = new HashSet();
this.intervalDelegate = new IntervalXYDelegate(this, false);
}
/**
* Returns the flag that controls whether or not x-values are removed from the dataset when
* the corresponding y-values are all <code>null</code>.
*
* @return a boolean.
*/
public boolean isAutoPrune() {
return this.autoPrune;
}
/**
* Adds a series to the collection and sends a {@link DatasetChangeEvent} to all registered
* listeners. The series should be configured to NOT allow duplicate x-values.
*
* @param series the series (<code>null</code> not permitted).
*/
public void addSeries(StackedBarTimeSeries series) {
if (series == null) {
throw new IllegalArgumentException("Null 'series' argument.");
}
updateXPoints(series);
this.data.add(series);
series.addChangeListener(this);
updateYPoints(data);
fireDatasetChanged();
}
/**
* Cummulates all series with lower index of list <code>data</code> with the later
* elements of <code>date</code>.
*
* @param data the series (<code>null</code> not permitted).
*/
private void updateYPoints(List data) {
if (data == null) {
throw new IllegalArgumentException("Null 'data' not permitted.");
}
StackedBarTimeSeries addedSeries = (StackedBarTimeSeries) this.data.get(this.data.size() - 1);
for (int seriesNo = 0; seriesNo < this.data.size() - 1; seriesNo++) {
StackedBarTimeSeries dataSeries = (StackedBarTimeSeries) this.data.get(seriesNo);
for (int itemNo = 0; itemNo < dataSeries.getItemCount(); itemNo++) {
TimeSeriesDataItem ts = dataSeries.getDataItem(itemNo);
if (ts.getValue() != null && addedSeries.getValue(itemNo) != null) {
ts.setValue(new Double(ts.getValue().doubleValue()
+ addedSeries.getValue(itemNo).doubleValue()));
}
}
}
}
/**
* Adds any unique x-values from 'series' to the dataset, and also adds any
* x-values that are in the dataset but not in 'series' to the series.
* @param series the series (<code>null</code> not permitted).
*/
private void updateXPoints(StackedBarTimeSeries series) {
if (series == null) {
throw new IllegalArgumentException("Null 'series' not permitted.");
}
HashSet seriesXPoints = new HashSet();
boolean savedState = this.propagateEvents;
this.propagateEvents = false;
for (int itemNo = 0; itemNo < series.getItemCount(); itemNo++) {
RegularTimePeriod xValue = series.getTimePeriod(itemNo);
seriesXPoints.add(xValue);
if (!this.xPoints.contains(xValue)) {
this.xPoints.add(xValue);
for (int seriesNo = 0; seriesNo < this.data.size(); seriesNo++) {
StackedBarTimeSeries dataSeries = (StackedBarTimeSeries) this.data.get(seriesNo);
if (!dataSeries.equals(series)) {
dataSeries.add(xValue, null);
}
}
}
}
Iterator iterator = this.xPoints.iterator();
while (iterator.hasNext()) {
RegularTimePeriod xPoint = (RegularTimePeriod) iterator.next();
if (!seriesXPoints.contains(xPoint)) {
series.add(xPoint, null);
}
}
this.propagateEvents = savedState;
}
/**
* Updates the x-values for all the series in the dataset.
*/
public void updateXPoints() {
this.propagateEvents = false;
for (int s = 0; s < this.data.size(); s++) {
updateXPoints((StackedBarTimeSeries) this.data.get(s));
}
if (this.autoPrune) {
prune();
}
this.propagateEvents = true;
}
/**
* Returns the number of series in the collection.
*
* @return the number of series in the collection.
*/
public int getSeriesCount() {
return this.data == null ? 0 : this.data.size();
}
/**
* Returns the number of x values in the dataset.
*
* @return the number of x values in the dataset.
*/
public int getItemCount() {
return this.xPoints == null ? 0 : this.xPoints.size();
}
/**
* Returns a series.
*
* @param series the series (zero-based index).
*
* @return the series (never <code>null</code>).
*/
public StackedBarTimeSeries getSeries(int series) {
if ((series < 0) || (series > getSeriesCount())) {
throw new IllegalArgumentException(
"XYSeriesCollection.getSeries(...): index outside valid range.");
}
return (StackedBarTimeSeries) this.data.get(series);
}
/**
* Returns the name of a series.
*
* @param series the series (zero-based index).
*
* @return the name of a series.
*/
public String getSeriesName(int series) {
// check arguments...delegated
return getSeries(series).getName();
}
/**
* Returns the number of items in the specified series.
*
* @param series the series (zero-based index).
*
* @return the number of items in the specified series.
*/
public int getItemCount(int series) {
// check arguments...delegated
return getSeries(series).getItemCount();
}
/**
* Returns the x-value for the specified series and item.
*
* @param series the series (zero-based index).
* @param item the item (zero-based index).
*
* @return the x-value for the specified series and item.
*/
public Number getXValue(int series, int item) {
StackedBarTimeSeries s = (StackedBarTimeSeries) this.data.get(series);
TimeSeriesDataItem ts = s.getDataItem(item);
return new Long(ts.getPeriod().getMiddleMillisecond());
}
/**
* Returns the starting X value for the specified series and item.
*
* @param series the series (zero-based index).
* @param item the item (zero-based index).
*
* @return The starting X value.
*/
public Number getStartXValue(int series, int item) {
return intervalDelegate.getStartXValue(series, item);
}
/**
* Returns the ending X value for the specified series and item.
*
* @param series the series (zero-based index).
* @param item the item (zero-based index).
*
* @return The ending X value.
*/
public Number getEndXValue(int series, int item) {
return intervalDelegate.getEndXValue(series, item);
}
/**
* Returns the y-value for the specified series and item.
*
* @param series the series (zero-based index).
* @param index the index of the item of interest (zero-based).
*
* @return the y-value for the specified series and item (possibly <code>null</code>).
*/
public Number getYValue(int series, int index) {
StackedBarTimeSeries ts = (StackedBarTimeSeries) this.data.get(series);
TimeSeriesDataItem dataItem = ts.getDataItem(index);
return dataItem.getValue();
}
/**
* Returns the starting Y value for the specified series and item.
*
* @param series the series (zero-based index).
* @param item the item (zero-based index).
*
* @return The starting Y value.
*/
public Number getStartYValue(int series, int item) {
return getYValue(series, item);
}
/**
* Returns the ending Y value for the specified series and item.
*
* @param series the series (zero-based index).
* @param item the item (zero-based index).
*
* @return The ending Y value.
*/
public Number getEndYValue(int series, int item) {
return getYValue(series, item);
}
/**
* Removes all the series from the collection and sends a {@link DatasetChangeEvent} to
* all registered listeners.
*/
public void removeAllSeries() {
// Unregister the collection as a change listener to each series in the collection.
for (int i = 0; i < this.data.size(); i++) {
StackedBarTimeSeries series = (StackedBarTimeSeries) this.data.get(i);
series.removeChangeListener(this);
}
// Remove all the series from the collection and notify listeners.
this.data.clear();
this.xPoints.clear();
this.intervalDelegate.seriesRemoved();
fireDatasetChanged();
}
/**
* Removes a series from the collection and sends a {@link DatasetChangeEvent} to all
* registered listeners.
*
* @param series the series (<code>null</code> not permitted).
*/
public void removeSeries(StackedBarTimeSeries series) {
// check arguments...
if (series == null) {
throw new IllegalArgumentException("Null 'series' argument.");
}
// remove the series...
if (this.data.contains(series)) {
series.removeChangeListener(this);
this.data.remove(series);
if (this.data.size() == 0) {
this.xPoints.clear();
}
this.intervalDelegate.seriesRemoved();
fireDatasetChanged();
}
}
/**
* Removes a series from the collection and sends a {@link DatasetChangeEvent} to all
* registered listeners.
*
* @param series the series (zero based index).
*/
public void removeSeries(int series) {
// check arguments...
if ((series < 0) || (series > getSeriesCount())) {
throw new IllegalArgumentException(
"XYSeriesCollection.removeSeries(...): index outside valid range.");
}
// fetch the series, remove the change listener, then remove the series.
StackedBarTimeSeries s = (StackedBarTimeSeries) this.data.get(series);
s.removeChangeListener(this);
this.data.remove(series);
if (this.data.size() == 0) {
this.xPoints.clear();
}
else if (this.autoPrune) {
prune();
}
this.intervalDelegate.seriesRemoved();
fireDatasetChanged();
}
/**
* Removes the items from all series for a given x value.
*
* @param x the x-value.
*/
public void removeAllValuesForX(RegularTimePeriod x) {
if (x == null) {
throw new IllegalArgumentException("Null 'x' argument.");
}
boolean savedState = this.propagateEvents;
this.propagateEvents = false;
for (int s = 0; s < this.data.size(); s++) {
StackedBarTimeSeries series = (StackedBarTimeSeries) this.data.get(s);
series.delete(x);
}
this.propagateEvents = savedState;
this.xPoints.remove(x);
this.intervalDelegate.seriesRemoved();
fireDatasetChanged();
}
/**
* Returns <code>true</code> if all the y-values for the specified x-value are <code>null</code>
* and false otherwise.
*
* @param x the x-value.
*
* @return a boolean.
*/
protected boolean canPrune(RegularTimePeriod x) {
for (int s = 0; s < this.data.size(); s++) {
StackedBarTimeSeries series = (StackedBarTimeSeries) this.data.get(s);
if (series.getValue(x) != null) {
return false;
}
}
return true;
}
/**
* Removes all x-values for which all the y-values are <code>null</code>.
*/
public void prune() {
HashSet hs = (HashSet) this.xPoints.clone();
Iterator iterator = hs.iterator();
while (iterator.hasNext()) {
RegularTimePeriod x = (RegularTimePeriod) iterator.next();
if (canPrune(x)) {
removeAllValuesForX(x);
}
}
}
/**
* This method receives notification when a series belonging to the dataset changes. It
* responds by updating the x-points for the entire dataset and sending a
* {@link DatasetChangeEvent} to all registered listeners.
*
* @param event information about the change.
*/
public void seriesChanged(SeriesChangeEvent event) {
if (this.propagateEvents) {
updateXPoints();
fireDatasetChanged();
}
}
/**
* Tests this collection for equality with an arbitrary object.
*
* @param obj the object (<code>null</code> permitted).
*
* @return A boolean.
*/
public boolean equals(Object obj) {
/*
* I wonder if these implementations of equals and hashCode are
* sound... (AS)
*/
if (obj == null) {
return false;
}
if (obj == this) {
return true;
}
if (obj instanceof DefaultTableStackedTimeSeriesDataset) {
DefaultTableStackedTimeSeriesDataset c = (DefaultTableStackedTimeSeriesDataset) obj;
return ObjectUtils.equal(this.data, c.data);
}
return false;
}
/**
* Returns a hash code.
*
* @return a hash code.
*/
public int hashCode() {
int result;
result = (this.data != null ? this.data.hashCode() : 0);
result = 29 * result + (this.xPoints != null ? this.xPoints.hashCode() : 0);
result = 29 * result + (this.propagateEvents ? 1 : 0);
result = 29 * result + (this.autoPrune ? 1 : 0);
return result;
}
/**
* @return the domain range
*/
public Range getDomainRange() {
return intervalDelegate.getDomainRange();
}
/**
* @return the maximum domain value.
*/
public Number getMaximumDomainValue() {
return intervalDelegate.getMaximumDomainValue();
}
/**
* @return the minimum domain value.
*/
public Number getMinimumDomainValue() {
return intervalDelegate.getMinimumDomainValue();
}
/**
* Returns the interval position factor.
* @return the interval position factor.
*/
public double getIntervalPositionFactor() {
return intervalDelegate.getIntervalPositionFactor();
}
/**
* Sets the interval position factor. Must be between 0.0 and 1.0 inclusive.
* If the factor is 0.5, the gap is in the middle of the x values. If it
* is lesser than 0.5, the gap is farther to the left and if greater than
* 0.5 it gets farther to the right.
*
* @param d the new interval position factor.
*/
public void setIntervalPositionFactor(double d) {
intervalDelegate.setIntervalPositionFactor(d);
fireDatasetChanged();
}
/**
* returns the full interval width.
*
* @return the interval width to use.
*/
public double getIntervalWidth() {
return intervalDelegate.getIntervalWidth();
}
/**
* Sets the interval width manually.
*
* @param d the new interval width.
*/
public void setIntervalWidth(double d) {
intervalDelegate.setIntervalWidth(d);
fireDatasetChanged();
}
/**
* Returns wether the interval width is automatically calculated or not.
*
* @return wether the width is automatically calcualted or not.
*/
public boolean isAutoWidth() {
return intervalDelegate.isAutoWidth();
}
/**
* Sets the flag that indicates wether the interval width is automatically
* calculated or not.
*
* @param b
*/
public void setAutoWidth(boolean b) {
intervalDelegate.setAutoWidth(b);
fireDatasetChanged();
}
}
Thanks and cheers
Heinrich