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 * CombinedDomainXYPlot.java
029 * -------------------------
030 * (C) Copyright 2001-2007, 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 *                   Nicolas Brodu;
038 *                   Petr Kubanek (bug 1606205);
039 *
040 * Changes:
041 * --------
042 * 06-Dec-2001 : Version 1 (BK);
043 * 12-Dec-2001 : Removed unnecessary 'throws' clause from constructor (DG);
044 * 18-Dec-2001 : Added plotArea attribute and get/set methods (BK);
045 * 22-Dec-2001 : Fixed bug in chartChanged with multiple combinations of 
046 *               CombinedPlots (BK);
047 * 08-Jan-2002 : Moved to new package com.jrefinery.chart.combination (DG);
048 * 25-Feb-2002 : Updated import statements (DG);
049 * 28-Feb-2002 : Readded "this.plotArea = plotArea" that was deleted from 
050 *               draw() method (BK);
051 * 26-Mar-2002 : Added an empty zoom method (this method needs to be written so
052 *               that combined plots will support zooming (DG);
053 * 29-Mar-2002 : Changed the method createCombinedAxis adding the creation of 
054 *               OverlaidSymbolicAxis and CombinedSymbolicAxis(AB);
055 * 23-Apr-2002 : Renamed CombinedPlot-->MultiXYPlot, and simplified the    
056 *               structure (DG);
057 * 23-May-2002 : Renamed (again) MultiXYPlot-->CombinedXYPlot (DG);
058 * 19-Jun-2002 : Added get/setGap() methods suggested by David Basten (DG);
059 * 25-Jun-2002 : Removed redundant imports (DG);
060 * 16-Jul-2002 : Draws shared axis after subplots (to fix missing gridlines),
061 *               added overrides of 'setSeriesPaint()' and 'setXYItemRenderer()'
062 *               that pass changes down to subplots (KF);
063 * 09-Oct-2002 : Added add(XYPlot) method (DG);
064 * 26-Mar-2003 : Implemented Serializable (DG);
065 * 16-May-2003 : Renamed CombinedXYPlot --> CombinedDomainXYPlot (DG);
066 * 04-Aug-2003 : Removed leftover code that was causing domain axis drawing 
067 *               problem (DG);
068 * 08-Aug-2003 : Adjusted totalWeight in remove() method (DG);
069 * 21-Aug-2003 : Implemented Cloneable (DG);
070 * 11-Sep-2003 : Fix cloning support (subplots) (NB);
071 * 15-Sep-2003 : Fixed error in cloning (DG);
072 * 16-Sep-2003 : Changed ChartRenderingInfo --> PlotRenderingInfo (DG);
073 * 17-Sep-2003 : Updated handling of 'clicks' (DG);
074 * 12-Nov-2004 : Implemented the new Zoomable interface (DG);
075 * 25-Nov-2004 : Small update to clone() implementation (DG);
076 * 21-Feb-2005 : The getLegendItems() method now returns the fixed legend
077 *               items if set (DG);
078 * 05-May-2005 : Removed unused draw() method (DG);
079 * ------------- JFREECHART 1.0.x ---------------------------------------------
080 * 23-Aug-2006 : Override setFixedRangeAxisSpace() to update subplots (DG);
081 * 06-Feb-2007 : Fixed bug 1606205, draw shared axis after subplots (DG);
082 * 23-Mar-2007 : Reverted previous patch (bug fix 1606205) (DG);
083 * 17-Apr-2007 : Added null argument checks to findSubplot() (DG);
084 *
085 */
086
087package org.jfree.chart.plot;
088
089import java.awt.Graphics2D;
090import java.awt.geom.Point2D;
091import java.awt.geom.Rectangle2D;
092import java.io.Serializable;
093import java.util.Collections;
094import java.util.Iterator;
095import java.util.List;
096
097import org.jfree.chart.LegendItemCollection;
098import org.jfree.chart.axis.AxisSpace;
099import org.jfree.chart.axis.AxisState;
100import org.jfree.chart.axis.NumberAxis;
101import org.jfree.chart.axis.ValueAxis;
102import org.jfree.chart.event.PlotChangeEvent;
103import org.jfree.chart.event.PlotChangeListener;
104import org.jfree.chart.renderer.xy.XYItemRenderer;
105import org.jfree.data.Range;
106import org.jfree.ui.RectangleEdge;
107import org.jfree.ui.RectangleInsets;
108import org.jfree.util.ObjectUtilities;
109import org.jfree.util.PublicCloneable;
110
111/**
112 * An extension of {@link XYPlot} that contains multiple subplots that share a 
113 * common domain axis.
114 */
115public class CombinedDomainXYPlot extends XYPlot 
116                                  implements Cloneable, PublicCloneable, 
117                                             Serializable,
118                                             PlotChangeListener {
119
120    /** For serialization. */
121    private static final long serialVersionUID = -7765545541261907383L;
122    
123    /** Storage for the subplot references. */
124    private List subplots;
125
126    /** Total weight of all charts. */
127    private int totalWeight = 0;
128
129    /** The gap between subplots. */
130    private double gap = 5.0;
131
132    /** Temporary storage for the subplot areas. */
133    private transient Rectangle2D[] subplotAreas;
134    // TODO:  the subplot areas needs to be moved out of the plot into the plot
135    //        state
136    
137    /**
138     * Default constructor.
139     */
140    public CombinedDomainXYPlot() {
141        this(new NumberAxis());      
142    }
143    
144    /**
145     * Creates a new combined plot that shares a domain axis among multiple 
146     * subplots.
147     *
148     * @param domainAxis  the shared axis.
149     */
150    public CombinedDomainXYPlot(ValueAxis domainAxis) {
151
152        super(
153            null,        // no data in the parent plot
154            domainAxis,
155            null,        // no range axis
156            null         // no rendereer
157        );  
158
159        this.subplots = new java.util.ArrayList();
160
161    }
162
163    /**
164     * Returns a string describing the type of plot.
165     *
166     * @return The type of plot.
167     */
168    public String getPlotType() {
169        return "Combined_Domain_XYPlot";
170    }
171
172    /**
173     * Sets the orientation for the plot (also changes the orientation for all 
174     * the subplots to match).
175     * 
176     * @param orientation  the orientation (<code>null</code> not allowed).
177     */
178    public void setOrientation(PlotOrientation orientation) {
179
180        super.setOrientation(orientation);
181        Iterator iterator = this.subplots.iterator();
182        while (iterator.hasNext()) {
183            XYPlot plot = (XYPlot) iterator.next();
184            plot.setOrientation(orientation);
185        }
186
187    }
188
189    /**
190     * Returns the range for the specified axis.  This is the combined range 
191     * of all the subplots.
192     *
193     * @param axis  the axis.
194     *
195     * @return The range (possibly <code>null</code>).
196     */
197    public Range getDataRange(ValueAxis axis) {
198
199        Range result = null;
200        if (this.subplots != null) {
201            Iterator iterator = this.subplots.iterator();
202            while (iterator.hasNext()) {
203                XYPlot subplot = (XYPlot) iterator.next();
204                result = Range.combine(result, subplot.getDataRange(axis));
205            }
206        }
207        return result;
208
209    }
210
211    /**
212     * Returns the gap between subplots, measured in Java2D units.
213     *
214     * @return The gap (in Java2D units).
215     */
216    public double getGap() {
217        return this.gap;
218    }
219
220    /**
221     * Sets the amount of space between subplots and sends a 
222     * {@link PlotChangeEvent} to all registered listeners.
223     *
224     * @param gap  the gap between subplots (in Java2D units).
225     */
226    public void setGap(double gap) {
227        this.gap = gap;
228        notifyListeners(new PlotChangeEvent(this));
229    }
230
231    /**
232     * Adds a subplot (with a default 'weight' of 1) and sends a 
233     * {@link PlotChangeEvent} to all registered listeners.
234     * <P>
235     * The domain axis for the subplot will be set to <code>null</code>.  You
236     * must ensure that the subplot has a non-null range axis.
237     *
238     * @param subplot  the subplot (<code>null</code> not permitted).
239     */
240    public void add(XYPlot subplot) {
241        // defer argument checking
242        add(subplot, 1);
243    }
244
245    /**
246     * Adds a subplot with the specified weight and sends a 
247     * {@link PlotChangeEvent} to all registered listeners.  The weight 
248     * determines how much space is allocated to the subplot relative to all 
249     * the other subplots.
250     * <P>
251     * The domain axis for the subplot will be set to <code>null</code>.  You
252     * must ensure that the subplot has a non-null range axis.
253     *
254     * @param subplot  the subplot (<code>null</code> not permitted).
255     * @param weight  the weight (must be >= 1).
256     */
257    public void add(XYPlot subplot, int weight) {
258
259        if (subplot == null) {
260            throw new IllegalArgumentException("Null 'subplot' argument.");
261        }
262        if (weight <= 0) {
263            throw new IllegalArgumentException("Require weight >= 1.");
264        }
265
266        // store the plot and its weight
267        subplot.setParent(this);
268        subplot.setWeight(weight);
269        subplot.setInsets(new RectangleInsets(0.0, 0.0, 0.0, 0.0), false);
270        subplot.setDomainAxis(null);
271        subplot.addChangeListener(this);
272        this.subplots.add(subplot);
273
274        // keep track of total weights
275        this.totalWeight += weight;
276
277        ValueAxis axis = getDomainAxis();
278        if (axis != null) {
279            axis.configure();
280        }
281        
282        notifyListeners(new PlotChangeEvent(this));
283
284    }
285
286    /**
287     * Removes a subplot from the combined chart and sends a 
288     * {@link PlotChangeEvent} to all registered listeners.
289     *
290     * @param subplot  the subplot (<code>null</code> not permitted).
291     */
292    public void remove(XYPlot subplot) {
293        if (subplot == null) {
294            throw new IllegalArgumentException(" Null 'subplot' argument.");   
295        }
296        int position = -1;
297        int size = this.subplots.size();
298        int i = 0;
299        while (position == -1 && i < size) {
300            if (this.subplots.get(i) == subplot) {
301                position = i;
302            }
303            i++;
304        }
305        if (position != -1) {
306            this.subplots.remove(position);
307            subplot.setParent(null);
308            subplot.removeChangeListener(this);
309            this.totalWeight -= subplot.getWeight();
310
311            ValueAxis domain = getDomainAxis();
312            if (domain != null) {
313                domain.configure();
314            }
315            notifyListeners(new PlotChangeEvent(this));
316        }
317    }
318
319    /**
320     * Returns the list of subplots.
321     *
322     * @return An unmodifiable list of subplots.
323     */
324    public List getSubplots() {
325        return Collections.unmodifiableList(this.subplots);
326    }
327
328    /**
329     * Calculates the axis space required.
330     * 
331     * @param g2  the graphics device.
332     * @param plotArea  the plot area.
333     * 
334     * @return The space.
335     */
336    protected AxisSpace calculateAxisSpace(Graphics2D g2, 
337                                           Rectangle2D plotArea) {
338        
339        AxisSpace space = new AxisSpace();
340        PlotOrientation orientation = getOrientation();
341        
342        // work out the space required by the domain axis...
343        AxisSpace fixed = getFixedDomainAxisSpace();
344        if (fixed != null) {
345            if (orientation == PlotOrientation.HORIZONTAL) {
346                space.setLeft(fixed.getLeft());
347                space.setRight(fixed.getRight());
348            }
349            else if (orientation == PlotOrientation.VERTICAL) {
350                space.setTop(fixed.getTop());
351                space.setBottom(fixed.getBottom());                
352            }
353        }
354        else {
355            ValueAxis xAxis = getDomainAxis();
356            RectangleEdge xEdge = Plot.resolveDomainAxisLocation(
357                    getDomainAxisLocation(), orientation);
358            if (xAxis != null) {
359                space = xAxis.reserveSpace(g2, this, plotArea, xEdge, space);
360            }
361        }
362        
363        Rectangle2D adjustedPlotArea = space.shrink(plotArea, null);
364        
365        // work out the maximum height or width of the non-shared axes...
366        int n = this.subplots.size();
367        this.subplotAreas = new Rectangle2D[n];
368        double x = adjustedPlotArea.getX();
369        double y = adjustedPlotArea.getY();
370        double usableSize = 0.0;
371        if (orientation == PlotOrientation.HORIZONTAL) {
372            usableSize = adjustedPlotArea.getWidth() - this.gap * (n - 1);
373        }
374        else if (orientation == PlotOrientation.VERTICAL) {
375            usableSize = adjustedPlotArea.getHeight() - this.gap * (n - 1);
376        }
377
378        for (int i = 0; i < n; i++) {
379            XYPlot plot = (XYPlot) this.subplots.get(i);
380
381            // calculate sub-plot area
382            if (orientation == PlotOrientation.HORIZONTAL) {
383                double w = usableSize * plot.getWeight() / this.totalWeight;
384                this.subplotAreas[i] = new Rectangle2D.Double(x, y, w, 
385                        adjustedPlotArea.getHeight());
386                x = x + w + this.gap;
387            }
388            else if (orientation == PlotOrientation.VERTICAL) {
389                double h = usableSize * plot.getWeight() / this.totalWeight;
390                this.subplotAreas[i] = new Rectangle2D.Double(x, y, 
391                        adjustedPlotArea.getWidth(), h);
392                y = y + h + this.gap;
393            }
394
395            AxisSpace subSpace = plot.calculateRangeAxisSpace(g2, 
396                    this.subplotAreas[i], null);
397            space.ensureAtLeast(subSpace);
398
399        }
400
401        return space;
402    }
403    
404    /**
405     * Draws the plot within the specified area on a graphics device.
406     * 
407     * @param g2  the graphics device.
408     * @param area  the plot area (in Java2D space).
409     * @param anchor  an anchor point in Java2D space (<code>null</code> 
410     *                permitted).
411     * @param parentState  the state from the parent plot, if there is one 
412     *                     (<code>null</code> permitted).
413     * @param info  collects chart drawing information (<code>null</code> 
414     *              permitted).
415     */
416    public void draw(Graphics2D g2,
417                     Rectangle2D area,
418                     Point2D anchor,
419                     PlotState parentState,
420                     PlotRenderingInfo info) {
421        
422        // set up info collection...
423        if (info != null) {
424            info.setPlotArea(area);
425        }
426
427        // adjust the drawing area for plot insets (if any)...
428        RectangleInsets insets = getInsets();
429        insets.trim(area);
430
431        AxisSpace space = calculateAxisSpace(g2, area);
432        Rectangle2D dataArea = space.shrink(area, null);
433
434        // set the width and height of non-shared axis of all sub-plots
435        setFixedRangeAxisSpaceForSubplots(space);
436
437        // draw the shared axis
438        ValueAxis axis = getDomainAxis();
439        RectangleEdge edge = getDomainAxisEdge();
440        double cursor = RectangleEdge.coordinate(dataArea, edge);
441        AxisState axisState = axis.draw(g2, cursor, area, dataArea, edge, info);
442        if (parentState == null) {
443            parentState = new PlotState();
444        }
445        parentState.getSharedAxisStates().put(axis, axisState);
446        
447        // draw all the subplots
448        for (int i = 0; i < this.subplots.size(); i++) {
449            XYPlot plot = (XYPlot) this.subplots.get(i);
450            PlotRenderingInfo subplotInfo = null;
451            if (info != null) {
452                subplotInfo = new PlotRenderingInfo(info.getOwner());
453                info.addSubplotInfo(subplotInfo);
454            }
455            plot.draw(g2, this.subplotAreas[i], anchor, parentState, 
456                    subplotInfo);
457        }
458
459        if (info != null) {
460            info.setDataArea(dataArea);
461        }
462        
463    }
464
465    /**
466     * Returns a collection of legend items for the plot.
467     *
468     * @return The legend items.
469     */
470    public LegendItemCollection getLegendItems() {        
471        LegendItemCollection result = getFixedLegendItems();
472        if (result == null) {
473            result = new LegendItemCollection();
474            if (this.subplots != null) {
475                Iterator iterator = this.subplots.iterator();
476                while (iterator.hasNext()) {
477                    XYPlot plot = (XYPlot) iterator.next();
478                    LegendItemCollection more = plot.getLegendItems();
479                    result.addAll(more);
480                }
481            }
482        }
483        return result;
484    }
485
486    /**
487     * Multiplies the range on the range axis/axes by the specified factor.
488     *
489     * @param factor  the zoom factor.
490     * @param info  the plot rendering info (<code>null</code> not permitted).
491     * @param source  the source point (<code>null</code> not permitted).
492     */
493    public void zoomRangeAxes(double factor, PlotRenderingInfo info, 
494                              Point2D source) {
495        // delegate 'info' and 'source' argument checks...
496        XYPlot subplot = findSubplot(info, source);
497        if (subplot != null) {
498            subplot.zoomRangeAxes(factor, info, source);
499        }
500        else {
501            // if the source point doesn't fall within a subplot, we do the
502            // zoom on all subplots...
503            Iterator iterator = getSubplots().iterator();
504            while (iterator.hasNext()) {
505                subplot = (XYPlot) iterator.next();
506                subplot.zoomRangeAxes(factor, info, source);
507            }
508        }
509    }
510
511    /**
512     * Zooms in on the range axes.
513     *
514     * @param lowerPercent  the lower bound.
515     * @param upperPercent  the upper bound.
516     * @param info  the plot rendering info (<code>null</code> not permitted).
517     * @param source  the source point (<code>null</code> not permitted).
518     */
519    public void zoomRangeAxes(double lowerPercent, double upperPercent, 
520                              PlotRenderingInfo info, Point2D source) {
521        // delegate 'info' and 'source' argument checks...
522        XYPlot subplot = findSubplot(info, source);
523        if (subplot != null) {
524            subplot.zoomRangeAxes(lowerPercent, upperPercent, info, source);
525        }
526        else {
527            // if the source point doesn't fall within a subplot, we do the
528            // zoom on all subplots...
529            Iterator iterator = getSubplots().iterator();
530            while (iterator.hasNext()) {
531                subplot = (XYPlot) iterator.next();
532                subplot.zoomRangeAxes(lowerPercent, upperPercent, info, source);
533            }
534        }
535    }
536
537    /**
538     * Returns the subplot (if any) that contains the (x, y) point (specified 
539     * in Java2D space).
540     * 
541     * @param info  the chart rendering info (<code>null</code> not permitted).
542     * @param source  the source point (<code>null</code> not permitted).
543     * 
544     * @return A subplot (possibly <code>null</code>).
545     */
546    public XYPlot findSubplot(PlotRenderingInfo info, Point2D source) {
547        if (info == null) {
548            throw new IllegalArgumentException("Null 'info' argument.");
549        }
550        if (source == null) {
551            throw new IllegalArgumentException("Null 'source' argument.");
552        }
553        XYPlot result = null;
554        int subplotIndex = info.getSubplotIndex(source);
555        if (subplotIndex >= 0) {
556            result =  (XYPlot) this.subplots.get(subplotIndex);
557        }
558        return result;
559    }
560    
561    /**
562     * Sets the item renderer FOR ALL SUBPLOTS.  Registered listeners are 
563     * notified that the plot has been modified.
564     * <P>
565     * Note: usually you will want to set the renderer independently for each 
566     * subplot, which is NOT what this method does.
567     *
568     * @param renderer the new renderer.
569     */
570    public void setRenderer(XYItemRenderer renderer) {
571
572        super.setRenderer(renderer);  // not strictly necessary, since the 
573                                      // renderer set for the
574                                      // parent plot is not used
575
576        Iterator iterator = this.subplots.iterator();
577        while (iterator.hasNext()) {
578            XYPlot plot = (XYPlot) iterator.next();
579            plot.setRenderer(renderer);
580        }
581
582    }
583
584    /**
585     * Sets the fixed range axis space.
586     *
587     * @param space  the space (<code>null</code> permitted).
588     */
589    public void setFixedRangeAxisSpace(AxisSpace space) {
590        super.setFixedRangeAxisSpace(space);
591        setFixedRangeAxisSpaceForSubplots(space);
592        this.notifyListeners(new PlotChangeEvent(this));
593    }
594    
595    /**
596     * Sets the size (width or height, depending on the orientation of the 
597     * plot) for the domain axis of each subplot.
598     *
599     * @param space  the space.
600     */
601    protected void setFixedRangeAxisSpaceForSubplots(AxisSpace space) {
602
603        Iterator iterator = this.subplots.iterator();
604        while (iterator.hasNext()) {
605            XYPlot plot = (XYPlot) iterator.next();
606            plot.setFixedRangeAxisSpace(space);
607        }
608
609    }
610
611    /**
612     * Handles a 'click' on the plot by updating the anchor values.
613     *
614     * @param x  x-coordinate, where the click occured.
615     * @param y  y-coordinate, where the click occured.
616     * @param info  object containing information about the plot dimensions.
617     */
618    public void handleClick(int x, int y, PlotRenderingInfo info) {
619        Rectangle2D dataArea = info.getDataArea();
620        if (dataArea.contains(x, y)) {
621            for (int i = 0; i < this.subplots.size(); i++) {
622                XYPlot subplot = (XYPlot) this.subplots.get(i);
623                PlotRenderingInfo subplotInfo = info.getSubplotInfo(i);
624                subplot.handleClick(x, y, subplotInfo);
625            }
626        }
627    }
628    
629    /**
630     * Receives a {@link PlotChangeEvent} and responds by notifying all 
631     * listeners.
632     * 
633     * @param event  the event.
634     */
635    public void plotChanged(PlotChangeEvent event) {
636        notifyListeners(event);
637    }
638
639    /**
640     * Tests this plot for equality with another object.
641     *
642     * @param obj  the other object.
643     *
644     * @return <code>true</code> or <code>false</code>.
645     */
646    public boolean equals(Object obj) {
647
648        if (obj == null) {
649            return false;
650        }
651
652        if (obj == this) {
653            return true;
654        }
655
656        if (!(obj instanceof CombinedDomainXYPlot)) {
657            return false;
658        }
659        if (!super.equals(obj)) {
660            return false;
661        }
662
663        CombinedDomainXYPlot p = (CombinedDomainXYPlot) obj;
664        if (this.totalWeight != p.totalWeight) {
665            return false;
666        }
667        if (this.gap != p.gap) {
668            return false;
669        }
670        if (!ObjectUtilities.equal(this.subplots, p.subplots)) {
671            return false;
672        }
673
674        return true;
675    }
676    
677    /**
678     * Returns a clone of the annotation.
679     * 
680     * @return A clone.
681     * 
682     * @throws CloneNotSupportedException  this class will not throw this 
683     *         exception, but subclasses (if any) might.
684     */
685    public Object clone() throws CloneNotSupportedException {
686        
687        CombinedDomainXYPlot result = (CombinedDomainXYPlot) super.clone(); 
688        result.subplots = (List) ObjectUtilities.deepClone(this.subplots);
689        for (Iterator it = result.subplots.iterator(); it.hasNext();) {
690            Plot child = (Plot) it.next();
691            child.setParent(result);
692        }
693        
694        // after setting up all the subplots, the shared domain axis may need 
695        // reconfiguring
696        ValueAxis domainAxis = result.getDomainAxis();
697        if (domainAxis != null) {
698            domainAxis.configure();
699        }
700        
701        return result;
702        
703    }
704    
705}