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