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 * DefaultTableXYDataset.java
029 * --------------------------
030 * (C) Copyright 2003-2021, by Richard Atkinson and Contributors.
031 *
032 * Original Author:  Richard Atkinson;
033 * Contributor(s):   Jody Brownell;
034 *                   David Gilbert;
035 *                   Andreas Schroeder;
036 * 
037 */
038
039package org.jfree.data.xy;
040
041import org.jfree.chart.api.PublicCloneable;
042import org.jfree.chart.internal.Args;
043import org.jfree.chart.internal.CloneUtils;
044import org.jfree.data.DomainInfo;
045import org.jfree.data.Range;
046import org.jfree.data.general.DatasetChangeEvent;
047import org.jfree.data.general.DatasetUtils;
048import org.jfree.data.general.SeriesChangeEvent;
049
050import java.util.ArrayList;
051import java.util.HashSet;
052import java.util.List;
053import java.util.Objects;
054
055/**
056 * An {@link XYDataset} where every series shares the same x-values (required
057 * for generating stacked area charts).
058 * 
059 * @param <S> The type for the series keys.
060 */
061public class DefaultTableXYDataset<S extends Comparable<S>> 
062        extends AbstractIntervalXYDataset<S>
063        implements TableXYDataset<S>, IntervalXYDataset<S>, DomainInfo,
064                   PublicCloneable {
065
066    /**
067     * Storage for the data - this list will contain zero, one or many
068     * XYSeries objects.
069     */
070    private List<XYSeries<S>> data = null;
071
072    /** Storage for the x values. */
073    private HashSet xPoints = null;
074
075    /** A flag that controls whether or not events are propogated. */
076    private boolean propagateEvents = true;
077
078    /** A flag that controls auto pruning. */
079    private boolean autoPrune = false;
080
081    /** The delegate used to control the interval width. */
082    private IntervalXYDelegate intervalDelegate;
083
084    /**
085     * Creates a new empty dataset.
086     */
087    public DefaultTableXYDataset() {
088        this(false);
089    }
090
091    /**
092     * Creates a new empty dataset.
093     *
094     * @param autoPrune  a flag that controls whether or not x-values are
095     *                   removed whenever the corresponding y-values are all
096     *                   {@code null}.
097     */
098    public DefaultTableXYDataset(boolean autoPrune) {
099        this.autoPrune = autoPrune;
100        this.data = new ArrayList<>();
101        this.xPoints = new HashSet();
102        this.intervalDelegate = new IntervalXYDelegate(this, false);
103        addChangeListener(this.intervalDelegate);
104    }
105
106    /**
107     * Returns the flag that controls whether or not x-values are removed from
108     * the dataset when the corresponding y-values are all {@code null}.
109     *
110     * @return A boolean.
111     */
112    public boolean isAutoPrune() {
113        return this.autoPrune;
114    }
115
116    /**
117     * Adds a series to the collection and sends a {@link DatasetChangeEvent}
118     * to all registered listeners.  The series should be configured to NOT
119     * allow duplicate x-values.
120     *
121     * @param series  the series ({@code null} not permitted).
122     */
123    public void addSeries(XYSeries<S> series) {
124        Args.nullNotPermitted(series, "series");
125        if (series.getAllowDuplicateXValues()) {
126            throw new IllegalArgumentException(
127                "Cannot accept XYSeries that allow duplicate values. "
128                + "Use XYSeries(seriesName, <sort>, false) constructor."
129            );
130        }
131        updateXPoints(series);
132        this.data.add(series);
133        series.addChangeListener(this);
134        fireDatasetChanged();
135    }
136
137    /**
138     * Adds any unique x-values from 'series' to the dataset, and also adds any
139     * x-values that are in the dataset but not in 'series' to the series.
140     *
141     * @param series  the series ({@code null} not permitted).
142     */
143    private void updateXPoints(XYSeries<S> series) {
144        Args.nullNotPermitted(series, "series");
145        HashSet seriesXPoints = new HashSet();
146        boolean savedState = this.propagateEvents;
147        this.propagateEvents = false;
148        for (int itemNo = 0; itemNo < series.getItemCount(); itemNo++) {
149            Number xValue = series.getX(itemNo);
150            seriesXPoints.add(xValue);
151            if (!this.xPoints.contains(xValue)) {
152                this.xPoints.add(xValue);
153                int seriesCount = this.data.size();
154                for (int seriesNo = 0; seriesNo < seriesCount; seriesNo++) {
155                    XYSeries<S> dataSeries = this.data.get(seriesNo);
156                    if (!dataSeries.equals(series)) {
157                        dataSeries.add(xValue, null);
158                    }
159                }
160            }
161        }
162        for (Object point : this.xPoints) {
163            Number xPoint = (Number) point;
164            if (!seriesXPoints.contains(xPoint)) {
165                series.add(xPoint, null);
166            }
167        }
168        this.propagateEvents = savedState;
169    }
170
171    /**
172     * Updates the x-values for all the series in the dataset.
173     */
174    public void updateXPoints() {
175        this.propagateEvents = false;
176        for (int s = 0; s < this.data.size(); s++) {
177            updateXPoints(this.data.get(s));
178        }
179        if (this.autoPrune) {
180            prune();
181        }
182        this.propagateEvents = true;
183    }
184
185    /**
186     * Returns the number of series in the collection.
187     *
188     * @return The series count.
189     */
190    @Override
191    public int getSeriesCount() {
192        return this.data.size();
193    }
194
195    /**
196     * Returns the number of x values in the dataset.
197     *
198     * @return The number of x values in the dataset.
199     */
200    @Override
201    public int getItemCount() {
202        if (this.xPoints == null) {
203            return 0;
204        }
205        else {
206            return this.xPoints.size();
207        }
208    }
209
210    /**
211     * Returns a series.
212     *
213     * @param series  the series (zero-based index).
214     *
215     * @return The series (never {@code null}).
216     */
217    public XYSeries<S> getSeries(int series) {
218        Args.requireInRange(series, "series", 0, this.data.size() - 1);
219        return this.data.get(series);
220    }
221
222    /**
223     * Returns the key for a series.
224     *
225     * @param series  the series (zero-based index).
226     *
227     * @return The key for a series.
228     */
229    @Override
230    public S getSeriesKey(int series) {
231        // check arguments...delegated
232        return getSeries(series).getKey();
233    }
234
235    /**
236     * Returns the number of items in the specified series.
237     *
238     * @param series  the series (zero-based index).
239     *
240     * @return The number of items in the specified series.
241     */
242    @Override
243    public int getItemCount(int series) {
244        // check arguments...delegated
245        return getSeries(series).getItemCount();
246    }
247
248    /**
249     * Returns the x-value for the specified series and item.
250     *
251     * @param series  the series (zero-based index).
252     * @param item  the item (zero-based index).
253     *
254     * @return The x-value for the specified series and item.
255     */
256    @Override
257    public Number getX(int series, int item) {
258        XYSeries<S> s = this.data.get(series);
259        return s.getX(item);
260
261    }
262
263    /**
264     * Returns the starting X value for the specified series and item.
265     *
266     * @param series  the series (zero-based index).
267     * @param item  the item (zero-based index).
268     *
269     * @return The starting X value.
270     */
271    @Override
272    public Number getStartX(int series, int item) {
273        return this.intervalDelegate.getStartX(series, item);
274    }
275
276    /**
277     * Returns the ending X value for the specified series and item.
278     *
279     * @param series  the series (zero-based index).
280     * @param item  the item (zero-based index).
281     *
282     * @return The ending X value.
283     */
284    @Override
285    public Number getEndX(int series, int item) {
286        return this.intervalDelegate.getEndX(series, item);
287    }
288
289    /**
290     * Returns the y-value for the specified series and item.
291     *
292     * @param series  the series (zero-based index).
293     * @param index  the index of the item of interest (zero-based).
294     *
295     * @return The y-value for the specified series and item (possibly
296     *         {@code null}).
297     */
298    @Override
299    public Number getY(int series, int index) {
300        XYSeries<S> s = this.data.get(series);
301        return s.getY(index);
302    }
303
304    /**
305     * Returns the starting Y value for the specified series and item.
306     *
307     * @param series  the series (zero-based index).
308     * @param item  the item (zero-based index).
309     *
310     * @return The starting Y value.
311     */
312    @Override
313    public Number getStartY(int series, int item) {
314        return getY(series, item);
315    }
316
317    /**
318     * Returns the ending Y value for the specified series and item.
319     *
320     * @param series  the series (zero-based index).
321     * @param item  the item (zero-based index).
322     *
323     * @return The ending Y value.
324     */
325    @Override
326    public Number getEndY(int series, int item) {
327        return getY(series, item);
328    }
329
330    /**
331     * Removes all the series from the collection and sends a
332     * {@link DatasetChangeEvent} to all registered listeners.
333     */
334    public void removeAllSeries() {
335
336        // Unregister the collection as a change listener to each series in
337        // the collection.
338        for (XYSeries<S> series : this.data) {
339            series.removeChangeListener(this);
340        }
341
342        // Remove all the series from the collection and notify listeners.
343        this.data.clear();
344        this.xPoints.clear();
345        fireDatasetChanged();
346    }
347
348    /**
349     * Removes a series from the collection and sends a
350     * {@link DatasetChangeEvent} to all registered listeners.
351     *
352     * @param series  the series ({@code null} not permitted).
353     */
354    public void removeSeries(XYSeries<S> series) {
355        Args.nullNotPermitted(series, "series");
356        if (this.data.contains(series)) {
357            series.removeChangeListener(this);
358            this.data.remove(series);
359            if (this.data.isEmpty()) {
360                this.xPoints.clear();
361            }
362            fireDatasetChanged();
363        }
364    }
365
366    /**
367     * Removes a series from the collection and sends a
368     * {@link DatasetChangeEvent} to all registered listeners.
369     *
370     * @param series  the series (zero based index).
371     */
372    public void removeSeries(int series) {
373        Args.requireInRange(series, "series", 0, this.data.size() - 1);
374
375        // fetch the series, remove the change listener, then remove the series.
376        XYSeries<S> s = this.data.get(series);
377        s.removeChangeListener(this);
378        this.data.remove(series);
379        if (this.data.isEmpty()) {
380            this.xPoints.clear();
381        }
382        else if (this.autoPrune) {
383            prune();
384        }
385        fireDatasetChanged();
386
387    }
388
389    /**
390     * Removes the items from all series for a given x value.
391     *
392     * @param x  the x-value.
393     */
394    public void removeAllValuesForX(Number x) {
395        Args.nullNotPermitted(x, "x");
396        boolean savedState = this.propagateEvents;
397        this.propagateEvents = false;
398        for (int s = 0; s < this.data.size(); s++) {
399            XYSeries<S> series = this.data.get(s);
400            series.remove(x);
401        }
402        this.propagateEvents = savedState;
403        this.xPoints.remove(x);
404        fireDatasetChanged();
405    }
406
407    /**
408     * Returns {@code true} if all the y-values for the specified x-value
409     * are {@code null} and {@code false} otherwise.
410     *
411     * @param x  the x-value.
412     *
413     * @return A boolean.
414     */
415    protected boolean canPrune(Number x) {
416        for (int s = 0; s < this.data.size(); s++) {
417            XYSeries<S> series = this.data.get(s);
418            if (series.getY(series.indexOf(x)) != null) {
419                return false;
420            }
421        }
422        return true;
423    }
424
425    /**
426     * Removes all x-values for which all the y-values are {@code null}.
427     */
428    public void prune() {
429        HashSet hs = (HashSet) this.xPoints.clone();
430        for (Object h : hs) {
431            Number x = (Number) h;
432            if (canPrune(x)) {
433                removeAllValuesForX(x);
434            }
435        }
436    }
437
438    /**
439     * This method receives notification when a series belonging to the dataset
440     * changes.  It responds by updating the x-points for the entire dataset
441     * and sending a {@link DatasetChangeEvent} to all registered listeners.
442     *
443     * @param event  information about the change.
444     */
445    @Override
446    public void seriesChanged(SeriesChangeEvent event) {
447        if (this.propagateEvents) {
448            updateXPoints();
449            fireDatasetChanged();
450        }
451    }
452
453    /**
454     * Tests this collection for equality with an arbitrary object.
455     *
456     * @param obj  the object ({@code null} permitted).
457     *
458     * @return A boolean.
459     */
460    @Override
461    public boolean equals(Object obj) {
462        if (obj == this) {
463            return true;
464        }
465        if (!(obj instanceof DefaultTableXYDataset)) {
466            return false;
467        }
468        DefaultTableXYDataset that = (DefaultTableXYDataset) obj;
469        if (this.autoPrune != that.autoPrune) {
470            return false;
471        }
472        if (this.propagateEvents != that.propagateEvents) {
473            return false;
474        }
475        if (!this.intervalDelegate.equals(that.intervalDelegate)) {
476            return false;
477        }
478        if (!Objects.equals(this.data, that.data)) {
479            return false;
480        }
481        return true;
482    }
483
484    /**
485     * Returns a hash code.
486     *
487     * @return A hash code.
488     */
489    @Override
490    public int hashCode() {
491        int result;
492        result = (this.data != null ? this.data.hashCode() : 0);
493        result = 29 * result
494                 + (this.xPoints != null ? this.xPoints.hashCode() : 0);
495        result = 29 * result + (this.propagateEvents ? 1 : 0);
496        result = 29 * result + (this.autoPrune ? 1 : 0);
497        return result;
498    }
499
500    /**
501     * Returns an independent copy of this dataset.
502     *
503     * @return A clone.
504     *
505     * @throws CloneNotSupportedException if there is some reason that cloning
506     *     cannot be performed.
507     */
508    @Override
509    public Object clone() throws CloneNotSupportedException {
510        DefaultTableXYDataset clone = (DefaultTableXYDataset) super.clone();
511        int seriesCount = this.data.size();
512        clone.data = new ArrayList<>(seriesCount);
513        for (XYSeries<S> series : this.data) {
514            clone.data.add(CloneUtils.clone(series));
515        }
516
517        clone.intervalDelegate = new IntervalXYDelegate(clone);
518        // need to configure the intervalDelegate to match the original
519        clone.intervalDelegate.setFixedIntervalWidth(getIntervalWidth());
520        clone.intervalDelegate.setAutoWidth(isAutoWidth());
521        clone.intervalDelegate.setIntervalPositionFactor(
522                getIntervalPositionFactor());
523        clone.updateXPoints();
524        return clone;
525    }
526
527    /**
528     * Returns the minimum x-value in the dataset.
529     *
530     * @param includeInterval  a flag that determines whether or not the
531     *                         x-interval is taken into account.
532     *
533     * @return The minimum value.
534     */
535    @Override
536    public double getDomainLowerBound(boolean includeInterval) {
537        return this.intervalDelegate.getDomainLowerBound(includeInterval);
538    }
539
540    /**
541     * Returns the maximum x-value in the dataset.
542     *
543     * @param includeInterval  a flag that determines whether or not the
544     *                         x-interval is taken into account.
545     *
546     * @return The maximum value.
547     */
548    @Override
549    public double getDomainUpperBound(boolean includeInterval) {
550        return this.intervalDelegate.getDomainUpperBound(includeInterval);
551    }
552
553    /**
554     * Returns the range of the values in this dataset's domain.
555     *
556     * @param includeInterval  a flag that determines whether or not the
557     *                         x-interval is taken into account.
558     *
559     * @return The range.
560     */
561    @Override
562    public Range getDomainBounds(boolean includeInterval) {
563        if (includeInterval) {
564            return this.intervalDelegate.getDomainBounds(includeInterval);
565        }
566        else {
567            return DatasetUtils.iterateDomainBounds(this, includeInterval);
568        }
569    }
570
571    /**
572     * Returns the interval position factor.
573     *
574     * @return The interval position factor.
575     */
576    public double getIntervalPositionFactor() {
577        return this.intervalDelegate.getIntervalPositionFactor();
578    }
579
580    /**
581     * Sets the interval position factor. Must be between 0.0 and 1.0 inclusive.
582     * If the factor is 0.5, the gap is in the middle of the x values. If it
583     * is lesser than 0.5, the gap is farther to the left and if greater than
584     * 0.5 it gets farther to the right.
585     *
586     * @param d the new interval position factor.
587     */
588    public void setIntervalPositionFactor(double d) {
589        this.intervalDelegate.setIntervalPositionFactor(d);
590        fireDatasetChanged();
591    }
592
593    /**
594     * returns the full interval width.
595     *
596     * @return The interval width to use.
597     */
598    public double getIntervalWidth() {
599        return this.intervalDelegate.getIntervalWidth();
600    }
601
602    /**
603     * Sets the interval width to a fixed value, and sends a
604     * {@link DatasetChangeEvent} to all registered listeners.
605     *
606     * @param d  the new interval width (must be &gt; 0).
607     */
608    public void setIntervalWidth(double d) {
609        this.intervalDelegate.setFixedIntervalWidth(d);
610        fireDatasetChanged();
611    }
612
613    /**
614     * Returns whether the interval width is automatically calculated or not.
615     *
616     * @return A flag that determines whether or not the interval width is
617     *         automatically calculated.
618     */
619    public boolean isAutoWidth() {
620        return this.intervalDelegate.isAutoWidth();
621    }
622
623    /**
624     * Sets the flag that indicates whether the interval width is automatically
625     * calculated or not.
626     *
627     * @param b  a boolean.
628     */
629    public void setAutoWidth(boolean b) {
630        this.intervalDelegate.setAutoWidth(b);
631        fireDatasetChanged();
632    }
633
634}