    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.axis;
    034
    035import java.awt.BasicStroke;
    036import java.awt.Color;
    037import java.awt.Graphics2D;
    038import java.awt.Paint;
    039import java.awt.Shape;
    040import java.awt.Stroke;
    041import java.awt.font.LineMetrics;
    042import java.awt.geom.Line2D;
    043import java.awt.geom.Point2D;
    044import java.io.IOException;
    045import java.io.ObjectInputStream;
    046import java.io.ObjectOutputStream;
    047import java.io.Serializable;
    048import java.util.ArrayList;
    049import java.util.LinkedHashMap;
    050import java.util.List;
    051import java.util.HashMap;
    052import java.util.Map;
    053
    054import com.orsoncharts.Chart3DHints;
    055import com.orsoncharts.ChartElementVisitor;
    056import com.orsoncharts.Range;
    057import com.orsoncharts.data.category.CategoryDataset3D;
    058import com.orsoncharts.graphics3d.RenderedElement;
    059import com.orsoncharts.graphics3d.RenderingInfo;
    060import com.orsoncharts.graphics3d.Utils2D;
    061import com.orsoncharts.interaction.InteractiveElementType;
    062import com.orsoncharts.label.CategoryLabelGenerator;
    063import com.orsoncharts.label.StandardCategoryLabelGenerator;
    064import com.orsoncharts.marker.CategoryMarker;
    065import com.orsoncharts.marker.CategoryMarkerType;
    066import com.orsoncharts.marker.Marker;
    067import com.orsoncharts.marker.MarkerData;
    068import com.orsoncharts.plot.CategoryPlot3D;
    069import com.orsoncharts.renderer.category.AreaRenderer3D;
    070import com.orsoncharts.util.ArgChecks;
    071import com.orsoncharts.util.ObjectUtils;
    072import com.orsoncharts.util.SerialUtils;
    073import com.orsoncharts.util.TextAnchor;
    074import com.orsoncharts.util.TextUtils;
    075
    076/**
    077 * An axis that displays categories.
    078 * <br><br>
    079 * NOTE: This class is serializable, but the serialization format is subject
    080 * to change in future releases and should not be relied upon for persisting
    081 * instances of this class.
    082 */
    083@SuppressWarnings("serial")
    084public class StandardCategoryAxis3D extends AbstractAxis3D
    085        implements CategoryAxis3D, Serializable {
    086
    087    /** The categories. */
    088    private List<Comparable<?>> categories;
    089
    090    /**
    091     * The axis range (never {@code null}).
    092     */
    093    private Range range;
    094
    095    private boolean inverted;
    096
    097    /** The percentage margin to leave at the lower end of the axis. */
    098    private double lowerMargin;
    099
    100    /** The percentage margin to leave at the upper end of the axis. */
    101    private double upperMargin;
    102
    103    /**
    104     * Hide half of the first category?  This brings the category label
    105     * closer to the beginning of the axis.  It is useful if the renderer
    106     * doesn't make full use of the category space for the first item.
    107     */
    108    private boolean firstCategoryHalfWidth = false;
    109
    110    /**
    111     * Hide half of the last category?  This brings the category label
    112     * closer to the end of the axis.  It is useful if the renderer
    113     * doesn't make full use of the category space for the last item.
    114     */
    115    private boolean lastCategoryHalfWidth = false;
    116
    117    /**
    118     * The tick mark length (in Java2D units).  When this is 0.0, no tick
    119     * marks will be drawn.
    120     */
    121    private double tickMarkLength;
    122
    123    /** The tick mark stroke (never {@code null}). */
    124    private transient Stroke tickMarkStroke;
    125
    126    /** The tick mark paint (never {@code null}). */
    127    private transient Paint tickMarkPaint;
    128
    129    /** The tick label generator. */
    130    private CategoryLabelGenerator tickLabelGenerator;
    131
    132    /**
    133     * The tick label offset (in Java2D units).  This is the gap between the
    134     * tick marks and their associated labels.
    135     */
    136    private double tickLabelOffset;
    137
    138    /** The orientation for the tick labels. */
    139    private LabelOrientation tickLabelOrientation;
    140
    141    /**
    142     * The maximum number of offset levels to use for tick labels on the axis.
    143     */
    144    private int maxTickLabelLevels = 3;
    145
    146    /**
    147     * The tick label factor (used as a multiplier for the tick label width
    148     * when checking for overlapping labels).
    149     */
    150    private double tickLabelFactor = 1.2;
    151
    152    /**
    153     * The markers for the axis (this may be empty, but not {@code null}).
    154     */
    155    private Map<String, CategoryMarker> markers;
    156
    157    /** A flag to indicate that this axis has been configured as a row axis. */
    158    private boolean isRowAxis;
    159
    160    /**
    161     * A flag to indicate that this axis has been configured as a column
    162     * axis.
    163     */
    164    private boolean isColumnAxis;
    165
    166    /**
    167     * Default constructor.
    168     */
    169    public StandardCategoryAxis3D() {
    170        this(null);
    171    }
    172
    173    /**
    174     * Creates a new axis with the specified label.
    175     *
    176     * @param label  the axis label ({@code null} permitted).
    177     */
    178    public StandardCategoryAxis3D(String label) {
    179        super(label);
    180        this.categories = new ArrayList<Comparable<?>>();
    181        this.range = new Range(0.0, 1.0);
    182        this.lowerMargin = 0.05;
    183        this.upperMargin = 0.05;
    184        this.firstCategoryHalfWidth = false;
    185        this.lastCategoryHalfWidth = false;
    186        this.tickMarkLength = 3.0;
    187        this.tickMarkPaint = Color.GRAY;
    188        this.tickMarkStroke = new BasicStroke(0.5f);
    189        this.tickLabelGenerator = new StandardCategoryLabelGenerator();
    190        this.tickLabelOffset = 5.0;
    191        this.tickLabelOrientation = LabelOrientation.PARALLEL;
    192        this.tickLabelFactor = 1.4;
    193        this.maxTickLabelLevels = 3;
    194        this.markers = new LinkedHashMap<String, CategoryMarker>();
    195        this.isRowAxis = false;
    196        this.isColumnAxis = false;
    197    }
    198
    199    /**
    200     * Returns {@code true} if this axis has been configured as a
    201     * row axis for the plot that it belongs to, and {@code false}
    202     * otherwise.
    203     *
    204     * @return A boolean.
    205     *
    206     * @since 1.3
    207     */
    208    @Override
    209    public boolean isRowAxis() {
    210        return isRowAxis;
    211    }
    212
    213    /**
    214     * Returns {@code true} if this axis has been configured as a
    215     * column axis for the plot that it belongs to, and {@code false}
    216     * otherwise.
    217     *
    218     * @return A boolean.
    219     *
    220     * @since 1.3
    221     */
    222    @Override
    223    public boolean isColumnAxis() {
    224        return isColumnAxis;
    225    }
    226
    227    /**
    228     * Returns the range for the axis.  By convention, the category axes have
    229     * a range from 0.0 to 1.0.
    230     *
    231     * @return The range.
    232     */
    233    @Override
    234    public Range getRange() {
    235        return this.range;
    236    }
    237
    238    /**
    239     * Sets the range for the axis and sends an {@link Axis3DChangeEvent} to
    240     * all registered listeners.
    241     *
    242     * @param lowerBound  the lower bound.
    243     * @param upperBound  the upper bound.
    244     */
    245    @Override
    246    public void setRange(double lowerBound, double upperBound) {
    247        setRange(new Range(lowerBound, upperBound));
    248    }
    249
    250    /**
    251     * Sets the range for the axis and sends an {@link Axis3DChangeEvent} to
    252     * all registered listeners. Note that changing the range for the
    253     * category axis will have no visible effect.
    254     *
    255     * @param range  the range ({@code null} not permitted).
    256     */
    257    @Override
    258    public void setRange(Range range) {
    259        ArgChecks.nullNotPermitted(range, "range");
    260        this.range = range;
    261        fireChangeEvent(true);
    262    }
    263
    264    /**
    265     * Returns the margin to leave at the lower end of the axis, as a
    266     * percentage of the axis length.  The default is {@code 0.05} (five
    267     * percent).
    268     *
    269     * @return The lower margin.
    270     */
    271    public double getLowerMargin() {
    272        return this.lowerMargin;
    273    }
    274
    275    /**
    276     * Sets the margin to leave at the lower end of the axis and sends an
    277     * {@link Axis3DChangeEvent} to all registered listeners.
    278     *
    279     * @param margin  the margin.
    280     */
    281    public void setLowerMargin(double margin) {
    282        this.lowerMargin = margin;
    283        fireChangeEvent(true);
    284    }
    285
    286    /**
    287     * Returns the margin to leave at the upper end of the axis, as a
    288     * percentage of the axis length.  The default is {@code 0.05} (five
    289     * percent).
    290     *
    291     * @return The lower margin.
    292     */
    293    public double getUpperMargin() {
    294        return this.upperMargin;
    295    }
    296
    297    /**
    298     * Sets the margin to leave at the upper end of the axis and sends an
    299     * {@link Axis3DChangeEvent} to all registered listeners.
    300     *
    301     * @param margin  the margin.
    302     */
    303    public void setUpperMargin(double margin) {
    304        this.upperMargin = margin;
    305        fireChangeEvent(true);
    306    }
    307
    308    /**
    309     * Returns {@code true} if the first category on the axis should
    310     * occupy half the normal width, and {@code false} otherwise.
    311     *
    312     * @return A boolean.
    313     *
    314     * @see #setFirstCategoryHalfWidth(boolean)
    315     */
    316    public boolean isFirstCategoryHalfWidth() {
    317        return this.firstCategoryHalfWidth;
    318    }
    319
    320    /**
    321     * Sets the flag that controls whether the first category on the axis
    322     * occupies a full or half width, and sends an {@link Axis3DChangeEvent}
    323     * to all registered listeners.  There are some renderers where the
    324     * charts look better when half-widths are used (for example,
    325     * {@link AreaRenderer3D}).
    326     *
    327     * @param half  half width?
    328     *
    329     * @see #setLastCategoryHalfWidth(boolean)
    330     */
    331    public void setFirstCategoryHalfWidth(boolean half) {
    332        this.firstCategoryHalfWidth = half;
    333        fireChangeEvent(true);
    334    }
    335
    336    /**
    337     * Returns {@code true} if the last category on the axis should
    338     * occupy half the normal width, and {@code false} otherwise.
    339     *
    340     * @return A boolean.
    341     *
    342     * @see #setLastCategoryHalfWidth(boolean)
    343     */
    344    public boolean isLastCategoryHalfWidth() {
    345        return this.lastCategoryHalfWidth;
    346    }
    347
    348    /**
    349     * Sets the flag that controls whether the last category on the axis
    350     * occupies a full or half width, and sends an {@link Axis3DChangeEvent}
    351     * to all registered listeners.  There are some renderers where the
    352     * charts look better when half-widths are used (for example,
    353     * {@link AreaRenderer3D}).
    354     *
    355     * @param half  half width?
    356     *
    357     * @see #setFirstCategoryHalfWidth(boolean)
    358     */
    359    public void setLastCategoryHalfWidth(boolean half) {
    360        this.lastCategoryHalfWidth = half;
    361        fireChangeEvent(true);
    362    }
    363
    364    /**
    365     * Returns the tick mark length (in Java2D units).  The default value
    366     * is {@code 3.0}.
    367     *
    368     * @return The tick mark length.
    369     */
    370    public double getTickMarkLength() {
    371        return this.tickMarkLength;
    372    }
    373
    374    /**
    375     * Sets the tick mark length (in Java2D units) and sends an
    376     * {@link Axis3DChangeEvent} to all registered listeners.  You can set
    377     * the length to {@code 0.0} if you don't want any tick marks on the
    378     * axis.
    379     *
    380     * @param length  the length (in Java2D units).
    381     */
    382    public void setTickMarkLength(double length) {
    383        this.tickMarkLength = length;
    384        fireChangeEvent(false);
    385    }
    386
    387    /**
    388     * Returns the paint used to draw the tick marks, if they are visible.
    389     * The default value is {@code Color.GRAY}.
    390     *
    391     * @return The paint used to draw the tick marks (never {@code null}).
    392     */
    393    public Paint getTickMarkPaint() {
    394        return this.tickMarkPaint;
    395    }
    396
    397    /**
    398     * Sets the paint used to draw the tick marks and sends an
    399     * {@link Axis3DChangeEvent} to all registered listeners.
    400     *
    401     * @param paint  the paint ({@code null} not permitted).
    402     */
    403    public void setTickMarkPaint(Paint paint) {
    404        ArgChecks.nullNotPermitted(paint, "paint");
    405        this.tickMarkPaint = paint;
    406        fireChangeEvent(false);
    407    }
    408
    409    /**
    410     * Returns the stroke used to draw the tick marks, if they are visible.
    411     * The default value is {@code new BasicStroke(0.5f)}.
    412     *
    413     * @return The stroke used to draw the tick marks (never {@code null}).
    414     */
    415    public Stroke getTickMarkStroke() {
    416        return this.tickMarkStroke;
    417    }
    418
    419    /**
    420     * Sets the stroke used to draw the tick marks and sends an
    421     * {@link Axis3DChangeEvent} to all registered listeners.
    422     *
    423     * @param stroke  the stroke ({@code null} not permitted).
    424     */
    425    public void setTickMarkStroke(Stroke stroke) {
    426        ArgChecks.nullNotPermitted(stroke, "stroke");
    427        this.tickMarkStroke = stroke;
    428        fireChangeEvent(false);
    429    }
    430
    431    /**
    432     * Returns the tick label generator for the axis.  This is an object that
    433     * is responsible for creating the category labels on the axis.  You can
    434     * plug in your own instance to take full control over the generation
    435     * of category labels.
    436     *
    437     * @return The tick label generator for the axis (never {@code null}).
    438     *
    439     * @since 1.2
    440     */
    441    public CategoryLabelGenerator getTickLabelGenerator() {
    442        return this.tickLabelGenerator;
    443    }
    444
    445    /**
    446     * Sets the tick label generator for the axis and sends a change event to
    447     * all registered listeners.
    448     *
    449     * @param generator  the generator ({@code null} not permitted).
    450     *
    451     * @since 1.2
    452     */
    453    public void setTickLabelGenerator(CategoryLabelGenerator generator) {
    454        ArgChecks.nullNotPermitted(generator, "generator");
    455        this.tickLabelGenerator = generator;
    456        fireChangeEvent(false);
    457    }
    458
    459    /**
    460     * Returns the offset between the tick marks and the tick labels.  The
    461     * default value is {@code 5.0}.
    462     *
    463     * @return The offset between the tick marks and the tick labels (in Java2D
    464     *     units).
    465     */
    466    public double getTickLabelOffset() {
    467        return this.tickLabelOffset;
    468    }
    469
    470    /**
    471     * Sets the offset between the tick marks and the tick labels and sends
    472     * a {@link Axis3DChangeEvent} to all registered listeners.
    473     *
    474     * @param offset  the offset.
    475     */
    476    public void setTickLabelOffset(double offset) {
    477        this.tickLabelOffset = offset;
    478        fireChangeEvent(false);
    479    }
    480
    481    /**
    482     * Returns the orientation for the tick labels.  The default value is
    483     * {@link LabelOrientation#PARALLEL}.
    484     *
    485     * @return The orientation for the tick labels (never {@code null}).
    486     *
    487     * @since 1.2
    488     */
    489    public LabelOrientation getTickLabelOrientation() {
    490        return this.tickLabelOrientation;
    491    }
    492
    493    /**
    494     * Sets the orientation for the tick labels and sends a change event to
    495     * all registered listeners.
    496     *
    497     * @param orientation  the orientation ({@code null} not permitted).
    498     *
    499     * @since 1.2
    500     */
    501    public void setTickLabelOrientation(LabelOrientation orientation) {
    502        ArgChecks.nullNotPermitted(orientation, "orientation");
    503        this.tickLabelOrientation = orientation;
    504        fireChangeEvent(false);
    505    }
    506
    507    /**
    508     * Returns the maximum number of offset levels for the category labels on
    509     * the axis.  The default value is 3.
    510     *
    511     * @return The maximum number of offset levels.
    512     *
    513     * @since 1.2
    514     */
    515    public int getMaxTickLabelLevels() {
    516        return this.maxTickLabelLevels;
    517    }
    518
    519    /**
    520     * Sets the maximum number of offset levels for the category labels on the
    521     * axis and sends a change event to all registered listeners.
    522     *
    523     * @param levels  the maximum number of levels.
    524     *
    525     * @since 1.2
    526     */
    527    public void setMaxTickLabelLevels(int levels) {
    528        this.maxTickLabelLevels = levels;
    529        fireChangeEvent(false);
    530    }
    531
    532    /**
    533     * Returns the tick label factor.  The default value is {@code 1.4}.
    534     *
    535     * @return The tick label factor.
    536     *
    537     * @since 1.2
    538     */
    539    public double getTickLabelFactor() {
    540        return this.tickLabelFactor;
    541    }
    542
    543    /**
    544     * Sets the tick label factor and sends a change event to all registered
    545     * listeners.
    546     *
    547     * @param factor  the new factor (should be at least 1.0).
    548     *
    549     * @since 1.2
    550     */
    551    public void setTickLabelFactor(double factor) {
    552        this.tickLabelFactor = factor;
    553        fireChangeEvent(false);
    554    }
    555
    556    /**
    557     * Returns the marker with the specified key, if there is one.
    558     *
    559     * @param key  the key ({@code null} not permitted).
    560     *
    561     * @return The marker (possibly {@code null}).
    562     *
    563     * @since 1.2
    564     */
    565    @Override
    566    public CategoryMarker getMarker(String key) {
    567        return this.markers.get(key);
    568    }
    569
    570    /**
    571     * Sets the marker for the specified key and sends a change event to
    572     * all registered listeners.  If there is an existing marker it is replaced
    573     * (and the axis will no longer listen for change events on the previous
    574     * marker).
    575     *
    576     * @param key  the key that identifies the marker ({@code null} not
    577     *         permitted).
    578     * @param marker  the marker ({@code null} permitted).
    579     *
    580     * @since 1.2
    581     */
    582    public void setMarker(String key, CategoryMarker marker) {
    583        CategoryMarker existing = this.markers.get(key);
    584        if (existing != null) {
    585            existing.removeChangeListener(this);
    586        }
    587        this.markers.put(key, marker);
    588        if (marker != null) {
    589            marker.addChangeListener(this);
    590        }
    591        fireChangeEvent(false);
    592    }
    593
    594    /**
    595     * Returns a new map containing the markers that are assigned to this axis.
    596     *
    597     * @return A map.
    598     *
    599     * @since 1.2
    600     */
    601    public Map<String, CategoryMarker> getMarkers() {
    602        return new LinkedHashMap<String, CategoryMarker>(this.markers);
    603    }
    604
    605    /**
    606     * Returns the width of a single category in the units of the axis
    607     * range.
    608     *
    609     * @return The width of a single category.
    610     */
    611    @Override
    612    public double getCategoryWidth() {
    613        double length = this.range.getLength();
    614        double start = this.range.getMin() + (this.lowerMargin * length);
    615        double end = this.range.getMax() - (this.upperMargin * length);
    616        double available = (end - start);
    617        return available / this.categories.size();
    618    }
    619
    620    /**
    621     * Configures the axis to be used as a row axis for the specified
    622     * plot.  This method is for internal use, you should not call it directly.
    623     *
    624     * @param plot  the plot ({@code null} not permitted).
    625     */
    626    @Override
    627    public void configureAsRowAxis(CategoryPlot3D plot) {
    628        ArgChecks.nullNotPermitted(plot, "plot");
    629        this.categories = plot.getDataset().getRowKeys();
    630        this.isColumnAxis = false;
    631        this.isRowAxis = true;
    632    }
    633
    634    /**
    635     * Configures the axis to be used as a column axis for the specified
    636     * plot.  This method is for internal use, you won't normally need to call
    637     * it directly.
    638     *
    639     * @param plot  the plot ({@code null} not permitted).
    640     */
    641    @Override
    642    public void configureAsColumnAxis(CategoryPlot3D plot) {
    643        ArgChecks.nullNotPermitted(plot, "plot");
    644        this.categories = plot.getDataset().getColumnKeys();
    645        this.isColumnAxis = true;
    646        this.isRowAxis = false;
    647    }
    648
    649    /**
    650     * Returns the value for the specified category, or {@code Double.NaN}
    651     * if the category is not registered on the axis.
    652     *
    653     * @param category  the category ({@code null} not permitted).
    654     *
    655     * @return The value.
    656     */
    657    @Override
    658    public double getCategoryValue(Comparable<?> category) {
    659        int index = this.categories.indexOf(category);
    660        if (index < 0) {
    661            return Double.NaN;
    662        }
    663        double length = this.range.getLength();
    664        double start = this.range.getMin() + (this.lowerMargin * length);
    665        double end = this.range.getMax() - (this.upperMargin * length);
    666        double available = (end - start);
    667        double categoryCount = this.categories.size();
    668        if (categoryCount == 1) {
    669            return (start + end) / 2.0;
    670        }
    671        if (this.firstCategoryHalfWidth) {
    672            categoryCount -= 0.5;
    673        }
    674        if (this.lastCategoryHalfWidth) {
    675            categoryCount -= 0.5;
    676        }
    677        double categoryWidth = 0.0;
    678        if (categoryCount > 0.0) {
    679            categoryWidth = available / categoryCount;
    680        }
    681        double adj = this.firstCategoryHalfWidth ? 0.0 : 0.5;
    682        return start + (adj + index) * categoryWidth;
    683    }
    684
    685    /**
    686     * Translates a value on the axis to the equivalent coordinate in the
    687     * 3D world used to construct a model of the chart.
    688     *
    689     * @param value  the value along the axis.
    690     * @param length  the length of one side of the 3D box containing the model.
    691     *
    692     * @return A coordinate in 3D space.
    693     */
    694    @Override
    695    public double translateToWorld(double value, double length) {
    696        double p = getRange().percent(value, isInverted());
    697        return length * p;
    698    }
    699
    700    /**
    701     * Draws the axis between the two points {@code pt0} and {@code pt1} in
    702     * Java2D space.
    703     *
    704     * @param g2  the graphics target ({@code null} not permitted).
    705     * @param pt0  the starting point for the axis ({@code null} not
    706     *     permitted).
    707     * @param pt1  the ending point for the axis ({@code null} not
    708     *     permitted).
    709     * @param opposingPt  a point on the opposite side of the line from the
    710     *         labels ({@code null} not permitted).
    711     * @param tickData  the tick data, contains positioning anchors calculated
    712     *     by the 3D engine ({@code null} not permitted).
    713     * @param info  an object to be populated with rendering info
    714     *     ({@code null} permitted).
    715     * @param hinting  perform element hinting?
    716     */
    717    @Override
    718    public void draw(Graphics2D g2, Point2D pt0, Point2D pt1,
    719            Point2D opposingPt, List<TickData> tickData, RenderingInfo info,
    720            boolean hinting) {
    721
    722        if (!isVisible()) {
    723            return;
    724        }
    725        if (pt0.equals(pt1)) { // if the axis starts and ends on the same point
    726            return;            // there is nothing we can draw
    727        }
    728
    729        // draw the axis line (if you want no line, setting the line color
    730        // to fully transparent will achieve this)
    731        g2.setStroke(getLineStroke());
    732        g2.setPaint(getLineColor());
    733        Line2D axisLine = new Line2D.Float(pt0, pt1);
    734        g2.draw(axisLine);
    735
    736        // draw the tick marks - during this pass we will also find the maximum
    737        // tick label width
    738        g2.setPaint(this.tickMarkPaint);
    739        g2.setStroke(this.tickMarkStroke);
    740        g2.setFont(getTickLabelFont());
    741        double maxTickLabelWidth = 0.0;
    742        for (TickData t : tickData) {
    743            if (this.tickMarkLength > 0.0) {
    744                Line2D tickLine = Utils2D.createPerpendicularLine(axisLine,
    745                        t.getAnchorPt(), this.tickMarkLength, opposingPt);
    746                g2.draw(tickLine);
    747            }
    748            String tickLabel = t.getKeyLabel();
    749            maxTickLabelWidth = Math.max(maxTickLabelWidth,
    750                    g2.getFontMetrics().stringWidth(tickLabel));
    751        }
    752
    753        double maxTickLabelDim = maxTickLabelWidth;
    754        if (getTickLabelsVisible()) {
    755            g2.setPaint(getTickLabelColor());
    756            if (this.tickLabelOrientation.equals(
    757                    LabelOrientation.PERPENDICULAR)) {
    758                drawPerpendicularTickLabels(g2, axisLine, opposingPt, tickData,
    759                        info, hinting);
    760            } else if (this.tickLabelOrientation.equals(
    761                    LabelOrientation.PARALLEL)) {
    762                maxTickLabelDim = drawParallelTickLabels(g2, axisLine,
    763                        opposingPt, tickData, maxTickLabelWidth, info, hinting);
    764            }
    765        } else {
    766            maxTickLabelDim = 0.0;
    767        }
    768
    769        // draw the axis label if there is one
    770        if (getLabel() != null) {
    771            Shape labelBounds = drawAxisLabel(getLabel(), g2, axisLine,
    772                    opposingPt, maxTickLabelDim + this.tickMarkLength
    773                    + this.tickLabelOffset + getLabelOffset(), info, hinting);
    774        }
    775    }
    776
    777    /**
    778     * Returns "row" if the axis has been configured as a row axis, "column" if
    779     * the axis has been configured as a column axis, and the empty string ("")
    780     * if the axis has not yet been configured.
    781     *
    782     * @return A string (never {@code null}).
    783     *
    784     * @since 1.3
    785     */
    786    @Override
    787    protected String axisStr() {
    788        String result = "";
    789        if (this.isRowAxis) {
    790            result = "row";
    791        } else if (this.isColumnAxis) {
    792            result = "column";
    793        }
    794        return result;
    795    }
    796
    797    private double drawParallelTickLabels(Graphics2D g2, Line2D axisLine,
    798            Point2D opposingPt, List<TickData> tickData,
    799            double maxTickLabelWidth, RenderingInfo info, boolean hinting) {
    800        int levels = 1;
    801        LineMetrics lm = g2.getFontMetrics().getLineMetrics("123", g2);
    802        double height = lm.getHeight();
    803        if (tickData.size() > 1) {
    804
    805            // work out how many offset levels we need to display the
    806            // categories without overlapping
    807            Point2D p0 = tickData.get(0).getAnchorPt();
    808            Point2D pN = tickData.get(tickData.size() - 1).getAnchorPt();
    809            double availableWidth = pN.distance(p0)
    810                    * tickData.size() / (tickData.size() - 1);
    811            int labelsPerLevel = (int) Math.floor(availableWidth /
    812                    (maxTickLabelWidth * tickLabelFactor));
    813            int levelsRequired = this.maxTickLabelLevels;
    814            if (labelsPerLevel > 0) {
    815                levelsRequired = this.categories.size() / labelsPerLevel + 1;
    816            }
    817            levels = Math.min(levelsRequired, this.maxTickLabelLevels);
    818        }
    819
    820        int index = 0;
    821        for (TickData t : tickData) {
    822            int level = index % levels;
    823            double adj = height * (level + 0.5);
    824            Line2D perpLine = Utils2D.createPerpendicularLine(axisLine,
    825                    t.getAnchorPt(), this.tickMarkLength
    826                    + this.tickLabelOffset + adj, opposingPt);
    827            double axisTheta = Utils2D.calculateTheta(axisLine);
    828            TextAnchor textAnchor = TextAnchor.CENTER;
    829            if (axisTheta >= Math.PI / 2.0) {
    830                axisTheta = axisTheta - Math.PI;
    831            } else if (axisTheta <= -Math.PI / 2) {
    832                axisTheta = axisTheta + Math.PI;
    833            }
    834            String tickLabel = t.getKeyLabel();
    835            if (hinting) {
    836                Map m = new HashMap<String, String>();
    837                m.put("ref", "{\"type\": \"categoryTickLabel\", \"axis\": \""
    838                        + axisStr() + "\", \"key\": \""
    839                        + t.getKey() + "\"}");
    840                g2.setRenderingHint(Chart3DHints.KEY_BEGIN_ELEMENT, m);
    841            }
    842
    843            Shape bounds = TextUtils.drawRotatedString(tickLabel, g2,
    844                    (float) perpLine.getX2(), (float) perpLine.getY2(),
    845                    textAnchor, axisTheta, textAnchor);
    846            if (hinting) {
    847                g2.setRenderingHint(Chart3DHints.KEY_END_ELEMENT, true);
    848            }
    849            if (info != null) {
    850                RenderedElement tickLabelElement = new RenderedElement(
    851                        InteractiveElementType.CATEGORY_AXIS_TICK_LABEL, bounds);
    852                tickLabelElement.setProperty("label", tickLabel);
    853                tickLabelElement.setProperty("axis", axisStr());
    854                info.addOffsetElement(tickLabelElement);
    855            }
    856            index++;
    857        }
    858        return height * levels;
    859    }
    860
    861    /**
    862     * Draws the category labels perpendicular to the axis.
    863     *
    864     * @param g2  the graphics target.
    865     * @param axisLine  the axis line.
    866     * @param opposingPt  an opposing point (used to indicate which side the
    867     *     labels will appear on).
    868     * @param tickData  the tick data.
    869     * @param info  if not {@code null} this will be populated with
    870     *     {@link RenderedElement} instances for the tick labels.
    871     * @param hinting
    872     */
    873    private void drawPerpendicularTickLabels(Graphics2D g2, Line2D axisLine,
    874            Point2D opposingPt, List<TickData> tickData, RenderingInfo info,
    875            boolean hinting) {
    876
    877        for (TickData t : tickData) {
    878            Line2D perpLine = Utils2D.createPerpendicularLine(axisLine,
    879                    t.getAnchorPt(), this.tickMarkLength
    880                    + this.tickLabelOffset, opposingPt);
    881            double perpTheta = Utils2D.calculateTheta(perpLine);
    882            TextAnchor textAnchor = TextAnchor.CENTER_LEFT;
    883            if (perpTheta >= Math.PI / 2.0) {
    884                perpTheta = perpTheta - Math.PI;
    885                textAnchor = TextAnchor.CENTER_RIGHT;
    886            } else if (perpTheta <= -Math.PI / 2) {
    887                perpTheta = perpTheta + Math.PI;
    888                textAnchor = TextAnchor.CENTER_RIGHT;
    889            }
    890            String tickLabel = t.getKeyLabel();
    891            if (hinting) {
    892                Map m = new HashMap<String, String>();
    893                m.put("ref", "{\"type\": \"categoryAxisLabel\", \"axis\": \""
    894                        + axisStr() + "\", \"key\": \""
    895                        + t.getKey() + "\"}");
    896                g2.setRenderingHint(Chart3DHints.KEY_BEGIN_ELEMENT, m);
    897            }
    898            Shape bounds = TextUtils.drawRotatedString(tickLabel, g2,
    899                    (float) perpLine.getX2(), (float) perpLine.getY2(),
    900                    textAnchor, perpTheta, textAnchor);
    901            if (hinting) {
    902                g2.setRenderingHint(Chart3DHints.KEY_END_ELEMENT, true);
    903            }
    904            if (info != null) {
    905                RenderedElement tickLabelElement = new RenderedElement(
    906                        InteractiveElementType.CATEGORY_AXIS_TICK_LABEL, bounds);
    907                tickLabelElement.setProperty("label", tickLabel);
    908                tickLabelElement.setProperty("axis", axisStr());
    909                info.addOffsetElement(tickLabelElement);
    910            }
    911        }
    912    }
    913
    914    /**
    915     * Generates the tick data for the axis (assumes the axis is being used
    916     * as the row axis).  The dataset is passed as an argument to provide the
    917     * opportunity to incorporate dataset-specific info into tick labels (for
    918     * example, a row label might show the total for that row in the dataset)
    919     * ---whether or not this is used depends on the axis implementation.
    920     *
    921     * @param dataset  the dataset ({@code null} not permitted).
    922     *
    923     * @return The tick data.
    924     *
    925     * @since 1.2
    926     */
    927    @Override
    928    public List<TickData> generateTickDataForRows(CategoryDataset3D dataset) {
    929        ArgChecks.nullNotPermitted(dataset, "dataset");
    930        List<TickData> result = new ArrayList<TickData>(this.categories.size());
    931        for (Comparable<?> key : this.categories) {
    932            double pos = this.range.percent(getCategoryValue(key));
    933            String label = this.tickLabelGenerator.generateRowLabel(dataset,
    934                    key);
    935            result.add(new TickData(pos, key, label));
    936        }
    937        return result;
    938    }
    939
    940    /**
    941     * Generates the tick data for the axis (assumes the axis is being used
    942     * as the row axis).  The dataset is passed as an argument to provide the
    943     * opportunity to incorporate dataset-specific info into tick labels (for
    944     * example, a row label might show the total for that row in the dataset)
    945     * ---whether or not this is used depends on the axis implementation.
    946     *
    947     * @param dataset  the dataset ({@code null} not permitted).
    948     *
    949     * @return The tick data.
    950     *
    951     * @since 1.2
    952     */
    953    @Override
    954    public List<TickData> generateTickDataForColumns(
    955            CategoryDataset3D dataset) {
    956        ArgChecks.nullNotPermitted(dataset, "dataset");
    957        List<TickData> result = new ArrayList<TickData>(this.categories.size());
    958        for (Comparable<?> key : this.categories) {
    959            double pos = this.range.percent(getCategoryValue(key));
    960            String label = this.tickLabelGenerator.generateColumnLabel(dataset,
    961                    key);
    962            result.add(new TickData(pos, key, label));
    963        }
    964        return result;
    965    }
    966
    967    /**
    968     * Generates and returns a list of marker data items for the axis.
    969     *
    970     * @return A list of marker data items (never {@code null}).
    971     */
    972    @Override
    973    public List<MarkerData> generateMarkerData() {
    974        List<MarkerData> result = new ArrayList<MarkerData>();
    975        for (Map.Entry<String, CategoryMarker> entry
    976                : this.markers.entrySet()) {
    977            CategoryMarker cm = entry.getValue();
    978            if (cm == null) {
    979                continue;
    980            }
    981            MarkerData markerData;
    982            if (cm.getType().equals(CategoryMarkerType.LINE)) {
    983                double pos = getCategoryValue(cm.getCategory());
    984                markerData = new MarkerData(entry.getKey(), pos);
    985                markerData.setLabelAnchor(cm.getLabel() != null
    986                            ? cm.getLabelAnchor() : null);
    987            } else if (cm.getType().equals(CategoryMarkerType.BAND)) {
    988                double pos = getCategoryValue(cm.getCategory());
    989                double width = getCategoryWidth();
    990                markerData = new MarkerData(entry.getKey(), pos - width / 2,
    991                        false, pos + width / 2, false);
    992                markerData.setLabelAnchor(cm.getLabel() != null
    993                            ? cm.getLabelAnchor() : null);
    994            } else {
    995                throw new RuntimeException("Unrecognised marker: "
    996                        + cm.getType());
    997            }
    998            result.add(markerData);
    999        }
    1000        return result;
    1001    }
    1002
    1003    /**
    1004     * Receives a {@link ChartElementVisitor}.  This method is part of a general
    1005     * mechanism for traversing the chart structure and performing operations
    1006     * on each element in the chart.  You will not normally call this method
    1007     * directly.
    1008     *
    1009     * @param visitor  the visitor ({@code null} not permitted).
    1010     *
    1011     * @since 1.2
    1012     */
    1013    @Override
    1014    public void receive(ChartElementVisitor visitor) {
    1015        for (Marker marker : this.markers.values()) {
    1016            marker.receive(visitor);
    1017        }
    1018        visitor.visit(this);
    1019    }
    1020
    1021    /**
    1022     * Tests this instance for equality with an arbitrary object.
    1023     *
    1024     * @param obj  the object to test against ({@code null} not permitted).
    1025     *
    1026     * @return A boolean.
    1027     */
    1028    @Override
    1029    public boolean equals(Object obj) {
    1030        if (obj == this) {
    1031            return true;
    1032        }
    1033        if (!(obj instanceof StandardCategoryAxis3D)) {
    1034            return false;
    1035        }
    1036        StandardCategoryAxis3D that = (StandardCategoryAxis3D) obj;
    1037        if (this.lowerMargin != that.lowerMargin) {
    1038            return false;
    1039        }
    1040        if (this.upperMargin != that.upperMargin) {
    1041            return false;
    1042        }
    1043        if (this.firstCategoryHalfWidth != that.firstCategoryHalfWidth) {
    1044            return false;
    1045        }
    1046        if (this.lastCategoryHalfWidth != that.lastCategoryHalfWidth) {
    1047            return false;
    1048        }
    1049        if (this.tickMarkLength != that.tickMarkLength) {
    1050            return false;
    1051        }
    1052        if (!ObjectUtils.equalsPaint(this.tickMarkPaint, that.tickMarkPaint)) {
    1053            return false;
    1054        }
    1055        if (!this.tickMarkStroke.equals(that.tickMarkStroke)) {
    1056            return false;
    1057        }
    1058        if (!this.tickLabelGenerator.equals(that.tickLabelGenerator)) {
    1059            return false;
    1060        }
    1061        if (this.tickLabelOffset != that.tickLabelOffset) {
    1062            return false;
    1063        }
    1064        if (!this.tickLabelOrientation.equals(that.tickLabelOrientation)) {
    1065            return false;
    1066        }
    1067        if (this.tickLabelFactor != that.tickLabelFactor) {
    1068            return false;
    1069        }
    1070        if (this.maxTickLabelLevels != that.maxTickLabelLevels) {
    1071            return false;
    1072        }
    1073        if (!this.markers.equals(that.markers)) {
    1074            return false;
    1075        }
    1076        return super.equals(obj);
    1077    }
    1078
    1079    /**
    1080     * Provides serialization support.
    1081     *
    1082     * @param stream  the output stream.
    1083     *
    1084     * @throws IOException  if there is an I/O error.
    1085     */
    1086    private void writeObject(ObjectOutputStream stream) throws IOException {
    1087        stream.defaultWriteObject();
    1088        SerialUtils.writePaint(this.tickMarkPaint, stream);
    1089        SerialUtils.writeStroke(this.tickMarkStroke, stream);
    1090    }
    1091
    1092    /**
    1093     * Provides serialization support.
    1094     *
    1095     * @param stream  the input stream.
    1096     *
    1097     * @throws IOException  if there is an I/O error.
    1098     * @throws ClassNotFoundException  if there is a classpath problem.
    1099     */
    1100    private void readObject(ObjectInputStream stream)
    1101        throws IOException, ClassNotFoundException {
    1102        stream.defaultReadObject();
    1103        this.tickMarkPaint = SerialUtils.readPaint(stream);
    1104        this.tickMarkStroke = SerialUtils.readStroke(stream);
    1105    }
    1106
    1107    /**
    1108     * Returns {@code true} if the axis inverts the order of the data items,
    1109     * and {@code false} otherwise.
    1110     *
    1111     * @return A boolean.
    1112     *
    1113     * @since 1.5
    1114     */
    1115    @Override
    1116    public boolean isInverted() {
    1117        return this.inverted;
    1118    }
    1119
    1120    /**
    1121     * Sets the flag that controls whether or not the axis inverts the order
    1122     * of the data items and sends an {@link Axis3DChangeEvent} to all
    1123     * registered listeners.
    1124     *
    1125     * @param inverted  the new flag value.
    1126     *
    1127     * @since 1.5
    1128     */
    1129    public void setInverted(boolean inverted) {
    1130        this.inverted = inverted;
    1131        fireChangeEvent(true);
    1132    }
    1133}