# HG changeset patch # User Ingo Weinzierl # Date 1274946074 0 # Node ID bb2679624c6a56c3e73c525f9f0f707768e85415 # Parent 8430269ec73bfb6b96a423a57c907f9ed28d9b6b Implemented a new histogram dataset that takes the width of a single bin as well as the number of bins for the histogram (issue288). gnv-artifacts/trunk@1130 c6561f87-3c4e-4783-a992-168aeb5c3f6f diff -r 8430269ec73b -r bb2679624c6a gnv-artifacts/ChangeLog --- a/gnv-artifacts/ChangeLog Wed May 26 17:01:29 2010 +0000 +++ b/gnv-artifacts/ChangeLog Thu May 27 07:41:14 2010 +0000 @@ -1,3 +1,21 @@ +2010-05-27 Ingo Weinzierl + + Issue288 + + * src/main/java/de/intevation/gnv/chart/DefaultHistogram.java: Use + AdvancedHistogramDataset as dataset for histograms instead of JFreeChart's + HistogramDataset. Adjusted the calculation of bin width and number of bins + for this. + + * src/main/java/de/intevation/gnv/chart/AdvancedHistogramDataset.java: This + class is mainly a copy of JFreeChart's class HistogramDataset. The + difference between these two classes is, that AdvancedHistogramDataset + takes an argument for setting the bin width as well as an argument to + adjust the number of bins - HistogramDataset just takes the number of + bins. The reason why AdvancedHistogramDataset doesn't inherit from + HistogramDataset is, that basic attributes of that class have private + access. + 2010-05-26 Ingo Weinzierl Issue289 diff -r 8430269ec73b -r bb2679624c6a gnv-artifacts/src/main/java/de/intevation/gnv/chart/AdvancedHistogramDataset.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/gnv-artifacts/src/main/java/de/intevation/gnv/chart/AdvancedHistogramDataset.java Thu May 27 07:41:14 2010 +0000 @@ -0,0 +1,509 @@ +package de.intevation.gnv.chart; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.log4j.Logger; + +import org.jfree.data.general.DatasetChangeEvent; +import org.jfree.data.xy.AbstractIntervalXYDataset; +import org.jfree.data.xy.IntervalXYDataset; + +import org.jfree.data.statistics.HistogramType; +import org.jfree.data.statistics.HistogramBin; + +import org.jfree.util.ObjectUtilities; +import org.jfree.util.PublicCloneable; + +/** + * This class extends the functionality of the internal JFreeChart class + * HistogramDataset. AdvancedHistogramDataset takes + * the number of bins OR a bin width. It is mainly a copy of this class. The + * reason why there is no inheritance from JFreeChart's internal class is, + * that basic attributes have private access there. + * + * @author Ingo Weinzierl + */ +public class AdvancedHistogramDataset +extends AbstractIntervalXYDataset +implements IntervalXYDataset, Cloneable, PublicCloneable, Serializable +{ + /** A list of maps. */ + protected List list; + + /** The histogram type. */ + protected HistogramType type; + + /** The width of a single bin. */ + protected double binWidth = -1; + + /** The number of bins. */ + protected int bins = -1; + + /** The logger */ + private static Logger logger = + Logger.getLogger(AdvancedHistogramDataset.class); + + + /** + * Creates a new (empty) dataset with a default type of + * {@link HistogramType}.FREQUENCY. + */ + public AdvancedHistogramDataset() { + this.list = new ArrayList(); + this.type = HistogramType.FREQUENCY; + } + + /** + * Creates a new (empty) dataset with a default type of + * {@link HistogramType}.FREQUENCY. + */ + public AdvancedHistogramDataset(int bins, double binWidth) { + this.list = new ArrayList(); + this.type = HistogramType.FREQUENCY; + this.bins = bins; + this.binWidth = binWidth; + } + + /** + * Returns the histogram type. + * + * @return The type (never null). + */ + public HistogramType getType() { + return this.type; + } + + /** + * Sets the histogram type and sends a {@link DatasetChangeEvent} to all + * registered listeners. + * + * @param type the type (null not permitted). + */ + public void setType(HistogramType type) { + if (type == null) { + throw new IllegalArgumentException("Null 'type' argument"); + } + this.type = type; + notifyListeners(new DatasetChangeEvent(this, this)); + } + + /** + * Adds a series to the dataset, using the specified number of bins. + * + * @param key the series key (null not permitted). + * @param values the values (null not permitted). + */ + public void addSeries(Comparable key, double[] values) { + double minimum = getMinimum(values); + double maximum = getMaximum(values); + addSeries(key, values, minimum, maximum); + } + + /** + * Adds a series to the dataset. Any data value less than minimum will be + * assigned to the first bin, and any data value greater than maximum will + * be assigned to the last bin. Values falling on the boundary of + * adjacent bins will be assigned to the higher indexed bin. + * + * @param key the series key (null not permitted). + * @param values the raw observations. + * @param bins the number of bins (must be at least 1). + * @param minimum the lower bound of the bin range. + * @param maximum the upper bound of the bin range. + */ + public void addSeries(Comparable key, + double[] values, + double minimum, + double maximum) { + + if (key == null) { + throw new IllegalArgumentException("Null 'key' argument."); + } + if (values == null) { + throw new IllegalArgumentException("Null 'values' argument."); + } + if (bins <= 0 && binWidth <= 0) { + throw new IllegalArgumentException( + "We need at least a bin width or the number of bins."); + } + + // There is a binWidth given to calculate the number of bins in this + // case + if (bins <= 0) { + double tmp = (maximum - minimum) / binWidth; + bins = (int) tmp; + bins = tmp % 1 > 0 ? bins + 1 : bins; + + double overlap = minimum + bins * binWidth - maximum; + tmp = minimum; + minimum -= overlap / 2; + + logger.debug("There is an overlap of " + overlap); + logger.info("The lower bound is moved left from " + + tmp + " to " + minimum); + + tmp = maximum; + maximum += overlap / 2; + logger.info("The upper bound is moved right from " + + tmp + " to " + maximum); + } + + if (bins <= 0) { + throw new IllegalArgumentException( + "The 'bins' value must be at least 1."); + } + + // in this case, there is a number of bins given, so we need to + // calculate the width of a single bin + if (binWidth <= 0) + binWidth = (maximum - minimum) / bins; + + logger.info("bin width: " + binWidth); + logger.info("number of bins: " + bins); + + double lower = minimum; + double upper; + List binList = new ArrayList(bins); + for (int i = 0; i < bins; i++) { + HistogramBin bin; + // make sure bins[bins.length]'s upper boundary ends at maximum + // to avoid the rounding issue. the bins[0] lower boundary is + // guaranteed start from min + if (i == bins - 1) { + bin = new HistogramBin(lower, maximum); + } + else { + upper = minimum + (i + 1) * binWidth; + bin = new HistogramBin(lower, upper); + lower = upper; + } + binList.add(bin); + } + // fill the bins + for (int i = 0; i < values.length; i++) { + int binIndex = bins - 1; + if (values[i] < maximum) { + double fraction = (values[i] - minimum) / (maximum - minimum); + if (fraction < 0.0) { + fraction = 0.0; + } + binIndex = (int) (fraction * bins); + // rounding could result in binIndex being equal to bins + // which will cause an IndexOutOfBoundsException - see bug + // report 1553088 + if (binIndex >= bins) { + binIndex = bins - 1; + } + } + HistogramBin bin = (HistogramBin) binList.get(binIndex); + bin.incrementCount(); + } + // generic map for each series + Map map = new HashMap(); + map.put("key", key); + map.put("bins", binList); + map.put("values.length", new Integer(values.length)); + map.put("bin width", new Double(binWidth)); + this.list.add(map); + } + + /** + * Returns the minimum value in an array of values. + * + * @param values the values (null not permitted and + * zero-length array not permitted). + * + * @return The minimum value. + */ + private double getMinimum(double[] values) { + if (values == null || values.length < 1) { + throw new IllegalArgumentException( + "Null or zero length 'values' argument."); + } + double min = Double.MAX_VALUE; + for (int i = 0; i < values.length; i++) { + if (values[i] < min) { + min = values[i]; + } + } + return min; + } + + /** + * Returns the maximum value in an array of values. + * + * @param values the values (null not permitted and + * zero-length array not permitted). + * + * @return The maximum value. + */ + private double getMaximum(double[] values) { + if (values == null || values.length < 1) { + throw new IllegalArgumentException( + "Null or zero length 'values' argument."); + } + double max = -Double.MAX_VALUE; + for (int i = 0; i < values.length; i++) { + if (values[i] > max) { + max = values[i]; + } + } + return max; + } + + /** + * Returns the bins for a series. + * + * @param series the series index (in the range 0 to + * getSeriesCount() - 1). + * + * @return A list of bins. + * + * @throws IndexOutOfBoundsException if series is outside the + * specified range. + */ + List getBins(int series) { + Map map = (Map) this.list.get(series); + return (List) map.get("bins"); + } + + /** + * Returns the total number of observations for a series. + * + * @param series the series index. + * + * @return The total. + */ + private int getTotal(int series) { + Map map = (Map) this.list.get(series); + return ((Integer) map.get("values.length")).intValue(); + } + + /** + * Returns the bin width for a series. + * + * @param series the series index (zero based). + * + * @return The bin width. + */ + private double getBinWidth(int series) { + if (binWidth > 0) + return binWidth; + + Map map = (Map) this.list.get(series); + return ((Double) map.get("bin width")).doubleValue(); + } + + /** + * Returns the number of series in the dataset. + * + * @return The series count. + */ + public int getSeriesCount() { + return this.list.size(); + } + + /** + * Returns the key for a series. + * + * @param series the series index (in the range 0 to + * getSeriesCount() - 1). + * + * @return The series key. + * + * @throws IndexOutOfBoundsException if series is outside the + * specified range. + */ + public Comparable getSeriesKey(int series) { + Map map = (Map) this.list.get(series); + return (Comparable) map.get("key"); + } + + /** + * Returns the number of data items for a series. + * + * @param series the series index (in the range 0 to + * getSeriesCount() - 1). + * + * @return The item count. + * + * @throws IndexOutOfBoundsException if series is outside the + * specified range. + */ + public int getItemCount(int series) { + return getBins(series).size(); + } + + /** + * Returns the X value for a bin. This value won't be used for plotting + * histograms, since the renderer will ignore it. But other renderers can + * use it (for example, you could use the dataset to create a line + * chart). + * + * @param series the series index (in the range 0 to + * getSeriesCount() - 1). + * @param item the item index (zero based). + * + * @return The start value. + * + * @throws IndexOutOfBoundsException if series is outside the + * specified range. + */ + public Number getX(int series, int item) { + List bins = getBins(series); + HistogramBin bin = (HistogramBin) bins.get(item); + double x = (bin.getStartBoundary() + bin.getEndBoundary()) / 2.; + return new Double(x); + } + + /** + * Returns the y-value for a bin (calculated to take into account the + * histogram type). + * + * @param series the series index (in the range 0 to + * getSeriesCount() - 1). + * @param item the item index (zero based). + * + * @return The y-value. + * + * @throws IndexOutOfBoundsException if series is outside the + * specified range. + */ + public Number getY(int series, int item) { + List bins = getBins(series); + HistogramBin bin = (HistogramBin) bins.get(item); + double total = getTotal(series); + double binWidth = getBinWidth(series); + + if (this.type == HistogramType.FREQUENCY) { + return new Double(bin.getCount()); + } + else if (this.type == HistogramType.RELATIVE_FREQUENCY) { + return new Double(bin.getCount() / total); + } + else if (this.type == HistogramType.SCALE_AREA_TO_1) { + return new Double(bin.getCount() / (binWidth * total)); + } + else { // pretty sure this shouldn't ever happen + throw new IllegalStateException(); + } + } + + /** + * Returns the start value for a bin. + * + * @param series the series index (in the range 0 to + * getSeriesCount() - 1). + * @param item the item index (zero based). + * + * @return The start value. + * + * @throws IndexOutOfBoundsException if series is outside the + * specified range. + */ + public Number getStartX(int series, int item) { + List bins = getBins(series); + HistogramBin bin = (HistogramBin) bins.get(item); + return new Double(bin.getStartBoundary()); + } + + /** + * Returns the end value for a bin. + * + * @param series the series index (in the range 0 to + * getSeriesCount() - 1). + * @param item the item index (zero based). + * + * @return The end value. + * + * @throws IndexOutOfBoundsException if series is outside the + * specified range. + */ + public Number getEndX(int series, int item) { + List bins = getBins(series); + HistogramBin bin = (HistogramBin) bins.get(item); + return new Double(bin.getEndBoundary()); + } + + /** + * Returns the start y-value for a bin (which is the same as the y-value, + * this method exists only to support the general form of the + * {@link IntervalXYDataset} interface). + * + * @param series the series index (in the range 0 to + * getSeriesCount() - 1). + * @param item the item index (zero based). + * + * @return The y-value. + * + * @throws IndexOutOfBoundsException if series is outside the + * specified range. + */ + public Number getStartY(int series, int item) { + return getY(series, item); + } + + /** + * Returns the end y-value for a bin (which is the same as the y-value, + * this method exists only to support the general form of the + * {@link IntervalXYDataset} interface). + * + * @param series the series index (in the range 0 to + * getSeriesCount() - 1). + * @param item the item index (zero based). + * + * @return The Y value. + * + * @throws IndexOutOfBoundsException if series is outside the + * specified range. + */ + public Number getEndY(int series, int item) { + return getY(series, item); + } + + /** + * Tests this dataset for equality with an arbitrary object. + * + * @param obj the object to test against (null permitted). + * + * @return A boolean. + */ + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof AdvancedHistogramDataset)) { + return false; + } + AdvancedHistogramDataset that = (AdvancedHistogramDataset) obj; + if (!ObjectUtilities.equal(this.type, that.type)) { + return false; + } + if (!ObjectUtilities.equal(this.list, that.list)) { + return false; + } + return true; + } + + /** + * Returns a clone of the dataset. + * + * @return A clone of the dataset. + * + * @throws CloneNotSupportedException if the object cannot be cloned. + */ + public Object clone() throws CloneNotSupportedException { + AdvancedHistogramDataset clone = (AdvancedHistogramDataset) super.clone(); + int seriesCount = getSeriesCount(); + clone.list = new java.util.ArrayList(seriesCount); + for (int i = 0; i < seriesCount; i++) { + clone.list.add(new HashMap((Map) this.list.get(i))); + } + return clone; + } +} +// vim:set ts=4 sw=4 si et sta sts=4 fenc=utf-8 : diff -r 8430269ec73b -r bb2679624c6a gnv-artifacts/src/main/java/de/intevation/gnv/chart/DefaultHistogram.java --- a/gnv-artifacts/src/main/java/de/intevation/gnv/chart/DefaultHistogram.java Wed May 26 17:01:29 2010 +0000 +++ b/gnv-artifacts/src/main/java/de/intevation/gnv/chart/DefaultHistogram.java Thu May 27 07:41:14 2010 +0000 @@ -1,9 +1,5 @@ package de.intevation.gnv.chart; -import java.text.NumberFormat; -import java.text.ParseException; - -import java.util.Locale; import java.util.Map; import org.apache.log4j.Logger; @@ -12,8 +8,6 @@ import org.jfree.chart.plot.XYPlot; -import org.jfree.data.statistics.HistogramDataset; - /** * Default implementation of {@link de.intevation.gnv.chart.AbstractHistogram}. * @@ -67,6 +61,8 @@ */ protected Map requestParameter; + protected double[] minmax = null; + /** * Constructor to create DefaultHistogram objects. @@ -91,10 +87,13 @@ // prepare data and create add them to histogram dataset String name = (String) data[0]; double[] values = toDouble((Double[]) data[1]); - int bins = getBinCount(values); - HistogramDataset dataset = new HistogramDataset(); - dataset.addSeries(name, values, bins); + double binWidth = getBinWidth(values); + int binCount = getBinCount(); + + AdvancedHistogramDataset dataset = + new AdvancedHistogramDataset(binCount, binWidth); + dataset.addSeries(name, values); plot.setDataset(0, dataset); } @@ -110,6 +109,9 @@ * @return Array which contains min and max value */ protected double[] getMinMax(double[] values) { + if (minmax != null) + return minmax; + double[] minmax = new double[2]; minmax[0] = Double.MAX_VALUE; minmax[1] = Double.MIN_VALUE; @@ -120,6 +122,8 @@ minmax[1] = values[i] > minmax[1] ? values[i] : minmax[1]; } + this.minmax = minmax; + return minmax; } @@ -144,38 +148,21 @@ /** - * Method to calculate the number of bins this chart should be parted into. - * The real calculation takes place in {@link #getBinCountByNumber} and - * {@link #getBinCountByWidth}. This method switches between these methods - * depending on the object stored in {@link #requestParameter}. - * - * @param values All values used in this histogram + * Method to retrieve the number of bins. * - * @return Number of bins + * @return the number of bins that is specified in requestParameter + * or -1 if the number of bins is not the dominant value to calculate the + * width of a single bin. */ - protected int getBinCount(double[] values) { - String param = (String) requestParameter.get(REQUEST_KEY_BIN_CHOICE); - - if (param != null && param.equalsIgnoreCase(REQUEST_KEY_BIN_WIDTH)) { - return getBinCountByWidth(values); + protected int getBinCount() { + // Return -1 to trigger a calculation of the number of bins in + // AdvancedHistogramDataset if the user chose the bin width as dominant + // value. + String choice = (String) requestParameter.get(REQUEST_KEY_BIN_CHOICE); + if (choice != null && choice.equalsIgnoreCase(REQUEST_KEY_BIN_WIDTH)) { + return -1; } - else { - return getBinCountByNumber(); - } - } - - /** - * Method to retrieve the number of bins. If {@link #requestParameter} - * contains a valid Integer at - * REQUEST_KEY_BIN_COUNT and this is smaller than or equal - * {@link #MAXIMAL_BINS}, this value is used. If no valid - * Integer is given or if the value in {@link #requestParameter} - * is bigger than {@link #MAXIMAL_BINS}, {@link #DEFAULT_BINS} is used. - * - * @return Number of bins - */ - protected int getBinCountByNumber() { int bins = -1; String param = (String) requestParameter.get(REQUEST_KEY_BIN_COUNT); @@ -196,16 +183,25 @@ /** - * Serves the number of bins depending on a given width for each bin, but - * maximum bin count is limited by {@link #MAXIMAL_BINS}. + * Serves width of a single bin. * * @param values All values in this histogram * - * @return Number of bins + * @return The bin width that is given in requestParameter or -1 if + * the bin width is not the dominant value for calculating the number of + * bins in the histogram. */ - protected int getBinCountByWidth(double[] values) { - int bins = -1; - String param = (String) requestParameter.get(REQUEST_KEY_BIN_WIDTH); + protected double getBinWidth(double[] values) { + // Return -1 to trigger a calculation of the bin width in + // AdvancedHistogramDataset if the user chose the number of bins as + // dominant value. + String choice = (String) requestParameter.get(REQUEST_KEY_BIN_CHOICE); + if (choice == null || !choice.equalsIgnoreCase(REQUEST_KEY_BIN_WIDTH)) { + return -1; + } + + int bins = -1; + String param = (String) requestParameter.get(REQUEST_KEY_BIN_WIDTH); double[] minmax = getMinMax(values); double totalWidth = minmax[1] - minmax[0]; @@ -215,9 +211,14 @@ bins = (int) Math.round(tmpBins); bins = bins <= 0 ? DEFAULT_BINS : bins; - bins = bins > MAXIMAL_BINS ? MAXIMAL_BINS : bins; - return bins; + // the calculated number of bins with the given width exceed the maximum + // number of bins. + if (bins > MAXIMAL_BINS) { + return totalWidth / (MAXIMAL_BINS); + } + + return binWidth; } } // vim:set ts=4 sw=4 si et sta sts=4 fenc=utf-8 :