001 /* ===========================================================
002 * JFreeChart : a free chart library for the Java(tm) platform
003 * ===========================================================
004 *
005 * (C) Copyright 2000-2006, 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-2006, 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.3 2006/10/30 13:11:11 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 *
057 */
058
059 package org.jfree.chart.plot;
060
061 import java.awt.Graphics2D;
062 import java.awt.geom.Point2D;
063 import java.awt.geom.Rectangle2D;
064 import java.io.Serializable;
065 import java.util.Collections;
066 import java.util.Iterator;
067 import java.util.List;
068
069 import org.jfree.chart.LegendItemCollection;
070 import org.jfree.chart.axis.AxisSpace;
071 import org.jfree.chart.axis.AxisState;
072 import org.jfree.chart.axis.CategoryAxis;
073 import org.jfree.chart.event.PlotChangeEvent;
074 import org.jfree.chart.event.PlotChangeListener;
075 import org.jfree.ui.RectangleEdge;
076 import org.jfree.ui.RectangleInsets;
077 import org.jfree.util.ObjectUtilities;
078 import org.jfree.util.PublicCloneable;
079
080 /**
081 * A combined category plot where the domain axis is shared.
082 */
083 public class CombinedDomainCategoryPlot extends CategoryPlot
084 implements Zoomable,
085 Cloneable, PublicCloneable,
086 Serializable,
087 PlotChangeListener {
088
089 /** For serialization. */
090 private static final long serialVersionUID = 8207194522653701572L;
091
092 /** Storage for the subplot references. */
093 private List subplots;
094
095 /** Total weight of all charts. */
096 private int totalWeight;
097
098 /** The gap between subplots. */
099 private double gap;
100
101 /** Temporary storage for the subplot areas. */
102 private transient Rectangle2D[] subplotAreas;
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("Null 'subplot' argument.");
171 }
172 if (weight < 1) {
173 throw new IllegalArgumentException("Require weight >= 1.");
174 }
175 subplot.setParent(this);
176 subplot.setWeight(weight);
177 subplot.setInsets(new RectangleInsets(0.0, 0.0, 0.0, 0.0));
178 subplot.setDomainAxis(null);
179 subplot.setOrientation(getOrientation());
180 subplot.addChangeListener(this);
181 this.subplots.add(subplot);
182 this.totalWeight += weight;
183 CategoryAxis axis = getDomainAxis();
184 if (axis != null) {
185 axis.configure();
186 }
187 notifyListeners(new PlotChangeEvent(this));
188 }
189
190 /**
191 * Removes a subplot from the combined chart. Potentially, this removes
192 * some unique categories from the overall union of the datasets...so the
193 * domain axis is reconfigured, then a {@link PlotChangeEvent} is sent to
194 * all registered listeners.
195 *
196 * @param subplot the subplot (<code>null</code> not permitted).
197 */
198 public void remove(CategoryPlot subplot) {
199 if (subplot == null) {
200 throw new IllegalArgumentException("Null 'subplot' argument.");
201 }
202 int position = -1;
203 int size = this.subplots.size();
204 int i = 0;
205 while (position == -1 && i < size) {
206 if (this.subplots.get(i) == subplot) {
207 position = i;
208 }
209 i++;
210 }
211 if (position != -1) {
212 this.subplots.remove(position);
213 subplot.setParent(null);
214 subplot.removeChangeListener(this);
215 this.totalWeight -= subplot.getWeight();
216
217 CategoryAxis domain = getDomainAxis();
218 if (domain != null) {
219 domain.configure();
220 }
221 notifyListeners(new PlotChangeEvent(this));
222 }
223 }
224
225 /**
226 * Returns the list of subplots.
227 *
228 * @return An unmodifiable list of subplots .
229 */
230 public List getSubplots() {
231 return Collections.unmodifiableList(this.subplots);
232 }
233
234 /**
235 * Returns the subplot (if any) that contains the (x, y) point (specified
236 * in Java2D space).
237 *
238 * @param info the chart rendering info.
239 * @param source the source point.
240 *
241 * @return A subplot (possibly <code>null</code>).
242 */
243 public CategoryPlot findSubplot(PlotRenderingInfo info, Point2D source) {
244 CategoryPlot result = null;
245 int subplotIndex = info.getSubplotIndex(source);
246 if (subplotIndex >= 0) {
247 result = (CategoryPlot) this.subplots.get(subplotIndex);
248 }
249 return result;
250 }
251
252 /**
253 * Multiplies the range on the range axis/axes by the specified factor.
254 *
255 * @param factor the zoom factor.
256 * @param info the plot rendering info.
257 * @param source the source point.
258 */
259 public void zoomRangeAxes(double factor, PlotRenderingInfo info,
260 Point2D source) {
261 CategoryPlot subplot = findSubplot(info, source);
262 if (subplot != null) {
263 subplot.zoomRangeAxes(factor, info, source);
264 }
265 }
266
267 /**
268 * Zooms in on the range axes.
269 *
270 * @param lowerPercent the lower bound.
271 * @param upperPercent the upper bound.
272 * @param info the plot rendering info.
273 * @param source the source point.
274 */
275 public void zoomRangeAxes(double lowerPercent, double upperPercent,
276 PlotRenderingInfo info, Point2D source) {
277 CategoryPlot subplot = findSubplot(info, source);
278 if (subplot != null) {
279 subplot.zoomRangeAxes(lowerPercent, upperPercent, info, source);
280 }
281 }
282
283 /**
284 * Calculates the space required for the axes.
285 *
286 * @param g2 the graphics device.
287 * @param plotArea the plot area.
288 *
289 * @return The space required for the axes.
290 */
291 protected AxisSpace calculateAxisSpace(Graphics2D g2,
292 Rectangle2D plotArea) {
293
294 AxisSpace space = new AxisSpace();
295 PlotOrientation orientation = getOrientation();
296
297 // work out the space required by the domain axis...
298 AxisSpace fixed = getFixedDomainAxisSpace();
299 if (fixed != null) {
300 if (orientation == PlotOrientation.HORIZONTAL) {
301 space.setLeft(fixed.getLeft());
302 space.setRight(fixed.getRight());
303 }
304 else if (orientation == PlotOrientation.VERTICAL) {
305 space.setTop(fixed.getTop());
306 space.setBottom(fixed.getBottom());
307 }
308 }
309 else {
310 CategoryAxis categoryAxis = getDomainAxis();
311 RectangleEdge categoryEdge = Plot.resolveDomainAxisLocation(
312 getDomainAxisLocation(), orientation);
313 if (categoryAxis != null) {
314 space = categoryAxis.reserveSpace(g2, this, plotArea,
315 categoryEdge, space);
316 }
317 else {
318 if (getDrawSharedDomainAxis()) {
319 space = getDomainAxis().reserveSpace(g2, this, plotArea,
320 categoryEdge, space);
321 }
322 }
323 }
324
325 Rectangle2D adjustedPlotArea = space.shrink(plotArea, null);
326
327 // work out the maximum height or width of the non-shared axes...
328 int n = this.subplots.size();
329 this.subplotAreas = new Rectangle2D[n];
330 double x = adjustedPlotArea.getX();
331 double y = adjustedPlotArea.getY();
332 double usableSize = 0.0;
333 if (orientation == PlotOrientation.HORIZONTAL) {
334 usableSize = adjustedPlotArea.getWidth() - this.gap * (n - 1);
335 }
336 else if (orientation == PlotOrientation.VERTICAL) {
337 usableSize = adjustedPlotArea.getHeight() - this.gap * (n - 1);
338 }
339
340 for (int i = 0; i < n; i++) {
341 CategoryPlot plot = (CategoryPlot) this.subplots.get(i);
342
343 // calculate sub-plot area
344 if (orientation == PlotOrientation.HORIZONTAL) {
345 double w = usableSize * plot.getWeight() / this.totalWeight;
346 this.subplotAreas[i] = new Rectangle2D.Double(x, y, w,
347 adjustedPlotArea.getHeight());
348 x = x + w + this.gap;
349 }
350 else if (orientation == PlotOrientation.VERTICAL) {
351 double h = usableSize * plot.getWeight() / this.totalWeight;
352 this.subplotAreas[i] = new Rectangle2D.Double(x, y,
353 adjustedPlotArea.getWidth(), h);
354 y = y + h + this.gap;
355 }
356
357 AxisSpace subSpace = plot.calculateRangeAxisSpace(g2,
358 this.subplotAreas[i], null);
359 space.ensureAtLeast(subSpace);
360
361 }
362
363 return space;
364 }
365
366 /**
367 * Draws the plot on a Java 2D graphics device (such as the screen or a
368 * printer). Will perform all the placement calculations for each of the
369 * sub-plots and then tell these to draw themselves.
370 *
371 * @param g2 the graphics device.
372 * @param area the area within which the plot (including axis labels)
373 * should be drawn.
374 * @param anchor the anchor point (<code>null</code> permitted).
375 * @param parentState the state from the parent plot, if there is one.
376 * @param info collects information about the drawing (<code>null</code>
377 * permitted).
378 */
379 public void draw(Graphics2D g2,
380 Rectangle2D area,
381 Point2D anchor,
382 PlotState parentState,
383 PlotRenderingInfo info) {
384
385 // set up info collection...
386 if (info != null) {
387 info.setPlotArea(area);
388 }
389
390 // adjust the drawing area for plot insets (if any)...
391 RectangleInsets insets = getInsets();
392 area.setRect(area.getX() + insets.getLeft(),
393 area.getY() + insets.getTop(),
394 area.getWidth() - insets.getLeft() - insets.getRight(),
395 area.getHeight() - insets.getTop() - insets.getBottom());
396
397
398 // calculate the data area...
399 setFixedRangeAxisSpaceForSubplots(null);
400 AxisSpace space = calculateAxisSpace(g2, area);
401 Rectangle2D dataArea = space.shrink(area, null);
402
403 // set the width and height of non-shared axis of all sub-plots
404 setFixedRangeAxisSpaceForSubplots(space);
405
406 // draw the shared axis
407 CategoryAxis axis = getDomainAxis();
408 RectangleEdge domainEdge = getDomainAxisEdge();
409 double cursor = RectangleEdge.coordinate(dataArea, domainEdge);
410 AxisState axisState = axis.draw(g2, cursor, area, dataArea,
411 domainEdge, info);
412 if (parentState == null) {
413 parentState = new PlotState();
414 }
415 parentState.getSharedAxisStates().put(axis, axisState);
416
417 // draw all the subplots
418 for (int i = 0; i < this.subplots.size(); i++) {
419 CategoryPlot plot = (CategoryPlot) this.subplots.get(i);
420 PlotRenderingInfo subplotInfo = null;
421 if (info != null) {
422 subplotInfo = new PlotRenderingInfo(info.getOwner());
423 info.addSubplotInfo(subplotInfo);
424 }
425 plot.draw(g2, this.subplotAreas[i], null, parentState, subplotInfo);
426 }
427
428 if (info != null) {
429 info.setDataArea(dataArea);
430 }
431
432 }
433
434 /**
435 * Sets the size (width or height, depending on the orientation of the
436 * plot) for the range axis of each subplot.
437 *
438 * @param space the space (<code>null</code> permitted).
439 */
440 protected void setFixedRangeAxisSpaceForSubplots(AxisSpace space) {
441
442 Iterator iterator = this.subplots.iterator();
443 while (iterator.hasNext()) {
444 CategoryPlot plot = (CategoryPlot) iterator.next();
445 plot.setFixedRangeAxisSpace(space);
446 }
447
448 }
449
450 /**
451 * Sets the orientation of the plot (and all subplots).
452 *
453 * @param orientation the orientation (<code>null</code> not permitted).
454 */
455 public void setOrientation(PlotOrientation orientation) {
456
457 super.setOrientation(orientation);
458
459 Iterator iterator = this.subplots.iterator();
460 while (iterator.hasNext()) {
461 CategoryPlot plot = (CategoryPlot) iterator.next();
462 plot.setOrientation(orientation);
463 }
464
465 }
466
467 /**
468 * Returns a collection of legend items for the plot.
469 *
470 * @return The legend items.
471 */
472 public LegendItemCollection getLegendItems() {
473 LegendItemCollection result = getFixedLegendItems();
474 if (result == null) {
475 result = new LegendItemCollection();
476 if (this.subplots != null) {
477 Iterator iterator = this.subplots.iterator();
478 while (iterator.hasNext()) {
479 CategoryPlot plot = (CategoryPlot) iterator.next();
480 LegendItemCollection more = plot.getLegendItems();
481 result.addAll(more);
482 }
483 }
484 }
485 return result;
486 }
487
488 /**
489 * Returns an unmodifiable list of the categories contained in all the
490 * subplots.
491 *
492 * @return The list.
493 */
494 public List getCategories() {
495 List result = new java.util.ArrayList();
496 if (this.subplots != null) {
497 Iterator iterator = this.subplots.iterator();
498 while (iterator.hasNext()) {
499 CategoryPlot plot = (CategoryPlot) iterator.next();
500 List more = plot.getCategories();
501 Iterator moreIterator = more.iterator();
502 while (moreIterator.hasNext()) {
503 Comparable category = (Comparable) moreIterator.next();
504 if (!result.contains(category)) {
505 result.add(category);
506 }
507 }
508 }
509 }
510 return Collections.unmodifiableList(result);
511 }
512
513 /**
514 * Overridden to return the categories in the subplots.
515 *
516 * @param axis ignored.
517 *
518 * @return A list of the categories in the subplots.
519 *
520 * @since 1.0.3
521 */
522 public List getCategoriesForAxis(CategoryAxis axis) {
523 // FIXME: this code means that it is not possible to use more than
524 // one domain axis for the combined plots...
525 return getCategories();
526 }
527
528 /**
529 * Handles a 'click' on the plot.
530 *
531 * @param x x-coordinate of the click.
532 * @param y y-coordinate of the click.
533 * @param info information about the plot's dimensions.
534 *
535 */
536 public void handleClick(int x, int y, PlotRenderingInfo info) {
537
538 Rectangle2D dataArea = info.getDataArea();
539 if (dataArea.contains(x, y)) {
540 for (int i = 0; i < this.subplots.size(); i++) {
541 CategoryPlot subplot = (CategoryPlot) this.subplots.get(i);
542 PlotRenderingInfo subplotInfo = info.getSubplotInfo(i);
543 subplot.handleClick(x, y, subplotInfo);
544 }
545 }
546
547 }
548
549 /**
550 * Receives a {@link PlotChangeEvent} and responds by notifying all
551 * listeners.
552 *
553 * @param event the event.
554 */
555 public void plotChanged(PlotChangeEvent event) {
556 notifyListeners(event);
557 }
558
559 /**
560 * Tests the plot for equality with an arbitrary object.
561 *
562 * @param obj the object (<code>null</code> permitted).
563 *
564 * @return A boolean.
565 */
566 public boolean equals(Object obj) {
567 if (obj == this) {
568 return true;
569 }
570 if (!(obj instanceof CombinedDomainCategoryPlot)) {
571 return false;
572 }
573 if (!super.equals(obj)) {
574 return false;
575 }
576 CombinedDomainCategoryPlot plot = (CombinedDomainCategoryPlot) obj;
577 if (!ObjectUtilities.equal(this.subplots, plot.subplots)) {
578 return false;
579 }
580 if (this.totalWeight != plot.totalWeight) {
581 return false;
582 }
583 if (this.gap != plot.gap) {
584 return false;
585 }
586 return true;
587 }
588
589 /**
590 * Returns a clone of the plot.
591 *
592 * @return A clone.
593 *
594 * @throws CloneNotSupportedException this class will not throw this
595 * exception, but subclasses (if any) might.
596 */
597 public Object clone() throws CloneNotSupportedException {
598
599 CombinedDomainCategoryPlot result
600 = (CombinedDomainCategoryPlot) super.clone();
601 result.subplots = (List) ObjectUtilities.deepClone(this.subplots);
602 for (Iterator it = result.subplots.iterator(); it.hasNext();) {
603 Plot child = (Plot) it.next();
604 child.setParent(result);
605 }
606 return result;
607
608 }
609
610 }