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: * CombinedDomainCategoryPlot.java
029: * -------------------------------
030: * (C) Copyright 2003-2007, by Object Refinery Limited.
031: *
032: * Original Author: David Gilbert (for Object Refinery Limited);
033: * Contributor(s): Nicolas Brodu;
034: *
035: * $Id: CombinedDomainCategoryPlot.java,v 1.9.2.4 2007/04/17 11:13:25 mungady Exp $
036: *
037: * Changes:
038: * --------
039: * 16-May-2003 : Version 1 (DG);
040: * 08-Aug-2003 : Adjusted totalWeight in remove() method (DG);
041: * 19-Aug-2003 : Added equals() method, implemented Cloneable and
042: * Serializable (DG);
043: * 11-Sep-2003 : Fix cloning support (subplots) (NB);
044: * 15-Sep-2003 : Implemented PublicCloneable (DG);
045: * 16-Sep-2003 : Changed ChartRenderingInfo --> PlotRenderingInfo (DG);
046: * 17-Sep-2003 : Updated handling of 'clicks' (DG);
047: * 04-May-2004 : Added getter/setter methods for 'gap' attribute (DG);
048: * 12-Nov-2004 : Implemented the Zoomable interface (DG);
049: * 25-Nov-2004 : Small update to clone() implementation (DG);
050: * 21-Feb-2005 : The getLegendItems() method now returns the fixed legend
051: * items if set (DG);
052: * 05-May-2005 : Updated draw() method parameters (DG);
053: * ------------- JFREECHART 1.0.x ---------------------------------------------
054: * 13-Sep-2006 : Updated API docs (DG);
055: * 30-Oct-2006 : Added new getCategoriesForAxis() override (DG);
056: * 17-Apr-2007 : Added null argument checks to findSubplot() (DG);
057: *
058: */
059:
060: package org.jfree.chart.plot;
061:
062: import java.awt.Graphics2D;
063: import java.awt.geom.Point2D;
064: import java.awt.geom.Rectangle2D;
065: import java.io.Serializable;
066: import java.util.Collections;
067: import java.util.Iterator;
068: import java.util.List;
069:
070: import org.jfree.chart.LegendItemCollection;
071: import org.jfree.chart.axis.AxisSpace;
072: import org.jfree.chart.axis.AxisState;
073: import org.jfree.chart.axis.CategoryAxis;
074: import org.jfree.chart.event.PlotChangeEvent;
075: import org.jfree.chart.event.PlotChangeListener;
076: import org.jfree.ui.RectangleEdge;
077: import org.jfree.ui.RectangleInsets;
078: import org.jfree.util.ObjectUtilities;
079: import org.jfree.util.PublicCloneable;
080:
081: /**
082: * A combined category plot where the domain axis is shared.
083: */
084: public class CombinedDomainCategoryPlot extends CategoryPlot implements
085: Zoomable, Cloneable, PublicCloneable, Serializable,
086: PlotChangeListener {
087:
088: /** For serialization. */
089: private static final long serialVersionUID = 8207194522653701572L;
090:
091: /** Storage for the subplot references. */
092: private List subplots;
093:
094: /** Total weight of all charts. */
095: private int totalWeight;
096:
097: /** The gap between subplots. */
098: private double gap;
099:
100: /** Temporary storage for the subplot areas. */
101: private transient Rectangle2D[] subplotAreas;
102:
103: // TODO: move the above to the plot state
104:
105: /**
106: * Default constructor.
107: */
108: public CombinedDomainCategoryPlot() {
109: this (new CategoryAxis());
110: }
111:
112: /**
113: * Creates a new plot.
114: *
115: * @param domainAxis the shared domain axis (<code>null</code> not
116: * permitted).
117: */
118: public CombinedDomainCategoryPlot(CategoryAxis domainAxis) {
119: super (null, domainAxis, null, null);
120: this .subplots = new java.util.ArrayList();
121: this .totalWeight = 0;
122: this .gap = 5.0;
123: }
124:
125: /**
126: * Returns the space between subplots.
127: *
128: * @return The gap (in Java2D units).
129: */
130: public double getGap() {
131: return this .gap;
132: }
133:
134: /**
135: * Sets the amount of space between subplots and sends a
136: * {@link PlotChangeEvent} to all registered listeners.
137: *
138: * @param gap the gap between subplots (in Java2D units).
139: */
140: public void setGap(double gap) {
141: this .gap = gap;
142: notifyListeners(new PlotChangeEvent(this ));
143: }
144:
145: /**
146: * Adds a subplot to the combined chart and sends a {@link PlotChangeEvent}
147: * to all registered listeners.
148: * <br><br>
149: * The domain axis for the subplot will be set to <code>null</code>. You
150: * must ensure that the subplot has a non-null range axis.
151: *
152: * @param subplot the subplot (<code>null</code> not permitted).
153: */
154: public void add(CategoryPlot subplot) {
155: add(subplot, 1);
156: }
157:
158: /**
159: * Adds a subplot to the combined chart and sends a {@link PlotChangeEvent}
160: * to all registered listeners.
161: * <br><br>
162: * The domain axis for the subplot will be set to <code>null</code>. You
163: * must ensure that the subplot has a non-null range axis.
164: *
165: * @param subplot the subplot (<code>null</code> not permitted).
166: * @param weight the weight (must be >= 1).
167: */
168: public void add(CategoryPlot subplot, int weight) {
169: if (subplot == null) {
170: throw new IllegalArgumentException(
171: "Null 'subplot' argument.");
172: }
173: if (weight < 1) {
174: throw new IllegalArgumentException("Require weight >= 1.");
175: }
176: subplot.setParent(this );
177: subplot.setWeight(weight);
178: subplot.setInsets(new RectangleInsets(0.0, 0.0, 0.0, 0.0));
179: subplot.setDomainAxis(null);
180: subplot.setOrientation(getOrientation());
181: subplot.addChangeListener(this );
182: this .subplots.add(subplot);
183: this .totalWeight += weight;
184: CategoryAxis axis = getDomainAxis();
185: if (axis != null) {
186: axis.configure();
187: }
188: notifyListeners(new PlotChangeEvent(this ));
189: }
190:
191: /**
192: * Removes a subplot from the combined chart. Potentially, this removes
193: * some unique categories from the overall union of the datasets...so the
194: * domain axis is reconfigured, then a {@link PlotChangeEvent} is sent to
195: * all registered listeners.
196: *
197: * @param subplot the subplot (<code>null</code> not permitted).
198: */
199: public void remove(CategoryPlot subplot) {
200: if (subplot == null) {
201: throw new IllegalArgumentException(
202: "Null 'subplot' argument.");
203: }
204: int position = -1;
205: int size = this .subplots.size();
206: int i = 0;
207: while (position == -1 && i < size) {
208: if (this .subplots.get(i) == subplot) {
209: position = i;
210: }
211: i++;
212: }
213: if (position != -1) {
214: this .subplots.remove(position);
215: subplot.setParent(null);
216: subplot.removeChangeListener(this );
217: this .totalWeight -= subplot.getWeight();
218:
219: CategoryAxis domain = getDomainAxis();
220: if (domain != null) {
221: domain.configure();
222: }
223: notifyListeners(new PlotChangeEvent(this ));
224: }
225: }
226:
227: /**
228: * Returns the list of subplots.
229: *
230: * @return An unmodifiable list of subplots .
231: */
232: public List getSubplots() {
233: return Collections.unmodifiableList(this .subplots);
234: }
235:
236: /**
237: * Returns the subplot (if any) that contains the (x, y) point (specified
238: * in Java2D space).
239: *
240: * @param info the chart rendering info (<code>null</code> not permitted).
241: * @param source the source point (<code>null</code> not permitted).
242: *
243: * @return A subplot (possibly <code>null</code>).
244: */
245: public CategoryPlot findSubplot(PlotRenderingInfo info,
246: Point2D source) {
247: if (info == null) {
248: throw new IllegalArgumentException("Null 'info' argument.");
249: }
250: if (source == null) {
251: throw new IllegalArgumentException(
252: "Null 'source' argument.");
253: }
254: CategoryPlot result = null;
255: int subplotIndex = info.getSubplotIndex(source);
256: if (subplotIndex >= 0) {
257: result = (CategoryPlot) this .subplots.get(subplotIndex);
258: }
259: return result;
260: }
261:
262: /**
263: * Multiplies the range on the range axis/axes by the specified factor.
264: *
265: * @param factor the zoom factor.
266: * @param info the plot rendering info (<code>null</code> not permitted).
267: * @param source the source point (<code>null</code> not permitted).
268: */
269: public void zoomRangeAxes(double factor, PlotRenderingInfo info,
270: Point2D source) {
271: // delegate 'info' and 'source' argument checks...
272: CategoryPlot subplot = findSubplot(info, source);
273: if (subplot != null) {
274: subplot.zoomRangeAxes(factor, info, source);
275: } else {
276: // if the source point doesn't fall within a subplot, we do the
277: // zoom on all subplots...
278: Iterator iterator = getSubplots().iterator();
279: while (iterator.hasNext()) {
280: subplot = (CategoryPlot) iterator.next();
281: subplot.zoomRangeAxes(factor, info, source);
282: }
283: }
284: }
285:
286: /**
287: * Zooms in on the range axes.
288: *
289: * @param lowerPercent the lower bound.
290: * @param upperPercent the upper bound.
291: * @param info the plot rendering info (<code>null</code> not permitted).
292: * @param source the source point (<code>null</code> not permitted).
293: */
294: public void zoomRangeAxes(double lowerPercent, double upperPercent,
295: PlotRenderingInfo info, Point2D source) {
296: // delegate 'info' and 'source' argument checks...
297: CategoryPlot subplot = findSubplot(info, source);
298: if (subplot != null) {
299: subplot.zoomRangeAxes(lowerPercent, upperPercent, info,
300: source);
301: } else {
302: // if the source point doesn't fall within a subplot, we do the
303: // zoom on all subplots...
304: Iterator iterator = getSubplots().iterator();
305: while (iterator.hasNext()) {
306: subplot = (CategoryPlot) iterator.next();
307: subplot.zoomRangeAxes(lowerPercent, upperPercent, info,
308: source);
309: }
310: }
311: }
312:
313: /**
314: * Calculates the space required for the axes.
315: *
316: * @param g2 the graphics device.
317: * @param plotArea the plot area.
318: *
319: * @return The space required for the axes.
320: */
321: protected AxisSpace calculateAxisSpace(Graphics2D g2,
322: Rectangle2D plotArea) {
323:
324: AxisSpace space = new AxisSpace();
325: PlotOrientation orientation = getOrientation();
326:
327: // work out the space required by the domain axis...
328: AxisSpace fixed = getFixedDomainAxisSpace();
329: if (fixed != null) {
330: if (orientation == PlotOrientation.HORIZONTAL) {
331: space.setLeft(fixed.getLeft());
332: space.setRight(fixed.getRight());
333: } else if (orientation == PlotOrientation.VERTICAL) {
334: space.setTop(fixed.getTop());
335: space.setBottom(fixed.getBottom());
336: }
337: } else {
338: CategoryAxis categoryAxis = getDomainAxis();
339: RectangleEdge categoryEdge = Plot
340: .resolveDomainAxisLocation(getDomainAxisLocation(),
341: orientation);
342: if (categoryAxis != null) {
343: space = categoryAxis.reserveSpace(g2, this , plotArea,
344: categoryEdge, space);
345: } else {
346: if (getDrawSharedDomainAxis()) {
347: space = getDomainAxis().reserveSpace(g2, this ,
348: plotArea, categoryEdge, space);
349: }
350: }
351: }
352:
353: Rectangle2D adjustedPlotArea = space.shrink(plotArea, null);
354:
355: // work out the maximum height or width of the non-shared axes...
356: int n = this .subplots.size();
357: this .subplotAreas = new Rectangle2D[n];
358: double x = adjustedPlotArea.getX();
359: double y = adjustedPlotArea.getY();
360: double usableSize = 0.0;
361: if (orientation == PlotOrientation.HORIZONTAL) {
362: usableSize = adjustedPlotArea.getWidth() - this .gap
363: * (n - 1);
364: } else if (orientation == PlotOrientation.VERTICAL) {
365: usableSize = adjustedPlotArea.getHeight() - this .gap
366: * (n - 1);
367: }
368:
369: for (int i = 0; i < n; i++) {
370: CategoryPlot plot = (CategoryPlot) this .subplots.get(i);
371:
372: // calculate sub-plot area
373: if (orientation == PlotOrientation.HORIZONTAL) {
374: double w = usableSize * plot.getWeight()
375: / this .totalWeight;
376: this .subplotAreas[i] = new Rectangle2D.Double(x, y, w,
377: adjustedPlotArea.getHeight());
378: x = x + w + this .gap;
379: } else if (orientation == PlotOrientation.VERTICAL) {
380: double h = usableSize * plot.getWeight()
381: / this .totalWeight;
382: this .subplotAreas[i] = new Rectangle2D.Double(x, y,
383: adjustedPlotArea.getWidth(), h);
384: y = y + h + this .gap;
385: }
386:
387: AxisSpace subSpace = plot.calculateRangeAxisSpace(g2,
388: this .subplotAreas[i], null);
389: space.ensureAtLeast(subSpace);
390:
391: }
392:
393: return space;
394: }
395:
396: /**
397: * Draws the plot on a Java 2D graphics device (such as the screen or a
398: * printer). Will perform all the placement calculations for each of the
399: * sub-plots and then tell these to draw themselves.
400: *
401: * @param g2 the graphics device.
402: * @param area the area within which the plot (including axis labels)
403: * should be drawn.
404: * @param anchor the anchor point (<code>null</code> permitted).
405: * @param parentState the state from the parent plot, if there is one.
406: * @param info collects information about the drawing (<code>null</code>
407: * permitted).
408: */
409: public void draw(Graphics2D g2, Rectangle2D area, Point2D anchor,
410: PlotState parentState, PlotRenderingInfo info) {
411:
412: // set up info collection...
413: if (info != null) {
414: info.setPlotArea(area);
415: }
416:
417: // adjust the drawing area for plot insets (if any)...
418: RectangleInsets insets = getInsets();
419: area.setRect(area.getX() + insets.getLeft(), area.getY()
420: + insets.getTop(), area.getWidth() - insets.getLeft()
421: - insets.getRight(), area.getHeight() - insets.getTop()
422: - insets.getBottom());
423:
424: // calculate the data area...
425: setFixedRangeAxisSpaceForSubplots(null);
426: AxisSpace space = calculateAxisSpace(g2, area);
427: Rectangle2D dataArea = space.shrink(area, null);
428:
429: // set the width and height of non-shared axis of all sub-plots
430: setFixedRangeAxisSpaceForSubplots(space);
431:
432: // draw the shared axis
433: CategoryAxis axis = getDomainAxis();
434: RectangleEdge domainEdge = getDomainAxisEdge();
435: double cursor = RectangleEdge.coordinate(dataArea, domainEdge);
436: AxisState axisState = axis.draw(g2, cursor, area, dataArea,
437: domainEdge, info);
438: if (parentState == null) {
439: parentState = new PlotState();
440: }
441: parentState.getSharedAxisStates().put(axis, axisState);
442:
443: // draw all the subplots
444: for (int i = 0; i < this .subplots.size(); i++) {
445: CategoryPlot plot = (CategoryPlot) this .subplots.get(i);
446: PlotRenderingInfo subplotInfo = null;
447: if (info != null) {
448: subplotInfo = new PlotRenderingInfo(info.getOwner());
449: info.addSubplotInfo(subplotInfo);
450: }
451: plot.draw(g2, this .subplotAreas[i], null, parentState,
452: subplotInfo);
453: }
454:
455: if (info != null) {
456: info.setDataArea(dataArea);
457: }
458:
459: }
460:
461: /**
462: * Sets the size (width or height, depending on the orientation of the
463: * plot) for the range axis of each subplot.
464: *
465: * @param space the space (<code>null</code> permitted).
466: */
467: protected void setFixedRangeAxisSpaceForSubplots(AxisSpace space) {
468:
469: Iterator iterator = this .subplots.iterator();
470: while (iterator.hasNext()) {
471: CategoryPlot plot = (CategoryPlot) iterator.next();
472: plot.setFixedRangeAxisSpace(space);
473: }
474:
475: }
476:
477: /**
478: * Sets the orientation of the plot (and all subplots).
479: *
480: * @param orientation the orientation (<code>null</code> not permitted).
481: */
482: public void setOrientation(PlotOrientation orientation) {
483:
484: super .setOrientation(orientation);
485:
486: Iterator iterator = this .subplots.iterator();
487: while (iterator.hasNext()) {
488: CategoryPlot plot = (CategoryPlot) iterator.next();
489: plot.setOrientation(orientation);
490: }
491:
492: }
493:
494: /**
495: * Returns a collection of legend items for the plot.
496: *
497: * @return The legend items.
498: */
499: public LegendItemCollection getLegendItems() {
500: LegendItemCollection result = getFixedLegendItems();
501: if (result == null) {
502: result = new LegendItemCollection();
503: if (this .subplots != null) {
504: Iterator iterator = this .subplots.iterator();
505: while (iterator.hasNext()) {
506: CategoryPlot plot = (CategoryPlot) iterator.next();
507: LegendItemCollection more = plot.getLegendItems();
508: result.addAll(more);
509: }
510: }
511: }
512: return result;
513: }
514:
515: /**
516: * Returns an unmodifiable list of the categories contained in all the
517: * subplots.
518: *
519: * @return The list.
520: */
521: public List getCategories() {
522: List result = new java.util.ArrayList();
523: if (this .subplots != null) {
524: Iterator iterator = this .subplots.iterator();
525: while (iterator.hasNext()) {
526: CategoryPlot plot = (CategoryPlot) iterator.next();
527: List more = plot.getCategories();
528: Iterator moreIterator = more.iterator();
529: while (moreIterator.hasNext()) {
530: Comparable category = (Comparable) moreIterator
531: .next();
532: if (!result.contains(category)) {
533: result.add(category);
534: }
535: }
536: }
537: }
538: return Collections.unmodifiableList(result);
539: }
540:
541: /**
542: * Overridden to return the categories in the subplots.
543: *
544: * @param axis ignored.
545: *
546: * @return A list of the categories in the subplots.
547: *
548: * @since 1.0.3
549: */
550: public List getCategoriesForAxis(CategoryAxis axis) {
551: // FIXME: this code means that it is not possible to use more than
552: // one domain axis for the combined plots...
553: return getCategories();
554: }
555:
556: /**
557: * Handles a 'click' on the plot.
558: *
559: * @param x x-coordinate of the click.
560: * @param y y-coordinate of the click.
561: * @param info information about the plot's dimensions.
562: *
563: */
564: public void handleClick(int x, int y, PlotRenderingInfo info) {
565:
566: Rectangle2D dataArea = info.getDataArea();
567: if (dataArea.contains(x, y)) {
568: for (int i = 0; i < this .subplots.size(); i++) {
569: CategoryPlot subplot = (CategoryPlot) this .subplots
570: .get(i);
571: PlotRenderingInfo subplotInfo = info.getSubplotInfo(i);
572: subplot.handleClick(x, y, subplotInfo);
573: }
574: }
575:
576: }
577:
578: /**
579: * Receives a {@link PlotChangeEvent} and responds by notifying all
580: * listeners.
581: *
582: * @param event the event.
583: */
584: public void plotChanged(PlotChangeEvent event) {
585: notifyListeners(event);
586: }
587:
588: /**
589: * Tests the plot for equality with an arbitrary object.
590: *
591: * @param obj the object (<code>null</code> permitted).
592: *
593: * @return A boolean.
594: */
595: public boolean equals(Object obj) {
596: if (obj == this ) {
597: return true;
598: }
599: if (!(obj instanceof CombinedDomainCategoryPlot)) {
600: return false;
601: }
602: if (!super .equals(obj)) {
603: return false;
604: }
605: CombinedDomainCategoryPlot plot = (CombinedDomainCategoryPlot) obj;
606: if (!ObjectUtilities.equal(this .subplots, plot.subplots)) {
607: return false;
608: }
609: if (this .totalWeight != plot.totalWeight) {
610: return false;
611: }
612: if (this .gap != plot.gap) {
613: return false;
614: }
615: return true;
616: }
617:
618: /**
619: * Returns a clone of the plot.
620: *
621: * @return A clone.
622: *
623: * @throws CloneNotSupportedException this class will not throw this
624: * exception, but subclasses (if any) might.
625: */
626: public Object clone() throws CloneNotSupportedException {
627:
628: CombinedDomainCategoryPlot result = (CombinedDomainCategoryPlot) super
629: .clone();
630: result.subplots = (List) ObjectUtilities
631: .deepClone(this .subplots);
632: for (Iterator it = result.subplots.iterator(); it.hasNext();) {
633: Plot child = (Plot) it.next();
634: child.setParent(result);
635: }
636: return result;
637:
638: }
639:
640: }
|