001/* ===========================================================
002 * JFreeChart : a free chart library for the Java(tm) platform
003 * ===========================================================
004 *
005 * (C) Copyright 2000-2022, by David Gilbert and Contributors.
006 *
007 * Project Info:  http://www.jfree.org/jfreechart/index.html
008 *
009 * This library is free software; you can redistribute it and/or modify it
010 * under the terms of the GNU Lesser General Public License as published by
011 * the Free Software Foundation; either version 2.1 of the License, or
012 * (at your option) any later version.
013 *
014 * This library is distributed in the hope that it will be useful, but
015 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
016 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
017 * License for more details.
018 *
019 * You should have received a copy of the GNU Lesser General Public
020 * License along with this library; if not, write to the Free Software
021 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301,
022 * USA.
023 *
024 * [Oracle and Java are registered trademarks of Oracle and/or its affiliates. 
025 * Other names may be trademarks of their respective owners.]
026 *
027 * ---------------------
028 * DefaultXYDataset.java
029 * ---------------------
030 * (C) Copyright 2006-2022, by David Gilbert and Contributors.
031 *
032 * Original Author:  David Gilbert;
033 * Contributor(s):   -;
034 *
035 */
036
037package org.jfree.data.xy;
038
039import java.util.ArrayList;
040import java.util.Arrays;
041import java.util.List;
042import org.jfree.chart.internal.Args;
043import org.jfree.chart.api.PublicCloneable;
044
045import org.jfree.data.DomainOrder;
046import org.jfree.data.general.DatasetChangeEvent;
047
048/**
049 * A default implementation of the {@link XYDataset} interface that stores
050 * data values in arrays of double primitives.
051 *
052 * @param <S> the type of the series keys ({@code String} is commonly used).
053 * 
054 * @since 1.0.2
055 */
056public class DefaultXYDataset<S extends Comparable<S>> 
057        extends AbstractXYDataset<S>
058        implements XYDataset<S>, PublicCloneable {
059
060    /**
061     * Storage for the series keys.  This list must be kept in sync with the
062     * seriesList.
063     */
064    private List<S> seriesKeys;
065
066    /**
067     * Storage for the series in the dataset.  We use a list because the
068     * order of the series is significant.  This list must be kept in sync
069     * with the seriesKeys list.
070     */
071    private List<double[][]> seriesList;
072
073    /**
074     * Creates a new {@code DefaultXYDataset} instance, initially
075     * containing no data.
076     */
077    public DefaultXYDataset() {
078        this.seriesKeys = new ArrayList<>();
079        this.seriesList = new ArrayList<>();
080    }
081
082    /**
083     * Returns the number of series in the dataset.
084     *
085     * @return The series count.
086     */
087    @Override
088    public int getSeriesCount() {
089        return this.seriesList.size();
090    }
091
092    /**
093     * Returns the key for a series.
094     *
095     * @param series  the series index (in the range {@code 0} to
096     *     {@code getSeriesCount() - 1}).
097     *
098     * @return The key for the series.
099     *
100     * @throws IllegalArgumentException if {@code series} is not in the
101     *     specified range.
102     */
103    @Override
104    public S getSeriesKey(int series) {
105        Args.requireInRange(series, "series", 0, this.seriesKeys.size() - 1);
106        return this.seriesKeys.get(series);
107    }
108
109    /**
110     * Returns the index of the series with the specified key, or -1 if there
111     * is no such series in the dataset.
112     *
113     * @param seriesKey  the series key ({@code null} permitted).
114     *
115     * @return The index, or -1.
116     */
117    @Override
118    public int indexOf(S seriesKey) {
119        return this.seriesKeys.indexOf(seriesKey);
120    }
121
122    /**
123     * Returns the order of the domain (x-) values in the dataset.  In this
124     * implementation, we cannot guarantee that the x-values are ordered, so
125     * this method returns {@code DomainOrder.NONE}.
126     *
127     * @return {@code DomainOrder.NONE}.
128     */
129    @Override
130    public DomainOrder getDomainOrder() {
131        return DomainOrder.NONE;
132    }
133
134    /**
135     * Returns the number of items in the specified series.
136     *
137     * @param series  the series index (in the range {@code 0} to
138     *     {@code getSeriesCount() - 1}).
139     *
140     * @return The item count.
141     *
142     * @throws IllegalArgumentException if {@code series} is not in the
143     *     specified range.
144     */
145    @Override
146    public int getItemCount(int series) {
147        if ((series < 0) || (series >= getSeriesCount())) {
148            throw new IllegalArgumentException("Series index out of bounds");
149        }
150        double[][] seriesArray = this.seriesList.get(series);
151        return seriesArray[0].length;
152    }
153
154    /**
155     * Returns the x-value for an item within a series.
156     *
157     * @param series  the series index (in the range {@code 0} to
158     *     {@code getSeriesCount() - 1}).
159     * @param item  the item index (in the range {@code 0} to
160     *     {@code getItemCount(series)}).
161     *
162     * @return The x-value.
163     *
164     * @throws ArrayIndexOutOfBoundsException if {@code series} is not
165     *     within the specified range.
166     * @throws ArrayIndexOutOfBoundsException if {@code item} is not
167     *     within the specified range.
168     *
169     * @see #getX(int, int)
170     */
171    @Override
172    public double getXValue(int series, int item) {
173        double[][] seriesData = this.seriesList.get(series);
174        return seriesData[0][item];
175    }
176
177    /**
178     * Returns the x-value for an item within a series.
179     *
180     * @param series  the series index (in the range {@code 0} to
181     *     {@code getSeriesCount() - 1}).
182     * @param item  the item index (in the range {@code 0} to
183     *     {@code getItemCount(series)}).
184     *
185     * @return The x-value.
186     *
187     * @throws ArrayIndexOutOfBoundsException if {@code series} is not
188     *     within the specified range.
189     * @throws ArrayIndexOutOfBoundsException if {@code item} is not
190     *     within the specified range.
191     *
192     * @see #getXValue(int, int)
193     */
194    @Override
195    public Number getX(int series, int item) {
196        return getXValue(series, item);
197    }
198
199    /**
200     * Returns the y-value for an item within a series.
201     *
202     * @param series  the series index (in the range {@code 0} to
203     *     {@code getSeriesCount() - 1}).
204     * @param item  the item index (in the range {@code 0} to
205     *     {@code getItemCount(series)}).
206     *
207     * @return The y-value.
208     *
209     * @throws ArrayIndexOutOfBoundsException if {@code series} is not
210     *     within the specified range.
211     * @throws ArrayIndexOutOfBoundsException if {@code item} is not
212     *     within the specified range.
213     *
214     * @see #getY(int, int)
215     */
216    @Override
217    public double getYValue(int series, int item) {
218        double[][] seriesData = this.seriesList.get(series);
219        return seriesData[1][item];
220    }
221
222    /**
223     * Returns the y-value for an item within a series.
224     *
225     * @param series  the series index (in the range {@code 0} to
226     *     {@code getSeriesCount() - 1}).
227     * @param item  the item index (in the range {@code 0} to
228     *     {@code getItemCount(series)}).
229     *
230     * @return The y-value.
231     *
232     * @throws ArrayIndexOutOfBoundsException if {@code series} is not
233     *     within the specified range.
234     * @throws ArrayIndexOutOfBoundsException if {@code item} is not
235     *     within the specified range.
236     *
237     * @see #getX(int, int)
238     */
239    @Override
240    public Number getY(int series, int item) {
241        return getYValue(series, item);
242    }
243
244    /**
245     * Adds a series or if a series with the same key already exists replaces
246     * the data for that series, then sends a {@link DatasetChangeEvent} to
247     * all registered listeners.
248     *
249     * @param seriesKey  the series key ({@code null} not permitted).
250     * @param data  the data (must be an array with length 2, containing two
251     *     arrays of equal length, the first containing the x-values and the
252     *     second containing the y-values).
253     */
254    public void addSeries(S seriesKey, double[][] data) {
255        if (seriesKey == null) {
256            throw new IllegalArgumentException(
257                    "The 'seriesKey' cannot be null.");
258        }
259        if (data == null) {
260            throw new IllegalArgumentException("The 'data' is null.");
261        }
262        if (data.length != 2) {
263            throw new IllegalArgumentException(
264                    "The 'data' array must have length == 2.");
265        }
266        if (data[0].length != data[1].length) {
267            throw new IllegalArgumentException(
268                "The 'data' array must contain two arrays with equal length.");
269        }
270        int seriesIndex = indexOf(seriesKey);
271        if (seriesIndex == -1) {  // add a new series
272            this.seriesKeys.add(seriesKey);
273            this.seriesList.add(data);
274        }
275        else {  // replace an existing series
276            this.seriesList.remove(seriesIndex);
277            this.seriesList.add(seriesIndex, data);
278        }
279        notifyListeners(new DatasetChangeEvent(this, this));
280    }
281
282    /**
283     * Removes a series from the dataset, then sends a
284     * {@link DatasetChangeEvent} to all registered listeners.
285     *
286     * @param seriesKey  the series key ({@code null} not permitted).
287     *
288     */
289    public void removeSeries(S seriesKey) {
290        int seriesIndex = indexOf(seriesKey);
291        if (seriesIndex >= 0) {
292            this.seriesKeys.remove(seriesIndex);
293            this.seriesList.remove(seriesIndex);
294            notifyListeners(new DatasetChangeEvent(this, this));
295        }
296    }
297
298    /**
299     * Tests this {@code DefaultXYDataset} instance for equality with an
300     * arbitrary object.  This method returns {@code true} if and only if:
301     * <ul>
302     * <li>{@code obj} is not {@code null};</li>
303     * <li>{@code obj} is an instance of {@code DefaultXYDataset};</li>
304     * <li>both datasets have the same number of series, each containing
305     *         exactly the same values.</li>
306     * </ul>
307     *
308     * @param obj  the object ({@code null} permitted).
309     *
310     * @return A boolean.
311     */
312    @Override
313    public boolean equals(Object obj) {
314        if (obj == this) {
315            return true;
316        }
317        if (!(obj instanceof DefaultXYDataset)) {
318            return false;
319        }
320        DefaultXYDataset that = (DefaultXYDataset) obj;
321        if (!this.seriesKeys.equals(that.seriesKeys)) {
322            return false;
323        }
324        for (int i = 0; i < this.seriesList.size(); i++) {
325            double[][] d1 = this.seriesList.get(i);
326            double[][] d2 = (double[][]) that.seriesList.get(i);
327            double[] d1x = d1[0];
328            double[] d2x = d2[0];
329            if (!Arrays.equals(d1x, d2x)) {
330                return false;
331            }
332            double[] d1y = d1[1];
333            double[] d2y = d2[1];
334            if (!Arrays.equals(d1y, d2y)) {
335                return false;
336            }
337        }
338        return true;
339    }
340
341    /**
342     * Returns a hash code for this instance.
343     *
344     * @return A hash code.
345     */
346    @Override
347    public int hashCode() {
348        int result;
349        result = this.seriesKeys.hashCode();
350        result = 29 * result + this.seriesList.hashCode();
351        return result;
352    }
353
354    /**
355     * Creates an independent copy of this dataset.
356     *
357     * @return The cloned dataset.
358     *
359     * @throws CloneNotSupportedException if there is a problem cloning the
360     *     dataset (for instance, if a non-cloneable object is used for a
361     *     series key).
362     */
363    @Override
364    public Object clone() throws CloneNotSupportedException {
365        DefaultXYDataset clone = (DefaultXYDataset) super.clone();
366        clone.seriesKeys = new ArrayList(this.seriesKeys);
367        clone.seriesList = new ArrayList(this.seriesList.size());
368        for (int i = 0; i < this.seriesList.size(); i++) {
369            double[][] data = this.seriesList.get(i);
370            double[] x = data[0];
371            double[] y = data[1];
372            double[] xx = new double[x.length];
373            double[] yy = new double[y.length];
374            System.arraycopy(x, 0, xx, 0, x.length);
375            System.arraycopy(y, 0, yy, 0, y.length);
376            clone.seriesList.add(i, new double[][] {xx, yy});
377        }
378        return clone;
379    }
380
381}