    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     * CombinedRangeCategoryPlot.java
    029     * ------------------------------
    030     * (C) Copyright 2003-2007, by Object Refinery Limited.
    031     *
    032     * Original Author:  David Gilbert (for Object Refinery Limited);
    033     * Contributor(s):   Nicolas Brodu;
    034     *
    035     * Changes:
    036     * --------
    037     * 16-May-2003 : Version 1 (DG);
    038     * 08-Aug-2003 : Adjusted totalWeight in remove() method (DG);
    039     * 19-Aug-2003 : Implemented Cloneable (DG);
    040     * 11-Sep-2003 : Fix cloning support (subplots) (NB);
    041     * 15-Sep-2003 : Implemented PublicCloneable.  Fixed errors in cloning and
    042     *               serialization (DG);
    043     * 16-Sep-2003 : Changed ChartRenderingInfo --> PlotRenderingInfo (DG);
    044     * 17-Sep-2003 : Updated handling of 'clicks' (DG);
    045     * 04-May-2004 : Added getter/setter methods for 'gap' attributes (DG);
    046     * 12-Nov-2004 : Implements the new Zoomable interface (DG);
    047     * 25-Nov-2004 : Small update to clone() implementation (DG);
    048     * 21-Feb-2005 : Fixed bug in remove() method (id = 1121172) (DG);
    049     * 21-Feb-2005 : The getLegendItems() method now returns the fixed legend
    050     *               items if set (DG);
    051     * 05-May-2005 : Updated draw() method parameters (DG);
    052     * 14-Nov-2007 : Updated setFixedDomainAxisSpaceForSubplots() method (DG);
    053     */
    054
    055    package org.jfree.chart.plot;
    056
    057    import java.awt.Graphics2D;
    058    import java.awt.geom.Point2D;
    059    import java.awt.geom.Rectangle2D;
    060    import java.io.IOException;
    061    import java.io.ObjectInputStream;
    062    import java.io.Serializable;
    063    import java.util.Collections;
    064    import java.util.Iterator;
    065    import java.util.List;
    066
    067    import org.jfree.chart.LegendItemCollection;
    068    import org.jfree.chart.axis.AxisSpace;
    069    import org.jfree.chart.axis.AxisState;
    070    import org.jfree.chart.axis.NumberAxis;
    071    import org.jfree.chart.axis.ValueAxis;
    072    import org.jfree.chart.event.PlotChangeEvent;
    073    import org.jfree.chart.event.PlotChangeListener;
    074    import org.jfree.data.Range;
    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 range axis is shared.
    082     */
    083    public class CombinedRangeCategoryPlot extends CategoryPlot
    084                                           implements Zoomable,
    085                                                      Cloneable, PublicCloneable,
    086                                                      Serializable,
    087                                                      PlotChangeListener {
    088
    089        /** For serialization. */
    090        private static final long serialVersionUID = 7260210007554504515L;
    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[] subplotArea;  // TODO: move to plot state
    103
    104        /**
    105         * Default constructor.
    106         */
    107        public CombinedRangeCategoryPlot() {
    108            this(new NumberAxis());
    109        }
    110
    111        /**
    112         * Creates a new plot.
    113         *
    114         * @param rangeAxis  the shared range axis.
    115         */
    116        public CombinedRangeCategoryPlot(ValueAxis rangeAxis) {
    117            super(null, null, rangeAxis, null);
    118            this.subplots = new java.util.ArrayList();
    119            this.totalWeight = 0;
    120            this.gap = 5.0;
    121        }
    122
    123        /**
    124         * Returns the space between subplots.
    125         *
    126         * @return The gap (in Java2D units).
    127         */
    128        public double getGap() {
    129            return this.gap;
    130        }
    131
    132        /**
    133         * Sets the amount of space between subplots and sends a
    134         * {@link PlotChangeEvent} to all registered listeners.
    135         *
    136         * @param gap  the gap between subplots (in Java2D units).
    137         */
    138        public void setGap(double gap) {
    139            this.gap = gap;
    140            notifyListeners(new PlotChangeEvent(this));
    141        }
    142
    143        /**
    144         * Adds a subplot (with a default 'weight' of 1) and sends a
    145         * {@link PlotChangeEvent} to all registered listeners.
    146         * <br><br>
    147         * You must ensure that the subplot has a non-null domain axis.  The range
    148         * axis for the subplot will be set to <code>null</code>.
    149         *
    150         * @param subplot  the subplot (<code>null</code> not permitted).
    151         */
    152        public void add(CategoryPlot subplot) {
    153            // defer argument checking
    154            add(subplot, 1);
    155        }
    156
    157        /**
    158         * Adds a subplot and sends a {@link PlotChangeEvent} to all registered
    159         * listeners.
    160         * <br><br>
    161         * You must ensure that the subplot has a non-null domain axis.  The range
    162         * axis for the subplot will be set to <code>null</code>.
    163         *
    164         * @param subplot  the subplot (<code>null</code> not permitted).
    165         * @param weight  the weight (must be >= 1).
    166         */
    167        public void add(CategoryPlot subplot, int weight) {
    168            if (subplot == null) {
    169                throw new IllegalArgumentException("Null 'subplot' argument.");
    170            }
    171            if (weight <= 0) {
    172                throw new IllegalArgumentException("Require weight >= 1.");
    173            }
    174            // store the plot and its weight
    175            subplot.setParent(this);
    176            subplot.setWeight(weight);
    177            subplot.setInsets(new RectangleInsets(0.0, 0.0, 0.0, 0.0));
    178            subplot.setRangeAxis(null);
    179            subplot.setOrientation(getOrientation());
    180            subplot.addChangeListener(this);
    181            this.subplots.add(subplot);
    182            this.totalWeight += weight;
    183
    184            // configure the range axis...
    185            ValueAxis axis = getRangeAxis();
    186            if (axis != null) {
    187                axis.configure();
    188            }
    189            notifyListeners(new PlotChangeEvent(this));
    190        }
    191
    192        /**
    193         * Removes a subplot from the combined chart.
    194         *
    195         * @param subplot  the subplot (<code>null</code> not permitted).
    196         */
    197        public void remove(CategoryPlot subplot) {
    198            if (subplot == null) {
    199                throw new IllegalArgumentException(" Null 'subplot' argument.");
    200            }
    201            int position = -1;
    202            int size = this.subplots.size();
    203            int i = 0;
    204            while (position == -1 && i < size) {
    205                if (this.subplots.get(i) == subplot) {
    206                    position = i;
    207                }
    208                i++;
    209            }
    210            if (position != -1) {
    211                this.subplots.remove(position);
    212                subplot.setParent(null);
    213                subplot.removeChangeListener(this);
    214                this.totalWeight -= subplot.getWeight();
    215
    216                ValueAxis range = getRangeAxis();
    217                if (range != null) {
    218                    range.configure();
    219                }
    220
    221                ValueAxis range2 = getRangeAxis(1);
    222                if (range2 != null) {
    223                    range2.configure();
    224                }
    225                notifyListeners(new PlotChangeEvent(this));
    226            }
    227        }
    228
    229        /**
    230         * Returns the list of subplots.
    231         *
    232         * @return The list (unmodifiable).
    233         */
    234        public List getSubplots() {
    235            return Collections.unmodifiableList(this.subplots);
    236        }
    237
    238        /**
    239         * Calculates the space required for the axes.
    240         *
    241         * @param g2  the graphics device.
    242         * @param plotArea  the plot area.
    243         *
    244         * @return The space required for the axes.
    245         */
    246        protected AxisSpace calculateAxisSpace(Graphics2D g2,
    247                                               Rectangle2D plotArea) {
    248
    249            AxisSpace space = new AxisSpace();
    250            PlotOrientation orientation = getOrientation();
    251
    252            // work out the space required by the domain axis...
    253            AxisSpace fixed = getFixedRangeAxisSpace();
    254            if (fixed != null) {
    255                if (orientation == PlotOrientation.VERTICAL) {
    256                    space.setLeft(fixed.getLeft());
    257                    space.setRight(fixed.getRight());
    258                }
    259                else if (orientation == PlotOrientation.HORIZONTAL) {
    260                    space.setTop(fixed.getTop());
    261                    space.setBottom(fixed.getBottom());
    262                }
    263            }
    264            else {
    265                ValueAxis valueAxis = getRangeAxis();
    266                RectangleEdge valueEdge = Plot.resolveRangeAxisLocation(
    267                        getRangeAxisLocation(), orientation);
    268                if (valueAxis != null) {
    269                    space = valueAxis.reserveSpace(g2, this, plotArea, valueEdge,
    270                            space);
    271                }
    272            }
    273
    274            Rectangle2D adjustedPlotArea = space.shrink(plotArea, null);
    275            // work out the maximum height or width of the non-shared axes...
    276            int n = this.subplots.size();
    277
    278            // calculate plotAreas of all sub-plots, maximum vertical/horizontal
    279            // axis width/height
    280            this.subplotArea = new Rectangle2D[n];
    281            double x = adjustedPlotArea.getX();
    282            double y = adjustedPlotArea.getY();
    283            double usableSize = 0.0;
    284            if (orientation == PlotOrientation.VERTICAL) {
    285                usableSize = adjustedPlotArea.getWidth() - this.gap * (n - 1);
    286            }
    287            else if (orientation == PlotOrientation.HORIZONTAL) {
    288                usableSize = adjustedPlotArea.getHeight() - this.gap * (n - 1);
    289            }
    290
    291            for (int i = 0; i < n; i++) {
    292                CategoryPlot plot = (CategoryPlot) this.subplots.get(i);
    293
    294                // calculate sub-plot area
    295                if (orientation == PlotOrientation.VERTICAL) {
    296                    double w = usableSize * plot.getWeight() / this.totalWeight;
    297                    this.subplotArea[i] = new Rectangle2D.Double(x, y, w,
    298                            adjustedPlotArea.getHeight());
    299                    x = x + w + this.gap;
    300                }
    301                else if (orientation == PlotOrientation.HORIZONTAL) {
    302                    double h = usableSize * plot.getWeight() / this.totalWeight;
    303                    this.subplotArea[i] = new Rectangle2D.Double(x, y,
    304                            adjustedPlotArea.getWidth(), h);
    305                    y = y + h + this.gap;
    306                }
    307
    308                AxisSpace subSpace = plot.calculateDomainAxisSpace(g2,
    309                        this.subplotArea[i], null);
    310                space.ensureAtLeast(subSpace);
    311
    312            }
    313
    314            return space;
    315        }
    316
    317        /**
    318         * Draws the plot on a Java 2D graphics device (such as the screen or a
    319         * printer).  Will perform all the placement calculations for each
    320         * sub-plots and then tell these to draw themselves.
    321         *
    322         * @param g2  the graphics device.
    323         * @param area  the area within which the plot (including axis labels)
    324         *              should be drawn.
    325         * @param anchor  the anchor point (<code>null</code> permitted).
    326         * @param parentState  the parent state.
    327         * @param info  collects information about the drawing (<code>null</code>
    328         *              permitted).
    329         */
    330        public void draw(Graphics2D g2, Rectangle2D area, Point2D anchor,
    331                         PlotState parentState,
    332                         PlotRenderingInfo info) {
    333
    334            // set up info collection...
    335            if (info != null) {
    336                info.setPlotArea(area);
    337            }
    338
    339            // adjust the drawing area for plot insets (if any)...
    340            RectangleInsets insets = getInsets();
    341            insets.trim(area);
    342
    343            // calculate the data area...
    344            AxisSpace space = calculateAxisSpace(g2, area);
    345            Rectangle2D dataArea = space.shrink(area, null);
    346
    347            // set the width and height of non-shared axis of all sub-plots
    348            setFixedDomainAxisSpaceForSubplots(space);
    349
    350            // draw the shared axis
    351            ValueAxis axis = getRangeAxis();
    352            RectangleEdge rangeEdge = getRangeAxisEdge();
    353            double cursor = RectangleEdge.coordinate(dataArea, rangeEdge);
    354            AxisState state = axis.draw(g2, cursor, area, dataArea, rangeEdge,
    355                    info);
    356            if (parentState == null) {
    357                parentState = new PlotState();
    358            }
    359            parentState.getSharedAxisStates().put(axis, state);
    360
    361            // draw all the charts
    362            for (int i = 0; i < this.subplots.size(); i++) {
    363                CategoryPlot plot = (CategoryPlot) this.subplots.get(i);
    364                PlotRenderingInfo subplotInfo = null;
    365                if (info != null) {
    366                    subplotInfo = new PlotRenderingInfo(info.getOwner());
    367                    info.addSubplotInfo(subplotInfo);
    368                }
    369                plot.draw(g2, this.subplotArea[i], null, parentState, subplotInfo);
    370            }
    371
    372            if (info != null) {
    373                info.setDataArea(dataArea);
    374            }
    375
    376        }
    377
    378        /**
    379         * Sets the orientation for the plot (and all the subplots).
    380         *
    381         * @param orientation  the orientation.
    382         */
    383        public void setOrientation(PlotOrientation orientation) {
    384
    385            super.setOrientation(orientation);
    386
    387            Iterator iterator = this.subplots.iterator();
    388            while (iterator.hasNext()) {
    389                CategoryPlot plot = (CategoryPlot) iterator.next();
    390                plot.setOrientation(orientation);
    391            }
    392
    393        }
    394
    395        /**
    396          * Returns the range for the axis.  This is the combined range of all the
    397          * subplots.
    398          *
    399          * @param axis  the axis.
    400          *
    401          * @return The range.
    402          */
    403         public Range getDataRange(ValueAxis axis) {
    404
    405             Range result = null;
    406             if (this.subplots != null) {
    407                 Iterator iterator = this.subplots.iterator();
    408                 while (iterator.hasNext()) {
    409                     CategoryPlot subplot = (CategoryPlot) iterator.next();
    410                     result = Range.combine(result, subplot.getDataRange(axis));
    411                 }
    412             }
    413             return result;
    414
    415         }
    416
    417        /**
    418         * Returns a collection of legend items for the plot.
    419         *
    420         * @return The legend items.
    421         */
    422        public LegendItemCollection getLegendItems() {
    423            LegendItemCollection result = getFixedLegendItems();
    424            if (result == null) {
    425                result = new LegendItemCollection();
    426                if (this.subplots != null) {
    427                    Iterator iterator = this.subplots.iterator();
    428                    while (iterator.hasNext()) {
    429                        CategoryPlot plot = (CategoryPlot) iterator.next();
    430                        LegendItemCollection more = plot.getLegendItems();
    431                        result.addAll(more);
    432                    }
    433                }
    434            }
    435            return result;
    436        }
    437
    438        /**
    439         * Sets the size (width or height, depending on the orientation of the
    440         * plot) for the domain axis of each subplot.
    441         *
    442         * @param space  the space.
    443         */
    444        protected void setFixedDomainAxisSpaceForSubplots(AxisSpace space) {
    445            Iterator iterator = this.subplots.iterator();
    446            while (iterator.hasNext()) {
    447                CategoryPlot plot = (CategoryPlot) iterator.next();
    448                plot.setFixedDomainAxisSpace(space, false);
    449            }
    450        }
    451
    452        /**
    453         * Handles a 'click' on the plot by updating the anchor value.
    454         *
    455         * @param x  x-coordinate of the click.
    456         * @param y  y-coordinate of the click.
    457         * @param info  information about the plot's dimensions.
    458         *
    459         */
    460        public void handleClick(int x, int y, PlotRenderingInfo info) {
    461
    462            Rectangle2D dataArea = info.getDataArea();
    463            if (dataArea.contains(x, y)) {
    464                for (int i = 0; i < this.subplots.size(); i++) {
    465                    CategoryPlot subplot = (CategoryPlot) this.subplots.get(i);
    466                    PlotRenderingInfo subplotInfo = info.getSubplotInfo(i);
    467                    subplot.handleClick(x, y, subplotInfo);
    468                }
    469            }
    470
    471        }
    472
    473        /**
    474         * Receives a {@link PlotChangeEvent} and responds by notifying all
    475         * listeners.
    476         *
    477         * @param event  the event.
    478         */
    479        public void plotChanged(PlotChangeEvent event) {
    480            notifyListeners(event);
    481        }
    482
    483        /**
    484         * Tests the plot for equality with an arbitrary object.
    485         *
    486         * @param obj  the object (<code>null</code> permitted).
    487         *
    488         * @return <code>true</code> or <code>false</code>.
    489         */
    490        public boolean equals(Object obj) {
    491            if (obj == this) {
    492                return true;
    493            }
    494            if (!(obj instanceof CombinedRangeCategoryPlot)) {
    495                return false;
    496            }
    497            if (!super.equals(obj)) {
    498                return false;
    499            }
    500            CombinedRangeCategoryPlot that = (CombinedRangeCategoryPlot) obj;
    501            if (!ObjectUtilities.equal(this.subplots, that.subplots)) {
    502                return false;
    503            }
    504            if (this.totalWeight != that.totalWeight) {
    505                return false;
    506            }
    507            if (this.gap != that.gap) {
    508                return false;
    509            }
    510            return true;
    511        }
    512
    513        /**
    514         * Returns a clone of the plot.
    515         *
    516         * @return A clone.
    517         *
    518         * @throws CloneNotSupportedException  this class will not throw this
    519         *         exception, but subclasses (if any) might.
    520         */
    521        public Object clone() throws CloneNotSupportedException {
    522            CombinedRangeCategoryPlot result
    523                = (CombinedRangeCategoryPlot) super.clone();
    524            result.subplots = (List) ObjectUtilities.deepClone(this.subplots);
    525            for (Iterator it = result.subplots.iterator(); it.hasNext();) {
    526                Plot child = (Plot) it.next();
    527                child.setParent(result);
    528            }
    529
    530            // after setting up all the subplots, the shared range axis may need
    531            // reconfiguring
    532            ValueAxis rangeAxis = result.getRangeAxis();
    533            if (rangeAxis != null) {
    534                rangeAxis.configure();
    535            }
    536
    537            return result;
    538        }
    539
    540        /**
    541         * Provides serialization support.
    542         *
    543         * @param stream  the input stream.
    544         *
    545         * @throws IOException  if there is an I/O error.
    546         * @throws ClassNotFoundException  if there is a classpath problem.
    547         */
    548        private void readObject(ObjectInputStream stream)
    549            throws IOException, ClassNotFoundException {
    550
    551            stream.defaultReadObject();
    552
    553            // the range axis is deserialized before the subplots, so its value
    554            // range is likely to be incorrect...
    555            ValueAxis rangeAxis = getRangeAxis();
    556            if (rangeAxis != null) {
    557                rangeAxis.configure();
    558            }
    559
    560        }
    561
    562    }