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