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