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