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 * $Id: CombinedRangeXYPlot.java,v 1.10.2.4 2007/03/23 14:38:52 mungady Exp $
042 *
043 * Changes:
044 * --------
045 * 06-Dec-2001 : Version 1 (BK);
046 * 12-Dec-2001 : Removed unnecessary 'throws' clause from constructor (DG);
047 * 18-Dec-2001 : Added plotArea attribute and get/set methods (BK);
048 * 22-Dec-2001 : Fixed bug in chartChanged with multiple combinations of
049 * CombinedPlots (BK);
050 * 08-Jan-2002 : Moved to new package com.jrefinery.chart.combination (DG);
051 * 25-Feb-2002 : Updated import statements (DG);
052 * 28-Feb-2002 : Readded "this.plotArea = plotArea" that was deleted from
053 * draw() method (BK);
054 * 26-Mar-2002 : Added an empty zoom method (this method needs to be written
055 * so that combined plots will support zooming (DG);
056 * 29-Mar-2002 : Changed the method createCombinedAxis adding the creation of
057 * OverlaidSymbolicAxis and CombinedSymbolicAxis(AB);
058 * 23-Apr-2002 : Renamed CombinedPlot-->MultiXYPlot, and simplified the
059 * structure (DG);
060 * 23-May-2002 : Renamed (again) MultiXYPlot-->CombinedXYPlot (DG);
061 * 19-Jun-2002 : Added get/setGap() methods suggested by David Basten (DG);
062 * 25-Jun-2002 : Removed redundant imports (DG);
063 * 16-Jul-2002 : Draws shared axis after subplots (to fix missing gridlines),
064 * added overrides of 'setSeriesPaint()' and 'setXYItemRenderer()'
065 * that pass changes down to subplots (KF);
066 * 09-Oct-2002 : Added add(XYPlot) method (DG);
067 * 26-Mar-2003 : Implemented Serializable (DG);
068 * 16-May-2003 : Renamed CombinedXYPlot --> CombinedRangeXYPlot (DG);
069 * 26-Jun-2003 : Fixed bug 755547 (DG);
070 * 16-Jul-2003 : Removed getSubPlots() method (duplicate of getSubplots()) (DG);
071 * 08-Aug-2003 : Adjusted totalWeight in remove() method (DG);
072 * 21-Aug-2003 : Implemented Cloneable (DG);
073 * 08-Sep-2003 : Added internationalization via use of properties
074 * resourceBundle (RFE 690236) (AL);
075 * 11-Sep-2003 : Fix cloning support (subplots) (NB);
076 * 15-Sep-2003 : Fixed error in cloning (DG);
077 * 16-Sep-2003 : Changed ChartRenderingInfo --> PlotRenderingInfo (DG);
078 * 17-Sep-2003 : Updated handling of 'clicks' (DG);
079 * 12-Nov-2004 : Implements the new Zoomable interface (DG);
080 * 25-Nov-2004 : Small update to clone() implementation (DG);
081 * 21-Feb-2005 : The getLegendItems() method now returns the fixed legend
082 * items if set (DG);
083 * 05-May-2005 : Removed unused draw() method (DG);
084 * ------------- JFREECHART 1.0.x ---------------------------------------------
085 * 13-Sep-2006 : Updated API docs (DG);
086 * 06-Feb-2007 : Fixed bug 1606205, draw shared axis after subplots (DG);
087 * 23-Mar-2007 : Reverted previous patch (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 subplot.setParent(null);
256 subplot.removeChangeListener(this);
257 this.totalWeight -= subplot.getWeight();
258 configureRangeAxes();
259 notifyListeners(new PlotChangeEvent(this));
260 }
261 }
262
263 /**
264 * Returns a list of the subplots.
265 *
266 * @return The list (unmodifiable).
267 */
268 public List getSubplots() {
269 return Collections.unmodifiableList(this.subplots);
270 }
271
272 /**
273 * Calculates the space required for the axes.
274 *
275 * @param g2 the graphics device.
276 * @param plotArea the plot area.
277 *
278 * @return The space required for the axes.
279 */
280 protected AxisSpace calculateAxisSpace(Graphics2D g2,
281 Rectangle2D plotArea) {
282
283 AxisSpace space = new AxisSpace();
284 PlotOrientation orientation = getOrientation();
285
286 // work out the space required by the domain axis...
287 AxisSpace fixed = getFixedRangeAxisSpace();
288 if (fixed != null) {
289 if (orientation == PlotOrientation.VERTICAL) {
290 space.setLeft(fixed.getLeft());
291 space.setRight(fixed.getRight());
292 }
293 else if (orientation == PlotOrientation.HORIZONTAL) {
294 space.setTop(fixed.getTop());
295 space.setBottom(fixed.getBottom());
296 }
297 }
298 else {
299 ValueAxis valueAxis = getRangeAxis();
300 RectangleEdge valueEdge = Plot.resolveRangeAxisLocation(
301 getRangeAxisLocation(), orientation
302 );
303 if (valueAxis != null) {
304 space = valueAxis.reserveSpace(g2, this, plotArea, valueEdge,
305 space);
306 }
307 }
308
309 Rectangle2D adjustedPlotArea = space.shrink(plotArea, null);
310 // work out the maximum height or width of the non-shared axes...
311 int n = this.subplots.size();
312
313 // calculate plotAreas of all sub-plots, maximum vertical/horizontal
314 // axis width/height
315 this.subplotAreas = new Rectangle2D[n];
316 double x = adjustedPlotArea.getX();
317 double y = adjustedPlotArea.getY();
318 double usableSize = 0.0;
319 if (orientation == PlotOrientation.VERTICAL) {
320 usableSize = adjustedPlotArea.getWidth() - this.gap * (n - 1);
321 }
322 else if (orientation == PlotOrientation.HORIZONTAL) {
323 usableSize = adjustedPlotArea.getHeight() - this.gap * (n - 1);
324 }
325
326 for (int i = 0; i < n; i++) {
327 XYPlot plot = (XYPlot) this.subplots.get(i);
328
329 // calculate sub-plot area
330 if (orientation == PlotOrientation.VERTICAL) {
331 double w = usableSize * plot.getWeight() / this.totalWeight;
332 this.subplotAreas[i] = new Rectangle2D.Double(x, y, w,
333 adjustedPlotArea.getHeight());
334 x = x + w + this.gap;
335 }
336 else if (orientation == PlotOrientation.HORIZONTAL) {
337 double h = usableSize * plot.getWeight() / this.totalWeight;
338 this.subplotAreas[i] = new Rectangle2D.Double(x, y,
339 adjustedPlotArea.getWidth(), h);
340 y = y + h + this.gap;
341 }
342
343 AxisSpace subSpace = plot.calculateDomainAxisSpace(g2,
344 this.subplotAreas[i], null);
345 space.ensureAtLeast(subSpace);
346
347 }
348
349 return space;
350 }
351
352 /**
353 * Draws the plot within the specified area on a graphics device.
354 *
355 * @param g2 the graphics device.
356 * @param area the plot area (in Java2D space).
357 * @param anchor an anchor point in Java2D space (<code>null</code>
358 * permitted).
359 * @param parentState the state from the parent plot, if there is one
360 * (<code>null</code> permitted).
361 * @param info collects chart drawing information (<code>null</code>
362 * permitted).
363 */
364 public void draw(Graphics2D g2,
365 Rectangle2D area,
366 Point2D anchor,
367 PlotState parentState,
368 PlotRenderingInfo info) {
369
370 // set up info collection...
371 if (info != null) {
372 info.setPlotArea(area);
373 }
374
375 // adjust the drawing area for plot insets (if any)...
376 RectangleInsets insets = getInsets();
377 insets.trim(area);
378
379 AxisSpace space = calculateAxisSpace(g2, area);
380 Rectangle2D dataArea = space.shrink(area, null);
381 //this.axisOffset.trim(dataArea);
382
383 // set the width and height of non-shared axis of all sub-plots
384 setFixedDomainAxisSpaceForSubplots(space);
385
386 // draw the shared axis
387 ValueAxis axis = getRangeAxis();
388 RectangleEdge edge = getRangeAxisEdge();
389 double cursor = RectangleEdge.coordinate(dataArea, edge);
390 AxisState axisState = axis.draw(g2, cursor, area, dataArea, edge, info);
391
392 if (parentState == null) {
393 parentState = new PlotState();
394 }
395 parentState.getSharedAxisStates().put(axis, axisState);
396
397 // draw all the charts
398 for (int i = 0; i < this.subplots.size(); i++) {
399 XYPlot plot = (XYPlot) this.subplots.get(i);
400 PlotRenderingInfo subplotInfo = null;
401 if (info != null) {
402 subplotInfo = new PlotRenderingInfo(info.getOwner());
403 info.addSubplotInfo(subplotInfo);
404 }
405 plot.draw(g2, this.subplotAreas[i], anchor, parentState,
406 subplotInfo);
407 }
408
409 if (info != null) {
410 info.setDataArea(dataArea);
411 }
412
413 }
414
415 /**
416 * Returns a collection of legend items for the plot.
417 *
418 * @return The legend items.
419 */
420 public LegendItemCollection getLegendItems() {
421 LegendItemCollection result = getFixedLegendItems();
422 if (result == null) {
423 result = new LegendItemCollection();
424
425 if (this.subplots != null) {
426 Iterator iterator = this.subplots.iterator();
427 while (iterator.hasNext()) {
428 XYPlot plot = (XYPlot) iterator.next();
429 LegendItemCollection more = plot.getLegendItems();
430 result.addAll(more);
431 }
432 }
433 }
434 return result;
435 }
436
437 /**
438 * Multiplies the range on the domain axis/axes by the specified factor.
439 *
440 * @param factor the zoom factor.
441 * @param info the plot rendering info.
442 * @param source the source point.
443 */
444 public void zoomDomainAxes(double factor, PlotRenderingInfo info,
445 Point2D source) {
446 XYPlot subplot = findSubplot(info, source);
447 if (subplot != null) {
448 subplot.zoomDomainAxes(factor, info, source);
449 }
450 }
451
452 /**
453 * Zooms in on the domain axes.
454 *
455 * @param lowerPercent the lower bound.
456 * @param upperPercent the upper bound.
457 * @param info the plot rendering info.
458 * @param source the source point.
459 */
460 public void zoomDomainAxes(double lowerPercent, double upperPercent,
461 PlotRenderingInfo info, Point2D source) {
462 XYPlot subplot = findSubplot(info, source);
463 if (subplot != null) {
464 subplot.zoomDomainAxes(lowerPercent, upperPercent, info, source);
465 }
466 }
467
468 /**
469 * Returns the subplot (if any) that contains the (x, y) point (specified
470 * in Java2D space).
471 *
472 * @param info the chart rendering info.
473 * @param source the source point.
474 *
475 * @return A subplot (possibly <code>null</code>).
476 */
477 public XYPlot findSubplot(PlotRenderingInfo info, Point2D source) {
478 XYPlot result = null;
479 int subplotIndex = info.getSubplotIndex(source);
480 if (subplotIndex >= 0) {
481 result = (XYPlot) this.subplots.get(subplotIndex);
482 }
483 return result;
484 }
485
486 /**
487 * Sets the item renderer FOR ALL SUBPLOTS. Registered listeners are
488 * notified that the plot has been modified.
489 * <P>
490 * Note: usually you will want to set the renderer independently for each
491 * subplot, which is NOT what this method does.
492 *
493 * @param renderer the new renderer.
494 */
495 public void setRenderer(XYItemRenderer renderer) {
496
497 super.setRenderer(renderer); // not strictly necessary, since the
498 // renderer set for the
499 // parent plot is not used
500
501 Iterator iterator = this.subplots.iterator();
502 while (iterator.hasNext()) {
503 XYPlot plot = (XYPlot) iterator.next();
504 plot.setRenderer(renderer);
505 }
506
507 }
508
509 /**
510 * Sets the orientation for the plot (and all its subplots).
511 *
512 * @param orientation the orientation.
513 */
514 public void setOrientation(PlotOrientation orientation) {
515
516 super.setOrientation(orientation);
517
518 Iterator iterator = this.subplots.iterator();
519 while (iterator.hasNext()) {
520 XYPlot plot = (XYPlot) iterator.next();
521 plot.setOrientation(orientation);
522 }
523
524 }
525
526 /**
527 * Returns the range for the axis. This is the combined range of all the
528 * subplots.
529 *
530 * @param axis the axis.
531 *
532 * @return The range.
533 */
534 public Range getDataRange(ValueAxis axis) {
535
536 Range result = null;
537 if (this.subplots != null) {
538 Iterator iterator = this.subplots.iterator();
539 while (iterator.hasNext()) {
540 XYPlot subplot = (XYPlot) iterator.next();
541 result = Range.combine(result, subplot.getDataRange(axis));
542 }
543 }
544 return result;
545
546 }
547
548 /**
549 * Sets the space (width or height, depending on the orientation of the
550 * plot) for the domain axis of each subplot.
551 *
552 * @param space the space.
553 */
554 protected void setFixedDomainAxisSpaceForSubplots(AxisSpace space) {
555
556 Iterator iterator = this.subplots.iterator();
557 while (iterator.hasNext()) {
558 XYPlot plot = (XYPlot) iterator.next();
559 plot.setFixedDomainAxisSpace(space);
560 }
561
562 }
563
564 /**
565 * Handles a 'click' on the plot by updating the anchor values...
566 *
567 * @param x x-coordinate, where the click occured.
568 * @param y y-coordinate, where the click occured.
569 * @param info object containing information about the plot dimensions.
570 */
571 public void handleClick(int x, int y, PlotRenderingInfo info) {
572
573 Rectangle2D dataArea = info.getDataArea();
574 if (dataArea.contains(x, y)) {
575 for (int i = 0; i < this.subplots.size(); i++) {
576 XYPlot subplot = (XYPlot) this.subplots.get(i);
577 PlotRenderingInfo subplotInfo = info.getSubplotInfo(i);
578 subplot.handleClick(x, y, subplotInfo);
579 }
580 }
581
582 }
583
584 /**
585 * Receives a {@link PlotChangeEvent} and responds by notifying all
586 * listeners.
587 *
588 * @param event the event.
589 */
590 public void plotChanged(PlotChangeEvent event) {
591 notifyListeners(event);
592 }
593
594 /**
595 * Tests this plot for equality with another object.
596 *
597 * @param obj the other object.
598 *
599 * @return <code>true</code> or <code>false</code>.
600 */
601 public boolean equals(Object obj) {
602
603 if (obj == this) {
604 return true;
605 }
606
607 if (!(obj instanceof CombinedRangeXYPlot)) {
608 return false;
609 }
610 if (!super.equals(obj)) {
611 return false;
612 }
613 CombinedRangeXYPlot that = (CombinedRangeXYPlot) obj;
614 if (!ObjectUtilities.equal(this.subplots, that.subplots)) {
615 return false;
616 }
617 if (this.totalWeight != that.totalWeight) {
618 return false;
619 }
620 if (this.gap != that.gap) {
621 return false;
622 }
623 return true;
624 }
625
626 /**
627 * Returns a clone of the plot.
628 *
629 * @return A clone.
630 *
631 * @throws CloneNotSupportedException this class will not throw this
632 * exception, but subclasses (if any) might.
633 */
634 public Object clone() throws CloneNotSupportedException {
635
636 CombinedRangeXYPlot result = (CombinedRangeXYPlot) super.clone();
637 result.subplots = (List) ObjectUtilities.deepClone(this.subplots);
638 for (Iterator it = result.subplots.iterator(); it.hasNext();) {
639 Plot child = (Plot) it.next();
640 child.setParent(result);
641 }
642
643 // after setting up all the subplots, the shared range axis may need
644 // reconfiguring
645 ValueAxis rangeAxis = result.getRangeAxis();
646 if (rangeAxis != null) {
647 rangeAxis.configure();
648 }
649
650 return result;
651 }
652
653 }