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 * CombinedRangeXYPlot.java
029 * ------------------------
030 * (C) Copyright 2001-2008, 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 * 27-Mar-2008 : Add documentation for getDataRange() method (DG);
091 * 31-Mar-2008 : Updated getSubplots() to return EMPTY_LIST for null
092 * subplots, as suggested by Richard West (DG);
093 * 28-Apr-2008 : Fixed zooming problem (see bug 1950037) (DG);
094 * 11-Aug-2008 : Don't store totalWeight of subplots, calculate it as
095 * required (DG);
096 *
097 */
098
099 package org.jfree.chart.plot;
100
101 import java.awt.Graphics2D;
102 import java.awt.geom.Point2D;
103 import java.awt.geom.Rectangle2D;
104 import java.util.Collections;
105 import java.util.Iterator;
106 import java.util.List;
107
108 import org.jfree.chart.LegendItemCollection;
109 import org.jfree.chart.axis.AxisSpace;
110 import org.jfree.chart.axis.AxisState;
111 import org.jfree.chart.axis.NumberAxis;
112 import org.jfree.chart.axis.ValueAxis;
113 import org.jfree.chart.event.PlotChangeEvent;
114 import org.jfree.chart.event.PlotChangeListener;
115 import org.jfree.chart.renderer.xy.XYItemRenderer;
116 import org.jfree.data.Range;
117 import org.jfree.ui.RectangleEdge;
118 import org.jfree.ui.RectangleInsets;
119 import org.jfree.util.ObjectUtilities;
120
121 /**
122 * An extension of {@link XYPlot} that contains multiple subplots that share a
123 * common range axis.
124 */
125 public class CombinedRangeXYPlot extends XYPlot
126 implements PlotChangeListener {
127
128 /** For serialization. */
129 private static final long serialVersionUID = -5177814085082031168L;
130
131 /** Storage for the subplot references. */
132 private List subplots;
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 configureRangeAxes();
229 fireChangeEvent();
230
231 }
232
233 /**
234 * Removes a subplot from the combined chart.
235 *
236 * @param subplot the subplot (<code>null</code> not permitted).
237 */
238 public void remove(XYPlot subplot) {
239 if (subplot == null) {
240 throw new IllegalArgumentException(" Null 'subplot' argument.");
241 }
242 int position = -1;
243 int size = this.subplots.size();
244 int i = 0;
245 while (position == -1 && i < size) {
246 if (this.subplots.get(i) == subplot) {
247 position = i;
248 }
249 i++;
250 }
251 if (position != -1) {
252 this.subplots.remove(position);
253 subplot.setParent(null);
254 subplot.removeChangeListener(this);
255 configureRangeAxes();
256 fireChangeEvent();
257 }
258 }
259
260 /**
261 * Returns the list of subplots. The returned list may be empty, but is
262 * never <code>null</code>.
263 *
264 * @return An unmodifiable list of subplots.
265 */
266 public List getSubplots() {
267 if (this.subplots != null) {
268 return Collections.unmodifiableList(this.subplots);
269 }
270 else {
271 return Collections.EMPTY_LIST;
272 }
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 int totalWeight = 0;
316 for (int i = 0; i < n; i++) {
317 XYPlot sub = (XYPlot) this.subplots.get(i);
318 totalWeight += sub.getWeight();
319 }
320
321 // calculate plotAreas of all sub-plots, maximum vertical/horizontal
322 // axis width/height
323 this.subplotAreas = new Rectangle2D[n];
324 double x = adjustedPlotArea.getX();
325 double y = adjustedPlotArea.getY();
326 double usableSize = 0.0;
327 if (orientation == PlotOrientation.VERTICAL) {
328 usableSize = adjustedPlotArea.getWidth() - this.gap * (n - 1);
329 }
330 else if (orientation == PlotOrientation.HORIZONTAL) {
331 usableSize = adjustedPlotArea.getHeight() - this.gap * (n - 1);
332 }
333
334 for (int i = 0; i < n; i++) {
335 XYPlot plot = (XYPlot) this.subplots.get(i);
336
337 // calculate sub-plot area
338 if (orientation == PlotOrientation.VERTICAL) {
339 double w = usableSize * plot.getWeight() / totalWeight;
340 this.subplotAreas[i] = new Rectangle2D.Double(x, y, w,
341 adjustedPlotArea.getHeight());
342 x = x + w + this.gap;
343 }
344 else if (orientation == PlotOrientation.HORIZONTAL) {
345 double h = usableSize * plot.getWeight() / totalWeight;
346 this.subplotAreas[i] = new Rectangle2D.Double(x, y,
347 adjustedPlotArea.getWidth(), h);
348 y = y + h + this.gap;
349 }
350
351 AxisSpace subSpace = plot.calculateDomainAxisSpace(g2,
352 this.subplotAreas[i], null);
353 space.ensureAtLeast(subSpace);
354
355 }
356
357 return space;
358 }
359
360 /**
361 * Draws the plot within the specified area on a graphics device.
362 *
363 * @param g2 the graphics device.
364 * @param area the plot area (in Java2D space).
365 * @param anchor an anchor point in Java2D space (<code>null</code>
366 * permitted).
367 * @param parentState the state from the parent plot, if there is one
368 * (<code>null</code> permitted).
369 * @param info collects chart drawing information (<code>null</code>
370 * permitted).
371 */
372 public void draw(Graphics2D g2,
373 Rectangle2D area,
374 Point2D anchor,
375 PlotState parentState,
376 PlotRenderingInfo info) {
377
378 // set up info collection...
379 if (info != null) {
380 info.setPlotArea(area);
381 }
382
383 // adjust the drawing area for plot insets (if any)...
384 RectangleInsets insets = getInsets();
385 insets.trim(area);
386
387 AxisSpace space = calculateAxisSpace(g2, area);
388 Rectangle2D dataArea = space.shrink(area, null);
389 //this.axisOffset.trim(dataArea);
390
391 // set the width and height of non-shared axis of all sub-plots
392 setFixedDomainAxisSpaceForSubplots(space);
393
394 // draw the shared axis
395 ValueAxis axis = getRangeAxis();
396 RectangleEdge edge = getRangeAxisEdge();
397 double cursor = RectangleEdge.coordinate(dataArea, edge);
398 AxisState axisState = axis.draw(g2, cursor, area, dataArea, edge, info);
399
400 if (parentState == null) {
401 parentState = new PlotState();
402 }
403 parentState.getSharedAxisStates().put(axis, axisState);
404
405 // draw all the charts
406 for (int i = 0; i < this.subplots.size(); i++) {
407 XYPlot plot = (XYPlot) this.subplots.get(i);
408 PlotRenderingInfo subplotInfo = null;
409 if (info != null) {
410 subplotInfo = new PlotRenderingInfo(info.getOwner());
411 info.addSubplotInfo(subplotInfo);
412 }
413 plot.draw(g2, this.subplotAreas[i], anchor, parentState,
414 subplotInfo);
415 }
416
417 if (info != null) {
418 info.setDataArea(dataArea);
419 }
420
421 }
422
423 /**
424 * Returns a collection of legend items for the plot.
425 *
426 * @return The legend items.
427 */
428 public LegendItemCollection getLegendItems() {
429 LegendItemCollection result = getFixedLegendItems();
430 if (result == null) {
431 result = new LegendItemCollection();
432
433 if (this.subplots != null) {
434 Iterator iterator = this.subplots.iterator();
435 while (iterator.hasNext()) {
436 XYPlot plot = (XYPlot) iterator.next();
437 LegendItemCollection more = plot.getLegendItems();
438 result.addAll(more);
439 }
440 }
441 }
442 return result;
443 }
444
445 /**
446 * Multiplies the range on the domain axis/axes by the specified factor.
447 *
448 * @param factor the zoom factor.
449 * @param info the plot rendering info (<code>null</code> not permitted).
450 * @param source the source point (<code>null</code> not permitted).
451 */
452 public void zoomDomainAxes(double factor, PlotRenderingInfo info,
453 Point2D source) {
454 zoomDomainAxes(factor, info, source, false);
455 }
456
457 /**
458 * Multiplies the range on the domain axis/axes by the specified factor.
459 *
460 * @param factor the zoom factor.
461 * @param info the plot rendering info (<code>null</code> not permitted).
462 * @param source the source point (<code>null</code> not permitted).
463 * @param useAnchor zoom about the anchor point?
464 */
465 public void zoomDomainAxes(double factor, PlotRenderingInfo info,
466 Point2D source, boolean useAnchor) {
467 // delegate 'info' and 'source' argument checks...
468 XYPlot subplot = findSubplot(info, source);
469 if (subplot != null) {
470 subplot.zoomDomainAxes(factor, info, source, useAnchor);
471 }
472 else {
473 // if the source point doesn't fall within a subplot, we do the
474 // zoom on all subplots...
475 Iterator iterator = getSubplots().iterator();
476 while (iterator.hasNext()) {
477 subplot = (XYPlot) iterator.next();
478 subplot.zoomDomainAxes(factor, info, source, useAnchor);
479 }
480 }
481 }
482
483 /**
484 * Zooms in on the domain axes.
485 *
486 * @param lowerPercent the lower bound.
487 * @param upperPercent the upper bound.
488 * @param info the plot rendering info (<code>null</code> not permitted).
489 * @param source the source point (<code>null</code> not permitted).
490 */
491 public void zoomDomainAxes(double lowerPercent, double upperPercent,
492 PlotRenderingInfo info, Point2D source) {
493 // delegate 'info' and 'source' argument checks...
494 XYPlot subplot = findSubplot(info, source);
495 if (subplot != null) {
496 subplot.zoomDomainAxes(lowerPercent, upperPercent, info, source);
497 }
498 else {
499 // if the source point doesn't fall within a subplot, we do the
500 // zoom on all subplots...
501 Iterator iterator = getSubplots().iterator();
502 while (iterator.hasNext()) {
503 subplot = (XYPlot) iterator.next();
504 subplot.zoomDomainAxes(lowerPercent, upperPercent, info,
505 source);
506 }
507 }
508 }
509
510 /**
511 * Returns the subplot (if any) that contains the (x, y) point (specified
512 * in Java2D space).
513 *
514 * @param info the chart rendering info (<code>null</code> not permitted).
515 * @param source the source point (<code>null</code> not permitted).
516 *
517 * @return A subplot (possibly <code>null</code>).
518 */
519 public XYPlot findSubplot(PlotRenderingInfo info, Point2D source) {
520 if (info == null) {
521 throw new IllegalArgumentException("Null 'info' argument.");
522 }
523 if (source == null) {
524 throw new IllegalArgumentException("Null 'source' argument.");
525 }
526 XYPlot result = null;
527 int subplotIndex = info.getSubplotIndex(source);
528 if (subplotIndex >= 0) {
529 result = (XYPlot) this.subplots.get(subplotIndex);
530 }
531 return result;
532 }
533
534 /**
535 * Sets the item renderer FOR ALL SUBPLOTS. Registered listeners are
536 * notified that the plot has been modified.
537 * <P>
538 * Note: usually you will want to set the renderer independently for each
539 * subplot, which is NOT what this method does.
540 *
541 * @param renderer the new renderer.
542 */
543 public void setRenderer(XYItemRenderer renderer) {
544
545 super.setRenderer(renderer); // not strictly necessary, since the
546 // renderer set for the
547 // parent plot is not used
548
549 Iterator iterator = this.subplots.iterator();
550 while (iterator.hasNext()) {
551 XYPlot plot = (XYPlot) iterator.next();
552 plot.setRenderer(renderer);
553 }
554
555 }
556
557 /**
558 * Sets the orientation for the plot (and all its subplots).
559 *
560 * @param orientation the orientation.
561 */
562 public void setOrientation(PlotOrientation orientation) {
563
564 super.setOrientation(orientation);
565
566 Iterator iterator = this.subplots.iterator();
567 while (iterator.hasNext()) {
568 XYPlot plot = (XYPlot) iterator.next();
569 plot.setOrientation(orientation);
570 }
571
572 }
573
574 /**
575 * Returns a range representing the extent of the data values in this plot
576 * (obtained from the subplots) that will be rendered against the specified
577 * axis. NOTE: This method is intended for internal JFreeChart use, and
578 * is public only so that code in the axis classes can call it. Since
579 * only the range axis is shared between subplots, the JFreeChart code
580 * will only call this method for the range values (although this is not
581 * checked/enforced).
582 *
583 * @param axis the axis.
584 *
585 * @return The range.
586 */
587 public Range getDataRange(ValueAxis axis) {
588 Range result = null;
589 if (this.subplots != null) {
590 Iterator iterator = this.subplots.iterator();
591 while (iterator.hasNext()) {
592 XYPlot subplot = (XYPlot) iterator.next();
593 result = Range.combine(result, subplot.getDataRange(axis));
594 }
595 }
596 return result;
597 }
598
599 /**
600 * Sets the space (width or height, depending on the orientation of the
601 * plot) for the domain axis of each subplot.
602 *
603 * @param space the space.
604 */
605 protected void setFixedDomainAxisSpaceForSubplots(AxisSpace space) {
606 Iterator iterator = this.subplots.iterator();
607 while (iterator.hasNext()) {
608 XYPlot plot = (XYPlot) iterator.next();
609 plot.setFixedDomainAxisSpace(space, false);
610 }
611 }
612
613 /**
614 * Handles a 'click' on the plot by updating the anchor values...
615 *
616 * @param x x-coordinate, where the click occured.
617 * @param y y-coordinate, where the click occured.
618 * @param info object containing information about the plot dimensions.
619 */
620 public void handleClick(int x, int y, PlotRenderingInfo info) {
621
622 Rectangle2D dataArea = info.getDataArea();
623 if (dataArea.contains(x, y)) {
624 for (int i = 0; i < this.subplots.size(); i++) {
625 XYPlot subplot = (XYPlot) this.subplots.get(i);
626 PlotRenderingInfo subplotInfo = info.getSubplotInfo(i);
627 subplot.handleClick(x, y, subplotInfo);
628 }
629 }
630
631 }
632
633 /**
634 * Receives a {@link PlotChangeEvent} and responds by notifying all
635 * listeners.
636 *
637 * @param event the event.
638 */
639 public void plotChanged(PlotChangeEvent event) {
640 notifyListeners(event);
641 }
642
643 /**
644 * Tests this plot for equality with another object.
645 *
646 * @param obj the other object.
647 *
648 * @return <code>true</code> or <code>false</code>.
649 */
650 public boolean equals(Object obj) {
651 if (obj == this) {
652 return true;
653 }
654 if (!(obj instanceof CombinedRangeXYPlot)) {
655 return false;
656 }
657 CombinedRangeXYPlot that = (CombinedRangeXYPlot) obj;
658 if (this.gap != that.gap) {
659 return false;
660 }
661 if (!ObjectUtilities.equal(this.subplots, that.subplots)) {
662 return false;
663 }
664 return super.equals(obj);
665 }
666
667 /**
668 * Returns a clone of the plot.
669 *
670 * @return A clone.
671 *
672 * @throws CloneNotSupportedException this class will not throw this
673 * exception, but subclasses (if any) might.
674 */
675 public Object clone() throws CloneNotSupportedException {
676
677 CombinedRangeXYPlot result = (CombinedRangeXYPlot) super.clone();
678 result.subplots = (List) ObjectUtilities.deepClone(this.subplots);
679 for (Iterator it = result.subplots.iterator(); it.hasNext();) {
680 Plot child = (Plot) it.next();
681 child.setParent(result);
682 }
683
684 // after setting up all the subplots, the shared range axis may need
685 // reconfiguring
686 ValueAxis rangeAxis = result.getRangeAxis();
687 if (rangeAxis != null) {
688 rangeAxis.configure();
689 }
690
691 return result;
692 }
693
694 }