changeset 1055:bb2679624c6a

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
author Ingo Weinzierl <ingo.weinzierl@intevation.de>
date Thu, 27 May 2010 07:41:14 +0000
parents 8430269ec73b
children 0318fda0001e
files gnv-artifacts/ChangeLog gnv-artifacts/src/main/java/de/intevation/gnv/chart/AdvancedHistogramDataset.java gnv-artifacts/src/main/java/de/intevation/gnv/chart/DefaultHistogram.java
diffstat 3 files changed, 573 insertions(+), 45 deletions(-) [+]
line wrap: on
line diff
--- 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 <ingo.weinzierl@intevation.de>
+
+	  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 <ingo.weinzierl@intevation.de>
 
 	  Issue289
--- /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
+ * <code>HistogramDataset</code>. <code>AdvancedHistogramDataset</code> 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 <a href="mailto:ingo.weinzierl@intevation.de">Ingo Weinzierl</a>
+ */
+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 <code>null</code>).
+     */
+    public HistogramType getType() {
+        return this.type;
+    }
+
+    /**
+     * Sets the histogram type and sends a {@link DatasetChangeEvent} to all
+     * registered listeners.
+     *
+     * @param type  the type (<code>null</code> 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 (<code>null</code> not permitted).
+     * @param values the values (<code>null</code> 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 (<code>null</code> 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 (<code>null</code> 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 (<code>null</code> 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 <code>0</code> to
+     *     <code>getSeriesCount() - 1</code>).
+     *
+     * @return A list of bins.
+     *
+     * @throws IndexOutOfBoundsException if <code>series</code> 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 <code>0</code> to
+     *     <code>getSeriesCount() - 1</code>).
+     *
+     * @return The series key.
+     *
+     * @throws IndexOutOfBoundsException if <code>series</code> 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 <code>0</code> to
+     *     <code>getSeriesCount() - 1</code>).
+     *
+     * @return The item count.
+     *
+     * @throws IndexOutOfBoundsException if <code>series</code> 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 <code>0</code> to
+     *     <code>getSeriesCount() - 1</code>).
+     * @param item  the item index (zero based).
+     *
+     * @return The start value.
+     *
+     * @throws IndexOutOfBoundsException if <code>series</code> 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 <code>0</code> to
+     *     <code>getSeriesCount() - 1</code>).
+     * @param item  the item index (zero based).
+     *
+     * @return The y-value.
+     *
+     * @throws IndexOutOfBoundsException if <code>series</code> 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 <code>0</code> to
+     *     <code>getSeriesCount() - 1</code>).
+     * @param item  the item index (zero based).
+     *
+     * @return The start value.
+     *
+     * @throws IndexOutOfBoundsException if <code>series</code> 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 <code>0</code> to
+     *     <code>getSeriesCount() - 1</code>).
+     * @param item  the item index (zero based).
+     *
+     * @return The end value.
+     *
+     * @throws IndexOutOfBoundsException if <code>series</code> 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 <code>0</code> to
+     *     <code>getSeriesCount() - 1</code>).
+     * @param item  the item index (zero based).
+     *
+     * @return The y-value.
+     *
+     * @throws IndexOutOfBoundsException if <code>series</code> 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 <code>0</code> to
+     *     <code>getSeriesCount() - 1</code>).
+     * @param item  the item index (zero based).
+     *
+     * @return The Y value.
+     *
+     * @throws IndexOutOfBoundsException if <code>series</code> 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 (<code>null</code> 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 :
--- 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 <i>requestParameter</i>
+     * 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 <code>Integer</code> at
-     * <code>REQUEST_KEY_BIN_COUNT</code> and this is smaller than or equal
-     * {@link #MAXIMAL_BINS}, this value is used. If no valid
-     * <code>Integer</code> 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 <i>requestParameter</i> 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 :

http://dive4elements.wald.intevation.org