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 * CategoryTableXYDataset.java
029 * ---------------------------
030 * (C) Copyright 2004-2020, by Andreas Schroeder and Contributors.
031 *
032 * Original Author:  Andreas Schroeder;
033 * Contributor(s):   David Gilbert;
034 *
035 */
036
037package org.jfree.data.xy;
038
039import org.jfree.chart.api.PublicCloneable;
040import org.jfree.data.DefaultKeyedValues2D;
041import org.jfree.data.DomainInfo;
042import org.jfree.data.Range;
043import org.jfree.data.general.DatasetChangeEvent;
044import org.jfree.data.general.DatasetUtils;
045
046/**
047 * An implementation variant of the {@link TableXYDataset} where every series
048 * shares the same x-values (required for generating stacked area charts).
049 * This implementation uses a {@link DefaultKeyedValues2D} Object as backend
050 * implementation and is hence more "category oriented" than the {@link
051 * DefaultTableXYDataset} implementation.
052 * <p>
053 * This implementation provides no means to remove data items yet.
054 * This is due to the lack of such facility in the DefaultKeyedValues2D class.
055 * <p>
056 * This class also implements the {@link IntervalXYDataset} interface, but this
057 * implementation is provisional.
058 */
059public class CategoryTableXYDataset extends AbstractIntervalXYDataset
060        implements TableXYDataset, IntervalXYDataset, DomainInfo,
061                   PublicCloneable {
062
063    /**
064     * The backing data structure.
065     */
066    private DefaultKeyedValues2D values;
067
068    /** A delegate for controlling the interval width. */
069    private IntervalXYDelegate intervalDelegate;
070
071    /**
072     * Creates a new empty CategoryTableXYDataset.
073     */
074    public CategoryTableXYDataset() {
075        this.values = new DefaultKeyedValues2D(true);
076        this.intervalDelegate = new IntervalXYDelegate(this);
077        addChangeListener(this.intervalDelegate);
078    }
079
080    /**
081     * Adds a data item to this dataset and sends a {@link DatasetChangeEvent}
082     * to all registered listeners.
083     *
084     * @param x  the x value.
085     * @param y  the y value.
086     * @param seriesName  the name of the series to add the data item.
087     */
088    public void add(double x, double y, String seriesName) {
089        add(x, y, seriesName, true);
090    }
091
092    /**
093     * Adds a data item to this dataset and, if requested, sends a
094     * {@link DatasetChangeEvent} to all registered listeners.
095     *
096     * @param x  the x value.
097     * @param y  the y value.
098     * @param seriesName  the name of the series to add the data item.
099     * @param notify  notify listeners?
100     */
101    public void add(Number x, Number y, String seriesName, boolean notify) {
102        this.values.addValue(y, (Comparable<?>) x, seriesName);
103        if (notify) {
104            fireDatasetChanged();
105        }
106    }
107
108    /**
109     * Removes a value from the dataset.
110     *
111     * @param x  the x-value.
112     * @param seriesName  the series name.
113     */
114    public void remove(double x, String seriesName) {
115        remove(x, seriesName, true);
116    }
117
118    /**
119     * Removes an item from the dataset.
120     *
121     * @param x  the x-value.
122     * @param seriesName  the series name.
123     * @param notify  notify listeners?
124     */
125    public void remove(Number x, String seriesName, boolean notify) {
126        this.values.removeValue((Comparable) x, seriesName);
127        if (notify) {
128            fireDatasetChanged();
129        }
130    }
131
132    /**
133     * Clears all data from the dataset and sends a {@link DatasetChangeEvent}
134     * to all registered listeners.
135     * 
136     * @since 1.0.14
137     */
138    public void clear() {
139        this.values.clear();
140        fireDatasetChanged();
141    }
142
143    /**
144     * Returns the number of series in the collection.
145     *
146     * @return The series count.
147     */
148    @Override
149    public int getSeriesCount() {
150        return this.values.getColumnCount();
151    }
152
153    /**
154     * Returns the key for a series.
155     *
156     * @param series  the series index (zero-based).
157     *
158     * @return The key for a series.
159     */
160    @Override
161    public Comparable<?> getSeriesKey(int series) {
162        return this.values.getColumnKey(series);
163    }
164
165    /**
166     * Returns the number of x values in the dataset.
167     *
168     * @return The item count.
169     */
170    @Override
171    public int getItemCount() {
172        return this.values.getRowCount();
173    }
174
175    /**
176     * Returns the number of items in the specified series.
177     * Returns the same as {@link CategoryTableXYDataset#getItemCount()}.
178     *
179     * @param series  the series index (zero-based).
180     *
181     * @return The item count.
182     */
183    @Override
184    public int getItemCount(int series) {
185        return getItemCount();  // all series have the same number of items in
186                                // this dataset
187    }
188
189    /**
190     * Returns the x-value for the specified series and item.
191     *
192     * @param series  the series index (zero-based).
193     * @param item  the item index (zero-based).
194     *
195     * @return The value.
196     */
197    @Override
198    public Number getX(int series, int item) {
199        return (Number) this.values.getRowKey(item);
200    }
201
202    /**
203     * Returns the starting X value for the specified series and item.
204     *
205     * @param series  the series index (zero-based).
206     * @param item  the item index (zero-based).
207     *
208     * @return The starting X value.
209     */
210    @Override
211    public Number getStartX(int series, int item) {
212        return this.intervalDelegate.getStartX(series, item);
213    }
214
215    /**
216     * Returns the ending X value for the specified series and item.
217     *
218     * @param series  the series index (zero-based).
219     * @param item  the item index (zero-based).
220     *
221     * @return The ending X value.
222     */
223    @Override
224    public Number getEndX(int series, int item) {
225        return this.intervalDelegate.getEndX(series, item);
226    }
227
228    /**
229     * Returns the y-value for the specified series and item.
230     *
231     * @param series  the series index (zero-based).
232     * @param item  the item index (zero-based).
233     *
234     * @return The y value (possibly {@code null}).
235     */
236    @Override
237    public Number getY(int series, int item) {
238        return this.values.getValue(item, series);
239    }
240
241    /**
242     * Returns the starting Y value for the specified series and item.
243     *
244     * @param series  the series index (zero-based).
245     * @param item  the item index (zero-based).
246     *
247     * @return The starting Y value.
248     */
249    @Override
250    public Number getStartY(int series, int item) {
251        return getY(series, item);
252    }
253
254    /**
255     * Returns the ending Y value for the specified series and item.
256     *
257     * @param series  the series index (zero-based).
258     * @param item  the item index (zero-based).
259     *
260     * @return The ending Y value.
261     */
262    @Override
263    public Number getEndY(int series, int item) {
264        return getY(series, item);
265    }
266
267    /**
268     * Returns the minimum x-value in the dataset.
269     *
270     * @param includeInterval  a flag that determines whether or not the
271     *                         x-interval is taken into account.
272     *
273     * @return The minimum value.
274     */
275    @Override
276    public double getDomainLowerBound(boolean includeInterval) {
277        return this.intervalDelegate.getDomainLowerBound(includeInterval);
278    }
279
280    /**
281     * Returns the maximum x-value in the dataset.
282     *
283     * @param includeInterval  a flag that determines whether or not the
284     *                         x-interval is taken into account.
285     *
286     * @return The maximum value.
287     */
288    @Override
289    public double getDomainUpperBound(boolean includeInterval) {
290        return this.intervalDelegate.getDomainUpperBound(includeInterval);
291    }
292
293    /**
294     * Returns the range of the values in this dataset's domain.
295     *
296     * @param includeInterval  a flag that determines whether or not the
297     *                         x-interval is taken into account.
298     *
299     * @return The range.
300     */
301    @Override
302    public Range getDomainBounds(boolean includeInterval) {
303        if (includeInterval) {
304            return this.intervalDelegate.getDomainBounds(includeInterval);
305        }
306        else {
307            return DatasetUtils.iterateDomainBounds(this, includeInterval);
308        }
309    }
310
311    /**
312     * Returns the interval position factor.
313     *
314     * @return The interval position factor.
315     */
316    public double getIntervalPositionFactor() {
317        return this.intervalDelegate.getIntervalPositionFactor();
318    }
319
320    /**
321     * Sets the interval position factor. Must be between 0.0 and 1.0 inclusive.
322     * If the factor is 0.5, the gap is in the middle of the x values. If it
323     * is lesser than 0.5, the gap is farther to the left and if greater than
324     * 0.5 it gets farther to the right.
325     *
326     * @param d  the new interval position factor.
327     */
328    public void setIntervalPositionFactor(double d) {
329        this.intervalDelegate.setIntervalPositionFactor(d);
330        fireDatasetChanged();
331    }
332
333    /**
334     * Returns the full interval width.
335     *
336     * @return The interval width to use.
337     */
338    public double getIntervalWidth() {
339        return this.intervalDelegate.getIntervalWidth();
340    }
341
342    /**
343     * Sets the interval width to a fixed value, and sends a
344     * {@link DatasetChangeEvent} to all registered listeners.
345     *
346     * @param d  the new interval width (must be &gt; 0).
347     */
348    public void setIntervalWidth(double d) {
349        this.intervalDelegate.setFixedIntervalWidth(d);
350        fireDatasetChanged();
351    }
352
353    /**
354     * Returns whether the interval width is automatically calculated or not.
355     *
356     * @return whether the width is automatically calculated or not.
357     */
358    public boolean isAutoWidth() {
359        return this.intervalDelegate.isAutoWidth();
360    }
361
362    /**
363     * Sets the flag that indicates whether the interval width is automatically
364     * calculated or not.
365     *
366     * @param b  the flag.
367     */
368    public void setAutoWidth(boolean b) {
369        this.intervalDelegate.setAutoWidth(b);
370        fireDatasetChanged();
371    }
372
373    /**
374     * Tests this dataset for equality with an arbitrary object.
375     *
376     * @param obj  the object ({@code null} permitted).
377     *
378     * @return A boolean.
379     */
380    @Override
381    public boolean equals(Object obj) {
382        if (!(obj instanceof CategoryTableXYDataset)) {
383            return false;
384        }
385        CategoryTableXYDataset that = (CategoryTableXYDataset) obj;
386        if (!this.intervalDelegate.equals(that.intervalDelegate)) {
387            return false;
388        }
389        if (!this.values.equals(that.values)) {
390            return false;
391        }
392        return true;
393    }
394
395    /**
396     * Returns an independent copy of this dataset.
397     *
398     * @return A clone.
399     *
400     * @throws CloneNotSupportedException if there is some reason that cloning
401     *     cannot be performed.
402     */
403    @Override
404    public Object clone() throws CloneNotSupportedException {
405        CategoryTableXYDataset clone = (CategoryTableXYDataset) super.clone();
406        clone.values = (DefaultKeyedValues2D) this.values.clone();
407        clone.intervalDelegate = new IntervalXYDelegate(clone);
408        // need to configure the intervalDelegate to match the original
409        clone.intervalDelegate.setFixedIntervalWidth(getIntervalWidth());
410        clone.intervalDelegate.setAutoWidth(isAutoWidth());
411        clone.intervalDelegate.setIntervalPositionFactor(
412                getIntervalPositionFactor());
413        return clone;
414    }
415
416}