001 /* ===========================================================
002 * JFreeChart : a free chart library for the Java(tm) platform
003 * ===========================================================
004 *
005 * (C) Copyright 2000-2008, 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 * CombinedDomainCategoryPlot.java
029 * -------------------------------
030 * (C) Copyright 2003-2008, by Object Refinery Limited.
031 *
032 * Original Author: David Gilbert (for Object Refinery Limited);
033 * Contributor(s): Nicolas Brodu;
034 *
035 * Changes:
036 * --------
037 * 16-May-2003 : Version 1 (DG);
038 * 08-Aug-2003 : Adjusted totalWeight in remove() method (DG);
039 * 19-Aug-2003 : Added equals() method, implemented Cloneable and
040 * Serializable (DG);
041 * 11-Sep-2003 : Fix cloning support (subplots) (NB);
042 * 15-Sep-2003 : Implemented PublicCloneable (DG);
043 * 16-Sep-2003 : Changed ChartRenderingInfo --> PlotRenderingInfo (DG);
044 * 17-Sep-2003 : Updated handling of 'clicks' (DG);
045 * 04-May-2004 : Added getter/setter methods for 'gap' attribute (DG);
046 * 12-Nov-2004 : Implemented the Zoomable interface (DG);
047 * 25-Nov-2004 : Small update to clone() implementation (DG);
048 * 21-Feb-2005 : The getLegendItems() method now returns the fixed legend
049 * items if set (DG);
050 * 05-May-2005 : Updated draw() method parameters (DG);
051 * ------------- JFREECHART 1.0.x ---------------------------------------------
052 * 13-Sep-2006 : Updated API docs (DG);
053 * 30-Oct-2006 : Added new getCategoriesForAxis() override (DG);
054 * 17-Apr-2007 : Added null argument checks to findSubplot() (DG);
055 * 14-Nov-2007 : Updated setFixedRangeAxisSpaceForSubplots() method (DG);
056 * 27-Mar-2008 : Add documentation for getDataRange() method (DG);
057 * 31-Mar-2008 : Updated getSubplots() to return EMPTY_LIST for null
058 * subplots, as suggested by Richard West (DG);
059 * 28-Apr-2008 : Fixed zooming problem (see bug 1950037) (DG);
060 * 26-Jun-2008 : Fixed crosshair support (DG);
061 * 11-Aug-2008 : Don't store totalWeight of subplots, calculate it as
062 * required (DG);
063 *
064 */
065
066 package org.jfree.chart.plot;
067
068 import java.awt.Graphics2D;
069 import java.awt.geom.Point2D;
070 import java.awt.geom.Rectangle2D;
071 import java.util.Collections;
072 import java.util.Iterator;
073 import java.util.List;
074
075 import org.jfree.chart.LegendItemCollection;
076 import org.jfree.chart.axis.AxisSpace;
077 import org.jfree.chart.axis.AxisState;
078 import org.jfree.chart.axis.CategoryAxis;
079 import org.jfree.chart.axis.ValueAxis;
080 import org.jfree.chart.event.PlotChangeEvent;
081 import org.jfree.chart.event.PlotChangeListener;
082 import org.jfree.data.Range;
083 import org.jfree.ui.RectangleEdge;
084 import org.jfree.ui.RectangleInsets;
085 import org.jfree.util.ObjectUtilities;
086
087 /**
088 * A combined category plot where the domain axis is shared.
089 */
090 public class CombinedDomainCategoryPlot extends CategoryPlot
091 implements PlotChangeListener {
092
093 /** For serialization. */
094 private static final long serialVersionUID = 8207194522653701572L;
095
096 /** Storage for the subplot references. */
097 private List subplots;
098
099 /** The gap between subplots. */
100 private double gap;
101
102 /** Temporary storage for the subplot areas. */
103 private transient Rectangle2D[] subplotAreas;
104 // TODO: move the above to the plot state
105
106 /**
107 * Default constructor.
108 */
109 public CombinedDomainCategoryPlot() {
110 this(new CategoryAxis());
111 }
112
113 /**
114 * Creates a new plot.
115 *
116 * @param domainAxis the shared domain axis (<code>null</code> not
117 * permitted).
118 */
119 public CombinedDomainCategoryPlot(CategoryAxis domainAxis) {
120 super(null, domainAxis, null, null);
121 this.subplots = new java.util.ArrayList();
122 this.gap = 5.0;
123 }
124
125 /**
126 * Returns the space between subplots.
127 *
128 * @return The gap (in Java2D units).
129 */
130 public double getGap() {
131 return this.gap;
132 }
133
134 /**
135 * Sets the amount of space between subplots and sends a
136 * {@link PlotChangeEvent} to all registered listeners.
137 *
138 * @param gap the gap between subplots (in Java2D units).
139 */
140 public void setGap(double gap) {
141 this.gap = gap;
142 fireChangeEvent();
143 }
144
145 /**
146 * Adds a subplot to the combined chart and sends a {@link PlotChangeEvent}
147 * to all registered listeners.
148 * <br><br>
149 * The domain axis for the subplot will be set to <code>null</code>. You
150 * must ensure that the subplot has a non-null range axis.
151 *
152 * @param subplot the subplot (<code>null</code> not permitted).
153 */
154 public void add(CategoryPlot subplot) {
155 add(subplot, 1);
156 }
157
158 /**
159 * Adds a subplot to the combined chart and sends a {@link PlotChangeEvent}
160 * to all registered listeners.
161 * <br><br>
162 * The domain axis for the subplot will be set to <code>null</code>. You
163 * must ensure that the subplot has a non-null range axis.
164 *
165 * @param subplot the subplot (<code>null</code> not permitted).
166 * @param weight the weight (must be >= 1).
167 */
168 public void add(CategoryPlot subplot, int weight) {
169 if (subplot == null) {
170 throw new IllegalArgumentException("Null 'subplot' argument.");
171 }
172 if (weight < 1) {
173 throw new IllegalArgumentException("Require weight >= 1.");
174 }
175 subplot.setParent(this);
176 subplot.setWeight(weight);
177 subplot.setInsets(new RectangleInsets(0.0, 0.0, 0.0, 0.0));
178 subplot.setDomainAxis(null);
179 subplot.setOrientation(getOrientation());
180 subplot.addChangeListener(this);
181 this.subplots.add(subplot);
182 CategoryAxis axis = getDomainAxis();
183 if (axis != null) {
184 axis.configure();
185 }
186 fireChangeEvent();
187 }
188
189 /**
190 * Removes a subplot from the combined chart. Potentially, this removes
191 * some unique categories from the overall union of the datasets...so the
192 * domain axis is reconfigured, then a {@link PlotChangeEvent} is sent to
193 * all registered listeners.
194 *
195 * @param subplot the subplot (<code>null</code> not permitted).
196 */
197 public void remove(CategoryPlot subplot) {
198 if (subplot == null) {
199 throw new IllegalArgumentException("Null 'subplot' argument.");
200 }
201 int position = -1;
202 int size = this.subplots.size();
203 int i = 0;
204 while (position == -1 && i < size) {
205 if (this.subplots.get(i) == subplot) {
206 position = i;
207 }
208 i++;
209 }
210 if (position != -1) {
211 this.subplots.remove(position);
212 subplot.setParent(null);
213 subplot.removeChangeListener(this);
214 CategoryAxis domain = getDomainAxis();
215 if (domain != null) {
216 domain.configure();
217 }
218 fireChangeEvent();
219 }
220 }
221
222 /**
223 * Returns the list of subplots. The returned list may be empty, but is
224 * never <code>null</code>.
225 *
226 * @return An unmodifiable list of subplots.
227 */
228 public List getSubplots() {
229 if (this.subplots != null) {
230 return Collections.unmodifiableList(this.subplots);
231 }
232 else {
233 return Collections.EMPTY_LIST;
234 }
235 }
236
237 /**
238 * Returns the subplot (if any) that contains the (x, y) point (specified
239 * in Java2D space).
240 *
241 * @param info the chart rendering info (<code>null</code> not permitted).
242 * @param source the source point (<code>null</code> not permitted).
243 *
244 * @return A subplot (possibly <code>null</code>).
245 */
246 public CategoryPlot findSubplot(PlotRenderingInfo info, Point2D source) {
247 if (info == null) {
248 throw new IllegalArgumentException("Null 'info' argument.");
249 }
250 if (source == null) {
251 throw new IllegalArgumentException("Null 'source' argument.");
252 }
253 CategoryPlot result = null;
254 int subplotIndex = info.getSubplotIndex(source);
255 if (subplotIndex >= 0) {
256 result = (CategoryPlot) this.subplots.get(subplotIndex);
257 }
258 return result;
259 }
260
261 /**
262 * Multiplies the range on the range axis/axes by the specified factor.
263 *
264 * @param factor the zoom factor.
265 * @param info the plot rendering info (<code>null</code> not permitted).
266 * @param source the source point (<code>null</code> not permitted).
267 */
268 public void zoomRangeAxes(double factor, PlotRenderingInfo info,
269 Point2D source) {
270 zoomRangeAxes(factor, info, source, false);
271 }
272
273 /**
274 * Multiplies the range on the range axis/axes by the specified factor.
275 *
276 * @param factor the zoom factor.
277 * @param info the plot rendering info (<code>null</code> not permitted).
278 * @param source the source point (<code>null</code> not permitted).
279 * @param useAnchor zoom about the anchor point?
280 */
281 public void zoomRangeAxes(double factor, PlotRenderingInfo info,
282 Point2D source, boolean useAnchor) {
283 // delegate 'info' and 'source' argument checks...
284 CategoryPlot subplot = findSubplot(info, source);
285 if (subplot != null) {
286 subplot.zoomRangeAxes(factor, info, source, useAnchor);
287 }
288 else {
289 // if the source point doesn't fall within a subplot, we do the
290 // zoom on all subplots...
291 Iterator iterator = getSubplots().iterator();
292 while (iterator.hasNext()) {
293 subplot = (CategoryPlot) iterator.next();
294 subplot.zoomRangeAxes(factor, info, source, useAnchor);
295 }
296 }
297 }
298
299 /**
300 * Zooms in on the range axes.
301 *
302 * @param lowerPercent the lower bound.
303 * @param upperPercent the upper bound.
304 * @param info the plot rendering info (<code>null</code> not permitted).
305 * @param source the source point (<code>null</code> not permitted).
306 */
307 public void zoomRangeAxes(double lowerPercent, double upperPercent,
308 PlotRenderingInfo info, Point2D source) {
309 // delegate 'info' and 'source' argument checks...
310 CategoryPlot subplot = findSubplot(info, source);
311 if (subplot != null) {
312 subplot.zoomRangeAxes(lowerPercent, upperPercent, info, source);
313 }
314 else {
315 // if the source point doesn't fall within a subplot, we do the
316 // zoom on all subplots...
317 Iterator iterator = getSubplots().iterator();
318 while (iterator.hasNext()) {
319 subplot = (CategoryPlot) iterator.next();
320 subplot.zoomRangeAxes(lowerPercent, upperPercent, info, source);
321 }
322 }
323 }
324
325 /**
326 * Calculates the space required for the axes.
327 *
328 * @param g2 the graphics device.
329 * @param plotArea the plot area.
330 *
331 * @return The space required for the axes.
332 */
333 protected AxisSpace calculateAxisSpace(Graphics2D g2,
334 Rectangle2D plotArea) {
335
336 AxisSpace space = new AxisSpace();
337 PlotOrientation orientation = getOrientation();
338
339 // work out the space required by the domain axis...
340 AxisSpace fixed = getFixedDomainAxisSpace();
341 if (fixed != null) {
342 if (orientation == PlotOrientation.HORIZONTAL) {
343 space.setLeft(fixed.getLeft());
344 space.setRight(fixed.getRight());
345 }
346 else if (orientation == PlotOrientation.VERTICAL) {
347 space.setTop(fixed.getTop());
348 space.setBottom(fixed.getBottom());
349 }
350 }
351 else {
352 CategoryAxis categoryAxis = getDomainAxis();
353 RectangleEdge categoryEdge = Plot.resolveDomainAxisLocation(
354 getDomainAxisLocation(), orientation);
355 if (categoryAxis != null) {
356 space = categoryAxis.reserveSpace(g2, this, plotArea,
357 categoryEdge, space);
358 }
359 else {
360 if (getDrawSharedDomainAxis()) {
361 space = getDomainAxis().reserveSpace(g2, this, plotArea,
362 categoryEdge, space);
363 }
364 }
365 }
366
367 Rectangle2D adjustedPlotArea = space.shrink(plotArea, null);
368
369 // work out the maximum height or width of the non-shared axes...
370 int n = this.subplots.size();
371 int totalWeight = 0;
372 for (int i = 0; i < n; i++) {
373 CategoryPlot sub = (CategoryPlot) this.subplots.get(i);
374 totalWeight += sub.getWeight();
375 }
376 this.subplotAreas = new Rectangle2D[n];
377 double x = adjustedPlotArea.getX();
378 double y = adjustedPlotArea.getY();
379 double usableSize = 0.0;
380 if (orientation == PlotOrientation.HORIZONTAL) {
381 usableSize = adjustedPlotArea.getWidth() - this.gap * (n - 1);
382 }
383 else if (orientation == PlotOrientation.VERTICAL) {
384 usableSize = adjustedPlotArea.getHeight() - this.gap * (n - 1);
385 }
386
387 for (int i = 0; i < n; i++) {
388 CategoryPlot plot = (CategoryPlot) this.subplots.get(i);
389
390 // calculate sub-plot area
391 if (orientation == PlotOrientation.HORIZONTAL) {
392 double w = usableSize * plot.getWeight() / totalWeight;
393 this.subplotAreas[i] = new Rectangle2D.Double(x, y, w,
394 adjustedPlotArea.getHeight());
395 x = x + w + this.gap;
396 }
397 else if (orientation == PlotOrientation.VERTICAL) {
398 double h = usableSize * plot.getWeight() / totalWeight;
399 this.subplotAreas[i] = new Rectangle2D.Double(x, y,
400 adjustedPlotArea.getWidth(), h);
401 y = y + h + this.gap;
402 }
403
404 AxisSpace subSpace = plot.calculateRangeAxisSpace(g2,
405 this.subplotAreas[i], null);
406 space.ensureAtLeast(subSpace);
407
408 }
409
410 return space;
411 }
412
413 /**
414 * Draws the plot on a Java 2D graphics device (such as the screen or a
415 * printer). Will perform all the placement calculations for each of the
416 * sub-plots and then tell these to draw themselves.
417 *
418 * @param g2 the graphics device.
419 * @param area the area within which the plot (including axis labels)
420 * should be drawn.
421 * @param anchor the anchor point (<code>null</code> permitted).
422 * @param parentState the state from the parent plot, if there is one.
423 * @param info collects information about the drawing (<code>null</code>
424 * permitted).
425 */
426 public void draw(Graphics2D g2,
427 Rectangle2D area,
428 Point2D anchor,
429 PlotState parentState,
430 PlotRenderingInfo info) {
431
432 // set up info collection...
433 if (info != null) {
434 info.setPlotArea(area);
435 }
436
437 // adjust the drawing area for plot insets (if any)...
438 RectangleInsets insets = getInsets();
439 area.setRect(area.getX() + insets.getLeft(),
440 area.getY() + insets.getTop(),
441 area.getWidth() - insets.getLeft() - insets.getRight(),
442 area.getHeight() - insets.getTop() - insets.getBottom());
443
444
445 // calculate the data area...
446 setFixedRangeAxisSpaceForSubplots(null);
447 AxisSpace space = calculateAxisSpace(g2, area);
448 Rectangle2D dataArea = space.shrink(area, null);
449
450 // set the width and height of non-shared axis of all sub-plots
451 setFixedRangeAxisSpaceForSubplots(space);
452
453 // draw the shared axis
454 CategoryAxis axis = getDomainAxis();
455 RectangleEdge domainEdge = getDomainAxisEdge();
456 double cursor = RectangleEdge.coordinate(dataArea, domainEdge);
457 AxisState axisState = axis.draw(g2, cursor, area, dataArea,
458 domainEdge, info);
459 if (parentState == null) {
460 parentState = new PlotState();
461 }
462 parentState.getSharedAxisStates().put(axis, axisState);
463
464 // draw all the subplots
465 for (int i = 0; i < this.subplots.size(); i++) {
466 CategoryPlot plot = (CategoryPlot) this.subplots.get(i);
467 PlotRenderingInfo subplotInfo = null;
468 if (info != null) {
469 subplotInfo = new PlotRenderingInfo(info.getOwner());
470 info.addSubplotInfo(subplotInfo);
471 }
472 Point2D subAnchor = null;
473 if (anchor != null && this.subplotAreas[i].contains(anchor)) {
474 subAnchor = anchor;
475 }
476 plot.draw(g2, this.subplotAreas[i], subAnchor, parentState,
477 subplotInfo);
478 }
479
480 if (info != null) {
481 info.setDataArea(dataArea);
482 }
483
484 }
485
486 /**
487 * Sets the size (width or height, depending on the orientation of the
488 * plot) for the range axis of each subplot.
489 *
490 * @param space the space (<code>null</code> permitted).
491 */
492 protected void setFixedRangeAxisSpaceForSubplots(AxisSpace space) {
493 Iterator iterator = this.subplots.iterator();
494 while (iterator.hasNext()) {
495 CategoryPlot plot = (CategoryPlot) iterator.next();
496 plot.setFixedRangeAxisSpace(space, false);
497 }
498 }
499
500 /**
501 * Sets the orientation of the plot (and all subplots).
502 *
503 * @param orientation the orientation (<code>null</code> not permitted).
504 */
505 public void setOrientation(PlotOrientation orientation) {
506
507 super.setOrientation(orientation);
508
509 Iterator iterator = this.subplots.iterator();
510 while (iterator.hasNext()) {
511 CategoryPlot plot = (CategoryPlot) iterator.next();
512 plot.setOrientation(orientation);
513 }
514
515 }
516
517 /**
518 * Returns a range representing the extent of the data values in this plot
519 * (obtained from the subplots) that will be rendered against the specified
520 * axis. NOTE: This method is intended for internal JFreeChart use, and
521 * is public only so that code in the axis classes can call it. Since,
522 * for this class, the domain axis is a {@link CategoryAxis}
523 * (not a <code>ValueAxis</code}) and subplots have independent range axes,
524 * the JFreeChart code will never call this method (although this is not
525 * checked/enforced).
526 *
527 * @param axis the axis.
528 *
529 * @return The range.
530 */
531 public Range getDataRange(ValueAxis axis) {
532 // override is only for documentation purposes
533 return super.getDataRange(axis);
534 }
535
536 /**
537 * Returns a collection of legend items for the plot.
538 *
539 * @return The legend items.
540 */
541 public LegendItemCollection getLegendItems() {
542 LegendItemCollection result = getFixedLegendItems();
543 if (result == null) {
544 result = new LegendItemCollection();
545 if (this.subplots != null) {
546 Iterator iterator = this.subplots.iterator();
547 while (iterator.hasNext()) {
548 CategoryPlot plot = (CategoryPlot) iterator.next();
549 LegendItemCollection more = plot.getLegendItems();
550 result.addAll(more);
551 }
552 }
553 }
554 return result;
555 }
556
557 /**
558 * Returns an unmodifiable list of the categories contained in all the
559 * subplots.
560 *
561 * @return The list.
562 */
563 public List getCategories() {
564 List result = new java.util.ArrayList();
565 if (this.subplots != null) {
566 Iterator iterator = this.subplots.iterator();
567 while (iterator.hasNext()) {
568 CategoryPlot plot = (CategoryPlot) iterator.next();
569 List more = plot.getCategories();
570 Iterator moreIterator = more.iterator();
571 while (moreIterator.hasNext()) {
572 Comparable category = (Comparable) moreIterator.next();
573 if (!result.contains(category)) {
574 result.add(category);
575 }
576 }
577 }
578 }
579 return Collections.unmodifiableList(result);
580 }
581
582 /**
583 * Overridden to return the categories in the subplots.
584 *
585 * @param axis ignored.
586 *
587 * @return A list of the categories in the subplots.
588 *
589 * @since 1.0.3
590 */
591 public List getCategoriesForAxis(CategoryAxis axis) {
592 // FIXME: this code means that it is not possible to use more than
593 // one domain axis for the combined plots...
594 return getCategories();
595 }
596
597 /**
598 * Handles a 'click' on the plot.
599 *
600 * @param x x-coordinate of the click.
601 * @param y y-coordinate of the click.
602 * @param info information about the plot's dimensions.
603 *
604 */
605 public void handleClick(int x, int y, PlotRenderingInfo info) {
606
607 Rectangle2D dataArea = info.getDataArea();
608 if (dataArea.contains(x, y)) {
609 for (int i = 0; i < this.subplots.size(); i++) {
610 CategoryPlot subplot = (CategoryPlot) this.subplots.get(i);
611 PlotRenderingInfo subplotInfo = info.getSubplotInfo(i);
612 subplot.handleClick(x, y, subplotInfo);
613 }
614 }
615
616 }
617
618 /**
619 * Receives a {@link PlotChangeEvent} and responds by notifying all
620 * listeners.
621 *
622 * @param event the event.
623 */
624 public void plotChanged(PlotChangeEvent event) {
625 notifyListeners(event);
626 }
627
628 /**
629 * Tests the plot for equality with an arbitrary object.
630 *
631 * @param obj the object (<code>null</code> permitted).
632 *
633 * @return A boolean.
634 */
635 public boolean equals(Object obj) {
636 if (obj == this) {
637 return true;
638 }
639 if (!(obj instanceof CombinedDomainCategoryPlot)) {
640 return false;
641 }
642 CombinedDomainCategoryPlot that = (CombinedDomainCategoryPlot) obj;
643 if (this.gap != that.gap) {
644 return false;
645 }
646 if (!ObjectUtilities.equal(this.subplots, that.subplots)) {
647 return false;
648 }
649 return super.equals(obj);
650 }
651
652 /**
653 * Returns a clone of the plot.
654 *
655 * @return A clone.
656 *
657 * @throws CloneNotSupportedException this class will not throw this
658 * exception, but subclasses (if any) might.
659 */
660 public Object clone() throws CloneNotSupportedException {
661
662 CombinedDomainCategoryPlot result
663 = (CombinedDomainCategoryPlot) super.clone();
664 result.subplots = (List) ObjectUtilities.deepClone(this.subplots);
665 for (Iterator it = result.subplots.iterator(); it.hasNext();) {
666 Plot child = (Plot) it.next();
667 child.setParent(result);
668 }
669 return result;
670
671 }
672
673 }