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
085 implements Zoomable,
086 Cloneable, PublicCloneable,
087 Serializable,
088 PlotChangeListener {
089
090 /** For serialization. */
091 private static final long serialVersionUID = 8207194522653701572L;
092
093 /** Storage for the subplot references. */
094 private List subplots;
095
096 /** Total weight of all charts. */
097 private int totalWeight;
098
099 /** The gap between subplots. */
100 private double gap;
101
102 /** Temporary storage for the subplot areas. */
103 private transient Rectangle2D[] subplotAreas;
104 // TODO: move the above to the plot state
105
106 /**
107 * Default constructor.
108 */
109 public CombinedDomainCategoryPlot() {
110 this(new CategoryAxis());
111 }
112
113 /**
114 * Creates a new plot.
115 *
116 * @param domainAxis the shared domain axis (<code>null</code> not
117 * permitted).
118 */
119 public CombinedDomainCategoryPlot(CategoryAxis domainAxis) {
120 super(null, domainAxis, null, null);
121 this.subplots = new java.util.ArrayList();
122 this.totalWeight = 0;
123 this.gap = 5.0;
124 }
125
126 /**
127 * Returns the space between subplots.
128 *
129 * @return The gap (in Java2D units).
130 */
131 public double getGap() {
132 return this.gap;
133 }
134
135 /**
136 * Sets the amount of space between subplots and sends a
137 * {@link PlotChangeEvent} to all registered listeners.
138 *
139 * @param gap the gap between subplots (in Java2D units).
140 */
141 public void setGap(double gap) {
142 this.gap = gap;
143 notifyListeners(new PlotChangeEvent(this));
144 }
145
146 /**
147 * Adds a subplot to the combined chart and sends a {@link PlotChangeEvent}
148 * to all registered listeners.
149 * <br><br>
150 * The domain axis for the subplot will be set to <code>null</code>. You
151 * must ensure that the subplot has a non-null range axis.
152 *
153 * @param subplot the subplot (<code>null</code> not permitted).
154 */
155 public void add(CategoryPlot subplot) {
156 add(subplot, 1);
157 }
158
159 /**
160 * Adds a subplot to the combined chart and sends a {@link PlotChangeEvent}
161 * to all registered listeners.
162 * <br><br>
163 * The domain axis for the subplot will be set to <code>null</code>. You
164 * must ensure that the subplot has a non-null range axis.
165 *
166 * @param subplot the subplot (<code>null</code> not permitted).
167 * @param weight the weight (must be >= 1).
168 */
169 public void add(CategoryPlot subplot, int weight) {
170 if (subplot == null) {
171 throw new IllegalArgumentException("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("Null 'subplot' argument.");
202 }
203 int position = -1;
204 int size = this.subplots.size();
205 int i = 0;
206 while (position == -1 && i < size) {
207 if (this.subplots.get(i) == subplot) {
208 position = i;
209 }
210 i++;
211 }
212 if (position != -1) {
213 this.subplots.remove(position);
214 subplot.setParent(null);
215 subplot.removeChangeListener(this);
216 this.totalWeight -= subplot.getWeight();
217
218 CategoryAxis domain = getDomainAxis();
219 if (domain != null) {
220 domain.configure();
221 }
222 notifyListeners(new PlotChangeEvent(this));
223 }
224 }
225
226 /**
227 * Returns the list of subplots.
228 *
229 * @return An unmodifiable list of subplots .
230 */
231 public List getSubplots() {
232 return Collections.unmodifiableList(this.subplots);
233 }
234
235 /**
236 * Returns the subplot (if any) that contains the (x, y) point (specified
237 * in Java2D space).
238 *
239 * @param info the chart rendering info (<code>null</code> not permitted).
240 * @param source the source point (<code>null</code> not permitted).
241 *
242 * @return A subplot (possibly <code>null</code>).
243 */
244 public CategoryPlot findSubplot(PlotRenderingInfo info, Point2D source) {
245 if (info == null) {
246 throw new IllegalArgumentException("Null 'info' argument.");
247 }
248 if (source == null) {
249 throw new IllegalArgumentException("Null 'source' argument.");
250 }
251 CategoryPlot result = null;
252 int subplotIndex = info.getSubplotIndex(source);
253 if (subplotIndex >= 0) {
254 result = (CategoryPlot) this.subplots.get(subplotIndex);
255 }
256 return result;
257 }
258
259 /**
260 * Multiplies the range on the range axis/axes by the specified factor.
261 *
262 * @param factor the zoom factor.
263 * @param info the plot rendering info (<code>null</code> not permitted).
264 * @param source the source point (<code>null</code> not permitted).
265 */
266 public void zoomRangeAxes(double factor, PlotRenderingInfo info,
267 Point2D source) {
268 // delegate 'info' and 'source' argument checks...
269 CategoryPlot subplot = findSubplot(info, source);
270 if (subplot != null) {
271 subplot.zoomRangeAxes(factor, info, source);
272 }
273 else {
274 // if the source point doesn't fall within a subplot, we do the
275 // zoom on all subplots...
276 Iterator iterator = getSubplots().iterator();
277 while (iterator.hasNext()) {
278 subplot = (CategoryPlot) iterator.next();
279 subplot.zoomRangeAxes(factor, info, source);
280 }
281 }
282 }
283
284 /**
285 * Zooms in on the range axes.
286 *
287 * @param lowerPercent the lower bound.
288 * @param upperPercent the upper bound.
289 * @param info the plot rendering info (<code>null</code> not permitted).
290 * @param source the source point (<code>null</code> not permitted).
291 */
292 public void zoomRangeAxes(double lowerPercent, double upperPercent,
293 PlotRenderingInfo info, Point2D source) {
294 // delegate 'info' and 'source' argument checks...
295 CategoryPlot subplot = findSubplot(info, source);
296 if (subplot != null) {
297 subplot.zoomRangeAxes(lowerPercent, upperPercent, info, source);
298 }
299 else {
300 // if the source point doesn't fall within a subplot, we do the
301 // zoom on all subplots...
302 Iterator iterator = getSubplots().iterator();
303 while (iterator.hasNext()) {
304 subplot = (CategoryPlot) iterator.next();
305 subplot.zoomRangeAxes(lowerPercent, upperPercent, info, source);
306 }
307 }
308 }
309
310 /**
311 * Calculates the space required for the axes.
312 *
313 * @param g2 the graphics device.
314 * @param plotArea the plot area.
315 *
316 * @return The space required for the axes.
317 */
318 protected AxisSpace calculateAxisSpace(Graphics2D g2,
319 Rectangle2D plotArea) {
320
321 AxisSpace space = new AxisSpace();
322 PlotOrientation orientation = getOrientation();
323
324 // work out the space required by the domain axis...
325 AxisSpace fixed = getFixedDomainAxisSpace();
326 if (fixed != null) {
327 if (orientation == PlotOrientation.HORIZONTAL) {
328 space.setLeft(fixed.getLeft());
329 space.setRight(fixed.getRight());
330 }
331 else if (orientation == PlotOrientation.VERTICAL) {
332 space.setTop(fixed.getTop());
333 space.setBottom(fixed.getBottom());
334 }
335 }
336 else {
337 CategoryAxis categoryAxis = getDomainAxis();
338 RectangleEdge categoryEdge = Plot.resolveDomainAxisLocation(
339 getDomainAxisLocation(), orientation);
340 if (categoryAxis != null) {
341 space = categoryAxis.reserveSpace(g2, this, plotArea,
342 categoryEdge, space);
343 }
344 else {
345 if (getDrawSharedDomainAxis()) {
346 space = getDomainAxis().reserveSpace(g2, this, plotArea,
347 categoryEdge, space);
348 }
349 }
350 }
351
352 Rectangle2D adjustedPlotArea = space.shrink(plotArea, null);
353
354 // work out the maximum height or width of the non-shared axes...
355 int n = this.subplots.size();
356 this.subplotAreas = new Rectangle2D[n];
357 double x = adjustedPlotArea.getX();
358 double y = adjustedPlotArea.getY();
359 double usableSize = 0.0;
360 if (orientation == PlotOrientation.HORIZONTAL) {
361 usableSize = adjustedPlotArea.getWidth() - this.gap * (n - 1);
362 }
363 else if (orientation == PlotOrientation.VERTICAL) {
364 usableSize = adjustedPlotArea.getHeight() - this.gap * (n - 1);
365 }
366
367 for (int i = 0; i < n; i++) {
368 CategoryPlot plot = (CategoryPlot) this.subplots.get(i);
369
370 // calculate sub-plot area
371 if (orientation == PlotOrientation.HORIZONTAL) {
372 double w = usableSize * plot.getWeight() / this.totalWeight;
373 this.subplotAreas[i] = new Rectangle2D.Double(x, y, w,
374 adjustedPlotArea.getHeight());
375 x = x + w + this.gap;
376 }
377 else if (orientation == PlotOrientation.VERTICAL) {
378 double h = usableSize * plot.getWeight() / this.totalWeight;
379 this.subplotAreas[i] = new Rectangle2D.Double(x, y,
380 adjustedPlotArea.getWidth(), h);
381 y = y + h + this.gap;
382 }
383
384 AxisSpace subSpace = plot.calculateRangeAxisSpace(g2,
385 this.subplotAreas[i], null);
386 space.ensureAtLeast(subSpace);
387
388 }
389
390 return space;
391 }
392
393 /**
394 * Draws the plot on a Java 2D graphics device (such as the screen or a
395 * printer). Will perform all the placement calculations for each of the
396 * sub-plots and then tell these to draw themselves.
397 *
398 * @param g2 the graphics device.
399 * @param area the area within which the plot (including axis labels)
400 * should be drawn.
401 * @param anchor the anchor point (<code>null</code> permitted).
402 * @param parentState the state from the parent plot, if there is one.
403 * @param info collects information about the drawing (<code>null</code>
404 * permitted).
405 */
406 public void draw(Graphics2D g2,
407 Rectangle2D area,
408 Point2D anchor,
409 PlotState parentState,
410 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(),
420 area.getY() + insets.getTop(),
421 area.getWidth() - insets.getLeft() - insets.getRight(),
422 area.getHeight() - insets.getTop() - insets.getBottom());
423
424
425 // calculate the data area...
426 setFixedRangeAxisSpaceForSubplots(null);
427 AxisSpace space = calculateAxisSpace(g2, area);
428 Rectangle2D dataArea = space.shrink(area, null);
429
430 // set the width and height of non-shared axis of all sub-plots
431 setFixedRangeAxisSpaceForSubplots(space);
432
433 // draw the shared axis
434 CategoryAxis axis = getDomainAxis();
435 RectangleEdge domainEdge = getDomainAxisEdge();
436 double cursor = RectangleEdge.coordinate(dataArea, domainEdge);
437 AxisState axisState = axis.draw(g2, cursor, area, dataArea,
438 domainEdge, info);
439 if (parentState == null) {
440 parentState = new PlotState();
441 }
442 parentState.getSharedAxisStates().put(axis, axisState);
443
444 // draw all the subplots
445 for (int i = 0; i < this.subplots.size(); i++) {
446 CategoryPlot plot = (CategoryPlot) this.subplots.get(i);
447 PlotRenderingInfo subplotInfo = null;
448 if (info != null) {
449 subplotInfo = new PlotRenderingInfo(info.getOwner());
450 info.addSubplotInfo(subplotInfo);
451 }
452 plot.draw(g2, this.subplotAreas[i], null, parentState, 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.next();
531 if (!result.contains(category)) {
532 result.add(category);
533 }
534 }
535 }
536 }
537 return Collections.unmodifiableList(result);
538 }
539
540 /**
541 * Overridden to return the categories in the subplots.
542 *
543 * @param axis ignored.
544 *
545 * @return A list of the categories in the subplots.
546 *
547 * @since 1.0.3
548 */
549 public List getCategoriesForAxis(CategoryAxis axis) {
550 // FIXME: this code means that it is not possible to use more than
551 // one domain axis for the combined plots...
552 return getCategories();
553 }
554
555 /**
556 * Handles a 'click' on the plot.
557 *
558 * @param x x-coordinate of the click.
559 * @param y y-coordinate of the click.
560 * @param info information about the plot's dimensions.
561 *
562 */
563 public void handleClick(int x, int y, PlotRenderingInfo info) {
564
565 Rectangle2D dataArea = info.getDataArea();
566 if (dataArea.contains(x, y)) {
567 for (int i = 0; i < this.subplots.size(); i++) {
568 CategoryPlot subplot = (CategoryPlot) this.subplots.get(i);
569 PlotRenderingInfo subplotInfo = info.getSubplotInfo(i);
570 subplot.handleClick(x, y, subplotInfo);
571 }
572 }
573
574 }
575
576 /**
577 * Receives a {@link PlotChangeEvent} and responds by notifying all
578 * listeners.
579 *
580 * @param event the event.
581 */
582 public void plotChanged(PlotChangeEvent event) {
583 notifyListeners(event);
584 }
585
586 /**
587 * Tests the plot for equality with an arbitrary object.
588 *
589 * @param obj the object (<code>null</code> permitted).
590 *
591 * @return A boolean.
592 */
593 public boolean equals(Object obj) {
594 if (obj == this) {
595 return true;
596 }
597 if (!(obj instanceof CombinedDomainCategoryPlot)) {
598 return false;
599 }
600 if (!super.equals(obj)) {
601 return false;
602 }
603 CombinedDomainCategoryPlot plot = (CombinedDomainCategoryPlot) obj;
604 if (!ObjectUtilities.equal(this.subplots, plot.subplots)) {
605 return false;
606 }
607 if (this.totalWeight != plot.totalWeight) {
608 return false;
609 }
610 if (this.gap != plot.gap) {
611 return false;
612 }
613 return true;
614 }
615
616 /**
617 * Returns a clone of the plot.
618 *
619 * @return A clone.
620 *
621 * @throws CloneNotSupportedException this class will not throw this
622 * exception, but subclasses (if any) might.
623 */
624 public Object clone() throws CloneNotSupportedException {
625
626 CombinedDomainCategoryPlot result
627 = (CombinedDomainCategoryPlot) super.clone();
628 result.subplots = (List) ObjectUtilities.deepClone(this.subplots);
629 for (Iterator it = result.subplots.iterator(); it.hasNext();) {
630 Plot child = (Plot) it.next();
631 child.setParent(result);
632 }
633 return result;
634
635 }
636
637 }