    001    /* ===========================================================
    002     * JFreeChart : a free chart library for the Java(tm) platform
    003     * ===========================================================
    004     *
    005     * (C) Copyright 2000-2006, 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-2006, by Object Refinery Limited.
    031     *
    032     * Original Author:  David Gilbert (for Object Refinery Limited);
    033     * Contributor(s):   Nicolas Brodu;
    034     *
    035     * $Id: CombinedDomainCategoryPlot.java,v 1.9.2.3 2006/10/30 13:11:11 mungady Exp $
    036     *
    037     * Changes:
    038     * --------
    039     * 16-May-2003 : Version 1 (DG);
    040     * 08-Aug-2003 : Adjusted totalWeight in remove() method (DG);
    041     * 19-Aug-2003 : Added equals() method, implemented Cloneable and
    042     *               Serializable (DG);
    043     * 11-Sep-2003 : Fix cloning support (subplots) (NB);
    044     * 15-Sep-2003 : Implemented PublicCloneable (DG);
    045     * 16-Sep-2003 : Changed ChartRenderingInfo --> PlotRenderingInfo (DG);
    046     * 17-Sep-2003 : Updated handling of 'clicks' (DG);
    047     * 04-May-2004 : Added getter/setter methods for 'gap' attribute (DG);
    048     * 12-Nov-2004 : Implemented the Zoomable interface (DG);
    049     * 25-Nov-2004 : Small update to clone() implementation (DG);
    050     * 21-Feb-2005 : The getLegendItems() method now returns the fixed legend
    051     *               items if set (DG);
    052     * 05-May-2005 : Updated draw() method parameters (DG);
    053     * ------------- JFREECHART 1.0.x ---------------------------------------------
    054     * 13-Sep-2006 : Updated API docs (DG);
    055     * 30-Oct-2006 : Added new getCategoriesForAxis() override (DG);
    056     *
    057     */
    058
    059    package org.jfree.chart.plot;
    060
    061    import java.awt.Graphics2D;
    062    import java.awt.geom.Point2D;
    063    import java.awt.geom.Rectangle2D;
    064    import java.io.Serializable;
    065    import java.util.Collections;
    066    import java.util.Iterator;
    067    import java.util.List;
    068
    069    import org.jfree.chart.LegendItemCollection;
    070    import org.jfree.chart.axis.AxisSpace;
    071    import org.jfree.chart.axis.AxisState;
    072    import org.jfree.chart.axis.CategoryAxis;
    073    import org.jfree.chart.event.PlotChangeEvent;
    074    import org.jfree.chart.event.PlotChangeListener;
    075    import org.jfree.ui.RectangleEdge;
    076    import org.jfree.ui.RectangleInsets;
    077    import org.jfree.util.ObjectUtilities;
    078    import org.jfree.util.PublicCloneable;
    079
    080    /**
    081     * A combined category plot where the domain axis is shared.
    082     */
    083    public class CombinedDomainCategoryPlot extends CategoryPlot
    084                                            implements Zoomable,
    085                                                       Cloneable, PublicCloneable,
    086                                                       Serializable,
    087                                                       PlotChangeListener {
    088
    089        /** For serialization. */
    090        private static final long serialVersionUID = 8207194522653701572L;
    091
    092        /** Storage for the subplot references. */
    093        private List subplots;
    094
    095        /** Total weight of all charts. */
    096        private int totalWeight;
    097
    098        /** The gap between subplots. */
    099        private double gap;
    100
    101        /** Temporary storage for the subplot areas. */
    102        private transient Rectangle2D[] subplotAreas;
    103        // TODO:  move the above to the plot state
    104
    105        /**
    106         * Default constructor.
    107         */
    108        public CombinedDomainCategoryPlot() {
    109            this(new CategoryAxis());
    110        }
    111
    112        /**
    113         * Creates a new plot.
    114         *
    115         * @param domainAxis  the shared domain axis (<code>null</code> not
    116         *                    permitted).
    117         */
    118        public CombinedDomainCategoryPlot(CategoryAxis domainAxis) {
    119            super(null, domainAxis, null, null);
    120            this.subplots = new java.util.ArrayList();
    121            this.totalWeight = 0;
    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            notifyListeners(new PlotChangeEvent(this));
    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            this.totalWeight += weight;
    183            CategoryAxis axis = getDomainAxis();
    184            if (axis != null) {
    185                axis.configure();
    186            }
    187            notifyListeners(new PlotChangeEvent(this));
    188        }
    189
    190        /**
    191         * Removes a subplot from the combined chart.  Potentially, this removes
    192         * some unique categories from the overall union of the datasets...so the
    193         * domain axis is reconfigured, then a {@link PlotChangeEvent} is sent to
    194         * all registered listeners.
    195         *
    196         * @param subplot  the subplot (<code>null</code> not permitted).
    197         */
    198        public void remove(CategoryPlot subplot) {
    199            if (subplot == null) {
    200                throw new IllegalArgumentException("Null 'subplot' argument.");
    201            }
    202            int position = -1;
    203            int size = this.subplots.size();
    204            int i = 0;
    205            while (position == -1 && i < size) {
    206                if (this.subplots.get(i) == subplot) {
    207                    position = i;
    208                }
    209                i++;
    210            }
    211            if (position != -1) {
    212                this.subplots.remove(position);
    213                subplot.setParent(null);
    214                subplot.removeChangeListener(this);
    215                this.totalWeight -= subplot.getWeight();
    216
    217                CategoryAxis domain = getDomainAxis();
    218                if (domain != null) {
    219                    domain.configure();
    220                }
    221                notifyListeners(new PlotChangeEvent(this));
    222            }
    223        }
    224
    225        /**
    226         * Returns the list of subplots.
    227         *
    228         * @return An unmodifiable list of subplots .
    229         */
    230        public List getSubplots() {
    231            return Collections.unmodifiableList(this.subplots);
    232        }
    233
    234        /**
    235         * Returns the subplot (if any) that contains the (x, y) point (specified
    236         * in Java2D space).
    237         *
    238         * @param info  the chart rendering info.
    239         * @param source  the source point.
    240         *
    241         * @return A subplot (possibly <code>null</code>).
    242         */
    243        public CategoryPlot findSubplot(PlotRenderingInfo info, Point2D source) {
    244            CategoryPlot result = null;
    245            int subplotIndex = info.getSubplotIndex(source);
    246            if (subplotIndex >= 0) {
    247                result =  (CategoryPlot) this.subplots.get(subplotIndex);
    248            }
    249            return result;
    250        }
    251
    252        /**
    253         * Multiplies the range on the range axis/axes by the specified factor.
    254         *
    255         * @param factor  the zoom factor.
    256         * @param info  the plot rendering info.
    257         * @param source  the source point.
    258         */
    259        public void zoomRangeAxes(double factor, PlotRenderingInfo info,
    260                                  Point2D source) {
    261            CategoryPlot subplot = findSubplot(info, source);
    262            if (subplot != null) {
    263                subplot.zoomRangeAxes(factor, info, source);
    264            }
    265        }
    266
    267        /**
    268         * Zooms in on the range axes.
    269         *
    270         * @param lowerPercent  the lower bound.
    271         * @param upperPercent  the upper bound.
    272         * @param info  the plot rendering info.
    273         * @param source  the source point.
    274         */
    275        public void zoomRangeAxes(double lowerPercent, double upperPercent,
    276                                  PlotRenderingInfo info, Point2D source) {
    277            CategoryPlot subplot = findSubplot(info, source);
    278            if (subplot != null) {
    279                subplot.zoomRangeAxes(lowerPercent, upperPercent, info, source);
    280            }
    281        }
    282
    283        /**
    284         * Calculates the space required for the axes.
    285         *
    286         * @param g2  the graphics device.
    287         * @param plotArea  the plot area.
    288         *
    289         * @return The space required for the axes.
    290         */
    291        protected AxisSpace calculateAxisSpace(Graphics2D g2,
    292                                               Rectangle2D plotArea) {
    293
    294            AxisSpace space = new AxisSpace();
    295            PlotOrientation orientation = getOrientation();
    296
    297            // work out the space required by the domain axis...
    298            AxisSpace fixed = getFixedDomainAxisSpace();
    299            if (fixed != null) {
    300                if (orientation == PlotOrientation.HORIZONTAL) {
    301                    space.setLeft(fixed.getLeft());
    302                    space.setRight(fixed.getRight());
    303                }
    304                else if (orientation == PlotOrientation.VERTICAL) {
    305                    space.setTop(fixed.getTop());
    306                    space.setBottom(fixed.getBottom());
    307                }
    308            }
    309            else {
    310                CategoryAxis categoryAxis = getDomainAxis();
    311                RectangleEdge categoryEdge = Plot.resolveDomainAxisLocation(
    312                        getDomainAxisLocation(), orientation);
    313                if (categoryAxis != null) {
    314                    space = categoryAxis.reserveSpace(g2, this, plotArea,
    315                            categoryEdge, space);
    316                }
    317                else {
    318                    if (getDrawSharedDomainAxis()) {
    319                        space = getDomainAxis().reserveSpace(g2, this, plotArea,
    320                                categoryEdge, space);
    321                    }
    322                }
    323            }
    324
    325            Rectangle2D adjustedPlotArea = space.shrink(plotArea, null);
    326
    327            // work out the maximum height or width of the non-shared axes...
    328            int n = this.subplots.size();
    329            this.subplotAreas = new Rectangle2D[n];
    330            double x = adjustedPlotArea.getX();
    331            double y = adjustedPlotArea.getY();
    332            double usableSize = 0.0;
    333            if (orientation == PlotOrientation.HORIZONTAL) {
    334                usableSize = adjustedPlotArea.getWidth() - this.gap * (n - 1);
    335            }
    336            else if (orientation == PlotOrientation.VERTICAL) {
    337                usableSize = adjustedPlotArea.getHeight() - this.gap * (n - 1);
    338            }
    339
    340            for (int i = 0; i < n; i++) {
    341                CategoryPlot plot = (CategoryPlot) this.subplots.get(i);
    342
    343                // calculate sub-plot area
    344                if (orientation == PlotOrientation.HORIZONTAL) {
    345                    double w = usableSize * plot.getWeight() / this.totalWeight;
    346                    this.subplotAreas[i] = new Rectangle2D.Double(x, y, w,
    347                            adjustedPlotArea.getHeight());
    348                    x = x + w + this.gap;
    349                }
    350                else if (orientation == PlotOrientation.VERTICAL) {
    351                    double h = usableSize * plot.getWeight() / this.totalWeight;
    352                    this.subplotAreas[i] = new Rectangle2D.Double(x, y,
    353                            adjustedPlotArea.getWidth(), h);
    354                    y = y + h + this.gap;
    355                }
    356
    357                AxisSpace subSpace = plot.calculateRangeAxisSpace(g2,
    358                        this.subplotAreas[i], null);
    359                space.ensureAtLeast(subSpace);
    360
    361            }
    362
    363            return space;
    364        }
    365
    366        /**
    367         * Draws the plot on a Java 2D graphics device (such as the screen or a
    368         * printer).  Will perform all the placement calculations for each of the
    369         * sub-plots and then tell these to draw themselves.
    370         *
    371         * @param g2  the graphics device.
    372         * @param area  the area within which the plot (including axis labels)
    373         *              should be drawn.
    374         * @param anchor  the anchor point (<code>null</code> permitted).
    375         * @param parentState  the state from the parent plot, if there is one.
    376         * @param info  collects information about the drawing (<code>null</code>
    377         *              permitted).
    378         */
    379        public void draw(Graphics2D g2,
    380                         Rectangle2D area,
    381                         Point2D anchor,
    382                         PlotState parentState,
    383                         PlotRenderingInfo info) {
    384
    385            // set up info collection...
    386            if (info != null) {
    387                info.setPlotArea(area);
    388            }
    389
    390            // adjust the drawing area for plot insets (if any)...
    391            RectangleInsets insets = getInsets();
    392            area.setRect(area.getX() + insets.getLeft(),
    393                    area.getY() + insets.getTop(),
    394                    area.getWidth() - insets.getLeft() - insets.getRight(),
    395                    area.getHeight() - insets.getTop() - insets.getBottom());
    396
    397
    398            // calculate the data area...
    399            setFixedRangeAxisSpaceForSubplots(null);
    400            AxisSpace space = calculateAxisSpace(g2, area);
    401            Rectangle2D dataArea = space.shrink(area, null);
    402
    403            // set the width and height of non-shared axis of all sub-plots
    404            setFixedRangeAxisSpaceForSubplots(space);
    405
    406            // draw the shared axis
    407            CategoryAxis axis = getDomainAxis();
    408            RectangleEdge domainEdge = getDomainAxisEdge();
    409            double cursor = RectangleEdge.coordinate(dataArea, domainEdge);
    410            AxisState axisState = axis.draw(g2, cursor, area, dataArea,
    411                    domainEdge, info);
    412            if (parentState == null) {
    413                parentState = new PlotState();
    414            }
    415            parentState.getSharedAxisStates().put(axis, axisState);
    416
    417            // draw all the subplots
    418            for (int i = 0; i < this.subplots.size(); i++) {
    419                CategoryPlot plot = (CategoryPlot) this.subplots.get(i);
    420                PlotRenderingInfo subplotInfo = null;
    421                if (info != null) {
    422                    subplotInfo = new PlotRenderingInfo(info.getOwner());
    423                    info.addSubplotInfo(subplotInfo);
    424                }
    425                plot.draw(g2, this.subplotAreas[i], null, parentState, subplotInfo);
    426            }
    427
    428            if (info != null) {
    429                info.setDataArea(dataArea);
    430            }
    431
    432        }
    433
    434        /**
    435         * Sets the size (width or height, depending on the orientation of the
    436         * plot) for the range axis of each subplot.
    437         *
    438         * @param space  the space (<code>null</code> permitted).
    439         */
    440        protected void setFixedRangeAxisSpaceForSubplots(AxisSpace space) {
    441
    442            Iterator iterator = this.subplots.iterator();
    443            while (iterator.hasNext()) {
    444                CategoryPlot plot = (CategoryPlot) iterator.next();
    445                plot.setFixedRangeAxisSpace(space);
    446            }
    447
    448        }
    449
    450        /**
    451         * Sets the orientation of the plot (and all subplots).
    452         *
    453         * @param orientation  the orientation (<code>null</code> not permitted).
    454         */
    455        public void setOrientation(PlotOrientation orientation) {
    456
    457            super.setOrientation(orientation);
    458
    459            Iterator iterator = this.subplots.iterator();
    460            while (iterator.hasNext()) {
    461                CategoryPlot plot = (CategoryPlot) iterator.next();
    462                plot.setOrientation(orientation);
    463            }
    464
    465        }
    466
    467        /**
    468         * Returns a collection of legend items for the plot.
    469         *
    470         * @return The legend items.
    471         */
    472        public LegendItemCollection getLegendItems() {
    473            LegendItemCollection result = getFixedLegendItems();
    474            if (result == null) {
    475                result = new LegendItemCollection();
    476                if (this.subplots != null) {
    477                    Iterator iterator = this.subplots.iterator();
    478                    while (iterator.hasNext()) {
    479                        CategoryPlot plot = (CategoryPlot) iterator.next();
    480                        LegendItemCollection more = plot.getLegendItems();
    481                        result.addAll(more);
    482                    }
    483                }
    484            }
    485            return result;
    486        }
    487
    488        /**
    489         * Returns an unmodifiable list of the categories contained in all the
    490         * subplots.
    491         *
    492         * @return The list.
    493         */
    494        public List getCategories() {
    495            List result = new java.util.ArrayList();
    496            if (this.subplots != null) {
    497                Iterator iterator = this.subplots.iterator();
    498                while (iterator.hasNext()) {
    499                    CategoryPlot plot = (CategoryPlot) iterator.next();
    500                    List more = plot.getCategories();
    501                    Iterator moreIterator = more.iterator();
    502                    while (moreIterator.hasNext()) {
    503                        Comparable category = (Comparable) moreIterator.next();
    504                        if (!result.contains(category)) {
    505                            result.add(category);
    506                        }
    507                    }
    508                }
    509            }
    510            return Collections.unmodifiableList(result);
    511        }
    512
    513        /**
    514         * Overridden to return the categories in the subplots.
    515         *
    516         * @param axis  ignored.
    517         *
    518         * @return A list of the categories in the subplots.
    519         *
    520         * @since 1.0.3
    521         */
    522        public List getCategoriesForAxis(CategoryAxis axis) {
    523            // FIXME:  this code means that it is not possible to use more than
    524            // one domain axis for the combined plots...
    525            return getCategories();
    526        }
    527
    528        /**
    529         * Handles a 'click' on the plot.
    530         *
    531         * @param x  x-coordinate of the click.
    532         * @param y  y-coordinate of the click.
    533         * @param info  information about the plot's dimensions.
    534         *
    535         */
    536        public void handleClick(int x, int y, PlotRenderingInfo info) {
    537
    538            Rectangle2D dataArea = info.getDataArea();
    539            if (dataArea.contains(x, y)) {
    540                for (int i = 0; i < this.subplots.size(); i++) {
    541                    CategoryPlot subplot = (CategoryPlot) this.subplots.get(i);
    542                    PlotRenderingInfo subplotInfo = info.getSubplotInfo(i);
    543                    subplot.handleClick(x, y, subplotInfo);
    544                }
    545            }
    546
    547        }
    548
    549        /**
    550         * Receives a {@link PlotChangeEvent} and responds by notifying all
    551         * listeners.
    552         *
    553         * @param event  the event.
    554         */
    555        public void plotChanged(PlotChangeEvent event) {
    556            notifyListeners(event);
    557        }
    558
    559        /**
    560         * Tests the plot for equality with an arbitrary object.
    561         *
    562         * @param obj  the object (<code>null</code> permitted).
    563         *
    564         * @return A boolean.
    565         */
    566        public boolean equals(Object obj) {
    567            if (obj == this) {
    568                return true;
    569            }
    570            if (!(obj instanceof CombinedDomainCategoryPlot)) {
    571                return false;
    572            }
    573            if (!super.equals(obj)) {
    574                return false;
    575            }
    576            CombinedDomainCategoryPlot plot = (CombinedDomainCategoryPlot) obj;
    577            if (!ObjectUtilities.equal(this.subplots, plot.subplots)) {
    578                return false;
    579            }
    580            if (this.totalWeight != plot.totalWeight) {
    581                return false;
    582            }
    583            if (this.gap != plot.gap) {
    584                return false;
    585            }
    586            return true;
    587        }
    588
    589        /**
    590         * Returns a clone of the plot.
    591         *
    592         * @return A clone.
    593         *
    594         * @throws CloneNotSupportedException  this class will not throw this
    595         *         exception, but subclasses (if any) might.
    596         */
    597        public Object clone() throws CloneNotSupportedException {
    598
    599            CombinedDomainCategoryPlot result
    600                = (CombinedDomainCategoryPlot) super.clone();
    601            result.subplots = (List) ObjectUtilities.deepClone(this.subplots);
    602            for (Iterator it = result.subplots.iterator(); it.hasNext();) {
    603                Plot child = (Plot) it.next();
    604                child.setParent(result);
    605            }
    606            return result;
    607
    608        }
    609
    610    }