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 :