    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.xyz;
    034
    035import java.awt.Color;
    036import java.io.Serializable;
    037
    038import com.orsoncharts.axis.Axis3D;
    039import com.orsoncharts.Range;
    040import com.orsoncharts.data.DataUtils;
    041import com.orsoncharts.data.xyz.XYZDataset;
    042import com.orsoncharts.plot.XYZPlot;
    043import com.orsoncharts.graphics3d.Dimension3D;
    044import com.orsoncharts.graphics3d.Object3D;
    045import com.orsoncharts.graphics3d.World;
    046import com.orsoncharts.renderer.Renderer3DChangeEvent;
    047import com.orsoncharts.util.ObjectUtils;
    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 * <object id="ABC" data="../../../../doc-files/XYZBarChart3DDemo1.svg"
    054 * type="image/svg+xml" width="500" height="359"></object>
    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}