    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}