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 *
095 */
096
097 package org.jfree.chart.plot;
098
099 import java.awt.Graphics2D;
100 import java.awt.geom.Point2D;
101 import java.awt.geom.Rectangle2D;
102 import java.util.Collections;
103 import java.util.Iterator;
104 import java.util.List;
105
106 import org.jfree.chart.LegendItemCollection;
107 import org.jfree.chart.axis.AxisSpace;
108 import org.jfree.chart.axis.AxisState;
109 import org.jfree.chart.axis.NumberAxis;
110 import org.jfree.chart.axis.ValueAxis;
111 import org.jfree.chart.event.PlotChangeEvent;
112 import org.jfree.chart.event.PlotChangeListener;
113 import org.jfree.chart.renderer.xy.XYItemRenderer;
114 import org.jfree.data.Range;
115 import org.jfree.ui.RectangleEdge;
116 import org.jfree.ui.RectangleInsets;
117 import org.jfree.util.ObjectUtilities;
118
119 /**
120 * An extension of {@link XYPlot} that contains multiple subplots that share a
121 * common range axis.
122 */
123 public class CombinedRangeXYPlot extends XYPlot
124 implements PlotChangeListener {
125
126 /** For serialization. */
127 private static final long serialVersionUID = -5177814085082031168L;
128
129 /** Storage for the subplot references. */
130 private List subplots;
131
132 /** Total weight of all charts. */
133 private int totalWeight = 0;
134
135 /** The gap between subplots. */
136 private double gap = 5.0;
137
138 /** Temporary storage for the subplot areas. */
139 private transient Rectangle2D[] subplotAreas;
140
141 /**
142 * Default constructor.
143 */
144 public CombinedRangeXYPlot() {
145 this(new NumberAxis());
146 }
147
148 /**
149 * Creates a new plot.
150 *
151 * @param rangeAxis the shared axis.
152 */
153 public CombinedRangeXYPlot(ValueAxis rangeAxis) {
154
155 super(null, // no data in the parent plot
156 null,
157 rangeAxis,
158 null);
159
160 this.subplots = new java.util.ArrayList();
161
162 }
163
164 /**
165 * Returns a string describing the type of plot.
166 *
167 * @return The type of plot.
168 */
169 public String getPlotType() {
170 return localizationResources.getString("Combined_Range_XYPlot");
171 }
172
173 /**
174 * Returns the space between subplots.
175 *
176 * @return The gap
177 */
178 public double getGap() {
179 return this.gap;
180 }
181
182 /**
183 * Sets the amount of space between subplots.
184 *
185 * @param gap the gap between subplots
186 */
187 public void setGap(double gap) {
188 this.gap = gap;
189 }
190
191 /**
192 * Adds a subplot, with a default 'weight' of 1.
193 * <br><br>
194 * You must ensure that the subplot has a non-null domain axis. The range
195 * axis for the subplot will be set to <code>null</code>.
196 *
197 * @param subplot the subplot.
198 */
199 public void add(XYPlot subplot) {
200 add(subplot, 1);
201 }
202
203 /**
204 * Adds a subplot with a particular weight (greater than or equal to one).
205 * The weight determines how much space is allocated to the subplot
206 * relative to all the other subplots.
207 * <br><br>
208 * You must ensure that the subplot has a non-null domain axis. The range
209 * axis for the subplot will be set to <code>null</code>.
210 *
211 * @param subplot the subplot.
212 * @param weight the weight (must be 1 or greater).
213 */
214 public void add(XYPlot subplot, int weight) {
215
216 // verify valid weight
217 if (weight <= 0) {
218 String msg = "The 'weight' must be positive.";
219 throw new IllegalArgumentException(msg);
220 }
221
222 // store the plot and its weight
223 subplot.setParent(this);
224 subplot.setWeight(weight);
225 subplot.setInsets(new RectangleInsets(0.0, 0.0, 0.0, 0.0));
226 subplot.setRangeAxis(null);
227 subplot.addChangeListener(this);
228 this.subplots.add(subplot);
229
230 // keep track of total weights
231 this.totalWeight += weight;
232 configureRangeAxes();
233 fireChangeEvent();
234
235 }
236
237 /**
238 * Removes a subplot from the combined chart.
239 *
240 * @param subplot the subplot (<code>null</code> not permitted).
241 */
242 public void remove(XYPlot subplot) {
243 if (subplot == null) {
244 throw new IllegalArgumentException(" Null 'subplot' argument.");
245 }
246 int position = -1;
247 int size = this.subplots.size();
248 int i = 0;
249 while (position == -1 && i < size) {
250 if (this.subplots.get(i) == subplot) {
251 position = i;
252 }
253 i++;
254 }
255 if (position != -1) {
256 this.subplots.remove(position);
257 subplot.setParent(null);
258 subplot.removeChangeListener(this);
259 this.totalWeight -= subplot.getWeight();
260 configureRangeAxes();
261 fireChangeEvent();
262 }
263 }
264
265 /**
266 * Returns the list of subplots. The returned list may be empty, but is
267 * never <code>null</code>.
268 *
269 * @return An unmodifiable list of subplots.
270 */
271 public List getSubplots() {
272 if (this.subplots != null) {
273 return Collections.unmodifiableList(this.subplots);
274 }
275 else {
276 return Collections.EMPTY_LIST;
277 }
278 }
279
280 /**
281 * Calculates the space required for the axes.
282 *
283 * @param g2 the graphics device.
284 * @param plotArea the plot area.
285 *
286 * @return The space required for the axes.
287 */
288 protected AxisSpace calculateAxisSpace(Graphics2D g2,
289 Rectangle2D plotArea) {
290
291 AxisSpace space = new AxisSpace();
292 PlotOrientation orientation = getOrientation();
293
294 // work out the space required by the domain axis...
295 AxisSpace fixed = getFixedRangeAxisSpace();
296 if (fixed != null) {
297 if (orientation == PlotOrientation.VERTICAL) {
298 space.setLeft(fixed.getLeft());
299 space.setRight(fixed.getRight());
300 }
301 else if (orientation == PlotOrientation.HORIZONTAL) {
302 space.setTop(fixed.getTop());
303 space.setBottom(fixed.getBottom());
304 }
305 }
306 else {
307 ValueAxis valueAxis = getRangeAxis();
308 RectangleEdge valueEdge = Plot.resolveRangeAxisLocation(
309 getRangeAxisLocation(), orientation
310 );
311 if (valueAxis != null) {
312 space = valueAxis.reserveSpace(g2, this, plotArea, valueEdge,
313 space);
314 }
315 }
316
317 Rectangle2D adjustedPlotArea = space.shrink(plotArea, null);
318 // work out the maximum height or width of the non-shared axes...
319 int n = this.subplots.size();
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() / this.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() / this.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
652 if (obj == this) {
653 return true;
654 }
655
656 if (!(obj instanceof CombinedRangeXYPlot)) {
657 return false;
658 }
659 if (!super.equals(obj)) {
660 return false;
661 }
662 CombinedRangeXYPlot that = (CombinedRangeXYPlot) obj;
663 if (!ObjectUtilities.equal(this.subplots, that.subplots)) {
664 return false;
665 }
666 if (this.totalWeight != that.totalWeight) {
667 return false;
668 }
669 if (this.gap != that.gap) {
670 return false;
671 }
672 return true;
673 }
674
675 /**
676 * Returns a clone of the plot.
677 *
678 * @return A clone.
679 *
680 * @throws CloneNotSupportedException this class will not throw this
681 * exception, but subclasses (if any) might.
682 */
683 public Object clone() throws CloneNotSupportedException {
684
685 CombinedRangeXYPlot result = (CombinedRangeXYPlot) super.clone();
686 result.subplots = (List) ObjectUtilities.deepClone(this.subplots);
687 for (Iterator it = result.subplots.iterator(); it.hasNext();) {
688 Plot child = (Plot) it.next();
689 child.setParent(result);
690 }
691
692 // after setting up all the subplots, the shared range axis may need
693 // reconfiguring
694 ValueAxis rangeAxis = result.getRangeAxis();
695 if (rangeAxis != null) {
696 rangeAxis.configure();
697 }
698
699 return result;
700 }
701
702 }