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