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