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    
062    package org.jfree.chart.plot;
063    
064    import java.awt.Graphics2D;
065    import java.awt.geom.Point2D;
066    import java.awt.geom.Rectangle2D;
067    import java.io.IOException;
068    import java.io.ObjectInputStream;
069    import java.util.Collections;
070    import java.util.Iterator;
071    import java.util.List;
072    
073    import org.jfree.chart.LegendItemCollection;
074    import org.jfree.chart.axis.AxisSpace;
075    import org.jfree.chart.axis.AxisState;
076    import org.jfree.chart.axis.NumberAxis;
077    import org.jfree.chart.axis.ValueAxis;
078    import org.jfree.chart.event.PlotChangeEvent;
079    import org.jfree.chart.event.PlotChangeListener;
080    import org.jfree.data.Range;
081    import org.jfree.ui.RectangleEdge;
082    import org.jfree.ui.RectangleInsets;
083    import org.jfree.util.ObjectUtilities;
084    
085    /**
086     * A combined category plot where the range axis is shared.
087     */
088    public 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    }