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 * CombinedRangeXYPlot.java
029 * ------------------------
030 * (C) Copyright 2001-2007, by Bill Kelemen and Contributors.
031 *
032 * Original Author: Bill Kelemen;
033 * Contributor(s): David Gilbert (for Object Refinery Limited);
034 * Anthony Boulestreau;
035 * David Basten;
036 * Kevin Frechette (for ISTI);
037 * Arnaud Lelievre;
038 * Nicolas Brodu;
039 * Petr Kubanek (bug 1606205);
040 *
041 * Changes:
042 * --------
043 * 06-Dec-2001 : Version 1 (BK);
044 * 12-Dec-2001 : Removed unnecessary 'throws' clause from constructor (DG);
045 * 18-Dec-2001 : Added plotArea attribute and get/set methods (BK);
046 * 22-Dec-2001 : Fixed bug in chartChanged with multiple combinations of
047 * CombinedPlots (BK);
048 * 08-Jan-2002 : Moved to new package com.jrefinery.chart.combination (DG);
049 * 25-Feb-2002 : Updated import statements (DG);
050 * 28-Feb-2002 : Readded "this.plotArea = plotArea" that was deleted from
051 * draw() method (BK);
052 * 26-Mar-2002 : Added an empty zoom method (this method needs to be written
053 * so that combined plots will support zooming (DG);
054 * 29-Mar-2002 : Changed the method createCombinedAxis adding the creation of
055 * OverlaidSymbolicAxis and CombinedSymbolicAxis(AB);
056 * 23-Apr-2002 : Renamed CombinedPlot-->MultiXYPlot, and simplified the
057 * structure (DG);
058 * 23-May-2002 : Renamed (again) MultiXYPlot-->CombinedXYPlot (DG);
059 * 19-Jun-2002 : Added get/setGap() methods suggested by David Basten (DG);
060 * 25-Jun-2002 : Removed redundant imports (DG);
061 * 16-Jul-2002 : Draws shared axis after subplots (to fix missing gridlines),
062 * added overrides of 'setSeriesPaint()' and 'setXYItemRenderer()'
063 * that pass changes down to subplots (KF);
064 * 09-Oct-2002 : Added add(XYPlot) method (DG);
065 * 26-Mar-2003 : Implemented Serializable (DG);
066 * 16-May-2003 : Renamed CombinedXYPlot --> CombinedRangeXYPlot (DG);
067 * 26-Jun-2003 : Fixed bug 755547 (DG);
068 * 16-Jul-2003 : Removed getSubPlots() method (duplicate of getSubplots()) (DG);
069 * 08-Aug-2003 : Adjusted totalWeight in remove() method (DG);
070 * 21-Aug-2003 : Implemented Cloneable (DG);
071 * 08-Sep-2003 : Added internationalization via use of properties
072 * resourceBundle (RFE 690236) (AL);
073 * 11-Sep-2003 : Fix cloning support (subplots) (NB);
074 * 15-Sep-2003 : Fixed error in cloning (DG);
075 * 16-Sep-2003 : Changed ChartRenderingInfo --> PlotRenderingInfo (DG);
076 * 17-Sep-2003 : Updated handling of 'clicks' (DG);
077 * 12-Nov-2004 : Implements the new Zoomable interface (DG);
078 * 25-Nov-2004 : Small update to clone() implementation (DG);
079 * 21-Feb-2005 : The getLegendItems() method now returns the fixed legend
080 * items if set (DG);
081 * 05-May-2005 : Removed unused draw() method (DG);
082 * ------------- JFREECHART 1.0.x ---------------------------------------------
083 * 13-Sep-2006 : Updated API docs (DG);
084 * 06-Feb-2007 : Fixed bug 1606205, draw shared axis after subplots (DG);
085 * 23-Mar-2007 : Reverted previous patch (DG);
086 * 17-Apr-2007 : Added null argument checks to findSubplot() (DG);
087 * 18-Jul-2007 : Fixed bug in removeSubplot (DG);
088 * 27-Nov-2007 : Modified setFixedDomainAxisSpaceForSubplots() so as not to
089 * trigger change events in subplots (DG);
090 *
091 */
092
093 package org.jfree.chart.plot;
094
095 import java.awt.Graphics2D;
096 import java.awt.geom.Point2D;
097 import java.awt.geom.Rectangle2D;
098 import java.io.Serializable;
099 import java.util.Collections;
100 import java.util.Iterator;
101 import java.util.List;
102
103 import org.jfree.chart.LegendItemCollection;
104 import org.jfree.chart.axis.AxisSpace;
105 import org.jfree.chart.axis.AxisState;
106 import org.jfree.chart.axis.NumberAxis;
107 import org.jfree.chart.axis.ValueAxis;
108 import org.jfree.chart.event.PlotChangeEvent;
109 import org.jfree.chart.event.PlotChangeListener;
110 import org.jfree.chart.renderer.xy.XYItemRenderer;
111 import org.jfree.data.Range;
112 import org.jfree.ui.RectangleEdge;
113 import org.jfree.ui.RectangleInsets;
114 import org.jfree.util.ObjectUtilities;
115 import org.jfree.util.PublicCloneable;
116
117 /**
118 * An extension of {@link XYPlot} that contains multiple subplots that share a
119 * common range axis.
120 */
121 public class CombinedRangeXYPlot extends XYPlot
122 implements Zoomable,
123 Cloneable, PublicCloneable,
124 Serializable,
125 PlotChangeListener {
126
127 /** For serialization. */
128 private static final long serialVersionUID = -5177814085082031168L;
129
130 /** Storage for the subplot references. */
131 private List subplots;
132
133 /** Total weight of all charts. */
134 private int totalWeight = 0;
135
136 /** The gap between subplots. */
137 private double gap = 5.0;
138
139 /** Temporary storage for the subplot areas. */
140 private transient Rectangle2D[] subplotAreas;
141
142 /**
143 * Default constructor.
144 */
145 public CombinedRangeXYPlot() {
146 this(new NumberAxis());
147 }
148
149 /**
150 * Creates a new plot.
151 *
152 * @param rangeAxis the shared axis.
153 */
154 public CombinedRangeXYPlot(ValueAxis rangeAxis) {
155
156 super(null, // no data in the parent plot
157 null,
158 rangeAxis,
159 null);
160
161 this.subplots = new java.util.ArrayList();
162
163 }
164
165 /**
166 * Returns a string describing the type of plot.
167 *
168 * @return The type of plot.
169 */
170 public String getPlotType() {
171 return localizationResources.getString("Combined_Range_XYPlot");
172 }
173
174 /**
175 * Returns the space between subplots.
176 *
177 * @return The gap
178 */
179 public double getGap() {
180 return this.gap;
181 }
182
183 /**
184 * Sets the amount of space between subplots.
185 *
186 * @param gap the gap between subplots
187 */
188 public void setGap(double gap) {
189 this.gap = gap;
190 }
191
192 /**
193 * Adds a subplot, with a default 'weight' of 1.
194 * <br><br>
195 * You must ensure that the subplot has a non-null domain axis. The range
196 * axis for the subplot will be set to <code>null</code>.
197 *
198 * @param subplot the subplot.
199 */
200 public void add(XYPlot subplot) {
201 add(subplot, 1);
202 }
203
204 /**
205 * Adds a subplot with a particular weight (greater than or equal to one).
206 * The weight determines how much space is allocated to the subplot
207 * relative to all the other subplots.
208 * <br><br>
209 * You must ensure that the subplot has a non-null domain axis. The range
210 * axis for the subplot will be set to <code>null</code>.
211 *
212 * @param subplot the subplot.
213 * @param weight the weight (must be 1 or greater).
214 */
215 public void add(XYPlot subplot, int weight) {
216
217 // verify valid weight
218 if (weight <= 0) {
219 String msg = "The 'weight' must be positive.";
220 throw new IllegalArgumentException(msg);
221 }
222
223 // store the plot and its weight
224 subplot.setParent(this);
225 subplot.setWeight(weight);
226 subplot.setInsets(new RectangleInsets(0.0, 0.0, 0.0, 0.0));
227 subplot.setRangeAxis(null);
228 subplot.addChangeListener(this);
229 this.subplots.add(subplot);
230
231 // keep track of total weights
232 this.totalWeight += weight;
233 configureRangeAxes();
234 notifyListeners(new PlotChangeEvent(this));
235
236 }
237
238 /**
239 * Removes a subplot from the combined chart.
240 *
241 * @param subplot the subplot (<code>null</code> not permitted).
242 */
243 public void remove(XYPlot subplot) {
244 if (subplot == null) {
245 throw new IllegalArgumentException(" Null 'subplot' argument.");
246 }
247 int position = -1;
248 int size = this.subplots.size();
249 int i = 0;
250 while (position == -1 && i < size) {
251 if (this.subplots.get(i) == subplot) {
252 position = i;
253 }
254 i++;
255 }
256 if (position != -1) {
257 this.subplots.remove(position);
258 subplot.setParent(null);
259 subplot.removeChangeListener(this);
260 this.totalWeight -= subplot.getWeight();
261 configureRangeAxes();
262 notifyListeners(new PlotChangeEvent(this));
263 }
264 }
265
266 /**
267 * Returns a list of the subplots.
268 *
269 * @return The list (unmodifiable).
270 */
271 public List getSubplots() {
272 return Collections.unmodifiableList(this.subplots);
273 }
274
275 /**
276 * Calculates the space required for the axes.
277 *
278 * @param g2 the graphics device.
279 * @param plotArea the plot area.
280 *
281 * @return The space required for the axes.
282 */
283 protected AxisSpace calculateAxisSpace(Graphics2D g2,
284 Rectangle2D plotArea) {
285
286 AxisSpace space = new AxisSpace();
287 PlotOrientation orientation = getOrientation();
288
289 // work out the space required by the domain axis...
290 AxisSpace fixed = getFixedRangeAxisSpace();
291 if (fixed != null) {
292 if (orientation == PlotOrientation.VERTICAL) {
293 space.setLeft(fixed.getLeft());
294 space.setRight(fixed.getRight());
295 }
296 else if (orientation == PlotOrientation.HORIZONTAL) {
297 space.setTop(fixed.getTop());
298 space.setBottom(fixed.getBottom());
299 }
300 }
301 else {
302 ValueAxis valueAxis = getRangeAxis();
303 RectangleEdge valueEdge = Plot.resolveRangeAxisLocation(
304 getRangeAxisLocation(), orientation
305 );
306 if (valueAxis != null) {
307 space = valueAxis.reserveSpace(g2, this, plotArea, valueEdge,
308 space);
309 }
310 }
311
312 Rectangle2D adjustedPlotArea = space.shrink(plotArea, null);
313 // work out the maximum height or width of the non-shared axes...
314 int n = this.subplots.size();
315
316 // calculate plotAreas of all sub-plots, maximum vertical/horizontal
317 // axis width/height
318 this.subplotAreas = new Rectangle2D[n];
319 double x = adjustedPlotArea.getX();
320 double y = adjustedPlotArea.getY();
321 double usableSize = 0.0;
322 if (orientation == PlotOrientation.VERTICAL) {
323 usableSize = adjustedPlotArea.getWidth() - this.gap * (n - 1);
324 }
325 else if (orientation == PlotOrientation.HORIZONTAL) {
326 usableSize = adjustedPlotArea.getHeight() - this.gap * (n - 1);
327 }
328
329 for (int i = 0; i < n; i++) {
330 XYPlot plot = (XYPlot) this.subplots.get(i);
331
332 // calculate sub-plot area
333 if (orientation == PlotOrientation.VERTICAL) {
334 double w = usableSize * plot.getWeight() / this.totalWeight;
335 this.subplotAreas[i] = new Rectangle2D.Double(x, y, w,
336 adjustedPlotArea.getHeight());
337 x = x + w + this.gap;
338 }
339 else if (orientation == PlotOrientation.HORIZONTAL) {
340 double h = usableSize * plot.getWeight() / this.totalWeight;
341 this.subplotAreas[i] = new Rectangle2D.Double(x, y,
342 adjustedPlotArea.getWidth(), h);
343 y = y + h + this.gap;
344 }
345
346 AxisSpace subSpace = plot.calculateDomainAxisSpace(g2,
347 this.subplotAreas[i], null);
348 space.ensureAtLeast(subSpace);
349
350 }
351
352 return space;
353 }
354
355 /**
356 * Draws the plot within the specified area on a graphics device.
357 *
358 * @param g2 the graphics device.
359 * @param area the plot area (in Java2D space).
360 * @param anchor an anchor point in Java2D space (<code>null</code>
361 * permitted).
362 * @param parentState the state from the parent plot, if there is one
363 * (<code>null</code> permitted).
364 * @param info collects chart drawing information (<code>null</code>
365 * permitted).
366 */
367 public void draw(Graphics2D g2,
368 Rectangle2D area,
369 Point2D anchor,
370 PlotState parentState,
371 PlotRenderingInfo info) {
372
373 // set up info collection...
374 if (info != null) {
375 info.setPlotArea(area);
376 }
377
378 // adjust the drawing area for plot insets (if any)...
379 RectangleInsets insets = getInsets();
380 insets.trim(area);
381
382 AxisSpace space = calculateAxisSpace(g2, area);
383 Rectangle2D dataArea = space.shrink(area, null);
384 //this.axisOffset.trim(dataArea);
385
386 // set the width and height of non-shared axis of all sub-plots
387 setFixedDomainAxisSpaceForSubplots(space);
388
389 // draw the shared axis
390 ValueAxis axis = getRangeAxis();
391 RectangleEdge edge = getRangeAxisEdge();
392 double cursor = RectangleEdge.coordinate(dataArea, edge);
393 AxisState axisState = axis.draw(g2, cursor, area, dataArea, edge, info);
394
395 if (parentState == null) {
396 parentState = new PlotState();
397 }
398 parentState.getSharedAxisStates().put(axis, axisState);
399
400 // draw all the charts
401 for (int i = 0; i < this.subplots.size(); i++) {
402 XYPlot plot = (XYPlot) this.subplots.get(i);
403 PlotRenderingInfo subplotInfo = null;
404 if (info != null) {
405 subplotInfo = new PlotRenderingInfo(info.getOwner());
406 info.addSubplotInfo(subplotInfo);
407 }
408 plot.draw(g2, this.subplotAreas[i], anchor, parentState,
409 subplotInfo);
410 }
411
412 if (info != null) {
413 info.setDataArea(dataArea);
414 }
415
416 }
417
418 /**
419 * Returns a collection of legend items for the plot.
420 *
421 * @return The legend items.
422 */
423 public LegendItemCollection getLegendItems() {
424 LegendItemCollection result = getFixedLegendItems();
425 if (result == null) {
426 result = new LegendItemCollection();
427
428 if (this.subplots != null) {
429 Iterator iterator = this.subplots.iterator();
430 while (iterator.hasNext()) {
431 XYPlot plot = (XYPlot) iterator.next();
432 LegendItemCollection more = plot.getLegendItems();
433 result.addAll(more);
434 }
435 }
436 }
437 return result;
438 }
439
440 /**
441 * Multiplies the range on the domain axis/axes by the specified factor.
442 *
443 * @param factor the zoom factor.
444 * @param info the plot rendering info (<code>null</code> not permitted).
445 * @param source the source point (<code>null</code> not permitted).
446 */
447 public void zoomDomainAxes(double factor, PlotRenderingInfo info,
448 Point2D source) {
449 // delegate 'info' and 'source' argument checks...
450 XYPlot subplot = findSubplot(info, source);
451 if (subplot != null) {
452 subplot.zoomDomainAxes(factor, info, source);
453 }
454 else {
455 // if the source point doesn't fall within a subplot, we do the
456 // zoom on all subplots...
457 Iterator iterator = getSubplots().iterator();
458 while (iterator.hasNext()) {
459 subplot = (XYPlot) iterator.next();
460 subplot.zoomDomainAxes(factor, info, source);
461 }
462 }
463 }
464
465 /**
466 * Zooms in on the domain axes.
467 *
468 * @param lowerPercent the lower bound.
469 * @param upperPercent the upper bound.
470 * @param info the plot rendering info (<code>null</code> not permitted).
471 * @param source the source point (<code>null</code> not permitted).
472 */
473 public void zoomDomainAxes(double lowerPercent, double upperPercent,
474 PlotRenderingInfo info, Point2D source) {
475 // delegate 'info' and 'source' argument checks...
476 XYPlot subplot = findSubplot(info, source);
477 if (subplot != null) {
478 subplot.zoomDomainAxes(lowerPercent, upperPercent, info, source);
479 }
480 else {
481 // if the source point doesn't fall within a subplot, we do the
482 // zoom on all subplots...
483 Iterator iterator = getSubplots().iterator();
484 while (iterator.hasNext()) {
485 subplot = (XYPlot) iterator.next();
486 subplot.zoomDomainAxes(lowerPercent, upperPercent, info,
487 source);
488 }
489 }
490 }
491
492 /**
493 * Returns the subplot (if any) that contains the (x, y) point (specified
494 * in Java2D space).
495 *
496 * @param info the chart rendering info (<code>null</code> not permitted).
497 * @param source the source point (<code>null</code> not permitted).
498 *
499 * @return A subplot (possibly <code>null</code>).
500 */
501 public XYPlot findSubplot(PlotRenderingInfo info, Point2D source) {
502 if (info == null) {
503 throw new IllegalArgumentException("Null 'info' argument.");
504 }
505 if (source == null) {
506 throw new IllegalArgumentException("Null 'source' argument.");
507 }
508 XYPlot result = null;
509 int subplotIndex = info.getSubplotIndex(source);
510 if (subplotIndex >= 0) {
511 result = (XYPlot) this.subplots.get(subplotIndex);
512 }
513 return result;
514 }
515
516 /**
517 * Sets the item renderer FOR ALL SUBPLOTS. Registered listeners are
518 * notified that the plot has been modified.
519 * <P>
520 * Note: usually you will want to set the renderer independently for each
521 * subplot, which is NOT what this method does.
522 *
523 * @param renderer the new renderer.
524 */
525 public void setRenderer(XYItemRenderer renderer) {
526
527 super.setRenderer(renderer); // not strictly necessary, since the
528 // renderer set for the
529 // parent plot is not used
530
531 Iterator iterator = this.subplots.iterator();
532 while (iterator.hasNext()) {
533 XYPlot plot = (XYPlot) iterator.next();
534 plot.setRenderer(renderer);
535 }
536
537 }
538
539 /**
540 * Sets the orientation for the plot (and all its subplots).
541 *
542 * @param orientation the orientation.
543 */
544 public void setOrientation(PlotOrientation orientation) {
545
546 super.setOrientation(orientation);
547
548 Iterator iterator = this.subplots.iterator();
549 while (iterator.hasNext()) {
550 XYPlot plot = (XYPlot) iterator.next();
551 plot.setOrientation(orientation);
552 }
553
554 }
555
556 /**
557 * Returns the range for the axis. This is the combined range of all the
558 * subplots.
559 *
560 * @param axis the axis.
561 *
562 * @return The range.
563 */
564 public Range getDataRange(ValueAxis axis) {
565
566 Range result = null;
567 if (this.subplots != null) {
568 Iterator iterator = this.subplots.iterator();
569 while (iterator.hasNext()) {
570 XYPlot subplot = (XYPlot) iterator.next();
571 result = Range.combine(result, subplot.getDataRange(axis));
572 }
573 }
574 return result;
575
576 }
577
578 /**
579 * Sets the space (width or height, depending on the orientation of the
580 * plot) for the domain axis of each subplot.
581 *
582 * @param space the space.
583 */
584 protected void setFixedDomainAxisSpaceForSubplots(AxisSpace space) {
585 Iterator iterator = this.subplots.iterator();
586 while (iterator.hasNext()) {
587 XYPlot plot = (XYPlot) iterator.next();
588 plot.setFixedDomainAxisSpace(space, false);
589 }
590 }
591
592 /**
593 * Handles a 'click' on the plot by updating the anchor values...
594 *
595 * @param x x-coordinate, where the click occured.
596 * @param y y-coordinate, where the click occured.
597 * @param info object containing information about the plot dimensions.
598 */
599 public void handleClick(int x, int y, PlotRenderingInfo info) {
600
601 Rectangle2D dataArea = info.getDataArea();
602 if (dataArea.contains(x, y)) {
603 for (int i = 0; i < this.subplots.size(); i++) {
604 XYPlot subplot = (XYPlot) this.subplots.get(i);
605 PlotRenderingInfo subplotInfo = info.getSubplotInfo(i);
606 subplot.handleClick(x, y, subplotInfo);
607 }
608 }
609
610 }
611
612 /**
613 * Receives a {@link PlotChangeEvent} and responds by notifying all
614 * listeners.
615 *
616 * @param event the event.
617 */
618 public void plotChanged(PlotChangeEvent event) {
619 notifyListeners(event);
620 }
621
622 /**
623 * Tests this plot for equality with another object.
624 *
625 * @param obj the other object.
626 *
627 * @return <code>true</code> or <code>false</code>.
628 */
629 public boolean equals(Object obj) {
630
631 if (obj == this) {
632 return true;
633 }
634
635 if (!(obj instanceof CombinedRangeXYPlot)) {
636 return false;
637 }
638 if (!super.equals(obj)) {
639 return false;
640 }
641 CombinedRangeXYPlot that = (CombinedRangeXYPlot) obj;
642 if (!ObjectUtilities.equal(this.subplots, that.subplots)) {
643 return false;
644 }
645 if (this.totalWeight != that.totalWeight) {
646 return false;
647 }
648 if (this.gap != that.gap) {
649 return false;
650 }
651 return true;
652 }
653
654 /**
655 * Returns a clone of the plot.
656 *
657 * @return A clone.
658 *
659 * @throws CloneNotSupportedException this class will not throw this
660 * exception, but subclasses (if any) might.
661 */
662 public Object clone() throws CloneNotSupportedException {
663
664 CombinedRangeXYPlot result = (CombinedRangeXYPlot) super.clone();
665 result.subplots = (List) ObjectUtilities.deepClone(this.subplots);
666 for (Iterator it = result.subplots.iterator(); it.hasNext();) {
667 Plot child = (Plot) it.next();
668 child.setParent(result);
669 }
670
671 // after setting up all the subplots, the shared range axis may need
672 // reconfiguring
673 ValueAxis rangeAxis = result.getRangeAxis();
674 if (rangeAxis != null) {
675 rangeAxis.configure();
676 }
677
678 return result;
679 }
680
681 }