001/* =========================================================== 002 * JFreeChart : a free chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C) Copyright 2000-2022, by David Gilbert and Contributors. 006 * 007 * Project Info: http://www.jfree.org/jfreechart/index.html 008 * 009 * This library is free software; you can redistribute it and/or modify it 010 * under the terms of the GNU Lesser General Public License as published by 011 * the Free Software Foundation; either version 2.1 of the License, or 012 * (at your option) any later version. 013 * 014 * This library is distributed in the hope that it will be useful, but 015 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 016 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public 017 * License for more details. 018 * 019 * You should have received a copy of the GNU Lesser General Public 020 * License along with this library; if not, write to the Free Software 021 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, 022 * USA. 023 * 024 * [Oracle and Java are registered trademarks of Oracle and/or its affiliates. 025 * Other names may be trademarks of their respective owners.] 026 * 027 * ----------------------- 028 * TimeTableXYDataset.java 029 * ----------------------- 030 * (C) Copyright 2004-2021, by Andreas Schroeder and Contributors. 031 * 032 * Original Author: Andreas Schroeder; 033 * Contributor(s): David Gilbert; 034 * Rob Eden; 035 * 036 */ 037 038package org.jfree.data.time; 039 040import java.util.Calendar; 041import java.util.List; 042import java.util.Locale; 043import java.util.Objects; 044import java.util.TimeZone; 045import org.jfree.chart.internal.Args; 046import org.jfree.chart.api.PublicCloneable; 047 048import org.jfree.data.DefaultKeyedValues2D; 049import org.jfree.data.DomainInfo; 050import org.jfree.data.Range; 051import org.jfree.data.general.DatasetChangeEvent; 052import org.jfree.data.xy.AbstractIntervalXYDataset; 053import org.jfree.data.xy.IntervalXYDataset; 054import org.jfree.data.xy.TableXYDataset; 055 056/** 057 * A dataset for regular time periods that implements the 058 * {@link TableXYDataset} interface. Note that the {@link TableXYDataset} 059 * interface requires all series to share the same set of x-values. When 060 * adding a new item {@code (x, y)} to one series, all other series 061 * automatically get a new item {@code (x, null)} unless a non-null item 062 * has already been specified. 063 * 064 * @see org.jfree.data.xy.TableXYDataset 065 */ 066public class TimeTableXYDataset extends AbstractIntervalXYDataset 067 implements Cloneable, PublicCloneable, IntervalXYDataset, DomainInfo, 068 TableXYDataset { 069 070 /** 071 * The data structure to store the values. Each column represents 072 * a series (elsewhere in JFreeChart rows are typically used for series, 073 * but it doesn't matter that much since this data structure is private 074 * and symmetrical anyway), each row contains values for the same 075 * {@link RegularTimePeriod} (the rows are sorted into ascending order). 076 */ 077 private DefaultKeyedValues2D values; 078 079 /** 080 * A flag that indicates that the domain is 'points in time'. If this flag 081 * is true, only the x-value (and not the x-interval) is used to determine 082 * the range of values in the domain. 083 */ 084 private boolean domainIsPointsInTime; 085 086 /** 087 * The point within each time period that is used for the X value when this 088 * collection is used as an {@link org.jfree.data.xy.XYDataset}. This can 089 * be the start, middle or end of the time period. 090 */ 091 private TimePeriodAnchor xPosition; 092 093 /** A working calendar (to recycle) */ 094 private Calendar workingCalendar; 095 096 /** 097 * Creates a new dataset. 098 */ 099 public TimeTableXYDataset() { 100 // defer argument checking 101 this(TimeZone.getDefault(), Locale.getDefault()); 102 } 103 104 /** 105 * Creates a new dataset with the given time zone. 106 * 107 * @param zone the time zone to use ({@code null} not permitted). 108 */ 109 public TimeTableXYDataset(TimeZone zone) { 110 // defer argument checking 111 this(zone, Locale.getDefault()); 112 } 113 114 /** 115 * Creates a new dataset with the given time zone and locale. 116 * 117 * @param zone the time zone to use ({@code null} not permitted). 118 * @param locale the locale to use ({@code null} not permitted). 119 */ 120 public TimeTableXYDataset(TimeZone zone, Locale locale) { 121 Args.nullNotPermitted(zone, "zone"); 122 Args.nullNotPermitted(locale, "locale"); 123 this.values = new DefaultKeyedValues2D(true); 124 this.workingCalendar = Calendar.getInstance(zone, locale); 125 this.xPosition = TimePeriodAnchor.START; 126 } 127 128 /** 129 * Returns a flag that controls whether the domain is treated as 'points in 130 * time'. 131 * <P> 132 * This flag is used when determining the max and min values for the domain. 133 * If true, then only the x-values are considered for the max and min 134 * values. If false, then the start and end x-values will also be taken 135 * into consideration. 136 * 137 * @return The flag. 138 * 139 * @see #setDomainIsPointsInTime(boolean) 140 */ 141 public boolean getDomainIsPointsInTime() { 142 return this.domainIsPointsInTime; 143 } 144 145 /** 146 * Sets a flag that controls whether the domain is treated as 'points in 147 * time', or time periods. A {@link DatasetChangeEvent} is sent to all 148 * registered listeners. 149 * 150 * @param flag the new value of the flag. 151 * 152 * @see #getDomainIsPointsInTime() 153 */ 154 public void setDomainIsPointsInTime(boolean flag) { 155 this.domainIsPointsInTime = flag; 156 notifyListeners(new DatasetChangeEvent(this, this)); 157 } 158 159 /** 160 * Returns the position within each time period that is used for the X 161 * value. 162 * 163 * @return The anchor position (never {@code null}). 164 * 165 * @see #setXPosition(TimePeriodAnchor) 166 */ 167 public TimePeriodAnchor getXPosition() { 168 return this.xPosition; 169 } 170 171 /** 172 * Sets the position within each time period that is used for the X values, 173 * then sends a {@link DatasetChangeEvent} to all registered listeners. 174 * 175 * @param anchor the anchor position ({@code null} not permitted). 176 * 177 * @see #getXPosition() 178 */ 179 public void setXPosition(TimePeriodAnchor anchor) { 180 Args.nullNotPermitted(anchor, "anchor"); 181 this.xPosition = anchor; 182 notifyListeners(new DatasetChangeEvent(this, this)); 183 } 184 185 /** 186 * Adds a new data item to the dataset and sends a 187 * {@link DatasetChangeEvent} to all registered listeners. 188 * 189 * @param period the time period. 190 * @param y the value for this period. 191 * @param seriesName the name of the series to add the value. 192 * 193 * @see #remove(TimePeriod, Comparable) 194 */ 195 public void add(TimePeriod period, double y, Comparable seriesName) { 196 add(period, y, seriesName, true); 197 } 198 199 /** 200 * Adds a new data item to the dataset and, if requested, sends a 201 * {@link DatasetChangeEvent} to all registered listeners. 202 * 203 * @param period the time period ({@code null} not permitted). 204 * @param y the value for this period ({@code null} permitted). 205 * @param seriesName the name of the series to add the value 206 * ({@code null} not permitted). 207 * @param notify whether dataset listener are notified or not. 208 * 209 * @see #remove(TimePeriod, Comparable, boolean) 210 */ 211 public void add(TimePeriod period, Number y, Comparable seriesName, 212 boolean notify) { 213 // here's a quirk - the API has been defined in terms of a plain 214 // TimePeriod, which cannot make use of the timezone and locale 215 // specified in the constructor...so we only do the time zone 216 // pegging if the period is an instanceof RegularTimePeriod 217 if (period instanceof RegularTimePeriod) { 218 RegularTimePeriod p = (RegularTimePeriod) period; 219 p.peg(this.workingCalendar); 220 } 221 this.values.addValue(y, period, seriesName); 222 if (notify) { 223 fireDatasetChanged(); 224 } 225 } 226 227 /** 228 * Removes an existing data item from the dataset. 229 * 230 * @param period the (existing!) time period of the value to remove 231 * ({@code null} not permitted). 232 * @param seriesName the (existing!) series name to remove the value 233 * ({@code null} not permitted). 234 * 235 * @see #add(TimePeriod, double, Comparable) 236 */ 237 public void remove(TimePeriod period, Comparable seriesName) { 238 remove(period, seriesName, true); 239 } 240 241 /** 242 * Removes an existing data item from the dataset and, if requested, 243 * sends a {@link DatasetChangeEvent} to all registered listeners. 244 * 245 * @param period the (existing!) time period of the value to remove 246 * ({@code null} not permitted). 247 * @param seriesName the (existing!) series name to remove the value 248 * ({@code null} not permitted). 249 * @param notify whether dataset listener are notified or not. 250 * 251 * @see #add(TimePeriod, double, Comparable) 252 */ 253 public void remove(TimePeriod period, Comparable seriesName, 254 boolean notify) { 255 this.values.removeValue(period, seriesName); 256 if (notify) { 257 fireDatasetChanged(); 258 } 259 } 260 261 /** 262 * Removes all data items from the dataset and sends a 263 * {@link DatasetChangeEvent} to all registered listeners. 264 * 265 * @since 1.0.7 266 */ 267 public void clear() { 268 if (this.values.getRowCount() > 0) { 269 this.values.clear(); 270 fireDatasetChanged(); 271 } 272 } 273 274 /** 275 * Returns the time period for the specified item. Bear in mind that all 276 * series share the same set of time periods. 277 * 278 * @param item the item index (0 <= i <= {@link #getItemCount()}). 279 * 280 * @return The time period. 281 */ 282 public TimePeriod getTimePeriod(int item) { 283 return (TimePeriod) this.values.getRowKey(item); 284 } 285 286 /** 287 * Returns the number of items in ALL series. 288 * 289 * @return The item count. 290 */ 291 @Override 292 public int getItemCount() { 293 return this.values.getRowCount(); 294 } 295 296 /** 297 * Returns the number of items in a series. This is the same value 298 * that is returned by {@link #getItemCount()} since all series 299 * share the same x-values (time periods). 300 * 301 * @param series the series (zero-based index, ignored). 302 * 303 * @return The number of items within the series. 304 */ 305 @Override 306 public int getItemCount(int series) { 307 return getItemCount(); 308 } 309 310 /** 311 * Returns the number of series in the dataset. 312 * 313 * @return The series count. 314 */ 315 @Override 316 public int getSeriesCount() { 317 return this.values.getColumnCount(); 318 } 319 320 /** 321 * Returns the key for a series. 322 * 323 * @param series the series (zero-based index). 324 * 325 * @return The key for the series. 326 */ 327 @Override 328 public Comparable getSeriesKey(int series) { 329 return this.values.getColumnKey(series); 330 } 331 332 /** 333 * Returns the x-value for an item within a series. The x-values may or 334 * may not be returned in ascending order, that is up to the class 335 * implementing the interface. 336 * 337 * @param series the series (zero-based index). 338 * @param item the item (zero-based index). 339 * 340 * @return The x-value. 341 */ 342 @Override 343 public Number getX(int series, int item) { 344 return getXValue(series, item); 345 } 346 347 /** 348 * Returns the x-value (as a double primitive) for an item within a series. 349 * 350 * @param series the series index (zero-based). 351 * @param item the item index (zero-based). 352 * 353 * @return The value. 354 */ 355 @Override 356 public double getXValue(int series, int item) { 357 TimePeriod period = (TimePeriod) this.values.getRowKey(item); 358 return getXValue(period); 359 } 360 361 /** 362 * Returns the starting X value for the specified series and item. 363 * 364 * @param series the series (zero-based index). 365 * @param item the item within a series (zero-based index). 366 * 367 * @return The starting X value for the specified series and item. 368 * 369 * @see #getStartXValue(int, int) 370 */ 371 @Override 372 public Number getStartX(int series, int item) { 373 return getStartXValue(series, item); 374 } 375 376 /** 377 * Returns the start x-value (as a double primitive) for an item within 378 * a series. 379 * 380 * @param series the series index (zero-based). 381 * @param item the item index (zero-based). 382 * 383 * @return The value. 384 */ 385 @Override 386 public double getStartXValue(int series, int item) { 387 TimePeriod period = (TimePeriod) this.values.getRowKey(item); 388 return period.getStart().getTime(); 389 } 390 391 /** 392 * Returns the ending X value for the specified series and item. 393 * 394 * @param series the series (zero-based index). 395 * @param item the item within a series (zero-based index). 396 * 397 * @return The ending X value for the specified series and item. 398 * 399 * @see #getEndXValue(int, int) 400 */ 401 @Override 402 public Number getEndX(int series, int item) { 403 return getEndXValue(series, item); 404 } 405 406 /** 407 * Returns the end x-value (as a double primitive) for an item within 408 * a series. 409 * 410 * @param series the series index (zero-based). 411 * @param item the item index (zero-based). 412 * 413 * @return The value. 414 */ 415 @Override 416 public double getEndXValue(int series, int item) { 417 TimePeriod period = (TimePeriod) this.values.getRowKey(item); 418 return period.getEnd().getTime(); 419 } 420 421 /** 422 * Returns the y-value for an item within a series. 423 * 424 * @param series the series (zero-based index). 425 * @param item the item (zero-based index). 426 * 427 * @return The y-value (possibly {@code null}). 428 */ 429 @Override 430 public Number getY(int series, int item) { 431 return this.values.getValue(item, series); 432 } 433 434 /** 435 * Returns the starting Y value for the specified series and item. 436 * 437 * @param series the series (zero-based index). 438 * @param item the item within a series (zero-based index). 439 * 440 * @return The starting Y value for the specified series and item. 441 */ 442 @Override 443 public Number getStartY(int series, int item) { 444 return getY(series, item); 445 } 446 447 /** 448 * Returns the ending Y value for the specified series and item. 449 * 450 * @param series the series (zero-based index). 451 * @param item the item within a series (zero-based index). 452 * 453 * @return The ending Y value for the specified series and item. 454 */ 455 @Override 456 public Number getEndY(int series, int item) { 457 return getY(series, item); 458 } 459 460 /** 461 * Returns the x-value for a time period. 462 * 463 * @param period the time period. 464 * 465 * @return The x-value. 466 */ 467 private long getXValue(TimePeriod period) { 468 long result = 0L; 469 if (this.xPosition == TimePeriodAnchor.START) { 470 result = period.getStart().getTime(); 471 } 472 else if (this.xPosition == TimePeriodAnchor.MIDDLE) { 473 long t0 = period.getStart().getTime(); 474 long t1 = period.getEnd().getTime(); 475 result = t0 + (t1 - t0) / 2L; 476 } 477 else if (this.xPosition == TimePeriodAnchor.END) { 478 result = period.getEnd().getTime(); 479 } 480 return result; 481 } 482 483 /** 484 * Returns the minimum x-value in the dataset. 485 * 486 * @param includeInterval a flag that determines whether or not the 487 * x-interval is taken into account. 488 * 489 * @return The minimum value. 490 */ 491 @Override 492 public double getDomainLowerBound(boolean includeInterval) { 493 double result = Double.NaN; 494 Range r = getDomainBounds(includeInterval); 495 if (r != null) { 496 result = r.getLowerBound(); 497 } 498 return result; 499 } 500 501 /** 502 * Returns the maximum x-value in the dataset. 503 * 504 * @param includeInterval a flag that determines whether or not the 505 * x-interval is taken into account. 506 * 507 * @return The maximum value. 508 */ 509 @Override 510 public double getDomainUpperBound(boolean includeInterval) { 511 double result = Double.NaN; 512 Range r = getDomainBounds(includeInterval); 513 if (r != null) { 514 result = r.getUpperBound(); 515 } 516 return result; 517 } 518 519 /** 520 * Returns the range of the values in this dataset's domain. 521 * 522 * @param includeInterval a flag that controls whether or not the 523 * x-intervals are taken into account. 524 * 525 * @return The range. 526 */ 527 @Override 528 public Range getDomainBounds(boolean includeInterval) { 529 List keys = this.values.getRowKeys(); 530 if (keys.isEmpty()) { 531 return null; 532 } 533 534 TimePeriod first = (TimePeriod) keys.get(0); 535 TimePeriod last = (TimePeriod) keys.get(keys.size() - 1); 536 537 if (!includeInterval || this.domainIsPointsInTime) { 538 return new Range(getXValue(first), getXValue(last)); 539 } 540 else { 541 return new Range(first.getStart().getTime(), 542 last.getEnd().getTime()); 543 } 544 } 545 546 /** 547 * Tests this dataset for equality with an arbitrary object. 548 * 549 * @param obj the object ({@code null} permitted). 550 * 551 * @return A boolean. 552 */ 553 @Override 554 public boolean equals(Object obj) { 555 if (obj == this) { 556 return true; 557 } 558 if (!(obj instanceof TimeTableXYDataset)) { 559 return false; 560 } 561 TimeTableXYDataset that = (TimeTableXYDataset) obj; 562 if (this.domainIsPointsInTime != that.domainIsPointsInTime) { 563 return false; 564 } 565 if (this.xPosition != that.xPosition) { 566 return false; 567 } 568 if (!this.workingCalendar.getTimeZone().equals( 569 that.workingCalendar.getTimeZone()) 570 ) { 571 return false; 572 } 573 if (!this.values.equals(that.values)) { 574 return false; 575 } 576 return true; 577 } 578 579 @Override 580 public int hashCode() 581 { 582 int hash = 7; 583 hash = 19 * hash + Objects.hashCode( this.values ); 584 hash = 19 * hash + ( this.domainIsPointsInTime ? 1 : 0 ); 585 hash = 19 * hash + Objects.hashCode( this.xPosition ); 586 hash = 19 * hash + Objects.hashCode( this.workingCalendar ); 587 return hash; 588 } 589 590 /** 591 * Returns a clone of this dataset. 592 * 593 * @return A clone. 594 * 595 * @throws CloneNotSupportedException if the dataset cannot be cloned. 596 */ 597 @Override 598 public Object clone() throws CloneNotSupportedException { 599 TimeTableXYDataset clone = (TimeTableXYDataset) super.clone(); 600 clone.values = (DefaultKeyedValues2D) this.values.clone(); 601 clone.workingCalendar = (Calendar) this.workingCalendar.clone(); 602 return clone; 603 } 604 605}