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