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