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