001/* ===========================================================
002 * Orson Charts : a 3D chart library for the Java(tm) platform
003 * ===========================================================
004 * 
005 * (C)opyright 2013-2022, by David Gilbert.  All rights reserved.
006 * 
007 * https://github.com/jfree/orson-charts
008 * 
009 * This program is free software: you can redistribute it and/or modify
010 * it under the terms of the GNU General Public License as published by
011 * the Free Software Foundation, either version 3 of the License, or
012 * (at your option) any later version.
013 *
014 * This program is distributed in the hope that it will be useful,
015 * but WITHOUT ANY WARRANTY; without even the implied warranty of
016 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
017 * GNU General Public License for more details.
018 *
019 * You should have received a copy of the GNU General Public License
020 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
021 * 
022 * [Oracle and Java are registered trademarks of Oracle and/or its affiliates. 
023 * Other names may be trademarks of their respective owners.]
024 * 
025 * If you do not wish to be bound by the terms of the GPL, an alternative
026 * commercial license can be purchased.  For details, please see visit the
027 * Orson Charts home page:
028 * 
029 * http://www.object-refinery.com/orsoncharts/index.html
030 * 
031 */
032
033package org.jfree.chart3d.data;
034
035import java.util.List;
036import org.jfree.chart3d.data.category.CategoryDataset3D;
037import org.jfree.chart3d.data.xyz.XYZDataset;
038import org.jfree.chart3d.data.xyz.XYZSeries;
039import org.jfree.chart3d.data.xyz.XYZSeriesCollection;
040import org.jfree.chart3d.internal.Args;
041
042/**
043 * Some utility methods for working with the various datasets and data
044 * structures available in Orson Charts.
045 */
046public class DataUtils {
047    
048    private DataUtils() {
049        // no need to create instances
050    }
051 
052    /**
053     * Returns the total of the values in the list.  Any {@code null}
054     * values are ignored.
055     * 
056     * @param values  the values ({@code null} not permitted).
057     * 
058     * @return The total of the values in the list. 
059     */
060    public static double total(Values<Number> values) {
061        double result = 0.0;
062        for (int i = 0; i < values.getItemCount(); i++) {
063            Number n = values.getValue(i);
064            if (n != null) {
065                result = result + n.doubleValue();
066            }
067        }
068        return result;
069    }
070    
071    /**
072     * Returns the count of the non-{@code null} entries in the dataset
073     * for the specified series.  An {@code IllegalArgumentException} is
074     * thrown if the {@code seriesKey} is not present in the data.
075     * 
076     * @param <S>  the series key (must implement Comparable).
077     * @param data  the dataset ({@code null} not permitted).
078     * @param seriesKey  the series key ({@code null} not permitted).
079     * 
080     * @return The count.
081     * 
082     * @since 1.2
083     */
084    public static <S extends Comparable<S>> int count(
085            KeyedValues3D<S,?,?,?> data, S seriesKey) {
086        Args.nullNotPermitted(data, "data");
087        Args.nullNotPermitted(seriesKey, "seriesKey");
088        int seriesIndex = data.getSeriesIndex(seriesKey);
089        if (seriesIndex < 0) {
090            throw new IllegalArgumentException("Series not found: " 
091                    + seriesKey);
092        }
093        int count = 0;
094        int rowCount = data.getRowCount();
095        int columnCount = data.getColumnCount();
096        for (int r = 0; r < rowCount; r++) {
097            for (int c = 0; c < columnCount; c++) {
098                Number n = (Number) data.getValue(seriesIndex, r, c);
099                if (n != null) {
100                    count++;
101                }
102            }
103        }
104        return count;
105    }
106        
107    /**
108     * Returns the count of the non-{@code null} entries in the dataset
109     * for the specified row (all series).
110     * 
111     * @param <R>  the row key (must implement Comparable).
112     * @param data  the dataset ({@code null} not permitted).
113     * @param rowKey  the row key ({@code null} not permitted).
114     * 
115     * @return The count.
116     * 
117     * @since 1.2
118     */
119    public static <R extends Comparable<R>> int countForRow(
120            KeyedValues3D<?, R, ?, ?> data, R rowKey) {
121        Args.nullNotPermitted(data, "data");
122        Args.nullNotPermitted(rowKey, "rowKey");
123        int rowIndex = data.getRowIndex(rowKey);
124        if (rowIndex < 0) {
125            throw new IllegalArgumentException("Row not found: " + rowKey);
126        }
127        int count = 0;
128        int seriesCount = data.getSeriesCount();
129        int columnCount = data.getColumnCount();
130        for (int s = 0; s < seriesCount; s++) {
131            for (int c = 0; c < columnCount; c++) {
132                Number n = (Number) data.getValue(s, rowIndex, c);
133                if (n != null) {
134                    count++;
135                }
136            }
137        }
138        return count;
139    }
140
141    /**
142     * Returns the count of the non-{@code null} entries in the dataset
143     * for the specified column (all series).
144     * 
145     * @param <C>  the column key (must implement Comparable).
146     * @param data  the dataset ({@code null} not permitted).
147     * @param columnKey  the column key ({@code null} not permitted).
148     * 
149     * @return The count.
150     * 
151     * @since 1.2
152     */
153    public static <C extends Comparable<C>> int countForColumn(
154            KeyedValues3D<?, ?, C, ?> data, C columnKey) {
155        Args.nullNotPermitted(data, "data");
156        Args.nullNotPermitted(columnKey, "columnKey");
157        int columnIndex = data.getColumnIndex(columnKey);
158        if (columnIndex < 0) {
159            throw new IllegalArgumentException("Column not found: " 
160                    + columnKey);
161        }
162        int count = 0;
163        int seriesCount = data.getSeriesCount();
164        int rowCount = data.getRowCount();
165        for (int s = 0; s < seriesCount; s++) {
166            for (int r = 0; r < rowCount; r++) {
167                Number n = (Number) data.getValue(s, r, columnIndex);
168                if (n != null) {
169                    count++;
170                }
171            }
172        }
173        return count;
174    }
175        
176    /**
177     * Returns the total of the non-{@code null} values in the dataset
178     * for the specified series.  If there is no series with the specified 
179     * key, this method will throw an {@code IllegalArgumentException}.
180     * 
181     * @param <S>  the series key (must implement Comparable).
182     * @param data  the dataset ({@code null} not permitted).
183     * @param seriesKey  the series key ({@code null} not permitted).
184     * 
185     * @return The total.
186     * 
187     * @since 1.2
188     */
189    public static <S extends Comparable<S>> double total(
190            KeyedValues3D<S, ?, ?, ? extends Number> data, S seriesKey) {
191        Args.nullNotPermitted(data, "data");
192        Args.nullNotPermitted(seriesKey, "seriesKey");
193        int seriesIndex = data.getSeriesIndex(seriesKey);
194        if (seriesIndex < 0) {
195            throw new IllegalArgumentException("Series not found: " 
196                    + seriesKey);
197        }
198        double total = 0.0;
199        int rowCount = data.getRowCount();
200        int columnCount = data.getColumnCount();
201        for (int r = 0; r < rowCount; r++) {
202            for (int c = 0; c < columnCount; c++) {
203                Number n = data.getValue(seriesIndex, r, c);
204                if (n != null) {
205                    total += n.doubleValue();
206                }
207            }
208        }
209        return total;
210    }
211
212    /**
213     * Returns the total of the non-{@code null} entries in the dataset
214     * for the specified row (all series).
215     * 
216     * @param <R>  the row key (must implement Comparable).
217     * @param data  the dataset ({@code null} not permitted).
218     * @param rowKey  the row key ({@code null} not permitted).
219     * 
220     * @return The total.
221     * 
222     * @since 1.2
223     */
224    public static <R extends Comparable<R>> double totalForRow(
225            KeyedValues3D<?, R, ?, ? extends Number> data, R rowKey) {
226        Args.nullNotPermitted(data, "data");
227        Args.nullNotPermitted(rowKey, "rowKey");
228        int rowIndex = data.getRowIndex(rowKey);
229        if (rowIndex < 0) {
230            throw new IllegalArgumentException("Row not found: " + rowKey);
231        }
232        double total = 0.0;
233        int seriesCount = data.getSeriesCount();
234        int columnCount = data.getColumnCount();
235        for (int s = 0; s < seriesCount; s++) {
236            for (int c = 0; c < columnCount; c++) {
237                Number n = data.getValue(s, rowIndex, c);
238                if (n != null) {
239                    total += n.doubleValue();
240                }
241            }
242        }
243        return total;
244    }
245
246    /**
247     * Returns the total of the non-{@code null} entries in the dataset
248     * for the specified column (all series).
249     * 
250     * @param <C>  the column key (must implement Comparable).
251     * @param data  the dataset ({@code null} not permitted).
252     * @param columnKey  the row key ({@code null} not permitted).
253     * 
254     * @return The total.
255     * 
256     * @since 1.2
257     */
258    public static <C extends Comparable<C>> double totalForColumn(
259            KeyedValues3D<?, ?, C, ? extends Number> data, C columnKey) {
260        Args.nullNotPermitted(data, "data");
261        Args.nullNotPermitted(columnKey, "columnKey");
262        int columnIndex = data.getColumnIndex(columnKey);
263        if (columnIndex < 0) {
264            throw new IllegalArgumentException("Column not found: " 
265                    + columnKey);
266        }
267        double total = 0.0;
268        int seriesCount = data.getSeriesCount();
269        int rowCount = data.getRowCount();
270        for (int s = 0; s < seriesCount; s++) {
271            for (int r = 0; r < rowCount; r++) {
272                Number n = data.getValue(s, r, columnIndex);
273                if (n != null) {
274                    total += n.doubleValue();
275                }
276            }
277        }
278        return total;
279    }
280
281    /**
282     * Returns the range of values in the specified data structure (a three
283     * dimensional cube).  If there is no data, this method returns
284     * {@code null}.
285     * 
286     * @param data  the data ({@code null} not permitted).
287     * 
288     * @return The range of data values (possibly {@code null}).
289     */
290    public static Range findValueRange(Values3D<? extends Number> data) {
291        return findValueRange(data, Double.NaN);
292    }
293
294    /**
295     * Returns the range of values in the specified data cube, or 
296     * {@code null} if there is no data.  The range will be expanded, if 
297     * required, to include the {@code base} value (unless it
298     * is {@code Double.NaN} in which case it is ignored).
299     * 
300     * @param data  the data ({@code null} not permitted).
301     * @param base  a value that must be included in the range (often 0).  This
302     *         argument is ignored if it is {@code Double.NaN}.
303     * 
304     * @return The range (possibly {@code null}). 
305     */
306    public static Range findValueRange(Values3D<? extends Number> data, 
307            double base) {
308        return findValueRange(data, base, true);
309    }
310    
311    /**
312    /**
313     * Returns the range of values in the specified data cube, or 
314     * {@code null} if there is no data.  The range will be expanded, if 
315     * required, to include the {@code base} value (unless it
316     * is {@code Double.NaN} in which case it is ignored).
317     * 
318     * @param data  the data ({@code null} not permitted).
319     * @param base  a value that must be included in the range (often 0).  This
320     *         argument is ignored if it is {@code Double.NaN}.
321     * @param finite  if {@code true} infinite values will be ignored.
322     * 
323     * @return The range (possibly {@code null}).   
324     * 
325     * @since 1.4
326     */
327    public static Range findValueRange(Values3D<? extends Number> data,
328            double base, boolean finite) {
329        Args.nullNotPermitted(data, "data");
330        double min = Double.POSITIVE_INFINITY;
331        double max = Double.NEGATIVE_INFINITY;
332        for (int series = 0; series < data.getSeriesCount(); series++) {
333            for (int row = 0; row < data.getRowCount(); row++) {
334                for (int col = 0; col < data.getColumnCount(); col++) {
335                    double d = data.getDoubleValue(series, row, col);
336                    if (!Double.isNaN(d)) {
337                        if (!finite || !Double.isInfinite(d)) {
338                            min = Math.min(min, d);
339                            max = Math.max(max, d);
340                        }
341                    }
342                }
343            }
344        }
345        // include the special value in the range
346        if (!Double.isNaN(base)) {
347             min = Math.min(min, base);
348             max = Math.max(max, base);
349        }
350        if (min <= max) {
351            return new Range(min, max);
352        } else {
353            return null;
354        }
355    }
356    
357    /**
358     * Finds the range of values in the dataset considering that each series
359     * is stacked on top of the other.
360     * 
361     * @param data  the data ({@code null} not permitted).
362     * 
363     * @return The range.
364     */
365    public static Range findStackedValueRange(Values3D<? extends Number> data) {
366        return findStackedValueRange(data, 0.0);
367    }
368    
369    /**
370     * Finds the range of values in the dataset considering that each series
371     * is stacked on top of the others, starting at the base value.
372     * 
373     * @param data  the data values ({@code null} not permitted).
374     * @param base  the base value.
375     * 
376     * @return The range.
377     */
378    public static Range findStackedValueRange(Values3D<? extends Number> data, 
379            double base) {
380        Args.nullNotPermitted(data, "data");
381        double min = base;
382        double max = base;
383        int seriesCount = data.getSeriesCount();
384        for (int row = 0; row < data.getRowCount(); row++) {
385            for (int col = 0; col < data.getColumnCount(); col++) {
386                double[] total = stackSubTotal(data, base, seriesCount, row, 
387                        col);
388                min = Math.min(min, total[0]);
389                max = Math.max(max, total[1]);
390            }
391        }
392        if (min <= max) {
393            return new Range(min, max);
394        } else {
395            return null;
396        }        
397    }
398    
399    /**
400     * Returns the positive and negative subtotals of the values for all the 
401     * series preceding the specified series.  
402     * <br><br>
403     * One application for this method is to compute the base values for 
404     * individual bars in a stacked bar chart.
405     * 
406     * @param data  the data ({@code null} not permitted).
407     * @param base  the initial base value (normally {@code 0.0}, but the 
408     *     values can be stacked from a different starting point).
409     * @param series  the index of the current series (series with lower indices
410     *     are included in the sub-totals).
411     * @param row  the row index of the required item.
412     * @param column  the column index of the required item.
413     * 
414     * @return The subtotals, where {@code result[0]} is the subtotal of
415     *     the negative data items, and {@code result[1]} is the subtotal
416     *     of the positive data items.
417     */
418    public static double[] stackSubTotal(Values3D<? extends Number> data, 
419            double base, int series, int row, int column) {
420        double neg = base;
421        double pos = base;
422        for (int s = 0; s < series; s++) {
423            double v = data.getDoubleValue(s, row, column);
424            if (v > 0.0) {
425                pos = pos + v;
426            } else if (v < 0.0) {
427                neg = neg + v;
428            }
429        }
430        return new double[] { neg, pos };
431    }
432
433    /**
434     * Returns the total of the non-{@code NaN} entries in the dataset
435     * for the specified series.
436     * 
437     * @param <S>  the series key (must implement Comparable).
438     * @param data  the dataset ({@code null} not permitted).
439     * @param seriesKey  the series key ({@code null} not permitted).
440     * 
441     * @return The count.
442     * 
443     * @since 1.2
444     */
445    public static <S extends Comparable<S>> double total(XYZDataset<S> data, 
446            S seriesKey) {
447        Args.nullNotPermitted(data, "data");
448        Args.nullNotPermitted(seriesKey, "seriesKey");
449        int seriesIndex = data.getSeriesIndex(seriesKey);
450        if (seriesIndex < 0) {
451            throw new IllegalArgumentException("Series not found: " 
452                    + seriesKey);
453        }
454        double total = 0;
455        int itemCount = data.getItemCount(seriesIndex);
456        for (int item = 0; item < itemCount; item++) {
457            double y = data.getY(seriesIndex, item);
458            if (!Double.isNaN(y)) {
459                total += y;
460            }
461        }
462        return total;
463    }
464    
465    /**
466     * Returns the range of x-values in the dataset by iterating over all
467     * values (and ignoring {@code Double.NaN} and infinite values). 
468     * If there are no values eligible for inclusion in the range, this method 
469     * returns {@code null}.     
470     *
471     * @param dataset  the dataset ({@code null} not permitted).
472     * 
473     * @return The range (possibly {@code null}).
474     */
475    public static Range findXRange(XYZDataset dataset) {
476        return findXRange(dataset, Double.NaN);    
477    }
478    
479    /**
480     * Returns the range of x-values in the dataset by iterating over all
481     * values (and ignoring {@code Double.NaN} values).  The range will be 
482     * extended if necessary to include {@code inc} (unless it is 
483     * {@code Double.NaN} in which case it is ignored).  Infinite values 
484     * in the dataset will be ignored.  If there are no values eligible for 
485     * inclusion in the range, this method returns {@code null}.
486     *
487     * @param dataset  the dataset ({@code null} not permitted).
488     * @param inc  an additional x-value to include.
489     * 
490     * @return The range (possibly {@code null}).
491     */
492    public static Range findXRange(XYZDataset dataset, double inc) {
493        return findXRange(dataset, inc, true);
494    }
495    
496    /**
497     * Returns the range of x-values in the dataset by iterating over all
498     * values (and ignoring {@code Double.NaN} values).  The range will be 
499     * extended if necessary to include {@code inc} (unless it is 
500     * {@code Double.NaN} in which case it is ignored).  If the
501     * {@code finite} flag is set, infinite values in the dataset will be 
502     * ignored.  If there are no values eligible for inclusion in the range, 
503     * this method returns {@code null}.
504     * 
505     * @param dataset  the dataset ({@code null} not permitted).
506     * @param inc  an additional x-value to include.
507     * @param finite  a flag indicating whether to exclude infinite values.
508     * 
509     * @return The range (possibly {@code null}).
510     * 
511     * @since 1.4
512     */
513    public static Range findXRange(XYZDataset dataset, double inc, 
514            boolean finite) {
515        Args.nullNotPermitted(dataset, "dataset");
516        double min = Double.POSITIVE_INFINITY;
517        double max = Double.NEGATIVE_INFINITY;
518        for (int s = 0; s < dataset.getSeriesCount(); s++) {
519            for (int i = 0; i < dataset.getItemCount(s); i++) {
520                double x = dataset.getX(s, i);
521                if (!Double.isNaN(x)) {
522                    if (!finite || !Double.isInfinite(x)) {
523                        min = Math.min(x, min);
524                        max = Math.max(x, max);
525                    }
526                }
527            }
528        }
529        if (!Double.isNaN(inc)) {
530            min = Math.min(inc, min);
531            max = Math.max(inc, max);
532        }
533        if (min <= max) {
534            return new Range(min, max);
535        } else {
536            return null;
537        }        
538    }
539    
540    /**
541     * Returns the range of y-values in the dataset by iterating over all
542     * values (and ignoring {@code Double.NaN} and infinite values). 
543     * If there are no values eligible for inclusion in the range, this method 
544     * returns {@code null}.     
545     *
546     * @param dataset  the dataset ({@code null} not permitted).
547     * 
548     * @return The range. 
549     */
550    public static Range findYRange(XYZDataset dataset) {
551        return findYRange(dataset, Double.NaN);
552    }
553    
554    /**
555     * Returns the range of y-values in the dataset by iterating over all
556     * values (and ignoring {@code Double.NaN} values).  The range will be 
557     * extended if necessary to include {@code inc} (unless it is 
558     * {@code Double.NaN} in which case it is ignored).  Infinite values 
559     * in the dataset will be ignored.  If there are no values eligible for 
560     * inclusion in the range, this method returns {@code null}.
561     *
562     * @param dataset  the dataset ({@code null} not permitted).
563     * @param inc  an additional x-value to include.
564     * 
565     * @return The range. 
566     */
567    public static Range findYRange(XYZDataset dataset, double inc) {
568        return findYRange(dataset, inc, true);
569    }
570    
571    /**
572     * Returns the range of y-values in the dataset by iterating over all
573     * values (and ignoring {@code Double.NaN} values).  The range will be 
574     * extended if necessary to include {@code inc} (unless it is 
575     * {@code Double.NaN} in which case it is ignored).  If the
576     * {@code finite} flag is set, infinite values in the dataset will be 
577     * ignored.  If there are no values eligible for inclusion in the range, 
578     * this method returns {@code null}.
579     * 
580     * @param dataset  the dataset ({@code null} not permitted).
581     * @param inc  an additional y-value to include.
582     * @param finite  a flag indicating whether to exclude infinite values.
583     * 
584     * @return The range (possibly {@code null}).
585     * 
586     * @since 1.4
587     */
588    public static Range findYRange(XYZDataset dataset, double inc, 
589            boolean finite) {
590        Args.nullNotPermitted(dataset, "dataset");
591        double min = Double.POSITIVE_INFINITY;
592        double max = Double.NEGATIVE_INFINITY;
593        for (int s = 0; s < dataset.getSeriesCount(); s++) {
594            for (int i = 0; i < dataset.getItemCount(s); i++) {
595                double y = dataset.getY(s, i);
596                if (!Double.isNaN(y)) {
597                    if (!finite || !Double.isInfinite(y)) {
598                        min = Math.min(y, min);
599                        max = Math.max(y, max);
600                    }
601                }
602            }
603        }
604        if (!Double.isNaN(inc)) {
605            min = Math.min(inc, min);
606            max = Math.max(inc, max);
607        }
608        if (min <= max) {
609            return new Range(min, max);
610        } else {
611            return null;
612        }        
613    }
614    
615    /**
616     * Returns the range of z-values in the dataset by iterating over all
617     * values (and ignoring {@code Double.NaN} and infinite values). 
618     * If there are no values eligible for inclusion in the range, this method 
619     * returns {@code null}.     
620     *
621     * @param dataset  the dataset ({@code null} not permitted).
622     * 
623     * @return The range (possibly {@code null}). 
624     */
625    public static Range findZRange(XYZDataset dataset) {
626        return findZRange(dataset, Double.NaN);
627    }
628    
629    /**
630     * Returns the range of z-values in the dataset by iterating over all
631     * values (and ignoring {@code Double.NaN} values).  The range will be 
632     * extended if necessary to include {@code inc} (unless it is 
633     * {@code Double.NaN} in which case it is ignored).  Infinite values 
634     * in the dataset will be ignored.  If there are no values eligible for 
635     * inclusion in the range, this method returns {@code null}.
636     *
637     * @param dataset  the dataset ({@code null} not permitted).
638     * @param inc  an additional x-value to include.
639     * 
640     * @return The range (possibly {@code null}).
641     */
642    public static Range findZRange(XYZDataset dataset, double inc) {
643        return findZRange(dataset, inc, true);
644    }
645    
646    /**
647     * Returns the range of z-values in the dataset by iterating over all
648     * values (and ignoring {@code Double.NaN} values).  The range will be 
649     * extended if necessary to include {@code inc} (unless it is 
650     * {@code Double.NaN} in which case it is ignored).  If the
651     * {@code finite} flag is set, infinite values in the dataset will be 
652     * ignored.  If there are no values eligible for inclusion in the range, 
653     * this method returns {@code null}.
654     * 
655     * @param dataset  the dataset ({@code null} not permitted).
656     * @param inc  an additional z-value to include.
657     * @param finite  a flag indicating whether to exclude infinite values.
658     * 
659     * @return The range (possibly {@code null}).
660     * 
661     * @since 1.4
662     */
663    public static Range findZRange(XYZDataset dataset, double inc, 
664            boolean finite) {
665        Args.nullNotPermitted(dataset, "dataset");
666        Args.finiteRequired(inc, "inc");
667        double min = Double.POSITIVE_INFINITY;
668        double max = Double.NEGATIVE_INFINITY;
669        for (int s = 0; s < dataset.getSeriesCount(); s++) {
670            for (int i = 0; i < dataset.getItemCount(s); i++) {
671                double z = dataset.getZ(s, i);
672                if (!Double.isNaN(z)) {
673                    if (!finite || !Double.isInfinite(z)) {
674                        min = Math.min(z, min);
675                        max = Math.max(z, max);
676                    }
677                }
678            }
679        }
680        if (!Double.isNaN(inc)) {
681            min = Math.min(inc, min);
682            max = Math.max(inc, max);
683        }
684        if (min <= max) {
685            return new Range(min, max);
686        } else {
687            return null;
688        }        
689    }
690
691    /**
692     * Creates an {@link XYZDataset} by extracting values from specified 
693     * rows in a {@link KeyedValues3D} instance, across all the available
694     * columns (items where any of the x, y or z values is {@code null} 
695     * are skipped).  The new dataset contains a copy of the data and is 
696     * completely independent of the {@code source} dataset.  
697     * <br><br>
698     * Note that {@link CategoryDataset3D} is an extension of 
699     * {@link KeyedValues3D} so you can use this method for any implementation
700     * of the {@code CategoryDataset3D} interface.
701     * 
702     * @param <S>  the series key (must implement Comparable).
703     * @param <R>  the row key (must implement Comparable).
704     * @param <C>  the column key (must implement Comparable).
705     * @param source  the source data ({@code null} not permitted).
706     * @param xRowKey  the row key for x-values ({@code null} not permitted).
707     * @param yRowKey  the row key for y-values ({@code null} not permitted).
708     * @param zRowKey  the row key for z-values ({@code null} not permitted).
709     * 
710     * @return A new dataset. 
711     * 
712     * @since 1.3
713     */
714    public static <S extends Comparable<S>, R extends Comparable<R>, 
715            C extends Comparable<C>> XYZDataset extractXYZDatasetFromRows(
716            KeyedValues3D<S, R, C, ? extends Number> source,
717            R xRowKey, R yRowKey, R zRowKey) {
718        return extractXYZDatasetFromRows(source, xRowKey, yRowKey, zRowKey,
719                NullConversion.SKIP, null);
720    }
721
722    /**
723     * Creates an {@link XYZDataset} by extracting values from specified 
724     * rows in a {@link KeyedValues3D} instance.  The new dataset contains 
725     * a copy of the data and is completely independent of the 
726     * {@code source} dataset.  Note that {@link CategoryDataset3D} is an 
727     * extension of {@link KeyedValues3D}.
728     * <br><br>
729     * Special handling is provided for items that contain {@code null}
730     * values.  The caller may pass in an {@code exceptions} list (
731     * normally empty) that will be populated with the keys of the items that
732     * receive special handling, if any.
733     * 
734     * @param <S>  the series key (must implement Comparable).
735     * @param <R>  the row key (must implement Comparable).
736     * @param <C>  the column key (must implement Comparable).
737     * @param source  the source data ({@code null} not permitted).
738     * @param xRowKey  the row key for x-values ({@code null} not permitted).
739     * @param yRowKey  the row key for y-values ({@code null} not permitted).
740     * @param zRowKey  the row key for z-values ({@code null} not permitted).
741     * @param nullConversion  specifies the treatment for {@code null} 
742     *         values in the dataset ({@code null} not permitted).
743     * @param exceptions  a list that, if not null, will be populated with 
744     *         keys for the items in the source dataset that contain 
745     *         {@code null} values ({@code null} permitted).
746     * 
747     * @return A new dataset. 
748     * 
749     * @since 1.3
750     */
751    public static <S extends Comparable<S>, R extends Comparable<R>, 
752            C extends Comparable<C>> XYZDataset extractXYZDatasetFromRows(
753            KeyedValues3D<S, R, C, ? extends Number> source,
754            R xRowKey, R yRowKey, R zRowKey, NullConversion nullConversion, 
755            List<KeyedValues3DItemKey> exceptions) {
756
757        Args.nullNotPermitted(source, "source");
758        Args.nullNotPermitted(xRowKey, "xRowKey");
759        Args.nullNotPermitted(yRowKey, "yRowKey");
760        Args.nullNotPermitted(zRowKey, "zRowKey");
761        XYZSeriesCollection<S> dataset = new XYZSeriesCollection<>();
762        for (S seriesKey : source.getSeriesKeys()) {
763            XYZSeries<S> series = new XYZSeries<>(seriesKey);
764            for (C colKey : source.getColumnKeys()) {
765                Number x = source.getValue(seriesKey, xRowKey, colKey);
766                Number y = source.getValue(seriesKey, yRowKey, colKey);
767                Number z = source.getValue(seriesKey, zRowKey, colKey);
768                if (x != null && y != null && z != null) {
769                    series.add(x.doubleValue(), y.doubleValue(), 
770                            z.doubleValue());
771                } else {
772                    if (exceptions != null) {
773                        // add only one exception per data value
774                        R rrKey = zRowKey;
775                        if (x == null) {
776                            rrKey = xRowKey;
777                        } else if (y == null) {
778                            rrKey = yRowKey;
779                        }
780                        exceptions.add(new KeyedValues3DItemKey<>(seriesKey, 
781                                rrKey, colKey));
782                    }
783                    if (nullConversion.equals(NullConversion.THROW_EXCEPTION)) {
784                        Comparable rrKey = zRowKey;
785                        if (x == null) {
786                            rrKey = yRowKey;
787                        } else if (y == null) {
788                            rrKey = yRowKey;
789                        }
790                        throw new RuntimeException("There is a null value for "
791                                + "the item [" + seriesKey +", " + rrKey + ", " 
792                                + colKey + "].");
793                    }
794                    if (nullConversion != NullConversion.SKIP) {
795                        double xx = convert(x, nullConversion);
796                        double yy = convert(y, nullConversion);
797                        double zz = convert(z, nullConversion);
798                        series.add(xx, yy, zz);
799                    }
800                }
801            }
802            dataset.add(series);
803        }
804        return dataset;
805    }
806        
807    /**
808     * Creates an {@link XYZDataset} by extracting values from specified 
809     * columns in a {@link KeyedValues3D} instance, across all the available
810     * rows (items where any of the x, y or z values is {@code null} are 
811     * skipped).  The new dataset contains a copy of the data and is completely
812     * independent of the {@code source} dataset.  
813     * <br><br>
814     * Note that {@link CategoryDataset3D} is an extension of 
815     * {@link KeyedValues3D} so you can use this method for any implementation
816     * of the {@code CategoryDataset3D} interface.
817     * 
818     * @param <S>  the series key (must implement Comparable).
819     * @param <R>  the row key (must implement Comparable).
820     * @param <C>  the column key (must implement Comparable).
821     * @param source  the source data ({@code null} not permitted).
822     * @param xColKey  the column key for x-values ({@code null} not permitted).
823     * @param yColKey  the column key for y-values ({@code null} not permitted).
824     * @param zColKey  the column key for z-values ({@code null} not permitted).
825     * 
826     * @return A new dataset. 
827     * 
828     * @since 1.3
829     */
830    public static <S extends Comparable<S>, R extends Comparable<R>, 
831            C extends Comparable<C>> XYZDataset<S> extractXYZDatasetFromColumns(
832            KeyedValues3D<S, R, C, ? extends Number> source,
833            C xColKey, C yColKey, C zColKey) {
834        return extractXYZDatasetFromColumns(source, xColKey, yColKey, zColKey,
835                NullConversion.SKIP, null);
836    }
837
838    /**
839     * Creates an {@link XYZDataset} by extracting values from specified 
840     * columns in a {@link KeyedValues3D} instance.  The new dataset contains 
841     * a copy of the data and is completely independent of the 
842     * {@code source} dataset.  Note that {@link CategoryDataset3D} is an 
843     * extension of {@link KeyedValues3D}.
844     * <br><br>
845     * Special handling is provided for items that contain {@code null}
846     * values.  The caller may pass in an {@code exceptions} list (
847     * normally empty) that will be populated with the keys of the items that
848     * receive special handling, if any.
849     * 
850     * @param <S>  the series key (must implement Comparable).
851     * @param <R>  the row key (must implement Comparable).
852     * @param <C>  the column key (must implement Comparable).
853     * @param source  the source data ({@code null} not permitted).
854     * @param xColKey  the column key for x-values ({@code null} not permitted).
855     * @param yColKey  the column key for y-values ({@code null} not permitted).
856     * @param zColKey  the column key for z-values ({@code null} not permitted).
857     * @param nullConversion  specifies the treatment for {@code null} 
858     *         values in the dataset ({@code null} not permitted).
859     * @param exceptions  a list that, if not null, will be populated with 
860     *         keys for the items in the source dataset that contain 
861     *         {@code null} values ({@code null} permitted).
862     * 
863     * @return A new dataset. 
864     * 
865     * @since 1.3
866     */
867    public static <S extends Comparable<S>, R extends Comparable<R>, 
868            C extends Comparable<C>> 
869            XYZDataset<S> extractXYZDatasetFromColumns(
870            KeyedValues3D<S, R, C, ? extends Number> source,
871            C xColKey, C yColKey, C zColKey, NullConversion nullConversion, 
872            List<KeyedValues3DItemKey> exceptions) {
873
874        Args.nullNotPermitted(source, "source");
875        Args.nullNotPermitted(xColKey, "xColKey");
876        Args.nullNotPermitted(yColKey, "yColKey");
877        Args.nullNotPermitted(zColKey, "zColKey");
878        XYZSeriesCollection<S> dataset = new XYZSeriesCollection<>();
879        for (S seriesKey : source.getSeriesKeys()) {
880            XYZSeries<S> series = new XYZSeries<>(seriesKey);
881            for (R rowKey : source.getRowKeys()) {
882                Number x = source.getValue(seriesKey, rowKey, xColKey);
883                Number y = source.getValue(seriesKey, rowKey, yColKey);
884                Number z = source.getValue(seriesKey, rowKey, zColKey);
885                if (x != null && y != null && z != null) {
886                    series.add(x.doubleValue(), y.doubleValue(), 
887                            z.doubleValue());
888                } else {
889                    if (exceptions != null) {
890                        // add only one key ref out of the possible 3 per item
891                        C ccKey = zColKey;
892                        if (x == null) {
893                            ccKey = xColKey;
894                        } else if (y == null) {
895                            ccKey = yColKey;
896                        }
897                        exceptions.add(new KeyedValues3DItemKey<>(seriesKey, 
898                                rowKey, ccKey));
899                    }
900                    if (nullConversion.equals(NullConversion.THROW_EXCEPTION)) {
901                        Comparable<?> ccKey = zColKey;
902                        if (x == null) {
903                            ccKey = xColKey;
904                        } else if (y == null) {
905                            ccKey = yColKey;
906                        }
907                        throw new RuntimeException("There is a null value for "
908                                + "the item [" + seriesKey +", " + rowKey + ", " 
909                                + ccKey + "].");
910                    }
911                    if (nullConversion != NullConversion.SKIP) {
912                        double xx = convert(x, nullConversion);
913                        double yy = convert(y, nullConversion);
914                        double zz = convert(z, nullConversion);
915                        series.add(xx, yy, zz);
916                    }
917                }
918            }
919            dataset.add(series);
920        }
921        return dataset;
922    }
923
924    /**
925     * Returns a double primitive for the specified number, with 
926     * {@code null} values returning {@code Double.NaN} except in the 
927     * case of {@code CONVERT_TO_ZERO} which returns 0.0.  Note that this 
928     * method does not throw an exception for {@code THROW_EXCEPTION}, it
929     * expects code higher up the call chain to handle that (because there is
930     * not enough information here to throw a useful exception).
931     * 
932     * @param n  the number ({@code null} permitted).
933     * @param nullConversion  the null conversion ({@code null} not 
934     *         permitted).
935     * 
936     * @return A double primitive. 
937     */
938    private static double convert(Number n, NullConversion nullConversion) {
939        if (n != null) {
940            return n.doubleValue();
941        } else {
942            if (nullConversion.equals(NullConversion.CONVERT_TO_ZERO)) {
943                return 0.0;
944            }
945            return Double.NaN;
946        }
947    }
948    
949}