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.category;
034
035import java.awt.Color;
036import java.io.Serializable;
037
038import org.jfree.chart3d.Chart3DFactory;
039import org.jfree.chart3d.axis.CategoryAxis3D;
040import org.jfree.chart3d.axis.ValueAxis3D;
041import org.jfree.chart3d.data.DataUtils;
042import org.jfree.chart3d.data.KeyedValues3DItemKey;
043import org.jfree.chart3d.data.Range;
044import org.jfree.chart3d.data.Values3D;
045import org.jfree.chart3d.data.category.CategoryDataset3D;
046import org.jfree.chart3d.graphics3d.Dimension3D;
047import org.jfree.chart3d.graphics3d.Object3D;
048import org.jfree.chart3d.graphics3d.World;
049import org.jfree.chart3d.internal.ObjectUtils;
050import org.jfree.chart3d.label.ItemLabelPositioning;
051import org.jfree.chart3d.plot.CategoryPlot3D;
052import org.jfree.chart3d.renderer.Renderer3DChangeEvent;
053
054/**
055 * A renderer for creating 3D bar charts from a {@link CategoryDataset3D} (for 
056 * use with a {@link CategoryPlot3D}).  For example:
057 * <div>
058 * <img src="../../../../../../doc-files/BarChart3DDemo1.svg" alt="BarChart3DDemo1.svg" 
059 * width="500" height="359">
060 * </div>
061 * (refer to {@code BarChart3DDemo1.java} for the code to generate the
062 * above chart).
063 * <br><br>
064 * There is a factory method to create a chart using this renderer - see
065 * {@link Chart3DFactory#createBarChart(String, String, CategoryDataset3D, 
066 * String, String, String)}.
067 * <br><br>
068 * NOTE: This class is serializable, but the serialization format is subject 
069 * to change in future releases and should not be relied upon for persisting 
070 * instances of this class.
071 */
072@SuppressWarnings("serial")
073public class BarRenderer3D extends AbstractCategoryRenderer3D 
074                implements Serializable {
075
076    /** The base of the bars - defaults to 0.0. */
077    private double base;
078    
079    /** The bar width as a percentage of the column width. */
080    private double barXWidth;
081    
082    /** The bar width as a percentage of the row width. */
083    private double barZWidth;
084    
085    /** 
086     * The color source used to fetch the color for the base of bars where
087     * the actual base of the bar is *outside* of the current axis range 
088     * (that is, the bar is "cropped").  If this is {@code null}, then 
089     * the regular bar color is used.
090     */
091    private CategoryColorSource baseColorSource;
092    
093    /**
094     * The paint source used to fetch the color for the top of bars where
095     * the actual top of the bar is *outside* of the current axis range 
096     * (that is, the bar is "cropped"). If this is {@code null} then the 
097     * bar top is always drawn using the series paint.
098     */
099    private CategoryColorSource topColorSource;
100        
101    /**
102     * Creates a new renderer with default attribute values.
103     */
104    public BarRenderer3D() {
105        this.base = 0.0;
106        this.barXWidth = 0.8;
107        this.barZWidth = 0.5;
108        this.baseColorSource = new StandardCategoryColorSource(Color.WHITE);
109        this.topColorSource = new StandardCategoryColorSource(Color.BLACK);
110    }
111    
112    /**
113     * Returns the base value for the bars.  The default value 
114     * is {@code 0.0}.
115     * 
116     * @return The base value for the bars.
117     * 
118     * @see #setBase(double) 
119     */
120    public double getBase() {
121        return this.base;
122    }
123    
124    /**
125     * Sets the base value for the bars and fires a 
126     * {@link org.jfree.chart3d.renderer.Renderer3DChangeEvent}.
127     * 
128     * @param base  the new base value.
129     * 
130     * @see #getBase() 
131     */
132    public void setBase(double base) {
133        this.base = base;
134        fireChangeEvent(true);
135    }
136    
137    /**
138     * Returns the bar width as a percentage of the column width.
139     * The default value is {@code 0.8} (the total width of each column
140     * in world units is {@code 1.0}, so the default leaves a small gap
141     * between each bar).
142     * 
143     * @return The bar width (in world units). 
144     */
145    public double getBarXWidth() {
146        return this.barXWidth;
147    }
148    
149    /**
150     * Sets the the bar width as a percentage of the column width and
151     * fires a {@link Renderer3DChangeEvent}.
152     * 
153     * @param barXWidth  the new width.
154     */
155    public void setBarXWidth(double barXWidth) {
156        this.barXWidth = barXWidth;
157        fireChangeEvent(true);
158    }
159
160    /**
161     * Returns the bar width as a percentage of the row width.
162     * The default value is {@code 0.8}.
163     * 
164     * @return The bar width. 
165     */
166    public double getBarZWidth() {
167        return this.barZWidth;
168    }
169    
170    /**
171     * Sets the the bar width as a percentage of the row width and
172     * fires a {@link org.jfree.chart3d.renderer.Renderer3DChangeEvent}.
173     * 
174     * @param barZWidth  the new width.
175     */
176    public void setBarZWidth(double barZWidth) {
177        this.barZWidth = barZWidth;
178        fireChangeEvent(true);
179    }
180    
181    /**
182     * Returns the object used to fetch the color for the base of bars
183     * where the base of the bar is "cropped" (on account of the base value
184     * falling outside of the bounds of the y-axis).  This is used to give a
185     * visual indication to the end-user that the bar on display is cropped.
186     * If this paint source is {@code null}, the regular series color
187     * will be used for the top of the bars.
188     * 
189     * @return A paint source (possibly {@code null}).
190     */
191    public CategoryColorSource getBaseColorSource() {
192        return this.baseColorSource;
193    }
194    
195    /**
196     * Sets the object that determines the color to use for the base of bars
197     * where the base value falls outside the axis range, and sends a
198     * {@link Renderer3DChangeEvent} to all registered listeners.  If you set 
199     * this to {@code null}, the regular series color will be used to draw
200     * the base of the bar, but it will be harder for the end-user to know that 
201     * only a section of the bar is visible in the chart.  Note that the 
202     * default base paint source returns {@code Color.WHITE} always.
203     * 
204     * @param source  the source ({@code null} permitted).
205     * 
206     * @see #getBaseColorSource() 
207     * @see #getTopColorSource()
208     */
209    public void setBaseColorSource(CategoryColorSource source) {
210        this.baseColorSource = source;
211        fireChangeEvent(true);
212    }
213    
214    /**
215     * Returns the object used to fetch the color for the top of bars
216     * where the top of the bar is "cropped" (on account of the data value
217     * falling outside of the bounds of the y-axis).  This is used to give a
218     * visual indication to the end-user that the bar on display is cropped.
219     * If this paint source is {@code null}, the regular series color
220     * will be used for the top of the bars.
221     * 
222     * @return A paint source (possibly {@code null}).
223     */
224    public CategoryColorSource getTopColorSource() {
225        return this.topColorSource;
226    }
227    
228    /**
229     * Sets the object used to fetch the color for the top of bars where the 
230     * top of the bar is "cropped", and sends a {@link Renderer3DChangeEvent}
231     * to all registered listeners.
232     * 
233     * @param source  the source ({@code null} permitted).
234     * 
235     * @see #getTopColorSource() 
236     * @see #getBaseColorSource() 
237     */
238    public void setTopColorSource(CategoryColorSource source) {
239        this.topColorSource = source;
240        fireChangeEvent(true);
241    }
242
243    /**
244     * Returns the range of values that will be required on the value axis
245     * to see all the data from the dataset.  We override the method to 
246     * include in the range the base value for the bars.
247     * 
248     * @param data  the data ({@code null} not permitted).
249     * 
250     * @return The range (possibly {@code null}) 
251     */
252    @Override
253    public Range findValueRange(Values3D<? extends Number> data) {
254        return DataUtils.findValueRange(data, this.base);
255    }
256
257    /**
258     * Constructs and places one item from the specified dataset into the given 
259     * world.  This method will be called by the {@link CategoryPlot3D} class
260     * while iterating over the items in the dataset.
261     * 
262     * @param dataset  the dataset ({@code null} not permitted).
263     * @param series  the series index.
264     * @param row  the row index.
265     * @param column  the column index.
266     * @param world  the world ({@code null} not permitted).
267     * @param dimensions  the plot dimensions ({@code null} not permitted).
268     * @param xOffset  the x-offset.
269     * @param yOffset  the y-offset.
270     * @param zOffset  the z-offset.
271     */
272    @Override
273    public void composeItem(CategoryDataset3D dataset, int series, int row, 
274            int column, World world, Dimension3D dimensions, 
275            double xOffset, double yOffset, double zOffset) {
276        
277        double value = dataset.getDoubleValue(series, row, column);
278        if (Double.isNaN(value)) {
279            return;
280        }
281        // delegate to a separate method that is reused by the 
282        // StackedBarRenderer3D subclass...
283        composeItem(value, this.base, dataset, series, row, column, world, 
284                dimensions, xOffset, yOffset, zOffset);
285    }
286    
287    /**
288     * Performs the actual work of composing a bar to represent one item in the
289     * dataset.  This method is reused by the {@link StackedBarRenderer3D}
290     * subclass.
291     * 
292     * @param value  the data value (top of the bar).
293     * @param barBase  the base value for the bar.
294     * @param dataset  the dataset.
295     * @param series  the series index.
296     * @param row  the row index.
297     * @param column  the column index.
298     * @param world  the world.
299     * @param dimensions  the plot dimensions.
300     * @param xOffset  the x-offset.
301     * @param yOffset  the y-offset.
302     * @param zOffset  the z-offset.
303     */
304    @SuppressWarnings("unchecked")
305    protected void composeItem(double value, double barBase, 
306            CategoryDataset3D dataset, int series, int row, int column,
307            World world, Dimension3D dimensions, double xOffset, 
308            double yOffset, double zOffset) {
309
310        Comparable<?> seriesKey = dataset.getSeriesKey(series);
311        Comparable<?> rowKey = dataset.getRowKey(row);
312        Comparable<?> columnKey = dataset.getColumnKey(column);
313        
314        double vlow = Math.min(barBase, value);
315        double vhigh = Math.max(barBase, value);
316
317        CategoryPlot3D plot = getPlot();
318        CategoryAxis3D rowAxis = plot.getRowAxis();
319        CategoryAxis3D columnAxis = plot.getColumnAxis();
320        ValueAxis3D valueAxis = plot.getValueAxis();
321        Range range = valueAxis.getRange();
322        if (!range.intersects(vlow, vhigh)) {
323            return; // the bar is not visible for the given axis range
324        }
325        
326        double vbase = range.peggedValue(vlow);
327        double vtop = range.peggedValue(vhigh);
328        boolean inverted = barBase > value;
329        
330        double rowValue = rowAxis.getCategoryValue(rowKey);
331        double columnValue = columnAxis.getCategoryValue(columnKey);
332
333        double width = dimensions.getWidth();
334        double height = dimensions.getHeight();
335        double depth = dimensions.getDepth();
336        double xx = columnAxis.translateToWorld(columnValue, width) + xOffset;
337        double yy = valueAxis.translateToWorld(vtop, height) + yOffset;
338        double zz = rowAxis.translateToWorld(rowValue, depth) + zOffset;
339
340        double xw = this.barXWidth * columnAxis.getCategoryWidth();
341        double zw = this.barZWidth * rowAxis.getCategoryWidth();
342        double xxw = columnAxis.translateToWorld(xw, width);
343        double xzw = rowAxis.translateToWorld(zw, depth);
344        double basew = valueAxis.translateToWorld(vbase, height) + yOffset;
345    
346        Color color = getColorSource().getColor(series, row, column);
347        Color baseColor = null;
348        if (this.baseColorSource != null && !range.contains(this.base)) {
349            baseColor = this.baseColorSource.getColor(series, row, column);
350        }
351        if (baseColor == null) {
352            baseColor = color;
353        }
354
355        Color topColor = null;
356        if (this.topColorSource != null && !range.contains(value)) {
357            topColor = this.topColorSource.getColor(series, row, column);
358        }
359        if (topColor == null) {
360            topColor = color;
361        }
362        Object3D bar = Object3D.createBar(xxw, xzw, xx, yy, zz, basew, 
363                color, baseColor, topColor, inverted);
364        KeyedValues3DItemKey itemKey = new KeyedValues3DItemKey(seriesKey, 
365                rowKey, columnKey);
366        bar.setProperty(Object3D.ITEM_KEY, itemKey);
367        world.add(bar);
368        drawItemLabels(world, dataset, itemKey, xx, yy, zz, basew, inverted);   
369    }
370
371    /**
372     * Draws the item labels.
373     * 
374     * @param world  the world.
375     * @param dataset  the dataset.
376     * @param itemKey  the item key.
377     * @param xw  the x-coordinate.
378     * @param yw  the y-coordinate.
379     * @param zw  the z-coordinate.
380     * @param basew  the base coordinate.
381     * @param inverted  is the y-axis inverted?
382     */
383    protected void drawItemLabels(World world, CategoryDataset3D dataset, 
384            KeyedValues3DItemKey itemKey, double xw, double yw, double zw, 
385            double basew, boolean inverted) {
386        ItemLabelPositioning positioning = getItemLabelPositioning();
387        if (getItemLabelGenerator() == null) {
388            return;
389        }
390        String label = getItemLabelGenerator().generateItemLabel(dataset, 
391                itemKey.getSeriesKey(), itemKey.getRowKey(), 
392                itemKey.getColumnKey());
393        if (label != null) {
394            Dimension3D dimensions = getPlot().getDimensions();
395            double dx = getItemLabelOffsets().getDX();
396            double dy = getItemLabelOffsets().getDY() * dimensions.getHeight();
397            double dz = getItemLabelOffsets().getDZ() * getBarZWidth();
398            double yy = yw;
399            if (inverted) {
400                yy = basew;
401                dy = -dy;
402            }
403            if (positioning.equals(ItemLabelPositioning.CENTRAL)) {
404                Object3D labelObj = Object3D.createLabelObject(label, 
405                        getItemLabelFont(), getItemLabelColor(), 
406                        getItemLabelBackgroundColor(), xw + dx, yy + dy, zw, 
407                        false, true);
408                labelObj.setProperty(Object3D.ITEM_KEY, itemKey);
409                world.add(labelObj);
410            } else if (positioning.equals(
411                    ItemLabelPositioning.FRONT_AND_BACK)) {
412                Object3D labelObj1 = Object3D.createLabelObject(label, 
413                        getItemLabelFont(), getItemLabelColor(), 
414                        getItemLabelBackgroundColor(), xw + dx, yy + dy, 
415                        zw + dz, false, false);
416                labelObj1.setProperty(Object3D.ITEM_KEY, itemKey);
417                world.add(labelObj1);
418                Object3D labelObj2 = Object3D.createLabelObject(label, 
419                        getItemLabelFont(), getItemLabelColor(), 
420                        getItemLabelBackgroundColor(), xw + dx, yy + dy, 
421                        zw - dz, true, false);
422                labelObj1.setProperty(Object3D.ITEM_KEY, itemKey);
423                world.add(labelObj2);
424            }
425        }        
426    }
427    
428    /**
429     * Tests this renderer for equality with an arbitrary object.
430     * 
431     * @param obj  the object ({@code null} permitted).
432     * 
433     * @return A boolean. 
434     */
435    @Override
436    public boolean equals(Object obj) {
437        if (obj == this) {
438            return true;
439        }
440        if (!(obj instanceof BarRenderer3D)) {
441            return false;
442        }
443        BarRenderer3D that = (BarRenderer3D) obj;
444        if (this.base != that.base) {
445            return false;
446        }
447        if (this.barXWidth != that.barXWidth) {
448            return false;
449        }
450        if (this.barZWidth != that.barZWidth) {
451            return false;
452        }
453        if (!ObjectUtils.equals(this.baseColorSource, that.baseColorSource)) {
454            return false;
455        }
456        if (!ObjectUtils.equals(this.topColorSource, that.topColorSource)) {
457            return false;
458        }
459        return super.equals(obj);
460    }
461}