    001    /* ===========================================================
    002     * JFreeChart : a free chart library for the Java(tm) platform
    003     * ===========================================================
    004     *
    005     * (C) Copyright 2000-2005, 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     * CombinedDomainXYPlot.java
    029     * -------------------------
    030     * (C) Copyright 2001-2005, by Bill Kelemen and Contributors.
    031     *
    032     * Original Author:  Bill Kelemen;
    033     * Contributor(s):   David Gilbert (for Object Refinery Limited);
    034     *                   Anthony Boulestreau;
    035     *                   David Basten;
    036     *                   Kevin Frechette (for ISTI);
    037     *                   Nicolas Brodu;
    038     *
    039     * $Id: CombinedDomainXYPlot.java,v 1.9.2.1 2005/10/25 20:52:07 mungady Exp $
    040     *
    041     * Changes:
    042     * --------
    043     * 06-Dec-2001 : Version 1 (BK);
    044     * 12-Dec-2001 : Removed unnecessary 'throws' clause from constructor (DG);
    045     * 18-Dec-2001 : Added plotArea attribute and get/set methods (BK);
    046     * 22-Dec-2001 : Fixed bug in chartChanged with multiple combinations of
    047     *               CombinedPlots (BK);
    048     * 08-Jan-2002 : Moved to new package com.jrefinery.chart.combination (DG);
    049     * 25-Feb-2002 : Updated import statements (DG);
    050     * 28-Feb-2002 : Readded "this.plotArea = plotArea" that was deleted from
    051     *               draw() method (BK);
    052     * 26-Mar-2002 : Added an empty zoom method (this method needs to be written so
    053     *               that combined plots will support zooming (DG);
    054     * 29-Mar-2002 : Changed the method createCombinedAxis adding the creation of
    055     *               OverlaidSymbolicAxis and CombinedSymbolicAxis(AB);
    056     * 23-Apr-2002 : Renamed CombinedPlot-->MultiXYPlot, and simplified the
    057     *               structure (DG);
    058     * 23-May-2002 : Renamed (again) MultiXYPlot-->CombinedXYPlot (DG);
    059     * 19-Jun-2002 : Added get/setGap() methods suggested by David Basten (DG);
    060     * 25-Jun-2002 : Removed redundant imports (DG);
    061     * 16-Jul-2002 : Draws shared axis after subplots (to fix missing gridlines),
    062     *               added overrides of 'setSeriesPaint()' and 'setXYItemRenderer()'
    063     *               that pass changes down to subplots (KF);
    064     * 09-Oct-2002 : Added add(XYPlot) method (DG);
    065     * 26-Mar-2003 : Implemented Serializable (DG);
    066     * 16-May-2003 : Renamed CombinedXYPlot --> CombinedDomainXYPlot (DG);
    067     * 04-Aug-2003 : Removed leftover code that was causing domain axis drawing
    068     *               problem (DG);
    069     * 08-Aug-2003 : Adjusted totalWeight in remove() method (DG);
    070     * 21-Aug-2003 : Implemented Cloneable (DG);
    071     * 11-Sep-2003 : Fix cloning support (subplots) (NB);
    072     * 15-Sep-2003 : Fixed error in cloning (DG);
    073     * 16-Sep-2003 : Changed ChartRenderingInfo --> PlotRenderingInfo (DG);
    074     * 17-Sep-2003 : Updated handling of 'clicks' (DG);
    075     * 12-Nov-2004 : Implemented the new Zoomable interface (DG);
    076     * 25-Nov-2004 : Small update to clone() implementation (DG);
    077     * 21-Feb-2005 : The getLegendItems() method now returns the fixed legend
    078     *               items if set (DG);
    079     * 05-May-2005 : Removed unused draw() method (DG);
    080     *
    081     */
    082
    083    package org.jfree.chart.plot;
    084
    085    import java.awt.Graphics2D;
    086    import java.awt.geom.Point2D;
    087    import java.awt.geom.Rectangle2D;
    088    import java.io.Serializable;
    089    import java.util.Collections;
    090    import java.util.Iterator;
    091    import java.util.List;
    092
    093    import org.jfree.chart.LegendItemCollection;
    094    import org.jfree.chart.axis.AxisSpace;
    095    import org.jfree.chart.axis.AxisState;
    096    import org.jfree.chart.axis.NumberAxis;
    097    import org.jfree.chart.axis.ValueAxis;
    098    import org.jfree.chart.event.PlotChangeEvent;
    099    import org.jfree.chart.event.PlotChangeListener;
    100    import org.jfree.chart.renderer.xy.XYItemRenderer;
    101    import org.jfree.data.Range;
    102    import org.jfree.ui.RectangleEdge;
    103    import org.jfree.ui.RectangleInsets;
    104    import org.jfree.util.ObjectUtilities;
    105    import org.jfree.util.PublicCloneable;
    106
    107    /**
    108     * An extension of {@link XYPlot} that contains multiple subplots that share a
    109     * common domain axis.
    110     */
    111    public class CombinedDomainXYPlot extends XYPlot
    112                                      implements Cloneable, PublicCloneable,
    113                                                 Serializable,
    114                                                 PlotChangeListener {
    115
    116        /** For serialization. */
    117        private static final long serialVersionUID = -7765545541261907383L;
    118
    119        /** Storage for the subplot references. */
    120        private List subplots;
    121
    122        /** Total weight of all charts. */
    123        private int totalWeight = 0;
    124
    125        /** The gap between subplots. */
    126        private double gap = 5.0;
    127
    128        /** Temporary storage for the subplot areas. */
    129        private transient Rectangle2D[] subplotAreas;
    130        // TODO:  the subplot areas needs to be moved out of the plot into the plot
    131        //        state
    132
    133        /**
    134         * Default constructor.
    135         */
    136        public CombinedDomainXYPlot() {
    137            this(new NumberAxis());
    138        }
    139
    140        /**
    141         * Creates a new combined plot that shares a domain axis among multiple
    142         * subplots.
    143         *
    144         * @param domainAxis  the shared axis.
    145         */
    146        public CombinedDomainXYPlot(ValueAxis domainAxis) {
    147
    148            super(
    149                null,        // no data in the parent plot
    150                domainAxis,
    151                null,        // no range axis
    152                null         // no rendereer
    153            );
    154
    155            this.subplots = new java.util.ArrayList();
    156
    157        }
    158
    159        /**
    160         * Returns a string describing the type of plot.
    161         *
    162         * @return The type of plot.
    163         */
    164        public String getPlotType() {
    165            return "Combined_Domain_XYPlot";
    166        }
    167
    168        /**
    169         * Sets the orientation for the plot (also changes the orientation for all
    170         * the subplots to match).
    171         *
    172         * @param orientation  the orientation (<code>null</code> not allowed).
    173         */
    174        public void setOrientation(PlotOrientation orientation) {
    175
    176            super.setOrientation(orientation);
    177            Iterator iterator = this.subplots.iterator();
    178            while (iterator.hasNext()) {
    179                XYPlot plot = (XYPlot) iterator.next();
    180                plot.setOrientation(orientation);
    181            }
    182
    183        }
    184
    185        /**
    186         * Returns the range for the specified axis.  This is the combined range
    187         * of all the subplots.
    188         *
    189         * @param axis  the axis.
    190         *
    191         * @return The range (possibly <code>null</code>).
    192         */
    193        public Range getDataRange(ValueAxis axis) {
    194
    195            Range result = null;
    196            if (this.subplots != null) {
    197                Iterator iterator = this.subplots.iterator();
    198                while (iterator.hasNext()) {
    199                    XYPlot subplot = (XYPlot) iterator.next();
    200                    result = Range.combine(result, subplot.getDataRange(axis));
    201                }
    202            }
    203            return result;
    204
    205        }
    206
    207        /**
    208         * Returns the gap between subplots, measured in Java2D units.
    209         *
    210         * @return The gap (in Java2D units).
    211         */
    212        public double getGap() {
    213            return this.gap;
    214        }
    215
    216        /**
    217         * Sets the amount of space between subplots and sends a
    218         * {@link PlotChangeEvent} to all registered listeners.
    219         *
    220         * @param gap  the gap between subplots (in Java2D units).
    221         */
    222        public void setGap(double gap) {
    223            this.gap = gap;
    224            notifyListeners(new PlotChangeEvent(this));
    225        }
    226
    227        /**
    228         * Adds a subplot (with a default 'weight' of 1) and sends a
    229         * {@link PlotChangeEvent} to all registered listeners.
    230         * <P>
    231         * The domain axis for the subplot will be set to <code>null</code>.
    232         *
    233         * @param subplot  the subplot (<code>null</code> not permitted).
    234         */
    235        public void add(XYPlot subplot) {
    236            // defer argument checking
    237            add(subplot, 1);
    238        }
    239
    240        /**
    241         * Adds a subplot with the specified weight and sends a
    242         * {@link PlotChangeEvent} to all registered listeners.  The weight
    243         * determines how much space is allocated to the subplot relative to all
    244         * the other subplots.
    245         * <P>
    246         * The domain axis for the subplot will be set to <code>null</code>.
    247         *
    248         * @param subplot  the subplot (<code>null</code> not permitted).
    249         * @param weight  the weight (must be >= 1).
    250         */
    251        public void add(XYPlot subplot, int weight) {
    252
    253            if (subplot == null) {
    254                throw new IllegalArgumentException("Null 'subplot' argument.");
    255            }
    256            if (weight <= 0) {
    257                throw new IllegalArgumentException("Require weight >= 1.");
    258            }
    259
    260            // store the plot and its weight
    261            subplot.setParent(this);
    262            subplot.setWeight(weight);
    263            subplot.setInsets(new RectangleInsets(0.0, 0.0, 0.0, 0.0), false);
    264            subplot.setDomainAxis(null);
    265            subplot.addChangeListener(this);
    266            this.subplots.add(subplot);
    267
    268            // keep track of total weights
    269            this.totalWeight += weight;
    270
    271            ValueAxis axis = getDomainAxis();
    272            if (axis != null) {
    273                axis.configure();
    274            }
    275
    276            notifyListeners(new PlotChangeEvent(this));
    277
    278        }
    279
    280        /**
    281         * Removes a subplot from the combined chart and sends a
    282         * {@link PlotChangeEvent} to all registered listeners.
    283         *
    284         * @param subplot  the subplot (<code>null</code> not permitted).
    285         */
    286        public void remove(XYPlot subplot) {
    287            if (subplot == null) {
    288                throw new IllegalArgumentException(" Null 'subplot' argument.");
    289            }
    290            int position = -1;
    291            int size = this.subplots.size();
    292            int i = 0;
    293            while (position == -1 && i < size) {
    294                if (this.subplots.get(i) == subplot) {
    295                    position = i;
    296                }
    297                i++;
    298            }
    299            if (position != -1) {
    300                this.subplots.remove(position);
    301                subplot.setParent(null);
    302                subplot.removeChangeListener(this);
    303                this.totalWeight -= subplot.getWeight();
    304
    305                ValueAxis domain = getDomainAxis();
    306                if (domain != null) {
    307                    domain.configure();
    308                }
    309                notifyListeners(new PlotChangeEvent(this));
    310            }
    311        }
    312
    313        /**
    314         * Returns the list of subplots.
    315         *
    316         * @return An unmodifiable list of subplots.
    317         */
    318        public List getSubplots() {
    319            return Collections.unmodifiableList(this.subplots);
    320        }
    321
    322        /**
    323         * Calculates the axis space required.
    324         *
    325         * @param g2  the graphics device.
    326         * @param plotArea  the plot area.
    327         *
    328         * @return The space.
    329         */
    330        protected AxisSpace calculateAxisSpace(Graphics2D g2,
    331                                               Rectangle2D plotArea) {
    332
    333            AxisSpace space = new AxisSpace();
    334            PlotOrientation orientation = getOrientation();
    335
    336            // work out the space required by the domain axis...
    337            AxisSpace fixed = getFixedDomainAxisSpace();
    338            if (fixed != null) {
    339                if (orientation == PlotOrientation.HORIZONTAL) {
    340                    space.setLeft(fixed.getLeft());
    341                    space.setRight(fixed.getRight());
    342                }
    343                else if (orientation == PlotOrientation.VERTICAL) {
    344                    space.setTop(fixed.getTop());
    345                    space.setBottom(fixed.getBottom());
    346                }
    347            }
    348            else {
    349                ValueAxis xAxis = getDomainAxis();
    350                RectangleEdge xEdge = Plot.resolveDomainAxisLocation(
    351                    getDomainAxisLocation(), orientation
    352                );
    353                if (xAxis != null) {
    354                    space = xAxis.reserveSpace(g2, this, plotArea, xEdge, space);
    355                }
    356            }
    357
    358            Rectangle2D adjustedPlotArea = space.shrink(plotArea, null);
    359
    360            // work out the maximum height or width of the non-shared axes...
    361            int n = this.subplots.size();
    362            this.subplotAreas = new Rectangle2D[n];
    363            double x = adjustedPlotArea.getX();
    364            double y = adjustedPlotArea.getY();
    365            double usableSize = 0.0;
    366            if (orientation == PlotOrientation.HORIZONTAL) {
    367                usableSize = adjustedPlotArea.getWidth() - this.gap * (n - 1);
    368            }
    369            else if (orientation == PlotOrientation.VERTICAL) {
    370                usableSize = adjustedPlotArea.getHeight() - this.gap * (n - 1);
    371            }
    372
    373            for (int i = 0; i < n; i++) {
    374                XYPlot plot = (XYPlot) this.subplots.get(i);
    375
    376                // calculate sub-plot area
    377                if (orientation == PlotOrientation.HORIZONTAL) {
    378                    double w = usableSize * plot.getWeight() / this.totalWeight;
    379                    this.subplotAreas[i] = new Rectangle2D.Double(
    380                        x, y, w, adjustedPlotArea.getHeight()
    381                    );
    382                    x = x + w + this.gap;
    383                }
    384                else if (orientation == PlotOrientation.VERTICAL) {
    385                    double h = usableSize * plot.getWeight() / this.totalWeight;
    386                    this.subplotAreas[i] = new Rectangle2D.Double(
    387                        x, y, adjustedPlotArea.getWidth(), h
    388                    );
    389                    y = y + h + this.gap;
    390                }
    391
    392                AxisSpace subSpace = plot.calculateRangeAxisSpace(
    393                    g2, this.subplotAreas[i], null
    394                );
    395                space.ensureAtLeast(subSpace);
    396
    397            }
    398
    399            return space;
    400        }
    401
    402        /**
    403         * Draws the plot within the specified area on a graphics device.
    404         *
    405         * @param g2  the graphics device.
    406         * @param area  the plot area (in Java2D space).
    407         * @param anchor  an anchor point in Java2D space (<code>null</code>
    408         *                permitted).
    409         * @param parentState  the state from the parent plot, if there is one
    410         *                     (<code>null</code> permitted).
    411         * @param info  collects chart drawing information (<code>null</code>
    412         *              permitted).
    413         */
    414        public void draw(Graphics2D g2,
    415                         Rectangle2D area,
    416                         Point2D anchor,
    417                         PlotState parentState,
    418                         PlotRenderingInfo info) {
    419
    420            // set up info collection...
    421            if (info != null) {
    422                info.setPlotArea(area);
    423            }
    424
    425            // adjust the drawing area for plot insets (if any)...
    426            RectangleInsets insets = getInsets();
    427            insets.trim(area);
    428
    429            AxisSpace space = calculateAxisSpace(g2, area);
    430            Rectangle2D dataArea = space.shrink(area, null);
    431
    432            // set the width and height of non-shared axis of all sub-plots
    433            setFixedRangeAxisSpaceForSubplots(space);
    434
    435            // draw the shared axis
    436            ValueAxis axis = getDomainAxis();
    437            RectangleEdge edge = getDomainAxisEdge();
    438            double cursor = RectangleEdge.coordinate(dataArea, edge);
    439            AxisState axisState = axis.draw(g2, cursor, area, dataArea, edge, info);
    440            if (parentState == null) {
    441                parentState = new PlotState();
    442            }
    443            parentState.getSharedAxisStates().put(axis, axisState);
    444
    445            // draw all the subplots
    446            for (int i = 0; i < this.subplots.size(); i++) {
    447                XYPlot plot = (XYPlot) this.subplots.get(i);
    448                PlotRenderingInfo subplotInfo = null;
    449                if (info != null) {
    450                    subplotInfo = new PlotRenderingInfo(info.getOwner());
    451                    info.addSubplotInfo(subplotInfo);
    452                }
    453                plot.draw(
    454                    g2, this.subplotAreas[i], anchor, parentState, subplotInfo
    455                );
    456            }
    457
    458            if (info != null) {
    459                info.setDataArea(dataArea);
    460            }
    461
    462        }
    463
    464        /**
    465         * Returns a collection of legend items for the plot.
    466         *
    467         * @return The legend items.
    468         */
    469        public LegendItemCollection getLegendItems() {
    470            LegendItemCollection result = getFixedLegendItems();
    471            if (result == null) {
    472                result = new LegendItemCollection();
    473                if (this.subplots != null) {
    474                    Iterator iterator = this.subplots.iterator();
    475                    while (iterator.hasNext()) {
    476                        XYPlot plot = (XYPlot) iterator.next();
    477                        LegendItemCollection more = plot.getLegendItems();
    478                        result.addAll(more);
    479                    }
    480                }
    481            }
    482            return result;
    483        }
    484
    485        /**
    486         * Multiplies the range on the range axis/axes by the specified factor.
    487         *
    488         * @param factor  the zoom factor.
    489         * @param info  the plot rendering info.
    490         * @param source  the source point.
    491         */
    492        public void zoomRangeAxes(double factor, PlotRenderingInfo info,
    493                                  Point2D source) {
    494            XYPlot subplot = findSubplot(info, source);
    495            if (subplot != null) {
    496                subplot.zoomRangeAxes(factor, info, source);
    497            }
    498        }
    499
    500        /**
    501         * Zooms in on the range axes.
    502         *
    503         * @param lowerPercent  the lower bound.
    504         * @param upperPercent  the upper bound.
    505         * @param info  the plot rendering info.
    506         * @param source  the source point.
    507         */
    508        public void zoomRangeAxes(double lowerPercent, double upperPercent,
    509                                  PlotRenderingInfo info, Point2D source) {
    510            XYPlot subplot = findSubplot(info, source);
    511            if (subplot != null) {
    512                subplot.zoomRangeAxes(lowerPercent, upperPercent, info, source);
    513            }
    514        }
    515
    516        /**
    517         * Returns the subplot (if any) that contains the (x, y) point (specified
    518         * in Java2D space).
    519         *
    520         * @param info  the chart rendering info.
    521         * @param source  the source point.
    522         *
    523         * @return A subplot (possibly <code>null</code>).
    524         */
    525        public XYPlot findSubplot(PlotRenderingInfo info, Point2D source) {
    526            XYPlot result = null;
    527            int subplotIndex = info.getSubplotIndex(source);
    528            if (subplotIndex >= 0) {
    529                result =  (XYPlot) this.subplots.get(subplotIndex);
    530            }
    531            return result;
    532        }
    533
    534        /**
    535         * Sets the item renderer FOR ALL SUBPLOTS.  Registered listeners are
    536         * notified that the plot has been modified.
    537         * <P>
    538         * Note: usually you will want to set the renderer independently for each
    539         * subplot, which is NOT what this method does.
    540         *
    541         * @param renderer the new renderer.
    542         */
    543        public void setRenderer(XYItemRenderer renderer) {
    544
    545            super.setRenderer(renderer);  // not strictly necessary, since the
    546                                          // renderer set for the
    547                                          // parent plot is not used
    548
    549            Iterator iterator = this.subplots.iterator();
    550            while (iterator.hasNext()) {
    551                XYPlot plot = (XYPlot) iterator.next();
    552                plot.setRenderer(renderer);
    553            }
    554
    555        }
    556
    557        /**
    558         * Sets the size (width or height, depending on the orientation of the
    559         * plot) for the domain axis of each subplot.
    560         *
    561         * @param space  the space.
    562         */
    563        protected void setFixedRangeAxisSpaceForSubplots(AxisSpace space) {
    564
    565            Iterator iterator = this.subplots.iterator();
    566            while (iterator.hasNext()) {
    567                XYPlot plot = (XYPlot) iterator.next();
    568                plot.setFixedRangeAxisSpace(space);
    569            }
    570
    571        }
    572
    573        /**
    574         * Handles a 'click' on the plot by updating the anchor values.
    575         *
    576         * @param x  x-coordinate, where the click occured.
    577         * @param y  y-coordinate, where the click occured.
    578         * @param info  object containing information about the plot dimensions.
    579         */
    580        public void handleClick(int x, int y, PlotRenderingInfo info) {
    581            Rectangle2D dataArea = info.getDataArea();
    582            if (dataArea.contains(x, y)) {
    583                for (int i = 0; i < this.subplots.size(); i++) {
    584                    XYPlot subplot = (XYPlot) this.subplots.get(i);
    585                    PlotRenderingInfo subplotInfo = info.getSubplotInfo(i);
    586                    subplot.handleClick(x, y, subplotInfo);
    587                }
    588            }
    589        }
    590
    591        /**
    592         * Receives a {@link PlotChangeEvent} and responds by notifying all
    593         * listeners.
    594         *
    595         * @param event  the event.
    596         */
    597        public void plotChanged(PlotChangeEvent event) {
    598            notifyListeners(event);
    599        }
    600
    601        /**
    602         * Tests this plot for equality with another object.
    603         *
    604         * @param obj  the other object.
    605         *
    606         * @return <code>true</code> or <code>false</code>.
    607         */
    608        public boolean equals(Object obj) {
    609
    610            if (obj == null) {
    611                return false;
    612            }
    613
    614            if (obj == this) {
    615                return true;
    616            }
    617
    618            if (!(obj instanceof CombinedDomainXYPlot)) {
    619                return false;
    620            }
    621            if (!super.equals(obj)) {
    622                return false;
    623            }
    624
    625            CombinedDomainXYPlot p = (CombinedDomainXYPlot) obj;
    626            if (this.totalWeight != p.totalWeight) {
    627                return false;
    628            }
    629            if (this.gap != p.gap) {
    630                return false;
    631            }
    632            if (!ObjectUtilities.equal(this.subplots, p.subplots)) {
    633                return false;
    634            }
    635
    636            return true;
    637        }
    638
    639        /**
    640         * Returns a clone of the annotation.
    641         *
    642         * @return A clone.
    643         *
    644         * @throws CloneNotSupportedException  this class will not throw this
    645         *         exception, but subclasses (if any) might.
    646         */
    647        public Object clone() throws CloneNotSupportedException {
    648
    649            CombinedDomainXYPlot result = (CombinedDomainXYPlot) super.clone();
    650            result.subplots = (List) ObjectUtilities.deepClone(this.subplots);
    651            for (Iterator it = result.subplots.iterator(); it.hasNext();) {
    652                Plot child = (Plot) it.next();
    653                child.setParent(result);
    654            }
    655
    656            // after setting up all the subplots, the shared domain axis may need
    657            // reconfiguring
    658            ValueAxis domainAxis = result.getDomainAxis();
    659            if (domainAxis != null) {
    660                domainAxis.configure();
    661            }
    662
    663            return result;
    664
    665        }
    666
    667    }