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 *
089 */
090
091 package org.jfree.chart.plot;
092
093 import java.awt.Graphics2D;
094 import java.awt.geom.Point2D;
095 import java.awt.geom.Rectangle2D;
096 import java.io.Serializable;
097 import java.util.Collections;
098 import java.util.Iterator;
099 import java.util.List;
100
101 import org.jfree.chart.LegendItemCollection;
102 import org.jfree.chart.axis.AxisSpace;
103 import org.jfree.chart.axis.AxisState;
104 import org.jfree.chart.axis.NumberAxis;
105 import org.jfree.chart.axis.ValueAxis;
106 import org.jfree.chart.event.PlotChangeEvent;
107 import org.jfree.chart.event.PlotChangeListener;
108 import org.jfree.chart.renderer.xy.XYItemRenderer;
109 import org.jfree.data.Range;
110 import org.jfree.ui.RectangleEdge;
111 import org.jfree.ui.RectangleInsets;
112 import org.jfree.util.ObjectUtilities;
113 import org.jfree.util.PublicCloneable;
114
115 /**
116 * An extension of {@link XYPlot} that contains multiple subplots that share a
117 * common range axis.
118 */
119 public class CombinedRangeXYPlot extends XYPlot
120 implements Zoomable,
121 Cloneable, PublicCloneable,
122 Serializable,
123 PlotChangeListener {
124
125 /** For serialization. */
126 private static final long serialVersionUID = -5177814085082031168L;
127
128 /** Storage for the subplot references. */
129 private List subplots;
130
131 /** Total weight of all charts. */
132 private int totalWeight = 0;
133
134 /** The gap between subplots. */
135 private double gap = 5.0;
136
137 /** Temporary storage for the subplot areas. */
138 private transient Rectangle2D[] subplotAreas;
139
140 /**
141 * Default constructor.
142 */
143 public CombinedRangeXYPlot() {
144 this(new NumberAxis());
145 }
146
147 /**
148 * Creates a new plot.
149 *
150 * @param rangeAxis the shared axis.
151 */
152 public CombinedRangeXYPlot(ValueAxis rangeAxis) {
153
154 super(null, // no data in the parent plot
155 null,
156 rangeAxis,
157 null);
158
159 this.subplots = new java.util.ArrayList();
160
161 }
162
163 /**
164 * Returns a string describing the type of plot.
165 *
166 * @return The type of plot.
167 */
168 public String getPlotType() {
169 return localizationResources.getString("Combined_Range_XYPlot");
170 }
171
172 /**
173 * Returns the space between subplots.
174 *
175 * @return The gap
176 */
177 public double getGap() {
178 return this.gap;
179 }
180
181 /**
182 * Sets the amount of space between subplots.
183 *
184 * @param gap the gap between subplots
185 */
186 public void setGap(double gap) {
187 this.gap = gap;
188 }
189
190 /**
191 * Adds a subplot, with a default 'weight' of 1.
192 * <br><br>
193 * You must ensure that the subplot has a non-null domain axis. The range
194 * axis for the subplot will be set to <code>null</code>.
195 *
196 * @param subplot the subplot.
197 */
198 public void add(XYPlot subplot) {
199 add(subplot, 1);
200 }
201
202 /**
203 * Adds a subplot with a particular weight (greater than or equal to one).
204 * The weight determines how much space is allocated to the subplot
205 * relative to all the other subplots.
206 * <br><br>
207 * You must ensure that the subplot has a non-null domain axis. The range
208 * axis for the subplot will be set to <code>null</code>.
209 *
210 * @param subplot the subplot.
211 * @param weight the weight (must be 1 or greater).
212 */
213 public void add(XYPlot subplot, int weight) {
214
215 // verify valid weight
216 if (weight <= 0) {
217 String msg = "The 'weight' must be positive.";
218 throw new IllegalArgumentException(msg);
219 }
220
221 // store the plot and its weight
222 subplot.setParent(this);
223 subplot.setWeight(weight);
224 subplot.setInsets(new RectangleInsets(0.0, 0.0, 0.0, 0.0));
225 subplot.setRangeAxis(null);
226 subplot.addChangeListener(this);
227 this.subplots.add(subplot);
228
229 // keep track of total weights
230 this.totalWeight += weight;
231 configureRangeAxes();
232 notifyListeners(new PlotChangeEvent(this));
233
234 }
235
236 /**
237 * Removes a subplot from the combined chart.
238 *
239 * @param subplot the subplot (<code>null</code> not permitted).
240 */
241 public void remove(XYPlot subplot) {
242 if (subplot == null) {
243 throw new IllegalArgumentException(" Null 'subplot' argument.");
244 }
245 int position = -1;
246 int size = this.subplots.size();
247 int i = 0;
248 while (position == -1 && i < size) {
249 if (this.subplots.get(i) == subplot) {
250 position = i;
251 }
252 i++;
253 }
254 if (position != -1) {
255 this.subplots.remove(position);
256 subplot.setParent(null);
257 subplot.removeChangeListener(this);
258 this.totalWeight -= subplot.getWeight();
259 configureRangeAxes();
260 notifyListeners(new PlotChangeEvent(this));
261 }
262 }
263
264 /**
265 * Returns a list of the subplots.
266 *
267 * @return The list (unmodifiable).
268 */
269 public List getSubplots() {
270 return Collections.unmodifiableList(this.subplots);
271 }
272
273 /**
274 * Calculates the space required for the axes.
275 *
276 * @param g2 the graphics device.
277 * @param plotArea the plot area.
278 *
279 * @return The space required for the axes.
280 */
281 protected AxisSpace calculateAxisSpace(Graphics2D g2,
282 Rectangle2D plotArea) {
283
284 AxisSpace space = new AxisSpace();
285 PlotOrientation orientation = getOrientation();
286
287 // work out the space required by the domain axis...
288 AxisSpace fixed = getFixedRangeAxisSpace();
289 if (fixed != null) {
290 if (orientation == PlotOrientation.VERTICAL) {
291 space.setLeft(fixed.getLeft());
292 space.setRight(fixed.getRight());
293 }
294 else if (orientation == PlotOrientation.HORIZONTAL) {
295 space.setTop(fixed.getTop());
296 space.setBottom(fixed.getBottom());
297 }
298 }
299 else {
300 ValueAxis valueAxis = getRangeAxis();
301 RectangleEdge valueEdge = Plot.resolveRangeAxisLocation(
302 getRangeAxisLocation(), orientation
303 );
304 if (valueAxis != null) {
305 space = valueAxis.reserveSpace(g2, this, plotArea, valueEdge,
306 space);
307 }
308 }
309
310 Rectangle2D adjustedPlotArea = space.shrink(plotArea, null);
311 // work out the maximum height or width of the non-shared axes...
312 int n = this.subplots.size();
313
314 // calculate plotAreas of all sub-plots, maximum vertical/horizontal
315 // axis width/height
316 this.subplotAreas = new Rectangle2D[n];
317 double x = adjustedPlotArea.getX();
318 double y = adjustedPlotArea.getY();
319 double usableSize = 0.0;
320 if (orientation == PlotOrientation.VERTICAL) {
321 usableSize = adjustedPlotArea.getWidth() - this.gap * (n - 1);
322 }
323 else if (orientation == PlotOrientation.HORIZONTAL) {
324 usableSize = adjustedPlotArea.getHeight() - this.gap * (n - 1);
325 }
326
327 for (int i = 0; i < n; i++) {
328 XYPlot plot = (XYPlot) this.subplots.get(i);
329
330 // calculate sub-plot area
331 if (orientation == PlotOrientation.VERTICAL) {
332 double w = usableSize * plot.getWeight() / this.totalWeight;
333 this.subplotAreas[i] = new Rectangle2D.Double(x, y, w,
334 adjustedPlotArea.getHeight());
335 x = x + w + this.gap;
336 }
337 else if (orientation == PlotOrientation.HORIZONTAL) {
338 double h = usableSize * plot.getWeight() / this.totalWeight;
339 this.subplotAreas[i] = new Rectangle2D.Double(x, y,
340 adjustedPlotArea.getWidth(), h);
341 y = y + h + this.gap;
342 }
343
344 AxisSpace subSpace = plot.calculateDomainAxisSpace(g2,
345 this.subplotAreas[i], null);
346 space.ensureAtLeast(subSpace);
347
348 }
349
350 return space;
351 }
352
353 /**
354 * Draws the plot within the specified area on a graphics device.
355 *
356 * @param g2 the graphics device.
357 * @param area the plot area (in Java2D space).
358 * @param anchor an anchor point in Java2D space (<code>null</code>
359 * permitted).
360 * @param parentState the state from the parent plot, if there is one
361 * (<code>null</code> permitted).
362 * @param info collects chart drawing information (<code>null</code>
363 * permitted).
364 */
365 public void draw(Graphics2D g2,
366 Rectangle2D area,
367 Point2D anchor,
368 PlotState parentState,
369 PlotRenderingInfo info) {
370
371 // set up info collection...
372 if (info != null) {
373 info.setPlotArea(area);
374 }
375
376 // adjust the drawing area for plot insets (if any)...
377 RectangleInsets insets = getInsets();
378 insets.trim(area);
379
380 AxisSpace space = calculateAxisSpace(g2, area);
381 Rectangle2D dataArea = space.shrink(area, null);
382 //this.axisOffset.trim(dataArea);
383
384 // set the width and height of non-shared axis of all sub-plots
385 setFixedDomainAxisSpaceForSubplots(space);
386
387 // draw the shared axis
388 ValueAxis axis = getRangeAxis();
389 RectangleEdge edge = getRangeAxisEdge();
390 double cursor = RectangleEdge.coordinate(dataArea, edge);
391 AxisState axisState = axis.draw(g2, cursor, area, dataArea, edge, info);
392
393 if (parentState == null) {
394 parentState = new PlotState();
395 }
396 parentState.getSharedAxisStates().put(axis, axisState);
397
398 // draw all the charts
399 for (int i = 0; i < this.subplots.size(); i++) {
400 XYPlot plot = (XYPlot) this.subplots.get(i);
401 PlotRenderingInfo subplotInfo = null;
402 if (info != null) {
403 subplotInfo = new PlotRenderingInfo(info.getOwner());
404 info.addSubplotInfo(subplotInfo);
405 }
406 plot.draw(g2, this.subplotAreas[i], anchor, parentState,
407 subplotInfo);
408 }
409
410 if (info != null) {
411 info.setDataArea(dataArea);
412 }
413
414 }
415
416 /**
417 * Returns a collection of legend items for the plot.
418 *
419 * @return The legend items.
420 */
421 public LegendItemCollection getLegendItems() {
422 LegendItemCollection result = getFixedLegendItems();
423 if (result == null) {
424 result = new LegendItemCollection();
425
426 if (this.subplots != null) {
427 Iterator iterator = this.subplots.iterator();
428 while (iterator.hasNext()) {
429 XYPlot plot = (XYPlot) iterator.next();
430 LegendItemCollection more = plot.getLegendItems();
431 result.addAll(more);
432 }
433 }
434 }
435 return result;
436 }
437
438 /**
439 * Multiplies the range on the domain axis/axes by the specified factor.
440 *
441 * @param factor the zoom factor.
442 * @param info the plot rendering info (<code>null</code> not permitted).
443 * @param source the source point (<code>null</code> not permitted).
444 */
445 public void zoomDomainAxes(double factor, PlotRenderingInfo info,
446 Point2D source) {
447 // delegate 'info' and 'source' argument checks...
448 XYPlot subplot = findSubplot(info, source);
449 if (subplot != null) {
450 subplot.zoomDomainAxes(factor, info, source);
451 }
452 else {
453 // if the source point doesn't fall within a subplot, we do the
454 // zoom on all subplots...
455 Iterator iterator = getSubplots().iterator();
456 while (iterator.hasNext()) {
457 subplot = (XYPlot) iterator.next();
458 subplot.zoomDomainAxes(factor, info, source);
459 }
460 }
461 }
462
463 /**
464 * Zooms in on the domain axes.
465 *
466 * @param lowerPercent the lower bound.
467 * @param upperPercent the upper bound.
468 * @param info the plot rendering info (<code>null</code> not permitted).
469 * @param source the source point (<code>null</code> not permitted).
470 */
471 public void zoomDomainAxes(double lowerPercent, double upperPercent,
472 PlotRenderingInfo info, Point2D source) {
473 // delegate 'info' and 'source' argument checks...
474 XYPlot subplot = findSubplot(info, source);
475 if (subplot != null) {
476 subplot.zoomDomainAxes(lowerPercent, upperPercent, info, source);
477 }
478 else {
479 // if the source point doesn't fall within a subplot, we do the
480 // zoom on all subplots...
481 Iterator iterator = getSubplots().iterator();
482 while (iterator.hasNext()) {
483 subplot = (XYPlot) iterator.next();
484 subplot.zoomDomainAxes(lowerPercent, upperPercent, info,
485 source);
486 }
487 }
488 }
489
490 /**
491 * Returns the subplot (if any) that contains the (x, y) point (specified
492 * in Java2D space).
493 *
494 * @param info the chart rendering info (<code>null</code> not permitted).
495 * @param source the source point (<code>null</code> not permitted).
496 *
497 * @return A subplot (possibly <code>null</code>).
498 */
499 public XYPlot findSubplot(PlotRenderingInfo info, Point2D source) {
500 if (info == null) {
501 throw new IllegalArgumentException("Null 'info' argument.");
502 }
503 if (source == null) {
504 throw new IllegalArgumentException("Null 'source' argument.");
505 }
506 XYPlot result = null;
507 int subplotIndex = info.getSubplotIndex(source);
508 if (subplotIndex >= 0) {
509 result = (XYPlot) this.subplots.get(subplotIndex);
510 }
511 return result;
512 }
513
514 /**
515 * Sets the item renderer FOR ALL SUBPLOTS. Registered listeners are
516 * notified that the plot has been modified.
517 * <P>
518 * Note: usually you will want to set the renderer independently for each
519 * subplot, which is NOT what this method does.
520 *
521 * @param renderer the new renderer.
522 */
523 public void setRenderer(XYItemRenderer renderer) {
524
525 super.setRenderer(renderer); // not strictly necessary, since the
526 // renderer set for the
527 // parent plot is not used
528
529 Iterator iterator = this.subplots.iterator();
530 while (iterator.hasNext()) {
531 XYPlot plot = (XYPlot) iterator.next();
532 plot.setRenderer(renderer);
533 }
534
535 }
536
537 /**
538 * Sets the orientation for the plot (and all its subplots).
539 *
540 * @param orientation the orientation.
541 */
542 public void setOrientation(PlotOrientation orientation) {
543
544 super.setOrientation(orientation);
545
546 Iterator iterator = this.subplots.iterator();
547 while (iterator.hasNext()) {
548 XYPlot plot = (XYPlot) iterator.next();
549 plot.setOrientation(orientation);
550 }
551
552 }
553
554 /**
555 * Returns the range for the axis. This is the combined range of all the
556 * subplots.
557 *
558 * @param axis the axis.
559 *
560 * @return The range.
561 */
562 public Range getDataRange(ValueAxis axis) {
563
564 Range result = null;
565 if (this.subplots != null) {
566 Iterator iterator = this.subplots.iterator();
567 while (iterator.hasNext()) {
568 XYPlot subplot = (XYPlot) iterator.next();
569 result = Range.combine(result, subplot.getDataRange(axis));
570 }
571 }
572 return result;
573
574 }
575
576 /**
577 * Sets the space (width or height, depending on the orientation of the
578 * plot) for the domain axis of each subplot.
579 *
580 * @param space the space.
581 */
582 protected void setFixedDomainAxisSpaceForSubplots(AxisSpace space) {
583
584 Iterator iterator = this.subplots.iterator();
585 while (iterator.hasNext()) {
586 XYPlot plot = (XYPlot) iterator.next();
587 plot.setFixedDomainAxisSpace(space);
588 }
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 }