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