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