1. /* ===========================================================
  2. * JFreeChart : a free chart library for the Java(tm) platform
  3. * ===========================================================
  4. *
  5. * (C) Copyright 2000-2005, by Object Refinery Limited and Contributors.
  6. *
  7. * Project Info: http://www.jfree.org/jfreechart/index.html
  8. *
  9. * This library is free software; you can redistribute it and/or modify it
  10. * under the terms of the GNU Lesser General Public License as published by
  11. * the Free Software Foundation; either version 2.1 of the License, or
  12. * (at your option) any later version.
  13. *
  14. * This library is distributed in the hope that it will be useful, but
  15. * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
  16. * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
  17. * License for more details.
  18. *
  19. * You should have received a copy of the GNU Lesser General Public License
  20. * along with this library; if not, write to the Free Software Foundation,
  21. * Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
  22. *
  23. * [Java is a trademark or registered trademark of Sun Microsystems, Inc.
  24. * in the United States and other countries.]
  25. *
  26. * --------------------------
  27. * DefaultTableXYDataset.java
  28. * --------------------------
  29. * (C) Copyright 2003-2005, by Richard Atkinson and Contributors.
  30. *
  31. * Original Author: Richard Atkinson;
  32. * Contributor(s): Jody Brownell;
  33. * David Gilbert (for Object Refinery Limited);
  34. * Andreas Schroeder;
  35. *
  36. * $Id: DefaultTableXYDataset.java,v 1.10 2005/02/22 08:25:47 mungady Exp $
  37. *
  38. * Changes:
  39. * --------
  40. * 27-Jul-2003 : XYDataset that forces each series to have a value for every
  41. * X-point which is essential for stacked XY area charts (RA);
  42. * 18-Aug-2003 : Fixed event notification when removing and updating
  43. * series (RA);
  44. * 22-Sep-2003 : Functionality moved from TableXYDataset to
  45. * DefaultTableXYDataset (RA);
  46. * 23-Dec-2003 : Added patch for large datasets, submitted by Jody
  47. * Brownell (DG);
  48. * 16-Feb-2004 : Added pruning methods (DG);
  49. * 31-Mar-2004 : Provisional implementation of IntervalXYDataset (AS);
  50. * 01-Apr-2004 : Sound implementation of IntervalXYDataset (AS);
  51. * 05-May-2004 : Now extends AbstractIntervalXYDataset (DG);
  52. * 15-Jul-2004 : Switched getX() with getXValue() and getY() with
  53. * getYValue() (DG);
  54. * 18-Aug-2004 : Moved from org.jfree.data --> org.jfree.data.xy (DG);
  55. * 11-Jan-2005 : Removed deprecated code in preparation for the 1.0.0
  56. * release (DG);
  57. *
  58. */
  59. package org.jfree.data.xy;
  60. import java.util.ArrayList;
  61. import java.util.HashSet;
  62. import java.util.Iterator;
  63. import java.util.List;
  64. import org.jfree.data.DomainInfo;
  65. import org.jfree.data.Range;
  66. import org.jfree.data.general.DatasetChangeEvent;
  67. import org.jfree.data.general.SeriesChangeEvent;
  68. import org.jfree.util.ObjectUtilities;
  69. /**
  70. * An {@link XYDataset} where every series shares the same x-values (required
  71. * for generating stacked area charts).
  72. *
  73. * @author Richard Atkinson
  74. */
  75. public class DefaultTableXYDataset extends AbstractIntervalXYDataset
  76. implements TableXYDataset,
  77. IntervalXYDataset, DomainInfo {
  78. /**
  79. * Storage for the data - this list will contain zero, one or many
  80. * XYSeries objects.
  81. */
  82. private List data = null;
  83. /** Storage for the x values. */
  84. private HashSet xPoints = null;
  85. /** A flag that controls whether or not events are propogated. */
  86. private boolean propagateEvents = true;
  87. /** A flag that controls auto pruning. */
  88. private boolean autoPrune = false;
  89. /** The delegate used to control the interval width. */
  90. private IntervalXYDelegate intervalDelegate;
  91. /**
  92. * Creates a new empty dataset.
  93. */
  94. public DefaultTableXYDataset() {
  95. this(false);
  96. }
  97. /**
  98. * Creates a new empty dataset.
  99. *
  100. * @param autoPrune a flag that controls whether or not x-values are
  101. * removed whenever the corresponding y-values are all
  102. * <code>null</code>.
  103. */
  104. public DefaultTableXYDataset(boolean autoPrune) {
  105. this.autoPrune = autoPrune;
  106. this.data = new ArrayList();
  107. this.xPoints = new HashSet();
  108. this.intervalDelegate = new IntervalXYDelegate(this, false);
  109. }
  110. /**
  111. * Returns the flag that controls whether or not x-values are removed from
  112. * the dataset when the corresponding y-values are all <code>null</code>.
  113. *
  114. * @return A boolean.
  115. */
  116. public boolean isAutoPrune() {
  117. return this.autoPrune;
  118. }
  119. /**
  120. * Adds a series to the collection and sends a {@link DatasetChangeEvent}
  121. * to all registered listeners. The series should be configured to NOT
  122. * allow duplicate x-values.
  123. *
  124. * @param series the series (<code>null</code> not permitted).
  125. */
  126. public void addSeries(XYSeries series) {
  127. if (series == null) {
  128. throw new IllegalArgumentException("Null 'series' argument.");
  129. }
  130. if (series.getAllowDuplicateXValues()) {
  131. throw new IllegalArgumentException(
  132. "Cannot accept XYSeries that allow duplicate values. "
  133. + "Use XYSeries(seriesName, <sort>, false) constructor."
  134. );
  135. }
  136. updateXPoints(series);
  137. this.data.add(series);
  138. series.addChangeListener(this);
  139. fireDatasetChanged();
  140. }
  141. /**
  142. * Adds any unique x-values from 'series' to the dataset, and also adds any
  143. * x-values that are in the dataset but not in 'series' to the series.
  144. *
  145. * @param series the series (<code>null</code> not permitted).
  146. */
  147. private void updateXPoints(XYSeries series) {
  148. if (series == null) {
  149. throw new IllegalArgumentException("Null 'series' not permitted.");
  150. }
  151. HashSet seriesXPoints = new HashSet();
  152. boolean savedState = this.propagateEvents;
  153. this.propagateEvents = false;
  154. for (int itemNo = 0; itemNo < series.getItemCount(); itemNo++) {
  155. Number xValue = series.getX(itemNo);
  156. seriesXPoints.add(xValue);
  157. if (!this.xPoints.contains(xValue)) {
  158. this.xPoints.add(xValue);
  159. int seriesCount = this.data.size();
  160. for (int seriesNo = 0; seriesNo < seriesCount; seriesNo++) {
  161. XYSeries dataSeries = (XYSeries) this.data.get(seriesNo);
  162. if (!dataSeries.equals(series)) {
  163. dataSeries.add(xValue, null);
  164. }
  165. }
  166. }
  167. }
  168. Iterator iterator = this.xPoints.iterator();
  169. while (iterator.hasNext()) {
  170. Number xPoint = (Number) iterator.next();
  171. if (!seriesXPoints.contains(xPoint)) {
  172. series.add(xPoint, null);
  173. }
  174. }
  175. this.propagateEvents = savedState;
  176. }
  177. /**
  178. * Updates the x-values for all the series in the dataset.
  179. */
  180. public void updateXPoints() {
  181. this.propagateEvents = false;
  182. for (int s = 0; s < this.data.size(); s++) {
  183. updateXPoints((XYSeries) this.data.get(s));
  184. }
  185. if (this.autoPrune) {
  186. prune();
  187. }
  188. this.propagateEvents = true;
  189. }
  190. /**
  191. * Returns the number of series in the collection.
  192. *
  193. * @return The series count.
  194. */
  195. public int getSeriesCount() {
  196. return this.data.size();
  197. }
  198. /**
  199. * Returns the number of x values in the dataset.
  200. *
  201. * @return The number of x values in the dataset.
  202. */
  203. public int getItemCount() {
  204. if (this.xPoints == null) {
  205. return 0;
  206. }
  207. else {
  208. return this.xPoints.size();
  209. }
  210. }
  211. /**
  212. * Returns a series.
  213. *
  214. * @param series the series (zero-based index).
  215. *
  216. * @return The series (never <code>null</code>).
  217. */
  218. public XYSeries getSeries(int series) {
  219. if ((series < 0) || (series > getSeriesCount())) {
  220. throw new IllegalArgumentException("Index outside valid range.");
  221. }
  222. return (XYSeries) this.data.get(series);
  223. }
  224. /**
  225. * Returns the name of a series.
  226. *
  227. * @param series the series (zero-based index).
  228. *
  229. * @return The name of a series.
  230. */
  231. public String getSeriesName(int series) {
  232. // check arguments...delegated
  233. return getSeries(series).getName();
  234. }
  235. /**
  236. * Returns the number of items in the specified series.
  237. *
  238. * @param series the series (zero-based index).
  239. *
  240. * @return The number of items in the specified series.
  241. */
  242. public int getItemCount(int series) {
  243. // check arguments...delegated
  244. return getSeries(series).getItemCount();
  245. }
  246. /**
  247. * Returns the x-value for the specified series and item.
  248. *
  249. * @param series the series (zero-based index).
  250. * @param item the item (zero-based index).
  251. *
  252. * @return The x-value for the specified series and item.
  253. */
  254. public Number getX(int series, int item) {
  255. XYSeries s = (XYSeries) this.data.get(series);
  256. XYDataItem dataItem = s.getDataItem(item);
  257. return dataItem.getX();
  258. }
  259. /**
  260. * Returns the starting X value for the specified series and item.
  261. *
  262. * @param series the series (zero-based index).
  263. * @param item the item (zero-based index).
  264. *
  265. * @return The starting X value.
  266. */
  267. public Number getStartX(int series, int item) {
  268. return this.intervalDelegate.getStartX(series, item);
  269. }
  270. /**
  271. * Returns the ending X value for the specified series and item.
  272. *
  273. * @param series the series (zero-based index).
  274. * @param item the item (zero-based index).
  275. *
  276. * @return The ending X value.
  277. */
  278. public Number getEndX(int series, int item) {
  279. return this.intervalDelegate.getEndX(series, item);
  280. }
  281. /**
  282. * Returns the y-value for the specified series and item.
  283. *
  284. * @param series the series (zero-based index).
  285. * @param index the index of the item of interest (zero-based).
  286. *
  287. * @return The y-value for the specified series and item (possibly
  288. * <code>null</code>).
  289. */
  290. public Number getY(int series, int index) {
  291. XYSeries ts = (XYSeries) this.data.get(series);
  292. XYDataItem dataItem = ts.getDataItem(index);
  293. return dataItem.getY();
  294. }
  295. /**
  296. * Returns the starting Y value for the specified series and item.
  297. *
  298. * @param series the series (zero-based index).
  299. * @param item the item (zero-based index).
  300. *
  301. * @return The starting Y value.
  302. */
  303. public Number getStartY(int series, int item) {
  304. return getY(series, item);
  305. }
  306. /**
  307. * Returns the ending Y value for the specified series and item.
  308. *
  309. * @param series the series (zero-based index).
  310. * @param item the item (zero-based index).
  311. *
  312. * @return The ending Y value.
  313. */
  314. public Number getEndY(int series, int item) {
  315. return getY(series, item);
  316. }
  317. /**
  318. * Removes all the series from the collection and sends a
  319. * {@link DatasetChangeEvent} to all registered listeners.
  320. */
  321. public void removeAllSeries() {
  322. // Unregister the collection as a change listener to each series in
  323. // the collection.
  324. for (int i = 0; i < this.data.size(); i++) {
  325. XYSeries series = (XYSeries) this.data.get(i);
  326. series.removeChangeListener(this);
  327. }
  328. // Remove all the series from the collection and notify listeners.
  329. this.data.clear();
  330. this.xPoints.clear();
  331. this.intervalDelegate.seriesRemoved();
  332. fireDatasetChanged();
  333. }
  334. /**
  335. * Removes a series from the collection and sends a
  336. * {@link DatasetChangeEvent} to all registered listeners.
  337. *
  338. * @param series the series (<code>null</code> not permitted).
  339. */
  340. public void removeSeries(XYSeries series) {
  341. // check arguments...
  342. if (series == null) {
  343. throw new IllegalArgumentException("Null 'series' argument.");
  344. }
  345. // remove the series...
  346. if (this.data.contains(series)) {
  347. series.removeChangeListener(this);
  348. this.data.remove(series);
  349. if (this.data.size() == 0) {
  350. this.xPoints.clear();
  351. }
  352. this.intervalDelegate.seriesRemoved();
  353. fireDatasetChanged();
  354. }
  355. }
  356. /**
  357. * Removes a series from the collection and sends a
  358. * {@link DatasetChangeEvent} to all registered listeners.
  359. *
  360. * @param series the series (zero based index).
  361. */
  362. public void removeSeries(int series) {
  363. // check arguments...
  364. if ((series < 0) || (series > getSeriesCount())) {
  365. throw new IllegalArgumentException("Index outside valid range.");
  366. }
  367. // fetch the series, remove the change listener, then remove the series.
  368. XYSeries s = (XYSeries) this.data.get(series);
  369. s.removeChangeListener(this);
  370. this.data.remove(series);
  371. if (this.data.size() == 0) {
  372. this.xPoints.clear();
  373. }
  374. else if (this.autoPrune) {
  375. prune();
  376. }
  377. this.intervalDelegate.seriesRemoved();
  378. fireDatasetChanged();
  379. }
  380. /**
  381. * Removes the items from all series for a given x value.
  382. *
  383. * @param x the x-value.
  384. */
  385. public void removeAllValuesForX(Number x) {
  386. if (x == null) {
  387. throw new IllegalArgumentException("Null 'x' argument.");
  388. }
  389. boolean savedState = this.propagateEvents;
  390. this.propagateEvents = false;
  391. for (int s = 0; s < this.data.size(); s++) {
  392. XYSeries series = (XYSeries) this.data.get(s);
  393. series.remove(x);
  394. }
  395. this.propagateEvents = savedState;
  396. this.xPoints.remove(x);
  397. this.intervalDelegate.seriesRemoved();
  398. fireDatasetChanged();
  399. }
  400. /**
  401. * Returns <code>true</code> if all the y-values for the specified x-value
  402. * are <code>null</code> and <code>false</code> otherwise.
  403. *
  404. * @param x the x-value.
  405. *
  406. * @return A boolean.
  407. */
  408. protected boolean canPrune(Number x) {
  409. for (int s = 0; s < this.data.size(); s++) {
  410. XYSeries series = (XYSeries) this.data.get(s);
  411. if (series.getY(series.indexOf(x)) != null) {
  412. return false;
  413. }
  414. }
  415. return true;
  416. }
  417. /**
  418. * Removes all x-values for which all the y-values are <code>null</code>.
  419. */
  420. public void prune() {
  421. HashSet hs = (HashSet) this.xPoints.clone();
  422. Iterator iterator = hs.iterator();
  423. while (iterator.hasNext()) {
  424. Number x = (Number) iterator.next();
  425. if (canPrune(x)) {
  426. removeAllValuesForX(x);
  427. }
  428. }
  429. }
  430. /**
  431. * This method receives notification when a series belonging to the dataset
  432. * changes. It responds by updating the x-points for the entire dataset
  433. * and sending a {@link DatasetChangeEvent} to all registered listeners.
  434. *
  435. * @param event information about the change.
  436. */
  437. public void seriesChanged(SeriesChangeEvent event) {
  438. if (this.propagateEvents) {
  439. updateXPoints();
  440. fireDatasetChanged();
  441. }
  442. }
  443. /**
  444. * Tests this collection for equality with an arbitrary object.
  445. *
  446. * @param obj the object (<code>null</code> permitted).
  447. *
  448. * @return A boolean.
  449. */
  450. public boolean equals(Object obj) {
  451. /*
  452. * I wonder if these implementations of equals and hashCode are
  453. * sound... (AS)
  454. */
  455. if (obj == this) {
  456. return true;
  457. }
  458. if (!(obj instanceof DefaultTableXYDataset)) {
  459. return false;
  460. }
  461. DefaultTableXYDataset that = (DefaultTableXYDataset) obj;
  462. if (this.autoPrune != that.autoPrune) {
  463. return false;
  464. }
  465. if (this.propagateEvents != that.propagateEvents) {
  466. return false;
  467. }
  468. if (!this.intervalDelegate.equals(that.intervalDelegate)) {
  469. return false;
  470. }
  471. if (!ObjectUtilities.equal(this.data, that.data)) {
  472. return false;
  473. }
  474. return true;
  475. }
  476. /**
  477. * Returns a hash code.
  478. *
  479. * @return A hash code.
  480. */
  481. public int hashCode() {
  482. int result;
  483. result = (this.data != null ? this.data.hashCode() : 0);
  484. result = 29 * result
  485. + (this.xPoints != null ? this.xPoints.hashCode() : 0);
  486. result = 29 * result + (this.propagateEvents ? 1 : 0);
  487. result = 29 * result + (this.autoPrune ? 1 : 0);
  488. return result;
  489. }
  490. /**
  491. * Returns the minimum x-value in the dataset.
  492. *
  493. * @param includeInterval a flag that determines whether or not the
  494. * x-interval is taken into account.
  495. *
  496. * @return The minimum value.
  497. */
  498. public double getDomainLowerBound(boolean includeInterval) {
  499. return this.intervalDelegate.getDomainLowerBound(includeInterval);
  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. public double getDomainUpperBound(boolean includeInterval) {
  510. return this.intervalDelegate.getDomainUpperBound(includeInterval);
  511. }
  512. /**
  513. * Returns the range of the values in this dataset's domain.
  514. *
  515. * @param includeInterval a flag that determines whether or not the
  516. * x-interval is taken into account.
  517. *
  518. * @return The range.
  519. */
  520. public Range getDomainBounds(boolean includeInterval) {
  521. return this.intervalDelegate.getDomainBounds(includeInterval);
  522. }
  523. /**
  524. * Returns the interval position factor.
  525. *
  526. * @return The interval position factor.
  527. */
  528. public double getIntervalPositionFactor() {
  529. return this.intervalDelegate.getIntervalPositionFactor();
  530. }
  531. /**
  532. * Sets the interval position factor. Must be between 0.0 and 1.0 inclusive.
  533. * If the factor is 0.5, the gap is in the middle of the x values. If it
  534. * is lesser than 0.5, the gap is farther to the left and if greater than
  535. * 0.5 it gets farther to the right.
  536. *
  537. * @param d the new interval position factor.
  538. */
  539. public void setIntervalPositionFactor(double d) {
  540. this.intervalDelegate.setIntervalPositionFactor(d);
  541. fireDatasetChanged();
  542. }
  543. /**
  544. * returns the full interval width.
  545. *
  546. * @return the interval width to use.
  547. */
  548. public double getIntervalWidth() {
  549. return this.intervalDelegate.getIntervalWidth();
  550. }
  551. /**
  552. * Sets the interval width manually.
  553. *
  554. * @param d the new interval width.
  555. */
  556. public void setIntervalWidth(double d) {
  557. this.intervalDelegate.setIntervalWidth(d);
  558. fireDatasetChanged();
  559. }
  560. /**
  561. * Returns whether the interval width is automatically calculated or not.
  562. *
  563. * @return A flag that determines whether or not the interval width is
  564. * automatically calculated.
  565. */
  566. public boolean isAutoWidth() {
  567. return this.intervalDelegate.isAutoWidth();
  568. }
  569. /**
  570. * Sets the flag that indicates whether the interval width is automatically
  571. * calculated or not.
  572. *
  573. * @param b a boolean.
  574. */
  575. public void setAutoWidth(boolean b) {
  576. this.intervalDelegate.setAutoWidth(b);
  577. fireDatasetChanged();
  578. }
  579. }