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     * CombinedDomainCategoryPlot.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 : 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     * 27-Mar-2008 : Add documentation for getDataRange() method (DG);
057     * 31-Mar-2008 : Updated getSubplots() to return EMPTY_LIST for null
058     *               subplots, as suggested by Richard West (DG);
059     * 28-Apr-2008 : Fixed zooming problem (see bug 1950037) (DG);
060     * 26-Jun-2008 : Fixed crosshair support (DG);
061     * 11-Aug-2008 : Don't store totalWeight of subplots, calculate it as
062     *               required (DG);
063     *
064     */
065    
066    package org.jfree.chart.plot;
067    
068    import java.awt.Graphics2D;
069    import java.awt.geom.Point2D;
070    import java.awt.geom.Rectangle2D;
071    import java.util.Collections;
072    import java.util.Iterator;
073    import java.util.List;
074    
075    import org.jfree.chart.LegendItemCollection;
076    import org.jfree.chart.axis.AxisSpace;
077    import org.jfree.chart.axis.AxisState;
078    import org.jfree.chart.axis.CategoryAxis;
079    import org.jfree.chart.axis.ValueAxis;
080    import org.jfree.chart.event.PlotChangeEvent;
081    import org.jfree.chart.event.PlotChangeListener;
082    import org.jfree.data.Range;
083    import org.jfree.ui.RectangleEdge;
084    import org.jfree.ui.RectangleInsets;
085    import org.jfree.util.ObjectUtilities;
086    
087    /**
088     * A combined category plot where the domain axis is shared.
089     */
090    public class CombinedDomainCategoryPlot extends CategoryPlot
091            implements PlotChangeListener {
092    
093        /** For serialization. */
094        private static final long serialVersionUID = 8207194522653701572L;
095    
096        /** Storage for the subplot references. */
097        private List subplots;
098    
099        /** The gap between subplots. */
100        private double gap;
101    
102        /** Temporary storage for the subplot areas. */
103        private transient Rectangle2D[] subplotAreas;
104        // TODO:  move the above to the plot state
105    
106        /**
107         * Default constructor.
108         */
109        public CombinedDomainCategoryPlot() {
110            this(new CategoryAxis());
111        }
112    
113        /**
114         * Creates a new plot.
115         *
116         * @param domainAxis  the shared domain axis (<code>null</code> not
117         *                    permitted).
118         */
119        public CombinedDomainCategoryPlot(CategoryAxis domainAxis) {
120            super(null, domainAxis, null, null);
121            this.subplots = new java.util.ArrayList();
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            fireChangeEvent();
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            CategoryAxis axis = getDomainAxis();
183            if (axis != null) {
184                axis.configure();
185            }
186            fireChangeEvent();
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                CategoryAxis domain = getDomainAxis();
215                if (domain != null) {
216                    domain.configure();
217                }
218                fireChangeEvent();
219            }
220        }
221    
222        /**
223         * Returns the list of subplots.  The returned list may be empty, but is
224         * never <code>null</code>.
225         *
226         * @return An unmodifiable list of subplots.
227         */
228        public List getSubplots() {
229            if (this.subplots != null) {
230                return Collections.unmodifiableList(this.subplots);
231            }
232            else {
233                return Collections.EMPTY_LIST;
234            }
235        }
236    
237        /**
238         * Returns the subplot (if any) that contains the (x, y) point (specified
239         * in Java2D space).
240         *
241         * @param info  the chart rendering info (<code>null</code> not permitted).
242         * @param source  the source point (<code>null</code> not permitted).
243         *
244         * @return A subplot (possibly <code>null</code>).
245         */
246        public CategoryPlot findSubplot(PlotRenderingInfo info, Point2D source) {
247            if (info == null) {
248                throw new IllegalArgumentException("Null 'info' argument.");
249            }
250            if (source == null) {
251                throw new IllegalArgumentException("Null 'source' argument.");
252            }
253            CategoryPlot result = null;
254            int subplotIndex = info.getSubplotIndex(source);
255            if (subplotIndex >= 0) {
256                result =  (CategoryPlot) this.subplots.get(subplotIndex);
257            }
258            return result;
259        }
260    
261        /**
262         * Multiplies the range on the range axis/axes by the specified factor.
263         *
264         * @param factor  the zoom factor.
265         * @param info  the plot rendering info (<code>null</code> not permitted).
266         * @param source  the source point (<code>null</code> not permitted).
267         */
268        public void zoomRangeAxes(double factor, PlotRenderingInfo info,
269                                  Point2D source) {
270            zoomRangeAxes(factor, info, source, false);
271        }
272    
273        /**
274         * Multiplies the range on the range axis/axes by the specified factor.
275         *
276         * @param factor  the zoom factor.
277         * @param info  the plot rendering info (<code>null</code> not permitted).
278         * @param source  the source point (<code>null</code> not permitted).
279         * @param useAnchor  zoom about the anchor point?
280         */
281        public void zoomRangeAxes(double factor, PlotRenderingInfo info,
282                                  Point2D source, boolean useAnchor) {
283            // delegate 'info' and 'source' argument checks...
284            CategoryPlot subplot = findSubplot(info, source);
285            if (subplot != null) {
286                subplot.zoomRangeAxes(factor, info, source, useAnchor);
287            }
288            else {
289                // if the source point doesn't fall within a subplot, we do the
290                // zoom on all subplots...
291                Iterator iterator = getSubplots().iterator();
292                while (iterator.hasNext()) {
293                    subplot = (CategoryPlot) iterator.next();
294                    subplot.zoomRangeAxes(factor, info, source, useAnchor);
295                }
296            }
297        }
298    
299        /**
300         * Zooms in on the range axes.
301         *
302         * @param lowerPercent  the lower bound.
303         * @param upperPercent  the upper bound.
304         * @param info  the plot rendering info (<code>null</code> not permitted).
305         * @param source  the source point (<code>null</code> not permitted).
306         */
307        public void zoomRangeAxes(double lowerPercent, double upperPercent,
308                                  PlotRenderingInfo info, Point2D source) {
309            // delegate 'info' and 'source' argument checks...
310            CategoryPlot subplot = findSubplot(info, source);
311            if (subplot != null) {
312                subplot.zoomRangeAxes(lowerPercent, upperPercent, info, source);
313            }
314            else {
315                // if the source point doesn't fall within a subplot, we do the
316                // zoom on all subplots...
317                Iterator iterator = getSubplots().iterator();
318                while (iterator.hasNext()) {
319                    subplot = (CategoryPlot) iterator.next();
320                    subplot.zoomRangeAxes(lowerPercent, upperPercent, info, source);
321                }
322            }
323        }
324    
325        /**
326         * Calculates the space required for the axes.
327         *
328         * @param g2  the graphics device.
329         * @param plotArea  the plot area.
330         *
331         * @return The space required for the axes.
332         */
333        protected AxisSpace calculateAxisSpace(Graphics2D g2,
334                                               Rectangle2D plotArea) {
335    
336            AxisSpace space = new AxisSpace();
337            PlotOrientation orientation = getOrientation();
338    
339            // work out the space required by the domain axis...
340            AxisSpace fixed = getFixedDomainAxisSpace();
341            if (fixed != null) {
342                if (orientation == PlotOrientation.HORIZONTAL) {
343                    space.setLeft(fixed.getLeft());
344                    space.setRight(fixed.getRight());
345                }
346                else if (orientation == PlotOrientation.VERTICAL) {
347                    space.setTop(fixed.getTop());
348                    space.setBottom(fixed.getBottom());
349                }
350            }
351            else {
352                CategoryAxis categoryAxis = getDomainAxis();
353                RectangleEdge categoryEdge = Plot.resolveDomainAxisLocation(
354                        getDomainAxisLocation(), orientation);
355                if (categoryAxis != null) {
356                    space = categoryAxis.reserveSpace(g2, this, plotArea,
357                            categoryEdge, space);
358                }
359                else {
360                    if (getDrawSharedDomainAxis()) {
361                        space = getDomainAxis().reserveSpace(g2, this, plotArea,
362                                categoryEdge, space);
363                    }
364                }
365            }
366    
367            Rectangle2D adjustedPlotArea = space.shrink(plotArea, null);
368    
369            // work out the maximum height or width of the non-shared axes...
370            int n = this.subplots.size();
371            int totalWeight = 0;
372            for (int i = 0; i < n; i++) {
373                CategoryPlot sub = (CategoryPlot) this.subplots.get(i);
374                totalWeight += sub.getWeight();
375            }
376            this.subplotAreas = new Rectangle2D[n];
377            double x = adjustedPlotArea.getX();
378            double y = adjustedPlotArea.getY();
379            double usableSize = 0.0;
380            if (orientation == PlotOrientation.HORIZONTAL) {
381                usableSize = adjustedPlotArea.getWidth() - this.gap * (n - 1);
382            }
383            else if (orientation == PlotOrientation.VERTICAL) {
384                usableSize = adjustedPlotArea.getHeight() - this.gap * (n - 1);
385            }
386    
387            for (int i = 0; i < n; i++) {
388                CategoryPlot plot = (CategoryPlot) this.subplots.get(i);
389    
390                // calculate sub-plot area
391                if (orientation == PlotOrientation.HORIZONTAL) {
392                    double w = usableSize * plot.getWeight() / totalWeight;
393                    this.subplotAreas[i] = new Rectangle2D.Double(x, y, w,
394                            adjustedPlotArea.getHeight());
395                    x = x + w + this.gap;
396                }
397                else if (orientation == PlotOrientation.VERTICAL) {
398                    double h = usableSize * plot.getWeight() / totalWeight;
399                    this.subplotAreas[i] = new Rectangle2D.Double(x, y,
400                            adjustedPlotArea.getWidth(), h);
401                    y = y + h + this.gap;
402                }
403    
404                AxisSpace subSpace = plot.calculateRangeAxisSpace(g2,
405                        this.subplotAreas[i], null);
406                space.ensureAtLeast(subSpace);
407    
408            }
409    
410            return space;
411        }
412    
413        /**
414         * Draws the plot on a Java 2D graphics device (such as the screen or a
415         * printer).  Will perform all the placement calculations for each of the
416         * sub-plots and then tell these to draw themselves.
417         *
418         * @param g2  the graphics device.
419         * @param area  the area within which the plot (including axis labels)
420         *              should be drawn.
421         * @param anchor  the anchor point (<code>null</code> permitted).
422         * @param parentState  the state from the parent plot, if there is one.
423         * @param info  collects information about the drawing (<code>null</code>
424         *              permitted).
425         */
426        public void draw(Graphics2D g2,
427                         Rectangle2D area,
428                         Point2D anchor,
429                         PlotState parentState,
430                         PlotRenderingInfo info) {
431    
432            // set up info collection...
433            if (info != null) {
434                info.setPlotArea(area);
435            }
436    
437            // adjust the drawing area for plot insets (if any)...
438            RectangleInsets insets = getInsets();
439            area.setRect(area.getX() + insets.getLeft(),
440                    area.getY() + insets.getTop(),
441                    area.getWidth() - insets.getLeft() - insets.getRight(),
442                    area.getHeight() - insets.getTop() - insets.getBottom());
443    
444    
445            // calculate the data area...
446            setFixedRangeAxisSpaceForSubplots(null);
447            AxisSpace space = calculateAxisSpace(g2, area);
448            Rectangle2D dataArea = space.shrink(area, null);
449    
450            // set the width and height of non-shared axis of all sub-plots
451            setFixedRangeAxisSpaceForSubplots(space);
452    
453            // draw the shared axis
454            CategoryAxis axis = getDomainAxis();
455            RectangleEdge domainEdge = getDomainAxisEdge();
456            double cursor = RectangleEdge.coordinate(dataArea, domainEdge);
457            AxisState axisState = axis.draw(g2, cursor, area, dataArea,
458                    domainEdge, info);
459            if (parentState == null) {
460                parentState = new PlotState();
461            }
462            parentState.getSharedAxisStates().put(axis, axisState);
463    
464            // draw all the subplots
465            for (int i = 0; i < this.subplots.size(); i++) {
466                CategoryPlot plot = (CategoryPlot) this.subplots.get(i);
467                PlotRenderingInfo subplotInfo = null;
468                if (info != null) {
469                    subplotInfo = new PlotRenderingInfo(info.getOwner());
470                    info.addSubplotInfo(subplotInfo);
471                }
472                Point2D subAnchor = null;
473                if (anchor != null && this.subplotAreas[i].contains(anchor)) {
474                    subAnchor = anchor;
475                }
476                plot.draw(g2, this.subplotAreas[i], subAnchor, parentState,
477                        subplotInfo);
478            }
479    
480            if (info != null) {
481                info.setDataArea(dataArea);
482            }
483    
484        }
485    
486        /**
487         * Sets the size (width or height, depending on the orientation of the
488         * plot) for the range axis of each subplot.
489         *
490         * @param space  the space (<code>null</code> permitted).
491         */
492        protected void setFixedRangeAxisSpaceForSubplots(AxisSpace space) {
493            Iterator iterator = this.subplots.iterator();
494            while (iterator.hasNext()) {
495                CategoryPlot plot = (CategoryPlot) iterator.next();
496                plot.setFixedRangeAxisSpace(space, false);
497            }
498        }
499    
500        /**
501         * Sets the orientation of the plot (and all subplots).
502         *
503         * @param orientation  the orientation (<code>null</code> not permitted).
504         */
505        public void setOrientation(PlotOrientation orientation) {
506    
507            super.setOrientation(orientation);
508    
509            Iterator iterator = this.subplots.iterator();
510            while (iterator.hasNext()) {
511                CategoryPlot plot = (CategoryPlot) iterator.next();
512                plot.setOrientation(orientation);
513            }
514    
515        }
516    
517        /**
518         * Returns a range representing the extent of the data values in this plot
519         * (obtained from the subplots) that will be rendered against the specified
520         * axis.  NOTE: This method is intended for internal JFreeChart use, and
521         * is public only so that code in the axis classes can call it.  Since,
522         * for this class, the domain axis is a {@link CategoryAxis}
523         * (not a <code>ValueAxis</code}) and subplots have independent range axes,
524         * the JFreeChart code will never call this method (although this is not
525         * checked/enforced).
526          *
527          * @param axis  the axis.
528          *
529          * @return The range.
530          */
531         public Range getDataRange(ValueAxis axis) {
532             // override is only for documentation purposes
533             return super.getDataRange(axis);
534         }
535    
536         /**
537         * Returns a collection of legend items for the plot.
538         *
539         * @return The legend items.
540         */
541        public LegendItemCollection getLegendItems() {
542            LegendItemCollection result = getFixedLegendItems();
543            if (result == null) {
544                result = new LegendItemCollection();
545                if (this.subplots != null) {
546                    Iterator iterator = this.subplots.iterator();
547                    while (iterator.hasNext()) {
548                        CategoryPlot plot = (CategoryPlot) iterator.next();
549                        LegendItemCollection more = plot.getLegendItems();
550                        result.addAll(more);
551                    }
552                }
553            }
554            return result;
555        }
556    
557        /**
558         * Returns an unmodifiable list of the categories contained in all the
559         * subplots.
560         *
561         * @return The list.
562         */
563        public List getCategories() {
564            List result = new java.util.ArrayList();
565            if (this.subplots != null) {
566                Iterator iterator = this.subplots.iterator();
567                while (iterator.hasNext()) {
568                    CategoryPlot plot = (CategoryPlot) iterator.next();
569                    List more = plot.getCategories();
570                    Iterator moreIterator = more.iterator();
571                    while (moreIterator.hasNext()) {
572                        Comparable category = (Comparable) moreIterator.next();
573                        if (!result.contains(category)) {
574                            result.add(category);
575                        }
576                    }
577                }
578            }
579            return Collections.unmodifiableList(result);
580        }
581    
582        /**
583         * Overridden to return the categories in the subplots.
584         *
585         * @param axis  ignored.
586         *
587         * @return A list of the categories in the subplots.
588         *
589         * @since 1.0.3
590         */
591        public List getCategoriesForAxis(CategoryAxis axis) {
592            // FIXME:  this code means that it is not possible to use more than
593            // one domain axis for the combined plots...
594            return getCategories();
595        }
596    
597        /**
598         * Handles a 'click' on the plot.
599         *
600         * @param x  x-coordinate of the click.
601         * @param y  y-coordinate of the click.
602         * @param info  information about the plot's dimensions.
603         *
604         */
605        public void handleClick(int x, int y, PlotRenderingInfo info) {
606    
607            Rectangle2D dataArea = info.getDataArea();
608            if (dataArea.contains(x, y)) {
609                for (int i = 0; i < this.subplots.size(); i++) {
610                    CategoryPlot subplot = (CategoryPlot) this.subplots.get(i);
611                    PlotRenderingInfo subplotInfo = info.getSubplotInfo(i);
612                    subplot.handleClick(x, y, subplotInfo);
613                }
614            }
615    
616        }
617    
618        /**
619         * Receives a {@link PlotChangeEvent} and responds by notifying all
620         * listeners.
621         *
622         * @param event  the event.
623         */
624        public void plotChanged(PlotChangeEvent event) {
625            notifyListeners(event);
626        }
627    
628        /**
629         * Tests the plot for equality with an arbitrary object.
630         *
631         * @param obj  the object (<code>null</code> permitted).
632         *
633         * @return A boolean.
634         */
635        public boolean equals(Object obj) {
636            if (obj == this) {
637                return true;
638            }
639            if (!(obj instanceof CombinedDomainCategoryPlot)) {
640                return false;
641            }
642            CombinedDomainCategoryPlot that = (CombinedDomainCategoryPlot) obj;
643            if (this.gap != that.gap) {
644                return false;
645            }
646            if (!ObjectUtilities.equal(this.subplots, that.subplots)) {
647                return false;
648            }
649            return super.equals(obj);
650        }
651    
652        /**
653         * Returns a clone of the plot.
654         *
655         * @return A clone.
656         *
657         * @throws CloneNotSupportedException  this class will not throw this
658         *         exception, but subclasses (if any) might.
659         */
660        public Object clone() throws CloneNotSupportedException {
661    
662            CombinedDomainCategoryPlot result
663                = (CombinedDomainCategoryPlot) super.clone();
664            result.subplots = (List) ObjectUtilities.deepClone(this.subplots);
665            for (Iterator it = result.subplots.iterator(); it.hasNext();) {
666                Plot child = (Plot) it.next();
667                child.setParent(result);
668            }
669            return result;
670    
671        }
672    
673    }