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