001/* ===========================================================
002 * JFreeChart : a free chart library for the Java(tm) platform
003 * ===========================================================
004 *
005 * (C) Copyright 2000-2022, by David Gilbert 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 * CombinedDomainXYPlot.java
029 * -------------------------
030 * (C) Copyright 2001-2021, by Bill Kelemen and Contributors.
031 *
032 * Original Author:  Bill Kelemen;
033 * Contributor(s):   David Gilbert;
034 *                   Anthony Boulestreau;
035 *                   David Basten;
036 *                   Kevin Frechette (for ISTI);
037 *                   Nicolas Brodu;
038 *                   Petr Kubanek (bug 1606205);
039 *                   Vladimir Shirokov (bug 986);
040 */
041
042package org.jfree.chart.plot;
043
044import java.awt.Graphics2D;
045import java.awt.geom.Point2D;
046import java.awt.geom.Rectangle2D;
047import java.util.ArrayList;
048import java.util.Collections;
049import java.util.List;
050import java.util.Objects;
051import org.jfree.chart.ChartElementVisitor;
052
053import org.jfree.chart.legend.LegendItemCollection;
054import org.jfree.chart.axis.AxisSpace;
055import org.jfree.chart.axis.AxisState;
056import org.jfree.chart.axis.NumberAxis;
057import org.jfree.chart.axis.ValueAxis;
058import org.jfree.chart.event.PlotChangeEvent;
059import org.jfree.chart.event.PlotChangeListener;
060import org.jfree.chart.renderer.xy.XYItemRenderer;
061import org.jfree.chart.api.RectangleEdge;
062import org.jfree.chart.api.RectangleInsets;
063import org.jfree.chart.internal.CloneUtils;
064import org.jfree.chart.internal.Args;
065import org.jfree.chart.util.ShadowGenerator;
066import org.jfree.data.Range;
067import org.jfree.data.general.DatasetChangeEvent;
068import org.jfree.data.xy.XYDataset;
069
070/**
071 * An extension of {@link XYPlot} that contains multiple subplots that share a
072 * common domain axis.
073 */
074public class CombinedDomainXYPlot<S extends Comparable<S>> extends XYPlot<S>
075        implements PlotChangeListener {
076
077    /** For serialization. */
078    private static final long serialVersionUID = -7765545541261907383L;
079
080    /** Storage for the subplot references (possibly empty but never null). */
081    private List<XYPlot> subplots;
082
083    /** The gap between subplots. */
084    private double gap = 5.0;
085
086    /** Temporary storage for the subplot areas. */
087    private transient Rectangle2D[] subplotAreas;
088    // TODO:  the subplot areas needs to be moved out of the plot into the plot
089    //        state
090
091    /**
092     * Default constructor.
093     */
094    public CombinedDomainXYPlot() {
095        this(new NumberAxis());
096    }
097
098    /**
099     * Creates a new combined plot that shares a domain axis among multiple
100     * subplots.
101     *
102     * @param domainAxis  the shared axis.
103     */
104    public CombinedDomainXYPlot(ValueAxis domainAxis) {
105        super(null,        // no data in the parent plot
106              domainAxis,
107              null,        // no range axis
108              null);       // no renderer
109        this.subplots = new ArrayList<>();
110    }
111
112    /**
113     * Returns a string describing the type of plot.
114     *
115     * @return The type of plot.
116     */
117    @Override
118    public String getPlotType() {
119        return "Combined_Domain_XYPlot";
120    }
121
122    /**
123     * Returns the gap between subplots, measured in Java2D units.
124     *
125     * @return The gap (in Java2D units).
126     *
127     * @see #setGap(double)
128     */
129    public double getGap() {
130        return this.gap;
131    }
132
133    /**
134     * Sets the amount of space between subplots and sends a
135     * {@link PlotChangeEvent} to all registered listeners.
136     *
137     * @param gap  the gap between subplots (in Java2D units).
138     *
139     * @see #getGap()
140     */
141    public void setGap(double gap) {
142        this.gap = gap;
143        fireChangeEvent();
144    }
145
146    /**
147     * Returns {@code true} if the range is pannable for at least one subplot,
148     * and {@code false} otherwise.
149     * 
150     * @return A boolean. 
151     */
152    @Override
153    public boolean isRangePannable() {
154        for (XYPlot subplot : this.subplots) {
155            if (subplot.isRangePannable()) {
156                return true;
157            }
158        }
159        return false;
160    }
161    
162    /**
163     * Sets the flag, on each of the subplots, that controls whether or not the 
164     * range is pannable.
165     * 
166     * @param pannable  the new flag value. 
167     */
168    @Override
169    public void setRangePannable(boolean pannable) {
170        for (XYPlot subplot : this.subplots) {
171            subplot.setRangePannable(pannable);
172        }        
173    }
174
175    /**
176     * Sets the orientation for the plot (also changes the orientation for all
177     * the subplots to match).
178     *
179     * @param orientation  the orientation ({@code null} not allowed).
180     */
181    @Override
182    public void setOrientation(PlotOrientation orientation) {
183        super.setOrientation(orientation);
184        for (XYPlot p : this.subplots) {
185            p.setOrientation(orientation);
186        }
187    }
188
189    /**
190     * Sets the shadow generator for the plot (and all subplots) and sends
191     * a {@link PlotChangeEvent} to all registered listeners.
192     * 
193     * @param generator  the new generator ({@code null} permitted).
194     */
195    @Override
196    public void setShadowGenerator(ShadowGenerator generator) {
197        setNotify(false);
198        super.setShadowGenerator(generator);
199        for (XYPlot p : this.subplots) {
200            p.setShadowGenerator(generator);
201        }
202        setNotify(true);
203    }
204
205    /**
206     * Returns a range representing the extent of the data values in this plot
207     * (obtained from the subplots) that will be rendered against the specified
208     * axis.  NOTE: This method is intended for internal JFreeChart use, and
209     * is public only so that code in the axis classes can call it.  Since
210     * only the domain axis is shared between subplots, the JFreeChart code
211     * will only call this method for the domain values (although this is not
212     * checked/enforced).
213     *
214     * @param axis  the axis.
215     *
216     * @return The range (possibly {@code null}).
217     */
218    @Override
219    public Range getDataRange(ValueAxis axis) {
220        if (this.subplots == null) {
221            return null;
222        }
223        Range result = null;
224        for (XYPlot p : this.subplots) {
225            result = Range.combine(result, p.getDataRange(axis));
226        }
227        return result;
228    }
229
230    /**
231     * Adds a subplot (with a default 'weight' of 1) and sends a
232     * {@link PlotChangeEvent} to all registered listeners.
233     * <P>
234     * The domain axis for the subplot will be set to {@code null}.  You
235     * must ensure that the subplot has a non-null range axis.
236     *
237     * @param subplot  the subplot ({@code null} not permitted).
238     */
239    public void add(XYPlot subplot) {
240        // defer argument checking
241        add(subplot, 1);
242    }
243
244    /**
245     * Adds a subplot with the specified weight and sends a
246     * {@link PlotChangeEvent} to all registered listeners.  The weight
247     * determines how much space is allocated to the subplot relative to all
248     * the other subplots.
249     * <P>
250     * The domain axis for the subplot will be set to {@code null}.  You
251     * must ensure that the subplot has a non-null range axis.
252     *
253     * @param subplot  the subplot ({@code null} not permitted).
254     * @param weight  the weight (must be &gt;= 1).
255     */
256    public void add(XYPlot subplot, int weight) {
257        Args.nullNotPermitted(subplot, "subplot");
258        if (weight <= 0) {
259            throw new IllegalArgumentException("Require weight >= 1.");
260        }
261
262        // store the plot and its weight
263        subplot.setParent(this);
264        subplot.setWeight(weight);
265        subplot.setInsets(RectangleInsets.ZERO_INSETS, false);
266        subplot.setDomainAxis(null);
267        subplot.addChangeListener(this);
268        this.subplots.add(subplot);
269
270        ValueAxis axis = getDomainAxis();
271        if (axis != null) {
272            axis.configure();
273        }
274        fireChangeEvent();
275    }
276
277    /**
278     * Removes a subplot from the combined chart and sends a
279     * {@link PlotChangeEvent} to all registered listeners.
280     *
281     * @param subplot  the subplot ({@code null} not permitted).
282     */
283    public void remove(XYPlot subplot) {
284        Args.nullNotPermitted(subplot, "subplot");
285        int position = -1;
286        int size = this.subplots.size();
287        int i = 0;
288        while (position == -1 && i < size) {
289            if (this.subplots.get(i) == subplot) {
290                position = i;
291            }
292            i++;
293        }
294        if (position != -1) {
295            this.subplots.remove(position);
296            subplot.setParent(null);
297            subplot.removeChangeListener(this);
298            ValueAxis domain = getDomainAxis();
299            if (domain != null) {
300                domain.configure();
301            }
302            fireChangeEvent();
303        }
304    }
305
306    /**
307     * Returns the list of subplots.  The returned list may be empty, but is
308     * never {@code null}.
309     *
310     * @return An unmodifiable list of subplots.
311     */
312    public List<XYPlot> getSubplots() {
313        return Collections.unmodifiableList(this.subplots);
314    }
315
316    /**
317     * Calculates the axis space required.
318     *
319     * @param g2  the graphics device.
320     * @param plotArea  the plot area.
321     *
322     * @return The space.
323     */
324    @Override
325    protected AxisSpace calculateAxisSpace(Graphics2D g2,
326            Rectangle2D plotArea) {
327
328        AxisSpace space = new AxisSpace();
329        PlotOrientation orientation = getOrientation();
330
331        // work out the space required by the domain axis...
332        AxisSpace fixed = getFixedDomainAxisSpace();
333        if (fixed != null) {
334            if (orientation == PlotOrientation.HORIZONTAL) {
335                space.setLeft(fixed.getLeft());
336                space.setRight(fixed.getRight());
337            }
338            else if (orientation == PlotOrientation.VERTICAL) {
339                space.setTop(fixed.getTop());
340                space.setBottom(fixed.getBottom());
341            }
342        }
343        else {
344            ValueAxis xAxis = getDomainAxis();
345            RectangleEdge xEdge = Plot.resolveDomainAxisLocation(
346                    getDomainAxisLocation(), orientation);
347            if (xAxis != null) {
348                space = xAxis.reserveSpace(g2, this, plotArea, xEdge, space);
349            }
350        }
351
352        Rectangle2D adjustedPlotArea = space.shrink(plotArea, null);
353
354        // work out the maximum height or width of the non-shared axes...
355        int n = this.subplots.size();
356        int totalWeight = 0;
357        for (int i = 0; i < n; i++) {
358            XYPlot sub = (XYPlot) this.subplots.get(i);
359            totalWeight += sub.getWeight();
360        }
361        this.subplotAreas = new Rectangle2D[n];
362        double x = adjustedPlotArea.getX();
363        double y = adjustedPlotArea.getY();
364        double usableSize = 0.0;
365        if (orientation == PlotOrientation.HORIZONTAL) {
366            usableSize = adjustedPlotArea.getWidth() - this.gap * (n - 1);
367        }
368        else if (orientation == PlotOrientation.VERTICAL) {
369            usableSize = adjustedPlotArea.getHeight() - this.gap * (n - 1);
370        }
371
372        for (int i = 0; i < n; i++) {
373            XYPlot plot = (XYPlot) this.subplots.get(i);
374
375            // calculate sub-plot area
376            if (orientation == PlotOrientation.HORIZONTAL) {
377                double w = usableSize * plot.getWeight() / totalWeight;
378                this.subplotAreas[i] = new Rectangle2D.Double(x, y, w,
379                        adjustedPlotArea.getHeight());
380                x = x + w + this.gap;
381            }
382            else if (orientation == PlotOrientation.VERTICAL) {
383                double h = usableSize * plot.getWeight() / totalWeight;
384                this.subplotAreas[i] = new Rectangle2D.Double(x, y,
385                        adjustedPlotArea.getWidth(), h);
386                y = y + h + this.gap;
387            }
388
389            AxisSpace subSpace = plot.calculateRangeAxisSpace(g2,
390                    this.subplotAreas[i], null);
391            space.ensureAtLeast(subSpace);
392
393        }
394
395        return space;
396    }
397
398    /**
399     * Receives a chart element visitor.  Many plot subclasses will override
400     * this method to handle their subcomponents.
401     * 
402     * @param visitor  the visitor ({@code null} not permitted).
403     */
404    @Override
405    public void receive(ChartElementVisitor visitor) {
406        subplots.forEach(subplot -> {
407            subplot.receive(visitor);
408        });
409        super.receive(visitor);
410    }
411
412    /**
413     * Draws the plot within the specified area on a graphics device.
414     *
415     * @param g2  the graphics device.
416     * @param area  the plot area (in Java2D space).
417     * @param anchor  an anchor point in Java2D space ({@code null}
418     *                permitted).
419     * @param parentState  the state from the parent plot, if there is one
420     *                     ({@code null} permitted).
421     * @param info  collects chart drawing information ({@code null}
422     *              permitted).
423     */
424    @Override
425    public void draw(Graphics2D g2, Rectangle2D area, Point2D anchor,
426            PlotState parentState, PlotRenderingInfo info) {
427
428        // set up info collection...
429        if (info != null) {
430            info.setPlotArea(area);
431        }
432
433        // adjust the drawing area for plot insets (if any)...
434        RectangleInsets insets = getInsets();
435        insets.trim(area);
436
437        setFixedRangeAxisSpaceForSubplots(null);
438        AxisSpace space = calculateAxisSpace(g2, area);
439        Rectangle2D dataArea = space.shrink(area, null);
440
441        // set the width and height of non-shared axis of all sub-plots
442        setFixedRangeAxisSpaceForSubplots(space);
443
444        // draw the shared axis
445        ValueAxis axis = getDomainAxis();
446        RectangleEdge edge = getDomainAxisEdge();
447        double cursor = RectangleEdge.coordinate(dataArea, edge);
448        AxisState axisState = axis.draw(g2, cursor, area, dataArea, edge, info);
449        if (parentState == null) {
450            parentState = new PlotState();
451        }
452        parentState.getSharedAxisStates().put(axis, axisState);
453
454        // draw all the subplots
455        for (int i = 0; i < this.subplots.size(); i++) {
456            XYPlot plot = (XYPlot) this.subplots.get(i);
457            PlotRenderingInfo subplotInfo = null;
458            if (info != null) {
459                subplotInfo = new PlotRenderingInfo(info.getOwner());
460                info.addSubplotInfo(subplotInfo);
461            }
462            plot.draw(g2, this.subplotAreas[i], anchor, parentState,
463                    subplotInfo);
464        }
465
466        if (info != null) {
467            info.setDataArea(dataArea);
468        }
469
470    }
471
472    /**
473     * Returns a collection of legend items for the plot.
474     *
475     * @return The legend items.
476     */
477    @Override
478    public LegendItemCollection getLegendItems() {
479        LegendItemCollection result = getFixedLegendItems();
480        if (result == null) {
481            result = new LegendItemCollection();
482            if (this.subplots != null) {
483                for (XYPlot plot : this.subplots) {
484                    LegendItemCollection more = plot.getLegendItems();
485                    result.addAll(more);
486                }
487            }
488        }
489        return result;
490    }
491
492    /**
493     * Multiplies the range on the range axis/axes by the specified factor.
494     *
495     * @param factor  the zoom factor.
496     * @param info  the plot rendering info ({@code null} not permitted).
497     * @param source  the source point ({@code null} not permitted).
498     */
499    @Override
500    public void zoomRangeAxes(double factor, PlotRenderingInfo info,
501                              Point2D source) {
502        zoomRangeAxes(factor, info, source, false);
503    }
504
505    /**
506     * Multiplies the range on the range axis/axes by the specified factor.
507     *
508     * @param factor  the zoom factor.
509     * @param state  the plot state.
510     * @param source  the source point (in Java2D coordinates).
511     * @param useAnchor  use source point as zoom anchor?
512     */
513    @Override
514    public void zoomRangeAxes(double factor, PlotRenderingInfo state,
515            Point2D source, boolean useAnchor) {
516        // delegate 'state' and 'source' argument checks...
517        XYPlot subplot = findSubplot(state, source);
518        if (subplot != null) {
519            subplot.zoomRangeAxes(factor, state, source, useAnchor);
520        } else {
521            // if the source point doesn't fall within a subplot, we do the
522            // zoom on all subplots...
523            for (XYPlot p : this.subplots) {
524                p.zoomRangeAxes(factor, state, source, useAnchor);
525            }
526        }
527    }
528
529    /**
530     * Zooms in on the range axes.
531     *
532     * @param lowerPercent  the lower bound.
533     * @param upperPercent  the upper bound.
534     * @param info  the plot rendering info ({@code null} not permitted).
535     * @param source  the source point ({@code null} not permitted).
536     */
537    @Override
538    public void zoomRangeAxes(double lowerPercent, double upperPercent,
539                              PlotRenderingInfo info, Point2D source) {
540        // delegate 'info' and 'source' argument checks...
541        XYPlot subplot = findSubplot(info, source);
542        if (subplot != null) {
543            subplot.zoomRangeAxes(lowerPercent, upperPercent, info, source);
544        } else {
545            // if the source point doesn't fall within a subplot, we do the
546            // zoom on all subplots...
547            for (XYPlot p : this.subplots) {
548                p.zoomRangeAxes(lowerPercent, upperPercent, info, source);
549            }
550        }
551    }
552
553    /**
554     * Pans all range axes by the specified percentage.
555     *
556     * @param panRange the distance to pan (as a percentage of the axis length).
557     * @param info  the plot info ({@code null} not permitted).
558     * @param source the source point where the pan action started.
559     */
560    @Override
561    public void panRangeAxes(double panRange, PlotRenderingInfo info,
562            Point2D source) {
563        XYPlot subplot = findSubplot(info, source);
564        if (subplot == null) {
565            return;
566        }
567        if (!subplot.isRangePannable()) {
568            return;
569        }
570        PlotRenderingInfo subplotInfo = info.getSubplotInfo(
571                info.getSubplotIndex(source));
572        if (subplotInfo == null) {
573            return;
574        }
575        for (int i = 0; i < subplot.getRangeAxisCount(); i++) {
576            ValueAxis rangeAxis = subplot.getRangeAxis(i);
577            if (rangeAxis != null) {
578                rangeAxis.pan(panRange);
579            }
580        }
581    }
582
583    /**
584     * Returns the subplot (if any) that contains the (x, y) point (specified
585     * in Java2D space).
586     *
587     * @param info  the chart rendering info ({@code null} not permitted).
588     * @param source  the source point ({@code null} not permitted).
589     *
590     * @return A subplot (possibly {@code null}).
591     */
592    public XYPlot findSubplot(PlotRenderingInfo info, Point2D source) {
593        Args.nullNotPermitted(info, "info");
594        Args.nullNotPermitted(source, "source");
595        XYPlot result = null;
596        int subplotIndex = info.getSubplotIndex(source);
597        if (subplotIndex >= 0) {
598            result =  (XYPlot) this.subplots.get(subplotIndex);
599        }
600        return result;
601    }
602
603    /**
604     * Sets the item renderer FOR ALL SUBPLOTS.  Registered listeners are
605     * notified that the plot has been modified.
606     * <P>
607     * Note: usually you will want to set the renderer independently for each
608     * subplot, which is NOT what this method does.
609     *
610     * @param renderer the new renderer.
611     */
612    @Override
613    public void setRenderer(XYItemRenderer renderer) {
614        super.setRenderer(renderer);  // not strictly necessary, since the
615                                      // renderer set for the
616                                      // parent plot is not used
617        for (XYPlot p : this.subplots) {
618            p.setRenderer(renderer);
619        }
620    }
621
622    /**
623     * Sets the fixed range axis space and sends a {@link PlotChangeEvent} to
624     * all registered listeners.
625     *
626     * @param space  the space ({@code null} permitted).
627     */
628    @Override
629    public void setFixedRangeAxisSpace(AxisSpace space) {
630        super.setFixedRangeAxisSpace(space);
631        setFixedRangeAxisSpaceForSubplots(space);
632        fireChangeEvent();
633    }
634
635    /**
636     * Sets the size (width or height, depending on the orientation of the
637     * plot) for the domain axis of each subplot.
638     *
639     * @param space  the space.
640     */
641    protected void setFixedRangeAxisSpaceForSubplots(AxisSpace space) {
642        for (XYPlot p : this.subplots) {
643            p.setFixedRangeAxisSpace(space, false);
644        }
645    }
646
647    /**
648     * Handles a 'click' on the plot by updating the anchor values.
649     *
650     * @param x  x-coordinate, where the click occurred.
651     * @param y  y-coordinate, where the click occurred.
652     * @param info  object containing information about the plot dimensions.
653     */
654    @Override
655    public void handleClick(int x, int y, PlotRenderingInfo info) {
656        Rectangle2D dataArea = info.getDataArea();
657        if (dataArea.contains(x, y)) {
658            for (int i = 0; i < this.subplots.size(); i++) {
659                XYPlot subplot = (XYPlot) this.subplots.get(i);
660                PlotRenderingInfo subplotInfo = info.getSubplotInfo(i);
661                subplot.handleClick(x, y, subplotInfo);
662            }
663        }
664    }
665
666    /**
667     * Receives notification of a change to the plot's dataset.
668     * <P>
669     * The axis ranges are updated if necessary.
670     *
671     * @param event  information about the event (not used here).
672     */
673    @Override
674    public void datasetChanged(DatasetChangeEvent event) {
675        super.datasetChanged(event);
676        if (this.subplots == null) {
677            return;  // this can happen during plot construction
678        }
679        XYDataset dataset = null;
680        if (event.getDataset() instanceof XYDataset) {
681            dataset = (XYDataset) event.getDataset();
682        }
683        for (XYPlot subplot : this.subplots) {
684            if (subplot.indexOf(dataset) >= 0) {
685                subplot.configureRangeAxes();
686            }
687        }
688    }
689
690    /**
691     * Receives a {@link PlotChangeEvent} and responds by notifying all
692     * listeners.
693     *
694     * @param event  the event.
695     */
696    @Override
697    public void plotChanged(PlotChangeEvent event) {
698        notifyListeners(event);
699    }
700
701    /**
702     * Tests this plot for equality with another object.
703     *
704     * @param obj  the other object.
705     *
706     * @return {@code true} or {@code false}.
707     */
708    @Override
709    public boolean equals(Object obj) {
710        if (obj == this) {
711            return true;
712        }
713        if (!(obj instanceof CombinedDomainXYPlot)) {
714            return false;
715        }
716        CombinedDomainXYPlot that = (CombinedDomainXYPlot) obj;
717        if (this.gap != that.gap) {
718            return false;
719        }
720        if (!Objects.equals(this.subplots, that.subplots)) {
721            return false;
722        }
723        return super.equals(obj);
724    }
725
726    /**
727     * Returns a clone of the annotation.
728     *
729     * @return A clone.
730     *
731     * @throws CloneNotSupportedException  this class will not throw this
732     *         exception, but subclasses (if any) might.
733     */
734    @Override
735    public Object clone() throws CloneNotSupportedException {
736
737        CombinedDomainXYPlot<S> result = (CombinedDomainXYPlot) super.clone();
738        result.subplots = CloneUtils.cloneList(this.subplots);
739        for (XYPlot<S> child : result.subplots) {
740            child.setParent(result);
741        }
742
743        // after setting up all the subplots, the shared domain axis may need
744        // reconfiguring
745        ValueAxis domainAxis = result.getDomainAxis();
746        if (domainAxis != null) {
747            domainAxis.configure();
748        }
749
750        return result;
751
752    }
753
754}