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