001/* ===========================================================
002 * JFreeChart : a free chart library for the Java(tm) platform
003 * ===========================================================
004 *
005 * (C) Copyright 2000-2014, 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 * CombinedDomainCategoryPlot.java
029 * -------------------------------
030 * (C) Copyright 2003-2014, 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 : Added equals() method, implemented Cloneable and
040 *               Serializable (DG);
041 * 11-Sep-2003 : Fix cloning support (subplots) (NB);
042 * 15-Sep-2003 : Implemented PublicCloneable (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' attribute (DG);
046 * 12-Nov-2004 : Implemented the Zoomable interface (DG);
047 * 25-Nov-2004 : Small update to clone() implementation (DG);
048 * 21-Feb-2005 : The getLegendItems() method now returns the fixed legend
049 *               items if set (DG);
050 * 05-May-2005 : Updated draw() method parameters (DG);
051 * ------------- JFREECHART 1.0.x ---------------------------------------------
052 * 13-Sep-2006 : Updated API docs (DG);
053 * 30-Oct-2006 : Added new getCategoriesForAxis() override (DG);
054 * 17-Apr-2007 : Added null argument checks to findSubplot() (DG);
055 * 14-Nov-2007 : Updated setFixedRangeAxisSpaceForSubplots() method (DG);
056 * 27-Mar-2008 : Add documentation for getDataRange() method (DG);
057 * 31-Mar-2008 : Updated getSubplots() to return EMPTY_LIST for null
058 *               subplots, as suggested by Richard West (DG);
059 * 28-Apr-2008 : Fixed zooming problem (see bug 1950037) (DG);
060 * 26-Jun-2008 : Fixed crosshair support (DG);
061 * 11-Aug-2008 : Don't store totalWeight of subplots, calculate it as
062 *               required (DG);
063 * 03-Jul-2013 : Use ParamChecks (DG);
064 *
065 */
066
067package org.jfree.chart.plot;
068
069import java.awt.Graphics2D;
070import java.awt.geom.Point2D;
071import java.awt.geom.Rectangle2D;
072import java.util.Collections;
073import java.util.Iterator;
074import java.util.List;
075
076import org.jfree.chart.LegendItemCollection;
077import org.jfree.chart.axis.AxisSpace;
078import org.jfree.chart.axis.AxisState;
079import org.jfree.chart.axis.CategoryAxis;
080import org.jfree.chart.axis.ValueAxis;
081import org.jfree.chart.event.PlotChangeEvent;
082import org.jfree.chart.event.PlotChangeListener;
083import org.jfree.chart.util.ParamChecks;
084import org.jfree.chart.util.ShadowGenerator;
085import org.jfree.data.Range;
086import org.jfree.ui.RectangleEdge;
087import org.jfree.ui.RectangleInsets;
088import org.jfree.util.ObjectUtilities;
089
090/**
091 * A combined category plot where the domain axis is shared.
092 */
093public class CombinedDomainCategoryPlot extends CategoryPlot
094        implements PlotChangeListener {
095
096    /** For serialization. */
097    private static final long serialVersionUID = 8207194522653701572L;
098
099    /** Storage for the subplot references. */
100    private List subplots;
101
102    /** The gap between subplots. */
103    private double gap;
104
105    /** Temporary storage for the subplot areas. */
106    private transient Rectangle2D[] subplotAreas;
107    // TODO:  move the above to the plot state
108
109    /**
110     * Default constructor.
111     */
112    public CombinedDomainCategoryPlot() {
113        this(new CategoryAxis());
114    }
115
116    /**
117     * Creates a new plot.
118     *
119     * @param domainAxis  the shared domain axis (<code>null</code> not
120     *                    permitted).
121     */
122    public CombinedDomainCategoryPlot(CategoryAxis domainAxis) {
123        super(null, domainAxis, null, null);
124        this.subplots = new java.util.ArrayList();
125        this.gap = 5.0;
126    }
127
128    /**
129     * Returns the space between subplots.  The default value is 5.0.
130     *
131     * @return The gap (in Java2D units).
132     *
133     * @see #setGap(double)
134     */
135    public double getGap() {
136        return this.gap;
137    }
138
139    /**
140     * Sets the amount of space between subplots and sends a
141     * {@link PlotChangeEvent} to all registered listeners.
142     *
143     * @param gap  the gap between subplots (in Java2D units).
144     *
145     * @see #getGap()
146     */
147    public void setGap(double gap) {
148        this.gap = gap;
149        fireChangeEvent();
150    }
151
152    /**
153     * Adds a subplot to the combined chart and sends a {@link PlotChangeEvent}
154     * to all registered listeners.
155     * <br><br>
156     * The domain axis for the subplot will be set to <code>null</code>.  You
157     * must ensure that the subplot has a non-null range axis.
158     *
159     * @param subplot  the subplot (<code>null</code> not permitted).
160     */
161    public void add(CategoryPlot subplot) {
162        add(subplot, 1);
163    }
164
165    /**
166     * Adds a subplot to the combined chart and sends a {@link PlotChangeEvent}
167     * to all registered listeners.
168     * <br><br>
169     * The domain axis for the subplot will be set to <code>null</code>.  You
170     * must ensure that the subplot has a non-null range axis.
171     *
172     * @param subplot  the subplot (<code>null</code> not permitted).
173     * @param weight  the weight (must be &gt;= 1).
174     */
175    public void add(CategoryPlot subplot, int weight) {
176        ParamChecks.nullNotPermitted(subplot, "subplot");
177        if (weight < 1) {
178            throw new IllegalArgumentException("Require weight >= 1.");
179        }
180        subplot.setParent(this);
181        subplot.setWeight(weight);
182        subplot.setInsets(new RectangleInsets(0.0, 0.0, 0.0, 0.0));
183        subplot.setDomainAxis(null);
184        subplot.setOrientation(getOrientation());
185        subplot.addChangeListener(this);
186        this.subplots.add(subplot);
187        CategoryAxis axis = getDomainAxis();
188        if (axis != null) {
189            axis.configure();
190        }
191        fireChangeEvent();
192    }
193
194    /**
195     * Removes a subplot from the combined chart.  Potentially, this removes
196     * some unique categories from the overall union of the datasets...so the
197     * domain axis is reconfigured, then a {@link PlotChangeEvent} is sent to
198     * all registered listeners.
199     *
200     * @param subplot  the subplot (<code>null</code> not permitted).
201     */
202    public void remove(CategoryPlot subplot) {
203        ParamChecks.nullNotPermitted(subplot, "subplot");
204        int position = -1;
205        int size = this.subplots.size();
206        int i = 0;
207        while (position == -1 && i < size) {
208            if (this.subplots.get(i) == subplot) {
209                position = i;
210            }
211            i++;
212        }
213        if (position != -1) {
214            this.subplots.remove(position);
215            subplot.setParent(null);
216            subplot.removeChangeListener(this);
217            CategoryAxis domain = getDomainAxis();
218            if (domain != null) {
219                domain.configure();
220            }
221            fireChangeEvent();
222        }
223    }
224
225    /**
226     * Returns the list of subplots.  The returned list may be empty, but is
227     * never <code>null</code>.
228     *
229     * @return An unmodifiable list of subplots.
230     */
231    public List getSubplots() {
232        if (this.subplots != null) {
233            return Collections.unmodifiableList(this.subplots);
234        }
235        else {
236            return Collections.EMPTY_LIST;
237        }
238    }
239
240    /**
241     * Returns the subplot (if any) that contains the (x, y) point (specified
242     * in Java2D space).
243     *
244     * @param info  the chart rendering info (<code>null</code> not permitted).
245     * @param source  the source point (<code>null</code> not permitted).
246     *
247     * @return A subplot (possibly <code>null</code>).
248     */
249    public CategoryPlot findSubplot(PlotRenderingInfo info, Point2D source) {
250        ParamChecks.nullNotPermitted(info, "info");
251        ParamChecks.nullNotPermitted(source, "source");
252        CategoryPlot result = null;
253        int subplotIndex = info.getSubplotIndex(source);
254        if (subplotIndex >= 0) {
255            result =  (CategoryPlot) this.subplots.get(subplotIndex);
256        }
257        return result;
258    }
259
260    /**
261     * Multiplies the range on the range axis/axes by the specified factor.
262     *
263     * @param factor  the zoom factor.
264     * @param info  the plot rendering info (<code>null</code> not permitted).
265     * @param source  the source point (<code>null</code> not permitted).
266     */
267    @Override
268    public void zoomRangeAxes(double factor, PlotRenderingInfo info,
269                              Point2D source) {
270        zoomRangeAxes(factor, info, source, false);
271    }
272
273    /**
274     * Multiplies the range on the range axis/axes by the specified factor.
275     *
276     * @param factor  the zoom factor.
277     * @param info  the plot rendering info (<code>null</code> not permitted).
278     * @param source  the source point (<code>null</code> not permitted).
279     * @param useAnchor  zoom about the anchor point?
280     */
281    @Override
282    public void zoomRangeAxes(double factor, PlotRenderingInfo info,
283                              Point2D source, boolean useAnchor) {
284        // delegate 'info' and 'source' argument checks...
285        CategoryPlot subplot = findSubplot(info, source);
286        if (subplot != null) {
287            subplot.zoomRangeAxes(factor, info, source, useAnchor);
288        }
289        else {
290            // if the source point doesn't fall within a subplot, we do the
291            // zoom on all subplots...
292            Iterator iterator = getSubplots().iterator();
293            while (iterator.hasNext()) {
294                subplot = (CategoryPlot) iterator.next();
295                subplot.zoomRangeAxes(factor, info, source, useAnchor);
296            }
297        }
298    }
299
300    /**
301     * Zooms in on the range axes.
302     *
303     * @param lowerPercent  the lower bound.
304     * @param upperPercent  the upper bound.
305     * @param info  the plot rendering info (<code>null</code> not permitted).
306     * @param source  the source point (<code>null</code> not permitted).
307     */
308    @Override
309    public void zoomRangeAxes(double lowerPercent, double upperPercent,
310                              PlotRenderingInfo info, Point2D source) {
311        // delegate 'info' and 'source' argument checks...
312        CategoryPlot subplot = findSubplot(info, source);
313        if (subplot != null) {
314            subplot.zoomRangeAxes(lowerPercent, upperPercent, info, source);
315        }
316        else {
317            // if the source point doesn't fall within a subplot, we do the
318            // zoom on all subplots...
319            Iterator iterator = getSubplots().iterator();
320            while (iterator.hasNext()) {
321                subplot = (CategoryPlot) iterator.next();
322                subplot.zoomRangeAxes(lowerPercent, upperPercent, info, source);
323            }
324        }
325    }
326
327    /**
328     * Calculates the space required for the axes.
329     *
330     * @param g2  the graphics device.
331     * @param plotArea  the plot area.
332     *
333     * @return The space required for the axes.
334     */
335    @Override
336    protected AxisSpace calculateAxisSpace(Graphics2D g2,
337                                           Rectangle2D plotArea) {
338
339        AxisSpace space = new AxisSpace();
340        PlotOrientation orientation = getOrientation();
341
342        // work out the space required by the domain axis...
343        AxisSpace fixed = getFixedDomainAxisSpace();
344        if (fixed != null) {
345            if (orientation == PlotOrientation.HORIZONTAL) {
346                space.setLeft(fixed.getLeft());
347                space.setRight(fixed.getRight());
348            }
349            else if (orientation == PlotOrientation.VERTICAL) {
350                space.setTop(fixed.getTop());
351                space.setBottom(fixed.getBottom());
352            }
353        }
354        else {
355            CategoryAxis categoryAxis = getDomainAxis();
356            RectangleEdge categoryEdge = Plot.resolveDomainAxisLocation(
357                    getDomainAxisLocation(), orientation);
358            if (categoryAxis != null) {
359                space = categoryAxis.reserveSpace(g2, this, plotArea,
360                        categoryEdge, space);
361            }
362            else {
363                if (getDrawSharedDomainAxis()) {
364                    space = getDomainAxis().reserveSpace(g2, this, plotArea,
365                            categoryEdge, space);
366                }
367            }
368        }
369
370        Rectangle2D adjustedPlotArea = space.shrink(plotArea, null);
371
372        // work out the maximum height or width of the non-shared axes...
373        int n = this.subplots.size();
374        int totalWeight = 0;
375        for (int i = 0; i < n; i++) {
376            CategoryPlot sub = (CategoryPlot) this.subplots.get(i);
377            totalWeight += sub.getWeight();
378        }
379        this.subplotAreas = new Rectangle2D[n];
380        double x = adjustedPlotArea.getX();
381        double y = adjustedPlotArea.getY();
382        double usableSize = 0.0;
383        if (orientation == PlotOrientation.HORIZONTAL) {
384            usableSize = adjustedPlotArea.getWidth() - this.gap * (n - 1);
385        }
386        else if (orientation == PlotOrientation.VERTICAL) {
387            usableSize = adjustedPlotArea.getHeight() - this.gap * (n - 1);
388        }
389
390        for (int i = 0; i < n; i++) {
391            CategoryPlot plot = (CategoryPlot) this.subplots.get(i);
392
393            // calculate sub-plot area
394            if (orientation == PlotOrientation.HORIZONTAL) {
395                double w = usableSize * plot.getWeight() / totalWeight;
396                this.subplotAreas[i] = new Rectangle2D.Double(x, y, w,
397                        adjustedPlotArea.getHeight());
398                x = x + w + this.gap;
399            }
400            else if (orientation == PlotOrientation.VERTICAL) {
401                double h = usableSize * plot.getWeight() / totalWeight;
402                this.subplotAreas[i] = new Rectangle2D.Double(x, y,
403                        adjustedPlotArea.getWidth(), h);
404                y = y + h + this.gap;
405            }
406
407            AxisSpace subSpace = plot.calculateRangeAxisSpace(g2,
408                    this.subplotAreas[i], null);
409            space.ensureAtLeast(subSpace);
410
411        }
412
413        return space;
414    }
415
416    /**
417     * Draws the plot on a Java 2D graphics device (such as the screen or a
418     * printer).  Will perform all the placement calculations for each of the
419     * sub-plots and then tell these to draw themselves.
420     *
421     * @param g2  the graphics device.
422     * @param area  the area within which the plot (including axis labels)
423     *              should be drawn.
424     * @param anchor  the anchor point (<code>null</code> permitted).
425     * @param parentState  the state from the parent plot, if there is one.
426     * @param info  collects information about the drawing (<code>null</code>
427     *              permitted).
428     */
429    @Override
430     public void draw(Graphics2D g2, Rectangle2D area, Point2D anchor,
431            PlotState parentState, PlotRenderingInfo info) {
432
433        // set up info collection...
434        if (info != null) {
435            info.setPlotArea(area);
436        }
437
438        // adjust the drawing area for plot insets (if any)...
439        RectangleInsets insets = getInsets();
440        area.setRect(area.getX() + insets.getLeft(),
441                area.getY() + insets.getTop(),
442                area.getWidth() - insets.getLeft() - insets.getRight(),
443                area.getHeight() - insets.getTop() - insets.getBottom());
444
445
446        // calculate the data area...
447        setFixedRangeAxisSpaceForSubplots(null);
448        AxisSpace space = calculateAxisSpace(g2, area);
449        Rectangle2D dataArea = space.shrink(area, null);
450
451        // set the width and height of non-shared axis of all sub-plots
452        setFixedRangeAxisSpaceForSubplots(space);
453
454        // draw the shared axis
455        CategoryAxis axis = getDomainAxis();
456        RectangleEdge domainEdge = getDomainAxisEdge();
457        double cursor = RectangleEdge.coordinate(dataArea, domainEdge);
458        AxisState axisState = axis.draw(g2, cursor, area, dataArea,
459                domainEdge, info);
460        if (parentState == null) {
461            parentState = new PlotState();
462        }
463        parentState.getSharedAxisStates().put(axis, axisState);
464
465        // draw all the subplots
466        for (int i = 0; i < this.subplots.size(); i++) {
467            CategoryPlot plot = (CategoryPlot) this.subplots.get(i);
468            PlotRenderingInfo subplotInfo = null;
469            if (info != null) {
470                subplotInfo = new PlotRenderingInfo(info.getOwner());
471                info.addSubplotInfo(subplotInfo);
472            }
473            Point2D subAnchor = null;
474            if (anchor != null && this.subplotAreas[i].contains(anchor)) {
475                subAnchor = anchor;
476            }
477            plot.draw(g2, this.subplotAreas[i], subAnchor, parentState,
478                    subplotInfo);
479        }
480
481        if (info != null) {
482            info.setDataArea(dataArea);
483        }
484
485    }
486
487    /**
488     * Sets the size (width or height, depending on the orientation of the
489     * plot) for the range axis of each subplot.
490     *
491     * @param space  the space (<code>null</code> permitted).
492     */
493    protected void setFixedRangeAxisSpaceForSubplots(AxisSpace space) {
494        Iterator iterator = this.subplots.iterator();
495        while (iterator.hasNext()) {
496            CategoryPlot plot = (CategoryPlot) iterator.next();
497            plot.setFixedRangeAxisSpace(space, false);
498        }
499    }
500
501    /**
502     * Sets the orientation of the plot (and all subplots).
503     *
504     * @param orientation  the orientation (<code>null</code> not permitted).
505     */
506    @Override
507    public void setOrientation(PlotOrientation orientation) {
508        super.setOrientation(orientation);
509        Iterator iterator = this.subplots.iterator();
510        while (iterator.hasNext()) {
511            CategoryPlot plot = (CategoryPlot) iterator.next();
512            plot.setOrientation(orientation);
513        }
514
515    }
516
517    /**
518     * Sets the shadow generator for the plot (and all subplots) and sends
519     * a {@link PlotChangeEvent} to all registered listeners.
520     * 
521     * @param generator  the new generator (<code>null</code> permitted).
522     */
523    @Override
524    public void setShadowGenerator(ShadowGenerator generator) {
525        setNotify(false);
526        super.setShadowGenerator(generator);
527        Iterator iterator = this.subplots.iterator();
528        while (iterator.hasNext()) {
529            CategoryPlot plot = (CategoryPlot) iterator.next();
530            plot.setShadowGenerator(generator);
531        }
532        setNotify(true);
533    }
534
535    /**
536     * Returns a range representing the extent of the data values in this plot
537     * (obtained from the subplots) that will be rendered against the specified
538     * axis.  NOTE: This method is intended for internal JFreeChart use, and
539     * is public only so that code in the axis classes can call it.  Since,
540     * for this class, the domain axis is a {@link CategoryAxis}
541     * (not a {@code ValueAxis}) and subplots have independent range axes,
542     * the JFreeChart code will never call this method (although this is not
543     * checked/enforced).
544      *
545      * @param axis  the axis.
546      *
547      * @return The range.
548      */
549    @Override
550     public Range getDataRange(ValueAxis axis) {
551         // override is only for documentation purposes
552         return super.getDataRange(axis);
553     }
554
555     /**
556     * Returns a collection of legend items for the plot.
557     *
558     * @return The legend items.
559     */
560    @Override
561    public LegendItemCollection getLegendItems() {
562        LegendItemCollection result = getFixedLegendItems();
563        if (result == null) {
564            result = new LegendItemCollection();
565            if (this.subplots != null) {
566                Iterator iterator = this.subplots.iterator();
567                while (iterator.hasNext()) {
568                    CategoryPlot plot = (CategoryPlot) iterator.next();
569                    LegendItemCollection more = plot.getLegendItems();
570                    result.addAll(more);
571                }
572            }
573        }
574        return result;
575    }
576
577    /**
578     * Returns an unmodifiable list of the categories contained in all the
579     * subplots.
580     *
581     * @return The list.
582     */
583    @Override
584    public List getCategories() {
585        List result = new java.util.ArrayList();
586        if (this.subplots != null) {
587            Iterator iterator = this.subplots.iterator();
588            while (iterator.hasNext()) {
589                CategoryPlot plot = (CategoryPlot) iterator.next();
590                List more = plot.getCategories();
591                Iterator moreIterator = more.iterator();
592                while (moreIterator.hasNext()) {
593                    Comparable category = (Comparable) moreIterator.next();
594                    if (!result.contains(category)) {
595                        result.add(category);
596                    }
597                }
598            }
599        }
600        return Collections.unmodifiableList(result);
601    }
602
603    /**
604     * Overridden to return the categories in the subplots.
605     *
606     * @param axis  ignored.
607     *
608     * @return A list of the categories in the subplots.
609     *
610     * @since 1.0.3
611     */
612    @Override
613    public List getCategoriesForAxis(CategoryAxis axis) {
614        // FIXME:  this code means that it is not possible to use more than
615        // one domain axis for the combined plots...
616        return getCategories();
617    }
618
619    /**
620     * Handles a 'click' on the plot.
621     *
622     * @param x  x-coordinate of the click.
623     * @param y  y-coordinate of the click.
624     * @param info  information about the plot's dimensions.
625     *
626     */
627    @Override
628    public void handleClick(int x, int y, PlotRenderingInfo info) {
629
630        Rectangle2D dataArea = info.getDataArea();
631        if (dataArea.contains(x, y)) {
632            for (int i = 0; i < this.subplots.size(); i++) {
633                CategoryPlot subplot = (CategoryPlot) this.subplots.get(i);
634                PlotRenderingInfo subplotInfo = info.getSubplotInfo(i);
635                subplot.handleClick(x, y, subplotInfo);
636            }
637        }
638
639    }
640
641    /**
642     * Receives a {@link PlotChangeEvent} and responds by notifying all
643     * listeners.
644     *
645     * @param event  the event.
646     */
647    @Override
648    public void plotChanged(PlotChangeEvent event) {
649        notifyListeners(event);
650    }
651
652    /**
653     * Tests the plot for equality with an arbitrary object.
654     *
655     * @param obj  the object (<code>null</code> permitted).
656     *
657     * @return A boolean.
658     */
659    @Override
660    public boolean equals(Object obj) {
661        if (obj == this) {
662            return true;
663        }
664        if (!(obj instanceof CombinedDomainCategoryPlot)) {
665            return false;
666        }
667        CombinedDomainCategoryPlot that = (CombinedDomainCategoryPlot) obj;
668        if (this.gap != that.gap) {
669            return false;
670        }
671        if (!ObjectUtilities.equal(this.subplots, that.subplots)) {
672            return false;
673        }
674        return super.equals(obj);
675    }
676
677    /**
678     * Returns a clone of the plot.
679     *
680     * @return A clone.
681     *
682     * @throws CloneNotSupportedException  this class will not throw this
683     *         exception, but subclasses (if any) might.
684     */
685    @Override
686    public Object clone() throws CloneNotSupportedException {
687
688        CombinedDomainCategoryPlot result
689            = (CombinedDomainCategoryPlot) super.clone();
690        result.subplots = (List) ObjectUtilities.deepClone(this.subplots);
691        for (Iterator it = result.subplots.iterator(); it.hasNext();) {
692            Plot child = (Plot) it.next();
693            child.setParent(result);
694        }
695        return result;
696
697    }
698
699}