content: /* ===========================================================
   * JFreeChart : a free chart library for the Java(tm) platform
   * ===========================================================
   *
   * (C) Copyright 2000-2012, by Object Refinery Limited and Contributors.
   *
   * Project Info:  http://www.jfree.org/jfreechart/index.html
   *
   * This library is free software; you can redistribute it and/or modify it
   * under the terms of the GNU Lesser General Public License as published by
   * the Free Software Foundation; either version 2.1 of the License, or
   * (at your option) any later version.
   *
   * This library is distributed in the hope that it will be useful, but
   * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
   * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
   * License for more details.
   *
   * You should have received a copy of the GNU Lesser General Public
   * License along with this library; if not, write to the Free Software
   * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301,
   * USA.
   *
   * [Oracle and Java are registered trademarks of Oracle and/or its affiliates.
   * Other names may be trademarks of their respective owners.]
   *
   * ---------------------
   * HistogramDataset.java
   * ---------------------
   * (C) Copyright 2003-2012, by Jelai Wang and Contributors.
   *
   * Original Author:  Jelai Wang (jelaiw AT mindspring.com);
   * Contributor(s):   David Gilbert (for Object Refinery Limited);
   *                   Cameron Hayne;
   *                   Rikard Bj?rklind;
   *                   Thomas A Caswell (patch 2902842);
   *
   * Changes
   * -------
   * 06-Jul-2003 : Version 1, contributed by Jelai Wang (DG);
   * 07-Jul-2003 : Changed package and added Javadocs (DG);
   * 15-Oct-2003 : Updated Javadocs and removed array sorting (JW);
   * 09-Jan-2004 : Added fix by "Z." posted in the JFreeChart forum (DG);
   * 01-Mar-2004 : Added equals() and clone() methods and implemented
   *               Serializable.  Also added new addSeries() method (DG);
   * 06-May-2004 : Now extends AbstractIntervalXYDataset (DG);
   * 15-Jul-2004 : Switched getX() with getXValue() and getY() with
   *               getYValue() (DG);
   * 20-May-2005 : Speed up binning - see patch 1026151 contributed by Cameron
   *               Hayne (DG);
   * 08-Jun-2005 : Fixed bug in getSeriesKey() method (DG);
   * 22-Nov-2005 : Fixed cast in getSeriesKey() method - see patch 1329287 (DG);
   * ------------- JFREECHART 1.0.x ---------------------------------------------
   * 03-Aug-2006 : Improved precision of bin boundary calculation (DG);
   * 07-Sep-2006 : Fixed bug 1553088 (DG);
   * 22-May-2008 : Implemented clone() method override (DG);
   * 08-Dec-2009 : Fire change event in addSeries() - see patch 2902842
   *               contributed by Thomas A Caswell (DG);
   * 17-Jun-2012 : Removed JCommon dependencies (DG);
   *
   */
  
  package org.jfree.data.statistics;
  
  import java.io.Serializable;
  import java.util.ArrayList;
  import java.util.HashMap;
  import java.util.List;
  import java.util.Map;
  
  import org.jfree.chart.util.ObjectUtilities;
  import org.jfree.chart.util.PublicCloneable;
  import org.jfree.data.general.DatasetChangeEvent;
  import org.jfree.data.xy.AbstractIntervalXYDataset;
  import org.jfree.data.xy.IntervalXYDataset;
  
  /**
   * A dataset that can be used for creating histograms.
   *
   * @see SimpleHistogramDataset
   */
  public class HistogramDataset extends AbstractIntervalXYDataset
          implements IntervalXYDataset, Cloneable, PublicCloneable,
                     Serializable {
  
      /** For serialization. */
      private static final long serialVersionUID = -6341668077370231153L;
  
      /** A list of maps. */
      private List<Map<String, Object>> list; //FIXME MMC this should really be an internal historgram type rather than object
  
      /** The histogram type. */
      private HistogramType type;
  
      /**
       * Creates a new (empty) dataset with a default type of
       * {@link HistogramType}.FREQUENCY.
       */
      public HistogramDataset() {
          this.list = new ArrayList<Map<String, Object>>();
          this.type = HistogramType.FREQUENCY;
      }
  
      /**
       * 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;
          fireDatasetChanged();
      }
  
      /**
       * Adds a series to the dataset, using the specified number of bins,
       * and sends a {@link DatasetChangeEvent} to all registered listeners.
       *
       * @param key  the series key (<code>null</code> not permitted).
       * @param values the values (<code>null</code> not permitted).
       * @param bins  the number of bins (must be at least 1).
       */
      public void addSeries(Comparable key, double[] values, int bins) {
          // defer argument checking...
          double minimum = getMinimum(values);
          double maximum = getMaximum(values);
          addSeries(key, values, bins, 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, int bins,
              double minimum, double maximum) {
  
          if (key == null) {
              throw new IllegalArgumentException("Null 'key' argument.");
          }
          if (values == null) {
              throw new IllegalArgumentException("Null 'values' argument.");
          }
          else if (bins < 1) {
              throw new IllegalArgumentException(
                      "The 'bins' value must be at least 1.");
          }
          double binWidth = (maximum - minimum) / bins;
  
          double lower = minimum;
          double upper;
          List<HistogramBin> binList = new ArrayList<HistogramBin>(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 (double value : values) {
              int binIndex = bins - 1;
              if (value < maximum) {
                  double fraction = (value - 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 = binList.get(binIndex);
              bin.incrementCount();
          }
          // generic map for each series
          Map<String, Object> map = new HashMap<String, Object>();
          map.put("key", key);
          map.put("bins", binList);
          map.put("values.length", values.length);
          map.put("bin width", binWidth);
          this.list.add(map);
          fireDatasetChanged();
      }
  
      /**
       * 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 (double value : values) {
              if (value < min) {
                  min = value;
              }
          }
          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 (double value : values) {
              if (value > max) {
                  max = value;
              }
          }
          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<HistogramBin> getBins(int series) {
          Map<String, Object> map = this.list.get(series);
          return (List<HistogramBin>) 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<String, Object> map = this.list.get(series);
          return (Integer) map.get("values.length");
      }
  
      /**
       * Returns the bin width for a series.
       *
       * @param series  the series index (zero based).
       *
       * @return The bin width.
       */
      private double getBinWidth(int series) {
          Map<String, Object> map = this.list.get(series);
          return (Double) map.get("bin width");
      }
  
      /**
       * Returns the number of series in the dataset.
       *
       * @return The series count.
       */
      @Override
      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.
       */
      @Override
      public Comparable getSeriesKey(int series) {
          Map<String, Object> 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.
       */
      @Override
      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.
       */
      @Override
      public Number getX(int series, int item) {
          List<HistogramBin> bins = getBins(series);
          HistogramBin bin = bins.get(item);
          double x = (bin.getStartBoundary() + bin.getEndBoundary()) / 2.;
          return 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.
       */
      @Override
      public Number getY(int series, int item) {
          List<HistogramBin> bins = getBins(series);
          HistogramBin bin = bins.get(item);
          double total = getTotal(series);
          double binWidth = getBinWidth(series);
  
          if (this.type == HistogramType.FREQUENCY) {
              return (double) bin.getCount();
          }
          else if (this.type == HistogramType.RELATIVE_FREQUENCY) {
              return bin.getCount() / total;
          }
          else if (this.type == HistogramType.SCALE_AREA_TO_1) {
              return 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.
       */
      @Override
      public Number getStartX(int series, int item) {
          List<HistogramBin> bins = getBins(series);
          HistogramBin bin = bins.get(item);
          return 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.
       */
      @Override
      public Number getEndX(int series, int item) {
          List<HistogramBin> bins = getBins(series);
          HistogramBin bin = bins.get(item);
          return 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.
       */
      @Override
      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.
       */
      @Override
      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.
       */
      @Override
      public boolean equals(Object obj) {
          if (obj == this) {
              return true;
          }
          if (!(obj instanceof HistogramDataset)) {
              return false;
          }
          HistogramDataset that = (HistogramDataset) 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.
       */
      @Override
      public Object clone() throws CloneNotSupportedException {
          HistogramDataset clone = (HistogramDataset) super.clone();
          int seriesCount = getSeriesCount();
          clone.list = new java.util.ArrayList<Map<String, Object>>(seriesCount);
          for (int i = 0; i < seriesCount; i++) {
              clone.list.add(new HashMap<String, Object>(this.list.get(i)));
          }
          return clone;
      }
  
  }
  