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