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