ingo@1115: /*
ingo@1115: * Copyright (c) 2010 by Intevation GmbH
ingo@1115: *
ingo@1115: * This program is free software under the LGPL (>=v2.1)
ingo@1115: * Read the file LGPL.txt coming with the software for details
ingo@1115: * or visit http://www.gnu.org/licenses/ if it does not exist.
ingo@1115: */
ingo@1115:
ingo@1055: package de.intevation.gnv.chart;
ingo@1055:
ingo@1055: import java.io.Serializable;
ingo@1055: import java.util.ArrayList;
ingo@1055: import java.util.HashMap;
ingo@1055: import java.util.List;
ingo@1055: import java.util.Map;
ingo@1055:
ingo@1055: import org.apache.log4j.Logger;
ingo@1055:
ingo@1055: import org.jfree.data.general.DatasetChangeEvent;
ingo@1055: import org.jfree.data.xy.AbstractIntervalXYDataset;
ingo@1055: import org.jfree.data.xy.IntervalXYDataset;
ingo@1055:
ingo@1055: import org.jfree.data.statistics.HistogramType;
ingo@1055: import org.jfree.data.statistics.HistogramBin;
ingo@1055:
ingo@1055: import org.jfree.util.ObjectUtilities;
ingo@1055: import org.jfree.util.PublicCloneable;
ingo@1055:
ingo@1055: /**
ingo@1055: * This class extends the functionality of the internal JFreeChart class
ingo@1055: * HistogramDataset
. AdvancedHistogramDataset
takes
ingo@1055: * the number of bins OR a bin width. It is mainly a copy of this class. The
ingo@1055: * reason why there is no inheritance from JFreeChart's internal class is,
ingo@1055: * that basic attributes have private access there.
ingo@1055: *
ingo@1055: * @author Ingo Weinzierl
ingo@1055: */
ingo@1055: public class AdvancedHistogramDataset
ingo@1055: extends AbstractIntervalXYDataset
ingo@1055: implements IntervalXYDataset, Cloneable, PublicCloneable, Serializable
ingo@1055: {
ingo@1055: /** A list of maps. */
ingo@1055: protected List list;
ingo@1055:
ingo@1055: /** The histogram type. */
ingo@1055: protected HistogramType type;
ingo@1055:
ingo@1055: /** The width of a single bin. */
ingo@1055: protected double binWidth = -1;
ingo@1055:
ingo@1055: /** The number of bins. */
ingo@1055: protected int bins = -1;
ingo@1055:
ingo@1055: /** The logger */
ingo@1055: private static Logger logger =
ingo@1055: Logger.getLogger(AdvancedHistogramDataset.class);
ingo@1055:
ingo@1055:
ingo@1055: /**
ingo@1055: * Creates a new (empty) dataset with a default type of
ingo@1055: * {@link HistogramType}.FREQUENCY.
ingo@1055: */
ingo@1055: public AdvancedHistogramDataset() {
ingo@1055: this.list = new ArrayList();
ingo@1055: this.type = HistogramType.FREQUENCY;
ingo@1055: }
ingo@1055:
ingo@1055: /**
ingo@1055: * Creates a new (empty) dataset with a default type of
ingo@1055: * {@link HistogramType}.FREQUENCY.
ingo@1055: */
ingo@1055: public AdvancedHistogramDataset(int bins, double binWidth) {
ingo@1055: this.list = new ArrayList();
ingo@1055: this.type = HistogramType.FREQUENCY;
ingo@1055: this.bins = bins;
ingo@1055: this.binWidth = binWidth;
ingo@1055: }
ingo@1055:
ingo@1055: /**
ingo@1055: * Returns the histogram type.
ingo@1055: *
ingo@1055: * @return The type (never null
).
ingo@1055: */
ingo@1055: public HistogramType getType() {
ingo@1055: return this.type;
ingo@1055: }
ingo@1055:
ingo@1055: /**
ingo@1055: * Sets the histogram type and sends a {@link DatasetChangeEvent} to all
ingo@1055: * registered listeners.
ingo@1055: *
ingo@1055: * @param type the type (null
not permitted).
ingo@1055: */
ingo@1055: public void setType(HistogramType type) {
ingo@1055: if (type == null) {
ingo@1055: throw new IllegalArgumentException("Null 'type' argument");
ingo@1055: }
ingo@1055: this.type = type;
ingo@1055: notifyListeners(new DatasetChangeEvent(this, this));
ingo@1055: }
ingo@1055:
ingo@1055: /**
ingo@1055: * Adds a series to the dataset, using the specified number of bins.
ingo@1055: *
ingo@1055: * @param key the series key (null
not permitted).
ingo@1055: * @param values the values (null
not permitted).
ingo@1055: */
ingo@1055: public void addSeries(Comparable key, double[] values) {
ingo@1055: double minimum = getMinimum(values);
ingo@1055: double maximum = getMaximum(values);
ingo@1055: addSeries(key, values, minimum, maximum);
ingo@1055: }
ingo@1055:
ingo@1055: /**
ingo@1055: * Adds a series to the dataset. Any data value less than minimum will be
ingo@1055: * assigned to the first bin, and any data value greater than maximum will
ingo@1055: * be assigned to the last bin. Values falling on the boundary of
ingo@1055: * adjacent bins will be assigned to the higher indexed bin.
ingo@1055: *
ingo@1055: * @param key the series key (null
not permitted).
ingo@1055: * @param values the raw observations.
ingo@1055: * @param bins the number of bins (must be at least 1).
ingo@1055: * @param minimum the lower bound of the bin range.
ingo@1055: * @param maximum the upper bound of the bin range.
ingo@1055: */
ingo@1055: public void addSeries(Comparable key,
ingo@1055: double[] values,
ingo@1055: double minimum,
ingo@1055: double maximum) {
ingo@1055:
ingo@1055: if (key == null) {
ingo@1055: throw new IllegalArgumentException("Null 'key' argument.");
ingo@1055: }
ingo@1055: if (values == null) {
ingo@1055: throw new IllegalArgumentException("Null 'values' argument.");
ingo@1055: }
ingo@1055: if (bins <= 0 && binWidth <= 0) {
ingo@1055: throw new IllegalArgumentException(
ingo@1055: "We need at least a bin width or the number of bins.");
ingo@1055: }
ingo@1055:
ingo@1055: // There is a binWidth given to calculate the number of bins in this
ingo@1055: // case
ingo@1055: if (bins <= 0) {
ingo@1055: double tmp = (maximum - minimum) / binWidth;
ingo@1055: bins = (int) tmp;
ingo@1055: bins = tmp % 1 > 0 ? bins + 1 : bins;
ingo@1055:
ingo@1055: double overlap = minimum + bins * binWidth - maximum;
ingo@1055: tmp = minimum;
ingo@1055: minimum -= overlap / 2;
ingo@1055:
ingo@1055: logger.debug("There is an overlap of " + overlap);
ingo@1055: logger.info("The lower bound is moved left from "
ingo@1055: + tmp + " to " + minimum);
ingo@1055:
ingo@1055: tmp = maximum;
ingo@1055: maximum += overlap / 2;
ingo@1055: logger.info("The upper bound is moved right from "
ingo@1055: + tmp + " to " + maximum);
ingo@1055: }
ingo@1055:
ingo@1055: if (bins <= 0) {
ingo@1114: bins = 1;
ingo@1055: }
ingo@1055:
ingo@1055: // in this case, there is a number of bins given, so we need to
ingo@1055: // calculate the width of a single bin
ingo@1055: if (binWidth <= 0)
ingo@1055: binWidth = (maximum - minimum) / bins;
ingo@1055:
ingo@1055: logger.info("bin width: " + binWidth);
ingo@1055: logger.info("number of bins: " + bins);
ingo@1055:
ingo@1055: double lower = minimum;
ingo@1055: double upper;
ingo@1055: List binList = new ArrayList(bins);
ingo@1055: for (int i = 0; i < bins; i++) {
ingo@1055: HistogramBin bin;
ingo@1055: // make sure bins[bins.length]'s upper boundary ends at maximum
ingo@1055: // to avoid the rounding issue. the bins[0] lower boundary is
ingo@1055: // guaranteed start from min
ingo@1055: if (i == bins - 1) {
ingo@1055: bin = new HistogramBin(lower, maximum);
ingo@1055: }
ingo@1055: else {
ingo@1055: upper = minimum + (i + 1) * binWidth;
ingo@1055: bin = new HistogramBin(lower, upper);
ingo@1055: lower = upper;
ingo@1055: }
ingo@1055: binList.add(bin);
ingo@1055: }
ingo@1055: // fill the bins
ingo@1055: for (int i = 0; i < values.length; i++) {
ingo@1055: int binIndex = bins - 1;
ingo@1055: if (values[i] < maximum) {
ingo@1055: double fraction = (values[i] - minimum) / (maximum - minimum);
ingo@1055: if (fraction < 0.0) {
ingo@1055: fraction = 0.0;
ingo@1055: }
ingo@1055: binIndex = (int) (fraction * bins);
ingo@1055: // rounding could result in binIndex being equal to bins
ingo@1055: // which will cause an IndexOutOfBoundsException - see bug
ingo@1055: // report 1553088
ingo@1055: if (binIndex >= bins) {
ingo@1055: binIndex = bins - 1;
ingo@1055: }
ingo@1055: }
ingo@1055: HistogramBin bin = (HistogramBin) binList.get(binIndex);
ingo@1055: bin.incrementCount();
ingo@1055: }
ingo@1055: // generic map for each series
ingo@1055: Map map = new HashMap();
ingo@1055: map.put("key", key);
ingo@1055: map.put("bins", binList);
ingo@1055: map.put("values.length", new Integer(values.length));
ingo@1055: map.put("bin width", new Double(binWidth));
ingo@1055: this.list.add(map);
ingo@1055: }
ingo@1055:
ingo@1055: /**
ingo@1055: * Returns the minimum value in an array of values.
ingo@1055: *
ingo@1055: * @param values the values (null
not permitted and
ingo@1055: * zero-length array not permitted).
ingo@1055: *
ingo@1055: * @return The minimum value.
ingo@1055: */
ingo@1055: private double getMinimum(double[] values) {
ingo@1055: if (values == null || values.length < 1) {
ingo@1055: throw new IllegalArgumentException(
ingo@1055: "Null or zero length 'values' argument.");
ingo@1055: }
ingo@1055: double min = Double.MAX_VALUE;
ingo@1055: for (int i = 0; i < values.length; i++) {
ingo@1055: if (values[i] < min) {
ingo@1055: min = values[i];
ingo@1055: }
ingo@1055: }
ingo@1055: return min;
ingo@1055: }
ingo@1055:
ingo@1055: /**
ingo@1055: * Returns the maximum value in an array of values.
ingo@1055: *
ingo@1055: * @param values the values (null
not permitted and
ingo@1055: * zero-length array not permitted).
ingo@1055: *
ingo@1055: * @return The maximum value.
ingo@1055: */
ingo@1055: private double getMaximum(double[] values) {
ingo@1055: if (values == null || values.length < 1) {
ingo@1055: throw new IllegalArgumentException(
ingo@1055: "Null or zero length 'values' argument.");
ingo@1055: }
ingo@1055: double max = -Double.MAX_VALUE;
ingo@1055: for (int i = 0; i < values.length; i++) {
ingo@1055: if (values[i] > max) {
ingo@1055: max = values[i];
ingo@1055: }
ingo@1055: }
ingo@1055: return max;
ingo@1055: }
ingo@1055:
ingo@1055: /**
ingo@1055: * Returns the bins for a series.
ingo@1055: *
ingo@1055: * @param series the series index (in the range 0
to
ingo@1055: * getSeriesCount() - 1
).
ingo@1055: *
ingo@1055: * @return A list of bins.
ingo@1055: *
ingo@1055: * @throws IndexOutOfBoundsException if series
is outside the
ingo@1055: * specified range.
ingo@1055: */
ingo@1055: List getBins(int series) {
ingo@1055: Map map = (Map) this.list.get(series);
ingo@1055: return (List) map.get("bins");
ingo@1055: }
ingo@1055:
ingo@1055: /**
ingo@1055: * Returns the total number of observations for a series.
ingo@1055: *
ingo@1055: * @param series the series index.
ingo@1055: *
ingo@1055: * @return The total.
ingo@1055: */
ingo@1055: private int getTotal(int series) {
ingo@1055: Map map = (Map) this.list.get(series);
ingo@1055: return ((Integer) map.get("values.length")).intValue();
ingo@1055: }
ingo@1055:
ingo@1055: /**
ingo@1055: * Returns the bin width for a series.
ingo@1055: *
ingo@1055: * @param series the series index (zero based).
ingo@1055: *
ingo@1055: * @return The bin width.
ingo@1055: */
ingo@1055: private double getBinWidth(int series) {
ingo@1055: if (binWidth > 0)
ingo@1055: return binWidth;
ingo@1055:
ingo@1055: Map map = (Map) this.list.get(series);
ingo@1055: return ((Double) map.get("bin width")).doubleValue();
ingo@1055: }
ingo@1055:
ingo@1055: /**
ingo@1055: * Returns the number of series in the dataset.
ingo@1055: *
ingo@1055: * @return The series count.
ingo@1055: */
ingo@1055: public int getSeriesCount() {
ingo@1055: return this.list.size();
ingo@1055: }
ingo@1055:
ingo@1055: /**
ingo@1055: * Returns the key for a series.
ingo@1055: *
ingo@1055: * @param series the series index (in the range 0
to
ingo@1055: * getSeriesCount() - 1
).
ingo@1055: *
ingo@1055: * @return The series key.
ingo@1055: *
ingo@1055: * @throws IndexOutOfBoundsException if series
is outside the
ingo@1055: * specified range.
ingo@1055: */
ingo@1055: public Comparable getSeriesKey(int series) {
ingo@1055: Map map = (Map) this.list.get(series);
ingo@1055: return (Comparable) map.get("key");
ingo@1055: }
ingo@1055:
ingo@1055: /**
ingo@1055: * Returns the number of data items for a series.
ingo@1055: *
ingo@1055: * @param series the series index (in the range 0
to
ingo@1055: * getSeriesCount() - 1
).
ingo@1055: *
ingo@1055: * @return The item count.
ingo@1055: *
ingo@1055: * @throws IndexOutOfBoundsException if series
is outside the
ingo@1055: * specified range.
ingo@1055: */
ingo@1055: public int getItemCount(int series) {
ingo@1055: return getBins(series).size();
ingo@1055: }
ingo@1055:
ingo@1055: /**
ingo@1055: * Returns the X value for a bin. This value won't be used for plotting
ingo@1055: * histograms, since the renderer will ignore it. But other renderers can
ingo@1055: * use it (for example, you could use the dataset to create a line
ingo@1055: * chart).
ingo@1055: *
ingo@1055: * @param series the series index (in the range 0
to
ingo@1055: * getSeriesCount() - 1
).
ingo@1055: * @param item the item index (zero based).
ingo@1055: *
ingo@1055: * @return The start value.
ingo@1055: *
ingo@1055: * @throws IndexOutOfBoundsException if series
is outside the
ingo@1055: * specified range.
ingo@1055: */
ingo@1055: public Number getX(int series, int item) {
ingo@1055: List bins = getBins(series);
ingo@1055: HistogramBin bin = (HistogramBin) bins.get(item);
ingo@1055: double x = (bin.getStartBoundary() + bin.getEndBoundary()) / 2.;
ingo@1055: return new Double(x);
ingo@1055: }
ingo@1055:
ingo@1055: /**
ingo@1055: * Returns the y-value for a bin (calculated to take into account the
ingo@1055: * histogram type).
ingo@1055: *
ingo@1055: * @param series the series index (in the range 0
to
ingo@1055: * getSeriesCount() - 1
).
ingo@1055: * @param item the item index (zero based).
ingo@1055: *
ingo@1055: * @return The y-value.
ingo@1055: *
ingo@1055: * @throws IndexOutOfBoundsException if series
is outside the
ingo@1055: * specified range.
ingo@1055: */
ingo@1055: public Number getY(int series, int item) {
ingo@1055: List bins = getBins(series);
ingo@1055: HistogramBin bin = (HistogramBin) bins.get(item);
ingo@1055: double total = getTotal(series);
ingo@1055: double binWidth = getBinWidth(series);
ingo@1055:
ingo@1055: if (this.type == HistogramType.FREQUENCY) {
ingo@1055: return new Double(bin.getCount());
ingo@1055: }
ingo@1055: else if (this.type == HistogramType.RELATIVE_FREQUENCY) {
ingo@1055: return new Double(bin.getCount() / total);
ingo@1055: }
ingo@1055: else if (this.type == HistogramType.SCALE_AREA_TO_1) {
ingo@1055: return new Double(bin.getCount() / (binWidth * total));
ingo@1055: }
ingo@1055: else { // pretty sure this shouldn't ever happen
ingo@1055: throw new IllegalStateException();
ingo@1055: }
ingo@1055: }
ingo@1055:
ingo@1055: /**
ingo@1055: * Returns the start value for a bin.
ingo@1055: *
ingo@1055: * @param series the series index (in the range 0
to
ingo@1055: * getSeriesCount() - 1
).
ingo@1055: * @param item the item index (zero based).
ingo@1055: *
ingo@1055: * @return The start value.
ingo@1055: *
ingo@1055: * @throws IndexOutOfBoundsException if series
is outside the
ingo@1055: * specified range.
ingo@1055: */
ingo@1055: public Number getStartX(int series, int item) {
ingo@1055: List bins = getBins(series);
ingo@1055: HistogramBin bin = (HistogramBin) bins.get(item);
ingo@1055: return new Double(bin.getStartBoundary());
ingo@1055: }
ingo@1055:
ingo@1055: /**
ingo@1055: * Returns the end value for a bin.
ingo@1055: *
ingo@1055: * @param series the series index (in the range 0
to
ingo@1055: * getSeriesCount() - 1
).
ingo@1055: * @param item the item index (zero based).
ingo@1055: *
ingo@1055: * @return The end value.
ingo@1055: *
ingo@1055: * @throws IndexOutOfBoundsException if series
is outside the
ingo@1055: * specified range.
ingo@1055: */
ingo@1055: public Number getEndX(int series, int item) {
ingo@1055: List bins = getBins(series);
ingo@1055: HistogramBin bin = (HistogramBin) bins.get(item);
ingo@1055: return new Double(bin.getEndBoundary());
ingo@1055: }
ingo@1055:
ingo@1055: /**
ingo@1055: * Returns the start y-value for a bin (which is the same as the y-value,
ingo@1055: * this method exists only to support the general form of the
ingo@1055: * {@link IntervalXYDataset} interface).
ingo@1055: *
ingo@1055: * @param series the series index (in the range 0
to
ingo@1055: * getSeriesCount() - 1
).
ingo@1055: * @param item the item index (zero based).
ingo@1055: *
ingo@1055: * @return The y-value.
ingo@1055: *
ingo@1055: * @throws IndexOutOfBoundsException if series
is outside the
ingo@1055: * specified range.
ingo@1055: */
ingo@1055: public Number getStartY(int series, int item) {
ingo@1055: return getY(series, item);
ingo@1055: }
ingo@1055:
ingo@1055: /**
ingo@1055: * Returns the end y-value for a bin (which is the same as the y-value,
ingo@1055: * this method exists only to support the general form of the
ingo@1055: * {@link IntervalXYDataset} interface).
ingo@1055: *
ingo@1055: * @param series the series index (in the range 0
to
ingo@1055: * getSeriesCount() - 1
).
ingo@1055: * @param item the item index (zero based).
ingo@1055: *
ingo@1055: * @return The Y value.
ingo@1055: *
ingo@1055: * @throws IndexOutOfBoundsException if series
is outside the
ingo@1055: * specified range.
ingo@1055: */
ingo@1055: public Number getEndY(int series, int item) {
ingo@1055: return getY(series, item);
ingo@1055: }
ingo@1055:
ingo@1055: /**
ingo@1055: * Tests this dataset for equality with an arbitrary object.
ingo@1055: *
ingo@1055: * @param obj the object to test against (null
permitted).
ingo@1055: *
ingo@1055: * @return A boolean.
ingo@1055: */
ingo@1055: public boolean equals(Object obj) {
ingo@1055: if (obj == this) {
ingo@1055: return true;
ingo@1055: }
ingo@1055: if (!(obj instanceof AdvancedHistogramDataset)) {
ingo@1055: return false;
ingo@1055: }
ingo@1055: AdvancedHistogramDataset that = (AdvancedHistogramDataset) obj;
ingo@1055: if (!ObjectUtilities.equal(this.type, that.type)) {
ingo@1055: return false;
ingo@1055: }
ingo@1055: if (!ObjectUtilities.equal(this.list, that.list)) {
ingo@1055: return false;
ingo@1055: }
ingo@1055: return true;
ingo@1055: }
ingo@1055:
ingo@1055: /**
ingo@1055: * Returns a clone of the dataset.
ingo@1055: *
ingo@1055: * @return A clone of the dataset.
ingo@1055: *
ingo@1055: * @throws CloneNotSupportedException if the object cannot be cloned.
ingo@1055: */
ingo@1055: public Object clone() throws CloneNotSupportedException {
ingo@1055: AdvancedHistogramDataset clone = (AdvancedHistogramDataset) super.clone();
ingo@1055: int seriesCount = getSeriesCount();
ingo@1055: clone.list = new java.util.ArrayList(seriesCount);
ingo@1055: for (int i = 0; i < seriesCount; i++) {
ingo@1055: clone.list.add(new HashMap((Map) this.list.get(i)));
ingo@1055: }
ingo@1055: return clone;
ingo@1055: }
ingo@1055: }
ingo@1055: // vim:set ts=4 sw=4 si et sta sts=4 fenc=utf-8 :