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 * CombinedRangeCategoryPlot.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 : Implemented Cloneable (DG);
040 * 11-Sep-2003 : Fix cloning support (subplots) (NB);
041 * 15-Sep-2003 : Implemented PublicCloneable.  Fixed errors in cloning and 
042 *               serialization (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' attributes (DG);
046 * 12-Nov-2004 : Implements the new Zoomable interface (DG);
047 * 25-Nov-2004 : Small update to clone() implementation (DG);
048 * 21-Feb-2005 : Fixed bug in remove() method (id = 1121172) (DG);
049 * 21-Feb-2005 : The getLegendItems() method now returns the fixed legend
050 *               items if set (DG);
051 * 05-May-2005 : Updated draw() method parameters (DG);
052 * 14-Nov-2007 : Updated setFixedDomainAxisSpaceForSubplots() method (DG);
053 */
054 
055package org.jfree.chart.plot;
056
057import java.awt.Graphics2D;
058import java.awt.geom.Point2D;
059import java.awt.geom.Rectangle2D;
060import java.io.IOException;
061import java.io.ObjectInputStream;
062import java.io.Serializable;
063import java.util.Collections;
064import java.util.Iterator;
065import java.util.List;
066
067import org.jfree.chart.LegendItemCollection;
068import org.jfree.chart.axis.AxisSpace;
069import org.jfree.chart.axis.AxisState;
070import org.jfree.chart.axis.NumberAxis;
071import org.jfree.chart.axis.ValueAxis;
072import org.jfree.chart.event.PlotChangeEvent;
073import org.jfree.chart.event.PlotChangeListener;
074import org.jfree.data.Range;
075import org.jfree.ui.RectangleEdge;
076import org.jfree.ui.RectangleInsets;
077import org.jfree.util.ObjectUtilities;
078import org.jfree.util.PublicCloneable;
079
080/**
081 * A combined category plot where the range axis is shared.
082 */
083public class CombinedRangeCategoryPlot extends CategoryPlot 
084                                       implements Zoomable,
085                                                  Cloneable, PublicCloneable, 
086                                                  Serializable,
087                                                  PlotChangeListener {
088
089    /** For serialization. */
090    private static final long serialVersionUID = 7260210007554504515L;
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[] subplotArea;  // TODO: move to plot state
103
104    /**
105     * Default constructor.
106     */
107    public CombinedRangeCategoryPlot() {
108        this(new NumberAxis());
109    }
110    
111    /**
112     * Creates a new plot.
113     *
114     * @param rangeAxis  the shared range axis.
115     */
116    public CombinedRangeCategoryPlot(ValueAxis rangeAxis) {
117        super(null, null, rangeAxis, null);
118        this.subplots = new java.util.ArrayList();
119        this.totalWeight = 0;
120        this.gap = 5.0;
121    }
122
123    /**
124     * Returns the space between subplots.
125     *
126     * @return The gap (in Java2D units).
127     */
128    public double getGap() {
129        return this.gap;
130    }
131
132    /**
133     * Sets the amount of space between subplots and sends a 
134     * {@link PlotChangeEvent} to all registered listeners.
135     *
136     * @param gap  the gap between subplots (in Java2D units).
137     */
138    public void setGap(double gap) {
139        this.gap = gap;
140        notifyListeners(new PlotChangeEvent(this));
141    }
142
143    /**
144     * Adds a subplot (with a default 'weight' of 1) and sends a 
145     * {@link PlotChangeEvent} to all registered listeners.
146     * <br><br>
147     * You must ensure that the subplot has a non-null domain axis.  The range
148     * axis for the subplot will be set to <code>null</code>.  
149     *
150     * @param subplot  the subplot (<code>null</code> not permitted).
151     */
152    public void add(CategoryPlot subplot) {
153        // defer argument checking
154        add(subplot, 1);
155    }
156
157    /**
158     * Adds a subplot and sends a {@link PlotChangeEvent} to all registered 
159     * listeners.
160     * <br><br>
161     * You must ensure that the subplot has a non-null domain axis.  The range
162     * axis for the subplot will be set to <code>null</code>.  
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 <= 0) {
172            throw new IllegalArgumentException("Require weight >= 1.");
173        }
174        // store the plot and its weight
175        subplot.setParent(this);
176        subplot.setWeight(weight);
177        subplot.setInsets(new RectangleInsets(0.0, 0.0, 0.0, 0.0));
178        subplot.setRangeAxis(null);
179        subplot.setOrientation(getOrientation());
180        subplot.addChangeListener(this);
181        this.subplots.add(subplot);
182        this.totalWeight += weight;
183        
184        // configure the range axis...
185        ValueAxis axis = getRangeAxis();
186        if (axis != null) {
187            axis.configure();
188        }
189        notifyListeners(new PlotChangeEvent(this));
190    }
191
192    /**
193     * Removes a subplot from the combined chart.
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            ValueAxis range = getRangeAxis();
217            if (range != null) {
218                range.configure();
219            }
220
221            ValueAxis range2 = getRangeAxis(1);
222            if (range2 != null) {
223                range2.configure();
224            }
225            notifyListeners(new PlotChangeEvent(this));
226        }
227    }
228
229    /**
230     * Returns the list of subplots.
231     *
232     * @return The list (unmodifiable).
233     */
234    public List getSubplots() {
235        return Collections.unmodifiableList(this.subplots);
236    }
237
238    /**
239     * Calculates the space required for the axes.
240     * 
241     * @param g2  the graphics device.
242     * @param plotArea  the plot area.
243     * 
244     * @return The space required for the axes.
245     */
246    protected AxisSpace calculateAxisSpace(Graphics2D g2, 
247                                           Rectangle2D plotArea) {
248        
249        AxisSpace space = new AxisSpace();  
250        PlotOrientation orientation = getOrientation();
251        
252        // work out the space required by the domain axis...
253        AxisSpace fixed = getFixedRangeAxisSpace();
254        if (fixed != null) {
255            if (orientation == PlotOrientation.VERTICAL) {
256                space.setLeft(fixed.getLeft());
257                space.setRight(fixed.getRight());
258            }
259            else if (orientation == PlotOrientation.HORIZONTAL) {
260                space.setTop(fixed.getTop());
261                space.setBottom(fixed.getBottom());                
262            }
263        }
264        else {
265            ValueAxis valueAxis = getRangeAxis();
266            RectangleEdge valueEdge = Plot.resolveRangeAxisLocation(
267                    getRangeAxisLocation(), orientation);
268            if (valueAxis != null) {
269                space = valueAxis.reserveSpace(g2, this, plotArea, valueEdge, 
270                        space);
271            }
272        }
273        
274        Rectangle2D adjustedPlotArea = space.shrink(plotArea, null);
275        // work out the maximum height or width of the non-shared axes...
276        int n = this.subplots.size();
277
278        // calculate plotAreas of all sub-plots, maximum vertical/horizontal 
279        // axis width/height
280        this.subplotArea = new Rectangle2D[n];
281        double x = adjustedPlotArea.getX();
282        double y = adjustedPlotArea.getY();
283        double usableSize = 0.0;
284        if (orientation == PlotOrientation.VERTICAL) {
285            usableSize = adjustedPlotArea.getWidth() - this.gap * (n - 1);
286        }
287        else if (orientation == PlotOrientation.HORIZONTAL) {
288            usableSize = adjustedPlotArea.getHeight() - this.gap * (n - 1);
289        }
290
291        for (int i = 0; i < n; i++) {
292            CategoryPlot plot = (CategoryPlot) this.subplots.get(i);
293
294            // calculate sub-plot area
295            if (orientation == PlotOrientation.VERTICAL) {
296                double w = usableSize * plot.getWeight() / this.totalWeight;
297                this.subplotArea[i] = new Rectangle2D.Double(x, y, w, 
298                        adjustedPlotArea.getHeight());
299                x = x + w + this.gap;
300            }
301            else if (orientation == PlotOrientation.HORIZONTAL) {
302                double h = usableSize * plot.getWeight() / this.totalWeight;
303                this.subplotArea[i] = new Rectangle2D.Double(x, y, 
304                        adjustedPlotArea.getWidth(), h);
305                y = y + h + this.gap;
306            }
307
308            AxisSpace subSpace = plot.calculateDomainAxisSpace(g2, 
309                    this.subplotArea[i], null);
310            space.ensureAtLeast(subSpace);
311
312        }
313
314        return space;
315    }
316
317    /**
318     * Draws the plot on a Java 2D graphics device (such as the screen or a 
319     * printer).  Will perform all the placement calculations for each 
320     * sub-plots and then tell these to draw themselves.
321     *
322     * @param g2  the graphics device.
323     * @param area  the area within which the plot (including axis labels)
324     *              should be drawn.
325     * @param anchor  the anchor point (<code>null</code> permitted).
326     * @param parentState  the parent state.
327     * @param info  collects information about the drawing (<code>null</code> 
328     *              permitted).
329     */
330    public void draw(Graphics2D g2, Rectangle2D area, Point2D anchor,
331                     PlotState parentState,
332                     PlotRenderingInfo info) {
333
334        // set up info collection...
335        if (info != null) {
336            info.setPlotArea(area);
337        }
338
339        // adjust the drawing area for plot insets (if any)...
340        RectangleInsets insets = getInsets();
341        insets.trim(area);
342
343        // calculate the data area...
344        AxisSpace space = calculateAxisSpace(g2, area);
345        Rectangle2D dataArea = space.shrink(area, null);
346
347        // set the width and height of non-shared axis of all sub-plots
348        setFixedDomainAxisSpaceForSubplots(space);
349
350        // draw the shared axis
351        ValueAxis axis = getRangeAxis();
352        RectangleEdge rangeEdge = getRangeAxisEdge();
353        double cursor = RectangleEdge.coordinate(dataArea, rangeEdge);
354        AxisState state = axis.draw(g2, cursor, area, dataArea, rangeEdge, 
355                info);
356        if (parentState == null) {
357            parentState = new PlotState();
358        }
359        parentState.getSharedAxisStates().put(axis, state);
360        
361        // draw all the charts
362        for (int i = 0; i < this.subplots.size(); i++) {
363            CategoryPlot plot = (CategoryPlot) this.subplots.get(i);
364            PlotRenderingInfo subplotInfo = null;
365            if (info != null) {
366                subplotInfo = new PlotRenderingInfo(info.getOwner());
367                info.addSubplotInfo(subplotInfo);
368            }
369            plot.draw(g2, this.subplotArea[i], null, parentState, subplotInfo);
370        }
371
372        if (info != null) {
373            info.setDataArea(dataArea);
374        }
375
376    }
377
378    /**
379     * Sets the orientation for the plot (and all the subplots).
380     * 
381     * @param orientation  the orientation.
382     */
383    public void setOrientation(PlotOrientation orientation) {
384
385        super.setOrientation(orientation);
386
387        Iterator iterator = this.subplots.iterator();
388        while (iterator.hasNext()) {
389            CategoryPlot plot = (CategoryPlot) iterator.next();
390            plot.setOrientation(orientation);
391        }
392
393    }
394    
395    /**
396      * Returns the range for the axis.  This is the combined range of all the
397      * subplots.
398      *
399      * @param axis  the axis.
400      *
401      * @return The range.
402      */
403     public Range getDataRange(ValueAxis axis) {
404
405         Range result = null;
406         if (this.subplots != null) {
407             Iterator iterator = this.subplots.iterator();
408             while (iterator.hasNext()) {
409                 CategoryPlot subplot = (CategoryPlot) iterator.next();
410                 result = Range.combine(result, subplot.getDataRange(axis));
411             }
412         }
413         return result;
414
415     }
416
417    /**
418     * Returns a collection of legend items for the plot.
419     *
420     * @return The legend items.
421     */
422    public LegendItemCollection getLegendItems() {
423        LegendItemCollection result = getFixedLegendItems();
424        if (result == null) {
425            result = new LegendItemCollection();
426            if (this.subplots != null) {
427                Iterator iterator = this.subplots.iterator();
428                while (iterator.hasNext()) {
429                    CategoryPlot plot = (CategoryPlot) iterator.next();
430                    LegendItemCollection more = plot.getLegendItems();
431                    result.addAll(more);
432                }
433            }
434        }
435        return result;
436    }
437    
438    /**
439     * Sets the size (width or height, depending on the orientation of the 
440     * plot) for the domain axis of each subplot.
441     *
442     * @param space  the space.
443     */
444    protected void setFixedDomainAxisSpaceForSubplots(AxisSpace space) {
445        Iterator iterator = this.subplots.iterator();
446        while (iterator.hasNext()) {
447            CategoryPlot plot = (CategoryPlot) iterator.next();
448            plot.setFixedDomainAxisSpace(space, false);
449        }
450    }
451
452    /**
453     * Handles a 'click' on the plot by updating the anchor value.
454     *
455     * @param x  x-coordinate of the click.
456     * @param y  y-coordinate of the click.
457     * @param info  information about the plot's dimensions.
458     *
459     */
460    public void handleClick(int x, int y, PlotRenderingInfo info) {
461
462        Rectangle2D dataArea = info.getDataArea();
463        if (dataArea.contains(x, y)) {
464            for (int i = 0; i < this.subplots.size(); i++) {
465                CategoryPlot subplot = (CategoryPlot) this.subplots.get(i);
466                PlotRenderingInfo subplotInfo = info.getSubplotInfo(i);
467                subplot.handleClick(x, y, subplotInfo);
468            }
469        }
470
471    }
472
473    /**
474     * Receives a {@link PlotChangeEvent} and responds by notifying all 
475     * listeners.
476     * 
477     * @param event  the event.
478     */
479    public void plotChanged(PlotChangeEvent event) {
480        notifyListeners(event);
481    }
482
483    /** 
484     * Tests the plot for equality with an arbitrary object.
485     * 
486     * @param obj  the object (<code>null</code> permitted).
487     * 
488     * @return <code>true</code> or <code>false</code>.
489     */
490    public boolean equals(Object obj) {
491        if (obj == this) {
492            return true;
493        }
494        if (!(obj instanceof CombinedRangeCategoryPlot)) {
495            return false;
496        }
497        if (!super.equals(obj)) {
498            return false;
499        }
500        CombinedRangeCategoryPlot that = (CombinedRangeCategoryPlot) obj;
501        if (!ObjectUtilities.equal(this.subplots, that.subplots)) {
502            return false;
503        }
504        if (this.totalWeight != that.totalWeight) {
505            return false;
506        }
507        if (this.gap != that.gap) {
508            return false;
509        }
510        return true;       
511    }
512
513    /**
514     * Returns a clone of the plot.
515     * 
516     * @return A clone.
517     * 
518     * @throws CloneNotSupportedException  this class will not throw this 
519     *         exception, but subclasses (if any) might.
520     */
521    public Object clone() throws CloneNotSupportedException {
522        CombinedRangeCategoryPlot result 
523            = (CombinedRangeCategoryPlot) super.clone(); 
524        result.subplots = (List) ObjectUtilities.deepClone(this.subplots);
525        for (Iterator it = result.subplots.iterator(); it.hasNext();) {
526            Plot child = (Plot) it.next();
527            child.setParent(result);
528        }
529        
530        // after setting up all the subplots, the shared range axis may need 
531        // reconfiguring
532        ValueAxis rangeAxis = result.getRangeAxis();
533        if (rangeAxis != null) {
534            rangeAxis.configure();
535        }
536        
537        return result;
538    }
539
540    /**
541     * Provides serialization support.
542     *
543     * @param stream  the input stream.
544     *
545     * @throws IOException  if there is an I/O error.
546     * @throws ClassNotFoundException  if there is a classpath problem.
547     */
548    private void readObject(ObjectInputStream stream) 
549        throws IOException, ClassNotFoundException {
550
551        stream.defaultReadObject();
552        
553        // the range axis is deserialized before the subplots, so its value 
554        // range is likely to be incorrect...
555        ValueAxis rangeAxis = getRangeAxis();
556        if (rangeAxis != null) {
557            rangeAxis.configure();
558        }
559        
560    }
561
562}