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