001 /* ===========================================================
002 * JFreeChart : a free chart library for the Java(tm) platform
003 * ===========================================================
004 *
005 * (C) Copyright 2000-2007, by Object Refinery Limited and Contributors.
006 *
007 * Project Info: http://www.jfree.org/jfreechart/index.html
008 *
009 * This library is free software; you can redistribute it and/or modify it
010 * under the terms of the GNU Lesser General Public License as published by
011 * the Free Software Foundation; either version 2.1 of the License, or
012 * (at your option) any later version.
013 *
014 * This library is distributed in the hope that it will be useful, but
015 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
016 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
017 * License for more details.
018 *
019 * You should have received a copy of the GNU Lesser General Public
020 * License along with this library; if not, write to the Free Software
021 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
022 * USA.
023 *
024 * [Java is a trademark or registered trademark of Sun Microsystems, Inc.
025 * in the United States and other countries.]
026 *
027 * ---------------------
028 * DatasetUtilities.java
029 * ---------------------
030 * (C) Copyright 2000-2007, by Object Refinery Limited and Contributors.
031 *
032 * Original Author: David Gilbert (for Object Refinery Limited);
033 * Contributor(s): Andrzej Porebski (bug fix);
034 * Jonathan Nash (bug fix);
035 * Richard Atkinson;
036 * Andreas Schroeder;
037 *
038 * $Id: DatasetUtilities.java,v 1.18.2.8 2007/06/12 14:54:30 mungady Exp $
039 *
040 * Changes (from 18-Sep-2001)
041 * --------------------------
042 * 18-Sep-2001 : Added standard header and fixed DOS encoding problem (DG);
043 * 22-Oct-2001 : Renamed DataSource.java --> Dataset.java etc. (DG);
044 * 15-Nov-2001 : Moved to package com.jrefinery.data.* in the JCommon class
045 * library (DG);
046 * Changed to handle null values from datasets (DG);
047 * Bug fix (thanks to Andrzej Porebski) - initial value now set
048 * to positive or negative infinity when iterating (DG);
049 * 22-Nov-2001 : Datasets with containing no data now return null for min and
050 * max calculations (DG);
051 * 13-Dec-2001 : Extended to handle HighLowDataset and IntervalXYDataset (DG);
052 * 15-Feb-2002 : Added getMinimumStackedRangeValue() and
053 * getMaximumStackedRangeValue() (DG);
054 * 28-Feb-2002 : Renamed Datasets.java --> DatasetUtilities.java (DG);
055 * 18-Mar-2002 : Fixed bug in min/max domain calculation for datasets that
056 * implement the CategoryDataset interface AND the XYDataset
057 * interface at the same time. Thanks to Jonathan Nash for the
058 * fix (DG);
059 * 23-Apr-2002 : Added getDomainExtent() and getRangeExtent() methods (DG);
060 * 13-Jun-2002 : Modified range measurements to handle
061 * IntervalCategoryDataset (DG);
062 * 12-Jul-2002 : Method name change in DomainInfo interface (DG);
063 * 30-Jul-2002 : Added pie dataset summation method (DG);
064 * 01-Oct-2002 : Added a method for constructing an XYDataset from a Function2D
065 * instance (DG);
066 * 24-Oct-2002 : Amendments required following changes to the CategoryDataset
067 * interface (DG);
068 * 18-Nov-2002 : Changed CategoryDataset to TableDataset (DG);
069 * 04-Mar-2003 : Added isEmpty(XYDataset) method (DG);
070 * 05-Mar-2003 : Added a method for creating a CategoryDataset from a
071 * KeyedValues instance (DG);
072 * 15-May-2003 : Renamed isEmpty --> isEmptyOrNull (DG);
073 * 25-Jun-2003 : Added limitPieDataset methods (RA);
074 * 26-Jun-2003 : Modified getDomainExtent() method to accept null datasets (DG);
075 * 27-Jul-2003 : Added getStackedRangeExtent(TableXYDataset data) (RA);
076 * 18-Aug-2003 : getStackedRangeExtent(TableXYDataset data) now handles null
077 * values (RA);
078 * 02-Sep-2003 : Added method to check for null or empty PieDataset (DG);
079 * 18-Sep-2003 : Fix for bug 803660 (getMaximumRangeValue for
080 * CategoryDataset) (DG);
081 * 20-Oct-2003 : Added getCumulativeRangeExtent() method (DG);
082 * 09-Jan-2003 : Added argument checking code to the createCategoryDataset()
083 * method (DG);
084 * 23-Mar-2004 : Fixed bug in getMaximumStackedRangeValue() method (DG);
085 * 31-Mar-2004 : Exposed the extent iteration algorithms to use one of them and
086 * applied noninstantiation pattern (AS);
087 * 11-May-2004 : Renamed getPieDatasetTotal --> calculatePieDatasetTotal (DG);
088 * 15-Jul-2004 : Switched getX() with getXValue() and getY() with getYValue();
089 * 24-Aug-2004 : Added argument checks to createCategoryDataset() method (DG);
090 * 04-Oct-2004 : Renamed ArrayUtils --> ArrayUtilities (DG);
091 * 06-Oct-2004 : Renamed findDomainExtent() --> findDomainBounds(),
092 * findRangeExtent() --> findRangeBounds() (DG);
093 * 07-Jan-2005 : Renamed findStackedRangeExtent() --> findStackedRangeBounds(),
094 * findCumulativeRangeExtent() --> findCumulativeRangeBounds(),
095 * iterateXYRangeExtent() --> iterateXYRangeBounds(),
096 * removed deprecated methods (DG);
097 * 03-Feb-2005 : The findStackedRangeBounds() methods now return null for
098 * empty datasets (DG);
099 * 03-Mar-2005 : Moved createNumberArray() and createNumberArray2D() methods
100 * from DatasetUtilities --> DataUtilities (DG);
101 * 22-Sep-2005 : Added new findStackedRangeBounds() method that takes base
102 * argument (DG);
103 * ------------- JFREECHART 1.0.x ---------------------------------------------
104 * 15-Mar-2007 : Added calculateStackTotal() method (DG);
105 *
106 */
107
108 package org.jfree.data.general;
109
110 import java.util.ArrayList;
111 import java.util.Iterator;
112 import java.util.List;
113
114 import org.jfree.data.DomainInfo;
115 import org.jfree.data.KeyToGroupMap;
116 import org.jfree.data.KeyedValues;
117 import org.jfree.data.Range;
118 import org.jfree.data.RangeInfo;
119 import org.jfree.data.category.CategoryDataset;
120 import org.jfree.data.category.DefaultCategoryDataset;
121 import org.jfree.data.category.IntervalCategoryDataset;
122 import org.jfree.data.function.Function2D;
123 import org.jfree.data.xy.IntervalXYDataset;
124 import org.jfree.data.xy.OHLCDataset;
125 import org.jfree.data.xy.TableXYDataset;
126 import org.jfree.data.xy.XYDataset;
127 import org.jfree.data.xy.XYSeries;
128 import org.jfree.data.xy.XYSeriesCollection;
129 import org.jfree.util.ArrayUtilities;
130
131 /**
132 * A collection of useful static methods relating to datasets.
133 */
134 public final class DatasetUtilities {
135
136 /**
137 * Private constructor for non-instanceability.
138 */
139 private DatasetUtilities() {
140 // now try to instantiate this ;-)
141 }
142
143 /**
144 * Calculates the total of all the values in a {@link PieDataset}. If
145 * the dataset contains negative or <code>null</code> values, they are
146 * ignored.
147 *
148 * @param dataset the dataset (<code>null</code> not permitted).
149 *
150 * @return The total.
151 */
152 public static double calculatePieDatasetTotal(PieDataset dataset) {
153 if (dataset == null) {
154 throw new IllegalArgumentException("Null 'dataset' argument.");
155 }
156 List keys = dataset.getKeys();
157 double totalValue = 0;
158 Iterator iterator = keys.iterator();
159 while (iterator.hasNext()) {
160 Comparable current = (Comparable) iterator.next();
161 if (current != null) {
162 Number value = dataset.getValue(current);
163 double v = 0.0;
164 if (value != null) {
165 v = value.doubleValue();
166 }
167 if (v > 0) {
168 totalValue = totalValue + v;
169 }
170 }
171 }
172 return totalValue;
173 }
174
175 /**
176 * Creates a pie dataset from a table dataset by taking all the values
177 * for a single row.
178 *
179 * @param dataset the dataset (<code>null</code> not permitted).
180 * @param rowKey the row key.
181 *
182 * @return A pie dataset.
183 */
184 public static PieDataset createPieDatasetForRow(CategoryDataset dataset,
185 Comparable rowKey) {
186 int row = dataset.getRowIndex(rowKey);
187 return createPieDatasetForRow(dataset, row);
188 }
189
190 /**
191 * Creates a pie dataset from a table dataset by taking all the values
192 * for a single row.
193 *
194 * @param dataset the dataset (<code>null</code> not permitted).
195 * @param row the row (zero-based index).
196 *
197 * @return A pie dataset.
198 */
199 public static PieDataset createPieDatasetForRow(CategoryDataset dataset,
200 int row) {
201 DefaultPieDataset result = new DefaultPieDataset();
202 int columnCount = dataset.getColumnCount();
203 for (int current = 0; current < columnCount; current++) {
204 Comparable columnKey = dataset.getColumnKey(current);
205 result.setValue(columnKey, dataset.getValue(row, current));
206 }
207 return result;
208 }
209
210 /**
211 * Creates a pie dataset from a table dataset by taking all the values
212 * for a single column.
213 *
214 * @param dataset the dataset (<code>null</code> not permitted).
215 * @param columnKey the column key.
216 *
217 * @return A pie dataset.
218 */
219 public static PieDataset createPieDatasetForColumn(CategoryDataset dataset,
220 Comparable columnKey) {
221 int column = dataset.getColumnIndex(columnKey);
222 return createPieDatasetForColumn(dataset, column);
223 }
224
225 /**
226 * Creates a pie dataset from a {@link CategoryDataset} by taking all the
227 * values for a single column.
228 *
229 * @param dataset the dataset (<code>null</code> not permitted).
230 * @param column the column (zero-based index).
231 *
232 * @return A pie dataset.
233 */
234 public static PieDataset createPieDatasetForColumn(CategoryDataset dataset,
235 int column) {
236 DefaultPieDataset result = new DefaultPieDataset();
237 int rowCount = dataset.getRowCount();
238 for (int i = 0; i < rowCount; i++) {
239 Comparable rowKey = dataset.getRowKey(i);
240 result.setValue(rowKey, dataset.getValue(i, column));
241 }
242 return result;
243 }
244
245 /**
246 * Creates a new pie dataset based on the supplied dataset, but modified
247 * by aggregating all the low value items (those whose value is lower
248 * than the <code>percentThreshold</code>) into a single item with the
249 * key "Other".
250 *
251 * @param source the source dataset (<code>null</code> not permitted).
252 * @param key a new key for the aggregated items (<code>null</code> not
253 * permitted).
254 * @param minimumPercent the percent threshold.
255 *
256 * @return The pie dataset with (possibly) aggregated items.
257 */
258 public static PieDataset createConsolidatedPieDataset(PieDataset source,
259 Comparable key,
260 double minimumPercent)
261 {
262 return DatasetUtilities.createConsolidatedPieDataset(
263 source, key, minimumPercent, 2
264 );
265 }
266
267 /**
268 * Creates a new pie dataset based on the supplied dataset, but modified
269 * by aggregating all the low value items (those whose value is lower
270 * than the <code>percentThreshold</code>) into a single item. The
271 * aggregated items are assigned the specified key. Aggregation only
272 * occurs if there are at least <code>minItems</code> items to aggregate.
273 *
274 * @param source the source dataset (<code>null</code> not permitted).
275 * @param key the key to represent the aggregated items.
276 * @param minimumPercent the percent threshold (ten percent is 0.10).
277 * @param minItems only aggregate low values if there are at least this
278 * many.
279 *
280 * @return The pie dataset with (possibly) aggregated items.
281 */
282 public static PieDataset createConsolidatedPieDataset(PieDataset source,
283 Comparable key,
284 double minimumPercent,
285 int minItems) {
286
287 DefaultPieDataset result = new DefaultPieDataset();
288 double total = DatasetUtilities.calculatePieDatasetTotal(source);
289
290 // Iterate and find all keys below threshold percentThreshold
291 List keys = source.getKeys();
292 ArrayList otherKeys = new ArrayList();
293 Iterator iterator = keys.iterator();
294 while (iterator.hasNext()) {
295 Comparable currentKey = (Comparable) iterator.next();
296 Number dataValue = source.getValue(currentKey);
297 if (dataValue != null) {
298 double value = dataValue.doubleValue();
299 if (value / total < minimumPercent) {
300 otherKeys.add(currentKey);
301 }
302 }
303 }
304
305 // Create new dataset with keys above threshold percentThreshold
306 iterator = keys.iterator();
307 double otherValue = 0;
308 while (iterator.hasNext()) {
309 Comparable currentKey = (Comparable) iterator.next();
310 Number dataValue = source.getValue(currentKey);
311 if (dataValue != null) {
312 if (otherKeys.contains(currentKey)
313 && otherKeys.size() >= minItems) {
314 // Do not add key to dataset
315 otherValue += dataValue.doubleValue();
316 }
317 else {
318 // Add key to dataset
319 result.setValue(currentKey, dataValue);
320 }
321 }
322 }
323 // Add other category if applicable
324 if (otherKeys.size() >= minItems) {
325 result.setValue(key, otherValue);
326 }
327 return result;
328 }
329
330 /**
331 * Creates a {@link CategoryDataset} that contains a copy of the data in an
332 * array (instances of <code>Double</code> are created to represent the
333 * data items).
334 * <p>
335 * Row and column keys are created by appending 0, 1, 2, ... to the
336 * supplied prefixes.
337 *
338 * @param rowKeyPrefix the row key prefix.
339 * @param columnKeyPrefix the column key prefix.
340 * @param data the data.
341 *
342 * @return The dataset.
343 */
344 public static CategoryDataset createCategoryDataset(String rowKeyPrefix,
345 String columnKeyPrefix,
346 double[][] data) {
347
348 DefaultCategoryDataset result = new DefaultCategoryDataset();
349 for (int r = 0; r < data.length; r++) {
350 String rowKey = rowKeyPrefix + (r + 1);
351 for (int c = 0; c < data[r].length; c++) {
352 String columnKey = columnKeyPrefix + (c + 1);
353 result.addValue(new Double(data[r][c]), rowKey, columnKey);
354 }
355 }
356 return result;
357
358 }
359
360 /**
361 * Creates a {@link CategoryDataset} that contains a copy of the data in
362 * an array.
363 * <p>
364 * Row and column keys are created by appending 0, 1, 2, ... to the
365 * supplied prefixes.
366 *
367 * @param rowKeyPrefix the row key prefix.
368 * @param columnKeyPrefix the column key prefix.
369 * @param data the data.
370 *
371 * @return The dataset.
372 */
373 public static CategoryDataset createCategoryDataset(String rowKeyPrefix,
374 String columnKeyPrefix,
375 Number[][] data) {
376
377 DefaultCategoryDataset result = new DefaultCategoryDataset();
378 for (int r = 0; r < data.length; r++) {
379 String rowKey = rowKeyPrefix + (r + 1);
380 for (int c = 0; c < data[r].length; c++) {
381 String columnKey = columnKeyPrefix + (c + 1);
382 result.addValue(data[r][c], rowKey, columnKey);
383 }
384 }
385 return result;
386
387 }
388
389 /**
390 * Creates a {@link CategoryDataset} that contains a copy of the data in
391 * an array (instances of <code>Double</code> are created to represent the
392 * data items).
393 * <p>
394 * Row and column keys are taken from the supplied arrays.
395 *
396 * @param rowKeys the row keys (<code>null</code> not permitted).
397 * @param columnKeys the column keys (<code>null</code> not permitted).
398 * @param data the data.
399 *
400 * @return The dataset.
401 */
402 public static CategoryDataset createCategoryDataset(Comparable[] rowKeys,
403 Comparable[] columnKeys,
404 double[][] data) {
405
406 // check arguments...
407 if (rowKeys == null) {
408 throw new IllegalArgumentException("Null 'rowKeys' argument.");
409 }
410 if (columnKeys == null) {
411 throw new IllegalArgumentException("Null 'columnKeys' argument.");
412 }
413 if (ArrayUtilities.hasDuplicateItems(rowKeys)) {
414 throw new IllegalArgumentException("Duplicate items in 'rowKeys'.");
415 }
416 if (ArrayUtilities.hasDuplicateItems(columnKeys)) {
417 throw new IllegalArgumentException(
418 "Duplicate items in 'columnKeys'."
419 );
420 }
421 if (rowKeys.length != data.length) {
422 throw new IllegalArgumentException(
423 "The number of row keys does not match the number of rows in "
424 + "the data array."
425 );
426 }
427 int columnCount = 0;
428 for (int r = 0; r < data.length; r++) {
429 columnCount = Math.max(columnCount, data[r].length);
430 }
431 if (columnKeys.length != columnCount) {
432 throw new IllegalArgumentException(
433 "The number of column keys does not match the number of "
434 + "columns in the data array."
435 );
436 }
437
438 // now do the work...
439 DefaultCategoryDataset result = new DefaultCategoryDataset();
440 for (int r = 0; r < data.length; r++) {
441 Comparable rowKey = rowKeys[r];
442 for (int c = 0; c < data[r].length; c++) {
443 Comparable columnKey = columnKeys[c];
444 result.addValue(new Double(data[r][c]), rowKey, columnKey);
445 }
446 }
447 return result;
448
449 }
450
451 /**
452 * Creates a {@link CategoryDataset} by copying the data from the supplied
453 * {@link KeyedValues} instance.
454 *
455 * @param rowKey the row key (<code>null</code> not permitted).
456 * @param rowData the row data (<code>null</code> not permitted).
457 *
458 * @return A dataset.
459 */
460 public static CategoryDataset createCategoryDataset(Comparable rowKey,
461 KeyedValues rowData) {
462
463 if (rowKey == null) {
464 throw new IllegalArgumentException("Null 'rowKey' argument.");
465 }
466 if (rowData == null) {
467 throw new IllegalArgumentException("Null 'rowData' argument.");
468 }
469 DefaultCategoryDataset result = new DefaultCategoryDataset();
470 for (int i = 0; i < rowData.getItemCount(); i++) {
471 result.addValue(rowData.getValue(i), rowKey, rowData.getKey(i));
472 }
473 return result;
474
475 }
476
477 /**
478 * Creates an {@link XYDataset} by sampling the specified function over a
479 * fixed range.
480 *
481 * @param f the function (<code>null</code> not permitted).
482 * @param start the start value for the range.
483 * @param end the end value for the range.
484 * @param samples the number of sample points (must be > 1).
485 * @param seriesKey the key to give the resulting series
486 * (<code>null</code> not permitted).
487 *
488 * @return A dataset.
489 */
490 public static XYDataset sampleFunction2D(Function2D f,
491 double start,
492 double end,
493 int samples,
494 Comparable seriesKey) {
495
496 if (f == null) {
497 throw new IllegalArgumentException("Null 'f' argument.");
498 }
499 if (seriesKey == null) {
500 throw new IllegalArgumentException("Null 'seriesKey' argument.");
501 }
502 if (start >= end) {
503 throw new IllegalArgumentException("Requires 'start' < 'end'.");
504 }
505 if (samples < 2) {
506 throw new IllegalArgumentException("Requires 'samples' > 1");
507 }
508
509 XYSeries series = new XYSeries(seriesKey);
510 double step = (end - start) / samples;
511 for (int i = 0; i <= samples; i++) {
512 double x = start + (step * i);
513 series.add(x, f.getValue(x));
514 }
515 XYSeriesCollection collection = new XYSeriesCollection(series);
516 return collection;
517
518 }
519
520 /**
521 * Returns <code>true</code> if the dataset is empty (or <code>null</code>),
522 * and <code>false</code> otherwise.
523 *
524 * @param dataset the dataset (<code>null</code> permitted).
525 *
526 * @return A boolean.
527 */
528 public static boolean isEmptyOrNull(PieDataset dataset) {
529
530 if (dataset == null) {
531 return true;
532 }
533
534 int itemCount = dataset.getItemCount();
535 if (itemCount == 0) {
536 return true;
537 }
538
539 for (int item = 0; item < itemCount; item++) {
540 Number y = dataset.getValue(item);
541 if (y != null) {
542 double yy = y.doubleValue();
543 if (yy > 0.0) {
544 return false;
545 }
546 }
547 }
548
549 return true;
550
551 }
552
553 /**
554 * Returns <code>true</code> if the dataset is empty (or <code>null</code>),
555 * and <code>false</code> otherwise.
556 *
557 * @param dataset the dataset (<code>null</code> permitted).
558 *
559 * @return A boolean.
560 */
561 public static boolean isEmptyOrNull(CategoryDataset dataset) {
562
563 if (dataset == null) {
564 return true;
565 }
566
567 int rowCount = dataset.getRowCount();
568 int columnCount = dataset.getColumnCount();
569 if (rowCount == 0 || columnCount == 0) {
570 return true;
571 }
572
573 for (int r = 0; r < rowCount; r++) {
574 for (int c = 0; c < columnCount; c++) {
575 if (dataset.getValue(r, c) != null) {
576 return false;
577 }
578
579 }
580 }
581
582 return true;
583
584 }
585
586 /**
587 * Returns <code>true</code> if the dataset is empty (or <code>null</code>),
588 * and <code>false</code> otherwise.
589 *
590 * @param dataset the dataset (<code>null</code> permitted).
591 *
592 * @return A boolean.
593 */
594 public static boolean isEmptyOrNull(XYDataset dataset) {
595
596 boolean result = true;
597
598 if (dataset != null) {
599 for (int s = 0; s < dataset.getSeriesCount(); s++) {
600 if (dataset.getItemCount(s) > 0) {
601 result = false;
602 continue;
603 }
604 }
605 }
606
607 return result;
608
609 }
610
611 /**
612 * Returns the range of values in the domain (x-values) of a dataset.
613 *
614 * @param dataset the dataset (<code>null</code> not permitted).
615 *
616 * @return The range of values (possibly <code>null</code>).
617 */
618 public static Range findDomainBounds(XYDataset dataset) {
619 return findDomainBounds(dataset, true);
620 }
621
622 /**
623 * Returns the range of values in the domain (x-values) of a dataset.
624 *
625 * @param dataset the dataset (<code>null</code> not permitted).
626 * @param includeInterval determines whether or not the x-interval is taken
627 * into account (only applies if the dataset is an
628 * {@link IntervalXYDataset}).
629 *
630 * @return The range of values (possibly <code>null</code>).
631 */
632 public static Range findDomainBounds(XYDataset dataset,
633 boolean includeInterval) {
634
635 if (dataset == null) {
636 throw new IllegalArgumentException("Null 'dataset' argument.");
637 }
638
639 Range result = null;
640 // if the dataset implements DomainInfo, life is easier
641 if (dataset instanceof DomainInfo) {
642 DomainInfo info = (DomainInfo) dataset;
643 result = info.getDomainBounds(includeInterval);
644 }
645 else {
646 result = iterateDomainBounds(dataset, includeInterval);
647 }
648 return result;
649
650 }
651
652 /**
653 * Iterates over the items in an {@link XYDataset} to find
654 * the range of x-values.
655 *
656 * @param dataset the dataset (<code>null</code> not permitted).
657 *
658 * @return The range (possibly <code>null</code>).
659 */
660 public static Range iterateDomainBounds(XYDataset dataset) {
661 return iterateDomainBounds(dataset, true);
662 }
663
664 /**
665 * Iterates over the items in an {@link XYDataset} to find
666 * the range of x-values.
667 *
668 * @param dataset the dataset (<code>null</code> not permitted).
669 * @param includeInterval a flag that determines, for an IntervalXYDataset,
670 * whether the x-interval or just the x-value is
671 * used to determine the overall range.
672 *
673 * @return The range (possibly <code>null</code>).
674 */
675 public static Range iterateDomainBounds(XYDataset dataset,
676 boolean includeInterval) {
677 if (dataset == null) {
678 throw new IllegalArgumentException("Null 'dataset' argument.");
679 }
680 double minimum = Double.POSITIVE_INFINITY;
681 double maximum = Double.NEGATIVE_INFINITY;
682 int seriesCount = dataset.getSeriesCount();
683 double lvalue;
684 double uvalue;
685 if (includeInterval && dataset instanceof IntervalXYDataset) {
686 IntervalXYDataset intervalXYData = (IntervalXYDataset) dataset;
687 for (int series = 0; series < seriesCount; series++) {
688 int itemCount = dataset.getItemCount(series);
689 for (int item = 0; item < itemCount; item++) {
690 lvalue = intervalXYData.getStartXValue(series, item);
691 uvalue = intervalXYData.getEndXValue(series, item);
692 minimum = Math.min(minimum, lvalue);
693 maximum = Math.max(maximum, uvalue);
694 }
695 }
696 }
697 else {
698 for (int series = 0; series < seriesCount; series++) {
699 int itemCount = dataset.getItemCount(series);
700 for (int item = 0; item < itemCount; item++) {
701 lvalue = dataset.getXValue(series, item);
702 uvalue = lvalue;
703 minimum = Math.min(minimum, lvalue);
704 maximum = Math.max(maximum, uvalue);
705 }
706 }
707 }
708 if (minimum > maximum) {
709 return null;
710 }
711 else {
712 return new Range(minimum, maximum);
713 }
714 }
715
716 /**
717 * Returns the range of values in the range for the dataset.
718 *
719 * @param dataset the dataset (<code>null</code> not permitted).
720 *
721 * @return The range (possibly <code>null</code>).
722 */
723 public static Range findRangeBounds(CategoryDataset dataset) {
724 return findRangeBounds(dataset, true);
725 }
726
727 /**
728 * Returns the range of values in the range for the dataset.
729 *
730 * @param dataset the dataset (<code>null</code> not permitted).
731 * @param includeInterval a flag that determines whether or not the
732 * y-interval is taken into account.
733 *
734 * @return The range (possibly <code>null</code>).
735 */
736 public static Range findRangeBounds(CategoryDataset dataset,
737 boolean includeInterval) {
738 if (dataset == null) {
739 throw new IllegalArgumentException("Null 'dataset' argument.");
740 }
741 Range result = null;
742 if (dataset instanceof RangeInfo) {
743 RangeInfo info = (RangeInfo) dataset;
744 result = info.getRangeBounds(includeInterval);
745 }
746 else {
747 result = iterateCategoryRangeBounds(dataset, includeInterval);
748 }
749 return result;
750 }
751
752 /**
753 * Returns the range of values in the range for the dataset. This method
754 * is the partner for the {@link #findDomainBounds(XYDataset)} method.
755 *
756 * @param dataset the dataset (<code>null</code> not permitted).
757 *
758 * @return The range (possibly <code>null</code>).
759 */
760 public static Range findRangeBounds(XYDataset dataset) {
761 return findRangeBounds(dataset, true);
762 }
763
764 /**
765 * Returns the range of values in the range for the dataset. This method
766 * is the partner for the {@link #findDomainBounds(XYDataset)} method.
767 *
768 * @param dataset the dataset (<code>null</code> not permitted).
769 * @param includeInterval a flag that determines whether or not the
770 * y-interval is taken into account.
771 *
772 *
773 * @return The range (possibly <code>null</code>).
774 */
775 public static Range findRangeBounds(XYDataset dataset,
776 boolean includeInterval) {
777 if (dataset == null) {
778 throw new IllegalArgumentException("Null 'dataset' argument.");
779 }
780 Range result = null;
781 if (dataset instanceof RangeInfo) {
782 RangeInfo info = (RangeInfo) dataset;
783 result = info.getRangeBounds(includeInterval);
784 }
785 else {
786 result = iterateXYRangeBounds(dataset);
787 }
788 return result;
789 }
790
791 /**
792 * Iterates over the data item of the category dataset to find
793 * the range bounds.
794 *
795 * @param dataset the dataset (<code>null</code> not permitted).
796 * @param includeInterval a flag that determines whether or not the
797 * y-interval is taken into account.
798 *
799 * @return The range (possibly <code>null</code>).
800 */
801 public static Range iterateCategoryRangeBounds(CategoryDataset dataset,
802 boolean includeInterval) {
803 double minimum = Double.POSITIVE_INFINITY;
804 double maximum = Double.NEGATIVE_INFINITY;
805 boolean interval = includeInterval
806 && dataset instanceof IntervalCategoryDataset;
807 int rowCount = dataset.getRowCount();
808 int columnCount = dataset.getColumnCount();
809 for (int row = 0; row < rowCount; row++) {
810 for (int column = 0; column < columnCount; column++) {
811 Number lvalue;
812 Number uvalue;
813 if (interval) {
814 IntervalCategoryDataset icd
815 = (IntervalCategoryDataset) dataset;
816 lvalue = icd.getStartValue(row, column);
817 uvalue = icd.getEndValue(row, column);
818 }
819 else {
820 lvalue = dataset.getValue(row, column);
821 uvalue = lvalue;
822 }
823 if (lvalue != null) {
824 minimum = Math.min(minimum, lvalue.doubleValue());
825 }
826 if (uvalue != null) {
827 maximum = Math.max(maximum, uvalue.doubleValue());
828 }
829 }
830 }
831 if (minimum == Double.POSITIVE_INFINITY) {
832 return null;
833 }
834 else {
835 return new Range(minimum, maximum);
836 }
837 }
838
839 /**
840 * Iterates over the data item of the xy dataset to find
841 * the range bounds.
842 *
843 * @param dataset the dataset (<code>null</code> not permitted).
844 *
845 * @return The range (possibly <code>null</code>).
846 */
847 public static Range iterateXYRangeBounds(XYDataset dataset) {
848 double minimum = Double.POSITIVE_INFINITY;
849 double maximum = Double.NEGATIVE_INFINITY;
850 int seriesCount = dataset.getSeriesCount();
851 for (int series = 0; series < seriesCount; series++) {
852 int itemCount = dataset.getItemCount(series);
853 for (int item = 0; item < itemCount; item++) {
854 double lvalue;
855 double uvalue;
856 if (dataset instanceof IntervalXYDataset) {
857 IntervalXYDataset intervalXYData
858 = (IntervalXYDataset) dataset;
859 lvalue = intervalXYData.getStartYValue(series, item);
860 uvalue = intervalXYData.getEndYValue(series, item);
861 }
862 else if (dataset instanceof OHLCDataset) {
863 OHLCDataset highLowData = (OHLCDataset) dataset;
864 lvalue = highLowData.getLowValue(series, item);
865 uvalue = highLowData.getHighValue(series, item);
866 }
867 else {
868 lvalue = dataset.getYValue(series, item);
869 uvalue = lvalue;
870 }
871 if (!Double.isNaN(lvalue)) {
872 minimum = Math.min(minimum, lvalue);
873 }
874 if (!Double.isNaN(uvalue)) {
875 maximum = Math.max(maximum, uvalue);
876 }
877 }
878 }
879 if (minimum == Double.POSITIVE_INFINITY) {
880 return null;
881 }
882 else {
883 return new Range(minimum, maximum);
884 }
885 }
886
887 /**
888 * Finds the minimum domain (or X) value for the specified dataset. This
889 * is easy if the dataset implements the {@link DomainInfo} interface (a
890 * good idea if there is an efficient way to determine the minimum value).
891 * Otherwise, it involves iterating over the entire data-set.
892 * <p>
893 * Returns <code>null</code> if all the data values in the dataset are
894 * <code>null</code>.
895 *
896 * @param dataset the dataset (<code>null</code> not permitted).
897 *
898 * @return The minimum value (possibly <code>null</code>).
899 */
900 public static Number findMinimumDomainValue(XYDataset dataset) {
901 if (dataset == null) {
902 throw new IllegalArgumentException("Null 'dataset' argument.");
903 }
904 Number result = null;
905 // if the dataset implements DomainInfo, life is easy
906 if (dataset instanceof DomainInfo) {
907 DomainInfo info = (DomainInfo) dataset;
908 return new Double(info.getDomainLowerBound(true));
909 }
910 else {
911 double minimum = Double.POSITIVE_INFINITY;
912 int seriesCount = dataset.getSeriesCount();
913 for (int series = 0; series < seriesCount; series++) {
914 int itemCount = dataset.getItemCount(series);
915 for (int item = 0; item < itemCount; item++) {
916
917 double value;
918 if (dataset instanceof IntervalXYDataset) {
919 IntervalXYDataset intervalXYData
920 = (IntervalXYDataset) dataset;
921 value = intervalXYData.getStartXValue(series, item);
922 }
923 else {
924 value = dataset.getXValue(series, item);
925 }
926 if (!Double.isNaN(value)) {
927 minimum = Math.min(minimum, value);
928 }
929
930 }
931 }
932 if (minimum == Double.POSITIVE_INFINITY) {
933 result = null;
934 }
935 else {
936 result = new Double(minimum);
937 }
938 }
939
940 return result;
941 }
942
943 /**
944 * Returns the maximum domain value for the specified dataset. This is
945 * easy if the dataset implements the {@link DomainInfo} interface (a good
946 * idea if there is an efficient way to determine the maximum value).
947 * Otherwise, it involves iterating over the entire data-set. Returns
948 * <code>null</code> if all the data values in the dataset are
949 * <code>null</code>.
950 *
951 * @param dataset the dataset (<code>null</code> not permitted).
952 *
953 * @return The maximum value (possibly <code>null</code>).
954 */
955 public static Number findMaximumDomainValue(XYDataset dataset) {
956 if (dataset == null) {
957 throw new IllegalArgumentException("Null 'dataset' argument.");
958 }
959 Number result = null;
960 // if the dataset implements DomainInfo, life is easy
961 if (dataset instanceof DomainInfo) {
962 DomainInfo info = (DomainInfo) dataset;
963 return new Double(info.getDomainUpperBound(true));
964 }
965
966 // hasn't implemented DomainInfo, so iterate...
967 else {
968 double maximum = Double.NEGATIVE_INFINITY;
969 int seriesCount = dataset.getSeriesCount();
970 for (int series = 0; series < seriesCount; series++) {
971 int itemCount = dataset.getItemCount(series);
972 for (int item = 0; item < itemCount; item++) {
973
974 double value;
975 if (dataset instanceof IntervalXYDataset) {
976 IntervalXYDataset intervalXYData
977 = (IntervalXYDataset) dataset;
978 value = intervalXYData.getEndXValue(series, item);
979 }
980 else {
981 value = dataset.getXValue(series, item);
982 }
983 if (!Double.isNaN(value)) {
984 maximum = Math.max(maximum, value);
985 }
986 }
987 }
988 if (maximum == Double.NEGATIVE_INFINITY) {
989 result = null;
990 }
991 else {
992 result = new Double(maximum);
993 }
994
995 }
996
997 return result;
998 }
999
1000 /**
1001 * Returns the minimum range value for the specified dataset. This is
1002 * easy if the dataset implements the {@link RangeInfo} interface (a good
1003 * idea if there is an efficient way to determine the minimum value).
1004 * Otherwise, it involves iterating over the entire data-set. Returns
1005 * <code>null</code> if all the data values in the dataset are
1006 * <code>null</code>.
1007 *
1008 * @param dataset the dataset (<code>null</code> not permitted).
1009 *
1010 * @return The minimum value (possibly <code>null</code>).
1011 */
1012 public static Number findMinimumRangeValue(CategoryDataset dataset) {
1013
1014 // check parameters...
1015 if (dataset == null) {
1016 throw new IllegalArgumentException("Null 'dataset' argument.");
1017 }
1018
1019 // work out the minimum value...
1020 if (dataset instanceof RangeInfo) {
1021 RangeInfo info = (RangeInfo) dataset;
1022 return new Double(info.getRangeLowerBound(true));
1023 }
1024
1025 // hasn't implemented RangeInfo, so we'll have to iterate...
1026 else {
1027 double minimum = Double.POSITIVE_INFINITY;
1028 int seriesCount = dataset.getRowCount();
1029 int itemCount = dataset.getColumnCount();
1030 for (int series = 0; series < seriesCount; series++) {
1031 for (int item = 0; item < itemCount; item++) {
1032 Number value;
1033 if (dataset instanceof IntervalCategoryDataset) {
1034 IntervalCategoryDataset icd
1035 = (IntervalCategoryDataset) dataset;
1036 value = icd.getStartValue(series, item);
1037 }
1038 else {
1039 value = dataset.getValue(series, item);
1040 }
1041 if (value != null) {
1042 minimum = Math.min(minimum, value.doubleValue());
1043 }
1044 }
1045 }
1046 if (minimum == Double.POSITIVE_INFINITY) {
1047 return null;
1048 }
1049 else {
1050 return new Double(minimum);
1051 }
1052
1053 }
1054
1055 }
1056
1057 /**
1058 * Returns the minimum range value for the specified dataset. This is
1059 * easy if the dataset implements the {@link RangeInfo} interface (a good
1060 * idea if there is an efficient way to determine the minimum value).
1061 * Otherwise, it involves iterating over the entire data-set. Returns
1062 * <code>null</code> if all the data values in the dataset are
1063 * <code>null</code>.
1064 *
1065 * @param dataset the dataset (<code>null</code> not permitted).
1066 *
1067 * @return The minimum value (possibly <code>null</code>).
1068 */
1069 public static Number findMinimumRangeValue(XYDataset dataset) {
1070
1071 if (dataset == null) {
1072 throw new IllegalArgumentException("Null 'dataset' argument.");
1073 }
1074
1075 // work out the minimum value...
1076 if (dataset instanceof RangeInfo) {
1077 RangeInfo info = (RangeInfo) dataset;
1078 return new Double(info.getRangeLowerBound(true));
1079 }
1080
1081 // hasn't implemented RangeInfo, so we'll have to iterate...
1082 else {
1083 double minimum = Double.POSITIVE_INFINITY;
1084 int seriesCount = dataset.getSeriesCount();
1085 for (int series = 0; series < seriesCount; series++) {
1086 int itemCount = dataset.getItemCount(series);
1087 for (int item = 0; item < itemCount; item++) {
1088
1089 double value;
1090 if (dataset instanceof IntervalXYDataset) {
1091 IntervalXYDataset intervalXYData
1092 = (IntervalXYDataset) dataset;
1093 value = intervalXYData.getStartYValue(series, item);
1094 }
1095 else if (dataset instanceof OHLCDataset) {
1096 OHLCDataset highLowData = (OHLCDataset) dataset;
1097 value = highLowData.getLowValue(series, item);
1098 }
1099 else {
1100 value = dataset.getYValue(series, item);
1101 }
1102 if (!Double.isNaN(value)) {
1103 minimum = Math.min(minimum, value);
1104 }
1105
1106 }
1107 }
1108 if (minimum == Double.POSITIVE_INFINITY) {
1109 return null;
1110 }
1111 else {
1112 return new Double(minimum);
1113 }
1114
1115 }
1116
1117 }
1118
1119 /**
1120 * Returns the maximum range value for the specified dataset. This is easy
1121 * if the dataset implements the {@link RangeInfo} interface (a good idea
1122 * if there is an efficient way to determine the maximum value).
1123 * Otherwise, it involves iterating over the entire data-set. Returns
1124 * <code>null</code> if all the data values are <code>null</code>.
1125 *
1126 * @param dataset the dataset (<code>null</code> not permitted).
1127 *
1128 * @return The maximum value (possibly <code>null</code>).
1129 */
1130 public static Number findMaximumRangeValue(CategoryDataset dataset) {
1131
1132 if (dataset == null) {
1133 throw new IllegalArgumentException("Null 'dataset' argument.");
1134 }
1135
1136 // work out the minimum value...
1137 if (dataset instanceof RangeInfo) {
1138 RangeInfo info = (RangeInfo) dataset;
1139 return new Double(info.getRangeUpperBound(true));
1140 }
1141
1142 // hasn't implemented RangeInfo, so we'll have to iterate...
1143 else {
1144
1145 double maximum = Double.NEGATIVE_INFINITY;
1146 int seriesCount = dataset.getRowCount();
1147 int itemCount = dataset.getColumnCount();
1148 for (int series = 0; series < seriesCount; series++) {
1149 for (int item = 0; item < itemCount; item++) {
1150 Number value;
1151 if (dataset instanceof IntervalCategoryDataset) {
1152 IntervalCategoryDataset icd
1153 = (IntervalCategoryDataset) dataset;
1154 value = icd.getEndValue(series, item);
1155 }
1156 else {
1157 value = dataset.getValue(series, item);
1158 }
1159 if (value != null) {
1160 maximum = Math.max(maximum, value.doubleValue());
1161 }
1162 }
1163 }
1164 if (maximum == Double.NEGATIVE_INFINITY) {
1165 return null;
1166 }
1167 else {
1168 return new Double(maximum);
1169 }
1170
1171 }
1172
1173 }
1174
1175 /**
1176 * Returns the maximum range value for the specified dataset. This is
1177 * easy if the dataset implements the {@link RangeInfo} interface (a good
1178 * idea if there is an efficient way to determine the maximum value).
1179 * Otherwise, it involves iterating over the entire data-set. Returns
1180 * <code>null</code> if all the data values are <code>null</code>.
1181 *
1182 * @param dataset the dataset (<code>null</code> not permitted).
1183 *
1184 * @return The maximum value (possibly <code>null</code>).
1185 */
1186 public static Number findMaximumRangeValue(XYDataset dataset) {
1187
1188 if (dataset == null) {
1189 throw new IllegalArgumentException("Null 'dataset' argument.");
1190 }
1191
1192 // work out the minimum value...
1193 if (dataset instanceof RangeInfo) {
1194 RangeInfo info = (RangeInfo) dataset;
1195 return new Double(info.getRangeUpperBound(true));
1196 }
1197
1198 // hasn't implemented RangeInfo, so we'll have to iterate...
1199 else {
1200
1201 double maximum = Double.NEGATIVE_INFINITY;
1202 int seriesCount = dataset.getSeriesCount();
1203 for (int series = 0; series < seriesCount; series++) {
1204 int itemCount = dataset.getItemCount(series);
1205 for (int item = 0; item < itemCount; item++) {
1206 double value;
1207 if (dataset instanceof IntervalXYDataset) {
1208 IntervalXYDataset intervalXYData
1209 = (IntervalXYDataset) dataset;
1210 value = intervalXYData.getEndYValue(series, item);
1211 }
1212 else if (dataset instanceof OHLCDataset) {
1213 OHLCDataset highLowData = (OHLCDataset) dataset;
1214 value = highLowData.getHighValue(series, item);
1215 }
1216 else {
1217 value = dataset.getYValue(series, item);
1218 }
1219 if (!Double.isNaN(value)) {
1220 maximum = Math.max(maximum, value);
1221 }
1222 }
1223 }
1224 if (maximum == Double.NEGATIVE_INFINITY) {
1225 return null;
1226 }
1227 else {
1228 return new Double(maximum);
1229 }
1230
1231 }
1232
1233 }
1234
1235 /**
1236 * Returns the minimum and maximum values for the dataset's range
1237 * (y-values), assuming that the series in one category are stacked.
1238 *
1239 * @param dataset the dataset (<code>null</code> not permitted).
1240 *
1241 * @return The range (<code>null</code> if the dataset contains no values).
1242 */
1243 public static Range findStackedRangeBounds(CategoryDataset dataset) {
1244 return findStackedRangeBounds(dataset, 0.0);
1245 }
1246
1247 /**
1248 * Returns the minimum and maximum values for the dataset's range
1249 * (y-values), assuming that the series in one category are stacked.
1250 *
1251 * @param dataset the dataset (<code>null</code> not permitted).
1252 * @param base the base value for the bars.
1253 *
1254 * @return The range (<code>null</code> if the dataset contains no values).
1255 */
1256 public static Range findStackedRangeBounds(CategoryDataset dataset,
1257 double base) {
1258 if (dataset == null) {
1259 throw new IllegalArgumentException("Null 'dataset' argument.");
1260 }
1261 Range result = null;
1262 double minimum = Double.POSITIVE_INFINITY;
1263 double maximum = Double.NEGATIVE_INFINITY;
1264 int categoryCount = dataset.getColumnCount();
1265 for (int item = 0; item < categoryCount; item++) {
1266 double positive = base;
1267 double negative = base;
1268 int seriesCount = dataset.getRowCount();
1269 for (int series = 0; series < seriesCount; series++) {
1270 Number number = dataset.getValue(series, item);
1271 if (number != null) {
1272 double value = number.doubleValue();
1273 if (value > 0.0) {
1274 positive = positive + value;
1275 }
1276 if (value < 0.0) {
1277 negative = negative + value;
1278 // '+', remember value is negative
1279 }
1280 }
1281 }
1282 minimum = Math.min(minimum, negative);
1283 maximum = Math.max(maximum, positive);
1284 }
1285 if (minimum <= maximum) {
1286 result = new Range(minimum, maximum);
1287 }
1288 return result;
1289
1290 }
1291
1292 /**
1293 * Returns the minimum and maximum values for the dataset's range
1294 * (y-values), assuming that the series in one category are stacked.
1295 *
1296 * @param dataset the dataset.
1297 * @param map a structure that maps series to groups.
1298 *
1299 * @return The value range (<code>null</code> if the dataset contains no
1300 * values).
1301 */
1302 public static Range findStackedRangeBounds(CategoryDataset dataset,
1303 KeyToGroupMap map) {
1304
1305 Range result = null;
1306 if (dataset != null) {
1307
1308 // create an array holding the group indices...
1309 int[] groupIndex = new int[dataset.getRowCount()];
1310 for (int i = 0; i < dataset.getRowCount(); i++) {
1311 groupIndex[i] = map.getGroupIndex(
1312 map.getGroup(dataset.getRowKey(i))
1313 );
1314 }
1315
1316 // minimum and maximum for each group...
1317 int groupCount = map.getGroupCount();
1318 double[] minimum = new double[groupCount];
1319 double[] maximum = new double[groupCount];
1320
1321 int categoryCount = dataset.getColumnCount();
1322 for (int item = 0; item < categoryCount; item++) {
1323 double[] positive = new double[groupCount];
1324 double[] negative = new double[groupCount];
1325 int seriesCount = dataset.getRowCount();
1326 for (int series = 0; series < seriesCount; series++) {
1327 Number number = dataset.getValue(series, item);
1328 if (number != null) {
1329 double value = number.doubleValue();
1330 if (value > 0.0) {
1331 positive[groupIndex[series]]
1332 = positive[groupIndex[series]] + value;
1333 }
1334 if (value < 0.0) {
1335 negative[groupIndex[series]]
1336 = negative[groupIndex[series]] + value;
1337 // '+', remember value is negative
1338 }
1339 }
1340 }
1341 for (int g = 0; g < groupCount; g++) {
1342 minimum[g] = Math.min(minimum[g], negative[g]);
1343 maximum[g] = Math.max(maximum[g], positive[g]);
1344 }
1345 }
1346 for (int j = 0; j < groupCount; j++) {
1347 result = Range.combine(
1348 result, new Range(minimum[j], maximum[j])