001/* ===========================================================
002 * Orson Charts : a 3D chart library for the Java(tm) platform
003 * ===========================================================
004 * 
005 * (C)opyright 2013-2022, by David Gilbert.  All rights reserved.
006 * 
007 * https://github.com/jfree/orson-charts
008 * 
009 * This program is free software: you can redistribute it and/or modify
010 * it under the terms of the GNU General Public License as published by
011 * the Free Software Foundation, either version 3 of the License, or
012 * (at your option) any later version.
013 *
014 * This program is distributed in the hope that it will be useful,
015 * but WITHOUT ANY WARRANTY; without even the implied warranty of
016 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
017 * GNU General Public License for more details.
018 *
019 * You should have received a copy of the GNU General Public License
020 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
021 * 
022 * [Oracle and Java are registered trademarks of Oracle and/or its affiliates. 
023 * Other names may be trademarks of their respective owners.]
024 * 
025 * If you do not wish to be bound by the terms of the GPL, an alternative
026 * commercial license can be purchased.  For details, please see visit the
027 * Orson Charts home page:
028 * 
029 * http://www.object-refinery.com/orsoncharts/index.html
030 * 
031 */
032
033package org.jfree.chart3d.renderer.xyz;
034
035import java.awt.Color;
036import java.io.Serializable;
037
038import org.jfree.chart3d.axis.Axis3D;
039import org.jfree.chart3d.data.DataUtils;
040import org.jfree.chart3d.data.Range;
041import org.jfree.chart3d.data.xyz.XYZDataset;
042import org.jfree.chart3d.graphics3d.Dimension3D;
043import org.jfree.chart3d.graphics3d.Object3D;
044import org.jfree.chart3d.graphics3d.World;
045import org.jfree.chart3d.internal.ObjectUtils;
046import org.jfree.chart3d.plot.XYZPlot;
047import org.jfree.chart3d.renderer.Renderer3DChangeEvent;
048
049/**
050 * A renderer that draws 3D bars on an {@link XYZPlot} using data from an
051 * {@link XYZDataset}.  Here is a sample:
052 * <div>
053 * <img src="../../../../../../doc-files/XYZBarChart3DDemo1.svg"  
054 * alt="XYZBarChart3DDemo1.svg" width="500" height="359">
055 * </div>
056 * (refer to {@code XYZBarChart3DDemo1.java} for the code to generate 
057 * the above chart).
058 * <br><br>
059 * NOTE: This class is serializable, but the serialization format is subject 
060 * to change in future releases and should not be relied upon for persisting 
061 * instances of this class.
062 */
063@SuppressWarnings("serial")
064public class BarXYZRenderer extends AbstractXYZRenderer implements XYZRenderer,
065        Serializable {
066 
067    /** The base value (normally 0.0, but can be modified). */
068    private double base;
069    
070    /** The width of the bars along the x-axis. */
071    private double barXWidth;
072    
073    /** The width of the bars along the z-axis. */
074    private double barZWidth;
075
076    /** 
077     * The color source used to fetch the color for the base of bars where
078     * the actual base of the bar is *outside* of the current axis range 
079     * (that is, the bar is "cropped").  If this is {@code null}, then 
080     * the regular bar color is used.
081     */
082    private XYZColorSource baseColorSource;
083    
084    /**
085     * The color source used to fetch the color for the top of bars where
086     * the actual top of the bar is *outside* of the current axis range 
087     * (that is, the bar is "cropped"). If this is {@code null} then the 
088     * bar top is always drawn using the series paint.
089     */
090    private XYZColorSource topColorSource;
091
092    /**
093     * Creates a new default instance.
094     */
095    public BarXYZRenderer() {
096        this.base = 0.0;
097        this.barXWidth = 0.8;
098        this.barZWidth = 0.8;
099        this.baseColorSource = new StandardXYZColorSource(Color.WHITE);
100        this.topColorSource = new StandardXYZColorSource(Color.BLACK);
101        
102    }
103    
104    /** 
105     * Returns the value for the base of the bars.  The default is 
106     * {@code 0.0}.
107     * 
108     * @return The value for the base of the bars.
109     */
110    public double getBase() {
111        return this.base;
112    }
113    
114    /**
115     * Sets the base value for the bars and sends a 
116     * {@link Renderer3DChangeEvent} to all registered listeners.
117     * 
118     * @param base  the base. 
119     */
120    public void setBase(double base) {
121        this.base = base;
122        fireChangeEvent(true);
123    }
124    
125    /**
126     * Returns the width of the bars in the direction of the x-axis, in the
127     * units of the x-axis.  The default value is {@code 0.8}.
128     * 
129     * @return The width of the bars. 
130     */
131    public double getBarXWidth() {
132        return this.barXWidth;
133    }
134    
135    /**
136     * Sets the width of the bars in the direction of the x-axis and sends a
137     * {@link Renderer3DChangeEvent} to all registered listeners.
138     * 
139     * @param width  the width. 
140     */
141    public void setBarXWidth(double width) {
142        this.barXWidth = width;
143        fireChangeEvent(true);
144    }
145
146    /**
147     * Returns the width of the bars in the direction of the z-axis, in the
148     * units of the z-axis.  The default value is {@code 0.8}.
149     * 
150     * @return The width of the bars. 
151     */
152    public double getBarZWidth() {
153        return this.barZWidth;
154    }
155    
156    /**
157     * Sets the width of the bars in the direction of the z-axis and sends a
158     * {@link Renderer3DChangeEvent} to all registered listeners.
159     * 
160     * @param width  the width. 
161     */
162    public void setBarZWidth(double width) {
163        this.barZWidth = width;
164        fireChangeEvent(true);
165    }
166 
167    /**
168     * Returns the object used to fetch the color for the base of bars
169     * where the base of the bar is "cropped" (on account of the base value
170     * falling outside of the bounds of the y-axis).  This is used to give a
171     * visual indication to the end-user that the bar on display is cropped.
172     * If this paint source is {@code null}, the regular series color
173     * will be used for the top of the bars.
174     * 
175     * @return A paint source (possibly {@code null}).
176     */
177    public XYZColorSource getBaseColorSource() {
178        return this.baseColorSource;
179    }
180    
181    /**
182     * Sets the object that determines the color to use for the base of bars
183     * where the base value falls outside the axis range, and sends a
184     * {@link Renderer3DChangeEvent} to all registered listeners.  If you set 
185     * this to {@code null}, the regular series color will be used to draw
186     * the base of the bar, but it will be harder for the end-user to know that 
187     * only a section of the bar is visible in the chart.  Note that the 
188     * default base paint source returns {@code Color.WHITE} always.
189     * 
190     * @param source  the source ({@code null} permitted).
191     * 
192     * @see #getBaseColorSource() 
193     * @see #getTopColorSource()
194     */
195    public void setBaseColorSource(XYZColorSource source) {
196        this.baseColorSource = source;
197        fireChangeEvent(true);
198    }
199    
200    /**
201     * Returns the object used to fetch the color for the top of bars
202     * where the top of the bar is "cropped" (on account of the data value
203     * falling outside of the bounds of the y-axis).  This is used to give a
204     * visual indication to the end-user that the bar on display is cropped.
205     * If this paint source is {@code null}, the regular series color
206     * will be used for the top of the bars.
207     * 
208     * @return A paint source (possibly {@code null}).
209     */
210    public XYZColorSource getTopColorSource() {
211        return this.topColorSource;
212    }
213    
214    /**
215     * Sets the object used to fetch the color for the top of bars where the 
216     * top of the bar is "cropped", and sends a {@link Renderer3DChangeEvent}
217     * to all registered listeners.
218     * 
219     * @param source  the source ({@code null} permitted).
220     * 
221     * @see #getTopColorSource() 
222     * @see #getBaseColorSource() 
223     */
224    public void setTopColorSource(XYZColorSource source) {
225        this.topColorSource = source;
226        fireChangeEvent(true);
227    }
228
229    /**
230     * Returns the range that needs to be set on the x-axis in order for this
231     * renderer to be able to display all the data in the supplied dataset.
232     * 
233     * @param dataset  the dataset ({@code null} not permitted).
234     * 
235     * @return The range ({@code null} if there is no data in the dataset). 
236     */
237    @Override
238    public Range findXRange(XYZDataset dataset) {
239        // delegate argument check...
240        Range xRange = DataUtils.findXRange(dataset);
241        if (xRange == null) {
242            return null;
243        }
244        double delta = this.barXWidth / 2.0;
245        return new Range(xRange.getMin() - delta, xRange.getMax() + delta);
246    }
247
248    /**
249     * Returns the range to use for the y-axis to ensure that all data values
250     * are visible on the chart.  This method is overridden to ensure that the
251     * base value is included.
252     * 
253     * @param dataset  the dataset ({@code null} not permitted).
254     * 
255     * @return The range ({@code null} when there is no data). 
256     */
257    @Override
258    public Range findYRange(XYZDataset dataset) {
259        return DataUtils.findYRange(dataset, this.base);
260    }
261    
262    /**
263     * Returns the range to use for the z-axis to ensure that all data values
264     * are visible on the chart.  This method is overridden to account for the
265     * bar widths.
266     * 
267     * @param dataset  the dataset ({@code null} not permitted).
268     * 
269     * @return The range ({@code null} when there is no data). 
270     */
271    @Override
272    public Range findZRange(XYZDataset dataset) {
273        Range zRange = DataUtils.findZRange(dataset);
274        if (zRange == null) {
275            return null;
276        }
277        double delta = this.barZWidth / 2.0;
278        return new Range(zRange.getMin() - delta, zRange.getMax() + delta);
279    }
280
281    /**
282     * Adds a single bar representing one item from the dataset.
283     * 
284     * @param dataset  the dataset.
285     * @param series  the series index.
286     * @param item  the item index.
287     * @param world  the world used to model the 3D chart.
288     * @param dimensions  the plot dimensions in 3D.
289     * @param xOffset  the x-offset.
290     * @param yOffset  the y-offset.
291     * @param zOffset  the z-offset.
292     */
293    @Override
294    public void composeItem(XYZDataset dataset, int series, int item, 
295            World world, Dimension3D dimensions, double xOffset, double yOffset, 
296            double zOffset) {
297
298        XYZPlot plot = getPlot();
299        Axis3D xAxis = plot.getXAxis();
300        Axis3D yAxis = plot.getYAxis();
301        Axis3D zAxis = plot.getZAxis();
302        double x = dataset.getX(series, item);
303        double y = dataset.getY(series, item);
304        double z = dataset.getZ(series, item);
305        double xdelta = this.barXWidth / 2.0;
306        double zdelta = this.barZWidth / 2.0;
307
308        double x0 = xAxis.getRange().peggedValue(x - xdelta);
309        double x1 = xAxis.getRange().peggedValue(x + xdelta);
310        double z0 = zAxis.getRange().peggedValue(z - zdelta);
311        double z1 = zAxis.getRange().peggedValue(z + zdelta);
312        if ((x1 <= x0) || (z1 <= z0)) {
313            return;
314        }
315        double ylow = Math.min(this.base, y);
316        double yhigh = Math.max(this.base, y);
317        Range range = yAxis.getRange();
318        if (!range.intersects(ylow, yhigh)) {
319            return; // the bar is not visible for the given axis range
320        }
321        double ybase = range.peggedValue(ylow);
322        double ytop = range.peggedValue(yhigh);
323        boolean inverted = this.base > y;
324        
325        double wx0 = xAxis.translateToWorld(x0, dimensions.getWidth());
326        double wx1 = xAxis.translateToWorld(x1, dimensions.getWidth());
327        double wy0 = yAxis.translateToWorld(ybase, dimensions.getHeight());
328        double wy1 = yAxis.translateToWorld(ytop, dimensions.getHeight());
329        double wz0 = zAxis.translateToWorld(z0, dimensions.getDepth());
330        double wz1 = zAxis.translateToWorld(z1, dimensions.getDepth());
331    
332        Color color = getColorSource().getColor(series, item);
333        Color baseColor = null;
334        if (this.baseColorSource != null && !range.contains(this.base)) {
335            baseColor = this.baseColorSource.getColor(series, item);
336        }
337        if (baseColor == null) {
338            baseColor = color;
339        }
340
341        Color topColor = null;
342        if (this.topColorSource != null && !range.contains(y)) {
343            topColor = this.topColorSource.getColor(series, item);
344        }
345        if (topColor == null) {
346            topColor = color;
347        }
348
349        Object3D bar = Object3D.createBar(wx1 - wx0, wz1 - wz0, 
350                ((wx0 + wx1) / 2.0) + xOffset, wy1 + yOffset, 
351                ((wz0 + wz1) / 2.0) + zOffset, wy0 + yOffset, color, 
352                baseColor, topColor, inverted);
353        world.add(bar);
354    }
355
356    /**
357     * Tests this renderer for equality with an arbitrary object.
358     * 
359     * @param obj  the object ({@code null} permitted).
360     * 
361     * @return A boolean. 
362     */
363    @Override
364    public boolean equals(Object obj) {
365        if (obj == this) {
366            return true;
367        }
368        if (!(obj instanceof BarXYZRenderer)) {
369            return false;
370        }
371        BarXYZRenderer that = (BarXYZRenderer) obj;
372        if (this.base != that.base) {
373            return false;
374        }
375        if (this.barXWidth != that.barXWidth) {
376            return false;
377        }
378        if (this.barZWidth != that.barZWidth) {
379            return false;
380        }
381        if (!ObjectUtils.equals(this.baseColorSource, that.baseColorSource)) {
382            return false;
383        }
384        if (!ObjectUtils.equals(this.topColorSource, that.topColorSource)) {
385            return false;
386        }
387        return super.equals(obj);
388    }
389
390}