/* =========================================================== * Orson Charts : a 3D chart library for the Java(tm) platform * =========================================================== * * (C)opyright 2013-2016, by Object Refinery Limited. All rights reserved. * * http://www.object-refinery.com/orsoncharts/index.html * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. * * [Oracle and Java are registered trademarks of Oracle and/or its affiliates. * Other names may be trademarks of their respective owners.] * * If you do not wish to be bound by the terms of the GPL, an alternative * commercial license can be purchased. For details, please see visit the * Orson Charts home page: * * http://www.object-refinery.com/orsoncharts/index.html * */ package com.orsoncharts.axis; import java.awt.BasicStroke; import java.awt.Color; import java.awt.Graphics2D; import java.awt.Paint; import java.awt.Shape; import java.awt.Stroke; import java.awt.font.LineMetrics; import java.awt.geom.Line2D; import java.awt.geom.Point2D; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.HashMap; import java.util.Map; import com.orsoncharts.Chart3DHints; import com.orsoncharts.ChartElementVisitor; import com.orsoncharts.Range; import com.orsoncharts.data.category.CategoryDataset3D; import com.orsoncharts.graphics3d.RenderedElement; import com.orsoncharts.graphics3d.RenderingInfo; import com.orsoncharts.graphics3d.Utils2D; import com.orsoncharts.interaction.InteractiveElementType; import com.orsoncharts.label.CategoryLabelGenerator; import com.orsoncharts.label.StandardCategoryLabelGenerator; import com.orsoncharts.marker.CategoryMarker; import com.orsoncharts.marker.CategoryMarkerType; import com.orsoncharts.marker.Marker; import com.orsoncharts.marker.MarkerData; import com.orsoncharts.plot.CategoryPlot3D; import com.orsoncharts.renderer.category.AreaRenderer3D; import com.orsoncharts.util.ArgChecks; import com.orsoncharts.util.ObjectUtils; import com.orsoncharts.util.SerialUtils; import com.orsoncharts.util.TextAnchor; import com.orsoncharts.util.TextUtils; /** * An axis that displays categories. * <br><br> * NOTE: This class is serializable, but the serialization format is subject * to change in future releases and should not be relied upon for persisting * instances of this class. */ @SuppressWarnings("serial") public class StandardCategoryAxis3D extends AbstractAxis3D implements CategoryAxis3D, Serializable { /** The categories. */ private List<Comparable<?>> categories; /** * The axis range (never {@code null}). */ private Range range; private boolean inverted; /** The percentage margin to leave at the lower end of the axis. */ private double lowerMargin; /** The percentage margin to leave at the upper end of the axis. */ private double upperMargin; /** * Hide half of the first category? This brings the category label * closer to the beginning of the axis. It is useful if the renderer * doesn't make full use of the category space for the first item. */ private boolean firstCategoryHalfWidth = false; /** * Hide half of the last category? This brings the category label * closer to the end of the axis. It is useful if the renderer * doesn't make full use of the category space for the last item. */ private boolean lastCategoryHalfWidth = false; /** * The tick mark length (in Java2D units). When this is 0.0, no tick * marks will be drawn. */ private double tickMarkLength; /** The tick mark stroke (never {@code null}). */ private transient Stroke tickMarkStroke; /** The tick mark paint (never {@code null}). */ private transient Paint tickMarkPaint; /** The tick label generator. */ private CategoryLabelGenerator tickLabelGenerator; /** * The tick label offset (in Java2D units). This is the gap between the * tick marks and their associated labels. */ private double tickLabelOffset; /** The orientation for the tick labels. */ private LabelOrientation tickLabelOrientation; /** * The maximum number of offset levels to use for tick labels on the axis. */ private int maxTickLabelLevels = 3; /** * The tick label factor (used as a multiplier for the tick label width * when checking for overlapping labels). */ private double tickLabelFactor = 1.2; /** * The markers for the axis (this may be empty, but not {@code null}). */ private Map<String, CategoryMarker> markers; /** A flag to indicate that this axis has been configured as a row axis. */ private boolean isRowAxis; /** * A flag to indicate that this axis has been configured as a column * axis. */ private boolean isColumnAxis; /** * Default constructor. */ public StandardCategoryAxis3D() { this(null); } /** * Creates a new axis with the specified label. * * @param label the axis label ({@code null} permitted). */ public StandardCategoryAxis3D(String label) { super(label); this.categories = new ArrayList<Comparable<?>>(); this.range = new Range(0.0, 1.0); this.lowerMargin = 0.05; this.upperMargin = 0.05; this.firstCategoryHalfWidth = false; this.lastCategoryHalfWidth = false; this.tickMarkLength = 3.0; this.tickMarkPaint = Color.GRAY; this.tickMarkStroke = new BasicStroke(0.5f); this.tickLabelGenerator = new StandardCategoryLabelGenerator(); this.tickLabelOffset = 5.0; this.tickLabelOrientation = LabelOrientation.PARALLEL; this.tickLabelFactor = 1.4; this.maxTickLabelLevels = 3; this.markers = new LinkedHashMap<String, CategoryMarker>(); this.isRowAxis = false; this.isColumnAxis = false; } /** * Returns {@code true} if this axis has been configured as a * row axis for the plot that it belongs to, and {@code false} * otherwise. * * @return A boolean. * * @since 1.3 */ @Override public boolean isRowAxis() { return isRowAxis; } /** * Returns {@code true} if this axis has been configured as a * column axis for the plot that it belongs to, and {@code false} * otherwise. * * @return A boolean. * * @since 1.3 */ @Override public boolean isColumnAxis() { return isColumnAxis; } /** * Returns the range for the axis. By convention, the category axes have * a range from 0.0 to 1.0. * * @return The range. */ @Override public Range getRange() { return this.range; } /** * Sets the range for the axis and sends an {@link Axis3DChangeEvent} to * all registered listeners. * * @param lowerBound the lower bound. * @param upperBound the upper bound. */ @Override public void setRange(double lowerBound, double upperBound) { setRange(new Range(lowerBound, upperBound)); } /** * Sets the range for the axis and sends an {@link Axis3DChangeEvent} to * all registered listeners. Note that changing the range for the * category axis will have no visible effect. * * @param range the range ({@code null} not permitted). */ @Override public void setRange(Range range) { ArgChecks.nullNotPermitted(range, "range"); this.range = range; fireChangeEvent(true); } /** * Returns the margin to leave at the lower end of the axis, as a * percentage of the axis length. The default is {@code 0.05} (five * percent). * * @return The lower margin. */ public double getLowerMargin() { return this.lowerMargin; } /** * Sets the margin to leave at the lower end of the axis and sends an * {@link Axis3DChangeEvent} to all registered listeners. * * @param margin the margin. */ public void setLowerMargin(double margin) { this.lowerMargin = margin; fireChangeEvent(true); } /** * Returns the margin to leave at the upper end of the axis, as a * percentage of the axis length. The default is {@code 0.05} (five * percent). * * @return The lower margin. */ public double getUpperMargin() { return this.upperMargin; } /** * Sets the margin to leave at the upper end of the axis and sends an * {@link Axis3DChangeEvent} to all registered listeners. * * @param margin the margin. */ public void setUpperMargin(double margin) { this.upperMargin = margin; fireChangeEvent(true); } /** * Returns {@code true} if the first category on the axis should * occupy half the normal width, and {@code false} otherwise. * * @return A boolean. * * @see #setFirstCategoryHalfWidth(boolean) */ public boolean isFirstCategoryHalfWidth() { return this.firstCategoryHalfWidth; } /** * Sets the flag that controls whether the first category on the axis * occupies a full or half width, and sends an {@link Axis3DChangeEvent} * to all registered listeners. There are some renderers where the * charts look better when half-widths are used (for example, * {@link AreaRenderer3D}). * * @param half half width? * * @see #setLastCategoryHalfWidth(boolean) */ public void setFirstCategoryHalfWidth(boolean half) { this.firstCategoryHalfWidth = half; fireChangeEvent(true); } /** * Returns {@code true} if the last category on the axis should * occupy half the normal width, and {@code false} otherwise. * * @return A boolean. * * @see #setLastCategoryHalfWidth(boolean) */ public boolean isLastCategoryHalfWidth() { return this.lastCategoryHalfWidth; } /** * Sets the flag that controls whether the last category on the axis * occupies a full or half width, and sends an {@link Axis3DChangeEvent} * to all registered listeners. There are some renderers where the * charts look better when half-widths are used (for example, * {@link AreaRenderer3D}). * * @param half half width? * * @see #setFirstCategoryHalfWidth(boolean) */ public void setLastCategoryHalfWidth(boolean half) { this.lastCategoryHalfWidth = half; fireChangeEvent(true); } /** * Returns the tick mark length (in Java2D units). The default value * is {@code 3.0}. * * @return The tick mark length. */ public double getTickMarkLength() { return this.tickMarkLength; } /** * Sets the tick mark length (in Java2D units) and sends an * {@link Axis3DChangeEvent} to all registered listeners. You can set * the length to {@code 0.0} if you don't want any tick marks on the * axis. * * @param length the length (in Java2D units). */ public void setTickMarkLength(double length) { this.tickMarkLength = length; fireChangeEvent(false); } /** * Returns the paint used to draw the tick marks, if they are visible. * The default value is {@code Color.GRAY}. * * @return The paint used to draw the tick marks (never {@code null}). */ public Paint getTickMarkPaint() { return this.tickMarkPaint; } /** * Sets the paint used to draw the tick marks and sends an * {@link Axis3DChangeEvent} to all registered listeners. * * @param paint the paint ({@code null} not permitted). */ public void setTickMarkPaint(Paint paint) { ArgChecks.nullNotPermitted(paint, "paint"); this.tickMarkPaint = paint; fireChangeEvent(false); } /** * Returns the stroke used to draw the tick marks, if they are visible. * The default value is {@code new BasicStroke(0.5f)}. * * @return The stroke used to draw the tick marks (never {@code null}). */ public Stroke getTickMarkStroke() { return this.tickMarkStroke; } /** * Sets the stroke used to draw the tick marks and sends an * {@link Axis3DChangeEvent} to all registered listeners. * * @param stroke the stroke ({@code null} not permitted). */ public void setTickMarkStroke(Stroke stroke) { ArgChecks.nullNotPermitted(stroke, "stroke"); this.tickMarkStroke = stroke; fireChangeEvent(false); } /** * Returns the tick label generator for the axis. This is an object that * is responsible for creating the category labels on the axis. You can * plug in your own instance to take full control over the generation * of category labels. * * @return The tick label generator for the axis (never {@code null}). * * @since 1.2 */ public CategoryLabelGenerator getTickLabelGenerator() { return this.tickLabelGenerator; } /** * Sets the tick label generator for the axis and sends a change event to * all registered listeners. * * @param generator the generator ({@code null} not permitted). * * @since 1.2 */ public void setTickLabelGenerator(CategoryLabelGenerator generator) { ArgChecks.nullNotPermitted(generator, "generator"); this.tickLabelGenerator = generator; fireChangeEvent(false); } /** * Returns the offset between the tick marks and the tick labels. The * default value is {@code 5.0}. * * @return The offset between the tick marks and the tick labels (in Java2D * units). */ public double getTickLabelOffset() { return this.tickLabelOffset; } /** * Sets the offset between the tick marks and the tick labels and sends * a {@link Axis3DChangeEvent} to all registered listeners. * * @param offset the offset. */ public void setTickLabelOffset(double offset) { this.tickLabelOffset = offset; fireChangeEvent(false); } /** * Returns the orientation for the tick labels. The default value is * {@link LabelOrientation#PARALLEL}. * * @return The orientation for the tick labels (never {@code null}). * * @since 1.2 */ public LabelOrientation getTickLabelOrientation() { return this.tickLabelOrientation; } /** * Sets the orientation for the tick labels and sends a change event to * all registered listeners. * * @param orientation the orientation ({@code null} not permitted). * * @since 1.2 */ public void setTickLabelOrientation(LabelOrientation orientation) { ArgChecks.nullNotPermitted(orientation, "orientation"); this.tickLabelOrientation = orientation; fireChangeEvent(false); } /** * Returns the maximum number of offset levels for the category labels on * the axis. The default value is 3. * * @return The maximum number of offset levels. * * @since 1.2 */ public int getMaxTickLabelLevels() { return this.maxTickLabelLevels; } /** * Sets the maximum number of offset levels for the category labels on the * axis and sends a change event to all registered listeners. * * @param levels the maximum number of levels. * * @since 1.2 */ public void setMaxTickLabelLevels(int levels) { this.maxTickLabelLevels = levels; fireChangeEvent(false); } /** * Returns the tick label factor. The default value is {@code 1.4}. * * @return The tick label factor. * * @since 1.2 */ public double getTickLabelFactor() { return this.tickLabelFactor; } /** * Sets the tick label factor and sends a change event to all registered * listeners. * * @param factor the new factor (should be at least 1.0). * * @since 1.2 */ public void setTickLabelFactor(double factor) { this.tickLabelFactor = factor; fireChangeEvent(false); } /** * Returns the marker with the specified key, if there is one. * * @param key the key ({@code null} not permitted). * * @return The marker (possibly {@code null}). * * @since 1.2 */ @Override public CategoryMarker getMarker(String key) { return this.markers.get(key); } /** * Sets the marker for the specified key and sends a change event to * all registered listeners. If there is an existing marker it is replaced * (and the axis will no longer listen for change events on the previous * marker). * * @param key the key that identifies the marker ({@code null} not * permitted). * @param marker the marker ({@code null} permitted). * * @since 1.2 */ public void setMarker(String key, CategoryMarker marker) { CategoryMarker existing = this.markers.get(key); if (existing != null) { existing.removeChangeListener(this); } this.markers.put(key, marker); if (marker != null) { marker.addChangeListener(this); } fireChangeEvent(false); } /** * Returns a new map containing the markers that are assigned to this axis. * * @return A map. * * @since 1.2 */ public Map<String, CategoryMarker> getMarkers() { return new LinkedHashMap<String, CategoryMarker>(this.markers); } /** * Returns the width of a single category in the units of the axis * range. * * @return The width of a single category. */ @Override public double getCategoryWidth() { double length = this.range.getLength(); double start = this.range.getMin() + (this.lowerMargin * length); double end = this.range.getMax() - (this.upperMargin * length); double available = (end - start); return available / this.categories.size(); } /** * Configures the axis to be used as a row axis for the specified * plot. This method is for internal use, you should not call it directly. * * @param plot the plot ({@code null} not permitted). */ @Override @SuppressWarnings("unchecked") public void configureAsRowAxis(CategoryPlot3D plot) { ArgChecks.nullNotPermitted(plot, "plot"); this.categories = plot.getDataset().getRowKeys(); this.isColumnAxis = false; this.isRowAxis = true; } /** * Configures the axis to be used as a column axis for the specified * plot. This method is for internal use, you won't normally need to call * it directly. * * @param plot the plot ({@code null} not permitted). */ @Override @SuppressWarnings("unchecked") public void configureAsColumnAxis(CategoryPlot3D plot) { ArgChecks.nullNotPermitted(plot, "plot"); this.categories = plot.getDataset().getColumnKeys(); this.isColumnAxis = true; this.isRowAxis = false; } /** * Returns the value for the specified category, or {@code Double.NaN} * if the category is not registered on the axis. * * @param category the category ({@code null} not permitted). * * @return The value. */ @Override public double getCategoryValue(Comparable<?> category) { int index = this.categories.indexOf(category); if (index < 0) { return Double.NaN; } double length = this.range.getLength(); double start = this.range.getMin() + (this.lowerMargin * length); double end = this.range.getMax() - (this.upperMargin * length); double available = (end - start); double categoryCount = this.categories.size(); if (categoryCount == 1) { return (start + end) / 2.0; } if (this.firstCategoryHalfWidth) { categoryCount -= 0.5; } if (this.lastCategoryHalfWidth) { categoryCount -= 0.5; } double categoryWidth = 0.0; if (categoryCount > 0.0) { categoryWidth = available / categoryCount; } double adj = this.firstCategoryHalfWidth ? 0.0 : 0.5; return start + (adj + index) * categoryWidth; } /** * Translates a value on the axis to the equivalent coordinate in the * 3D world used to construct a model of the chart. * * @param value the value along the axis. * @param length the length of one side of the 3D box containing the model. * * @return A coordinate in 3D space. */ @Override public double translateToWorld(double value, double length) { double p = getRange().percent(value, isInverted()); return length * p; } /** * Draws the axis between the two points {@code pt0} and {@code pt1} in * Java2D space. * * @param g2 the graphics target ({@code null} not permitted). * @param pt0 the starting point for the axis ({@code null} not * permitted). * @param pt1 the ending point for the axis ({@code null} not * permitted). * @param opposingPt a point on the opposite side of the line from the * labels ({@code null} not permitted). * @param tickData the tick data, contains positioning anchors calculated * by the 3D engine ({@code null} not permitted). * @param info an object to be populated with rendering info * ({@code null} permitted). * @param hinting perform element hinting? */ @Override public void draw(Graphics2D g2, Point2D pt0, Point2D pt1, Point2D opposingPt, List<TickData> tickData, RenderingInfo info, boolean hinting) { if (!isVisible()) { return; } if (pt0.equals(pt1)) { // if the axis starts and ends on the same point return; // there is nothing we can draw } // draw the axis line (if you want no line, setting the line color // to fully transparent will achieve this) g2.setStroke(getLineStroke()); g2.setPaint(getLineColor()); Line2D axisLine = new Line2D.Float(pt0, pt1); g2.draw(axisLine); // draw the tick marks - during this pass we will also find the maximum // tick label width g2.setPaint(this.tickMarkPaint); g2.setStroke(this.tickMarkStroke); g2.setFont(getTickLabelFont()); double maxTickLabelWidth = 0.0; for (TickData t : tickData) { if (this.tickMarkLength > 0.0) { Line2D tickLine = Utils2D.createPerpendicularLine(axisLine, t.getAnchorPt(), this.tickMarkLength, opposingPt); g2.draw(tickLine); } String tickLabel = t.getKeyLabel(); maxTickLabelWidth = Math.max(maxTickLabelWidth, g2.getFontMetrics().stringWidth(tickLabel)); } double maxTickLabelDim = maxTickLabelWidth; if (getTickLabelsVisible()) { g2.setPaint(getTickLabelColor()); if (this.tickLabelOrientation.equals( LabelOrientation.PERPENDICULAR)) { drawPerpendicularTickLabels(g2, axisLine, opposingPt, tickData, info, hinting); } else if (this.tickLabelOrientation.equals( LabelOrientation.PARALLEL)) { maxTickLabelDim = drawParallelTickLabels(g2, axisLine, opposingPt, tickData, maxTickLabelWidth, info, hinting); } } else { maxTickLabelDim = 0.0; } // draw the axis label if there is one if (getLabel() != null) { Shape labelBounds = drawAxisLabel(getLabel(), g2, axisLine, opposingPt, maxTickLabelDim + this.tickMarkLength + this.tickLabelOffset + getLabelOffset(), info, hinting); } } /** * Returns "row" if the axis has been configured as a row axis, "column" if * the axis has been configured as a column axis, and the empty string ("") * if the axis has not yet been configured. * * @return A string (never {@code null}). * * @since 1.3 */ @Override protected String axisStr() { String result = ""; if (this.isRowAxis) { result = "row"; } else if (this.isColumnAxis) { result = "column"; } return result; } private double drawParallelTickLabels(Graphics2D g2, Line2D axisLine, Point2D opposingPt, List<TickData> tickData, double maxTickLabelWidth, RenderingInfo info, boolean hinting) { int levels = 1; LineMetrics lm = g2.getFontMetrics().getLineMetrics("123", g2); double height = lm.getHeight(); if (tickData.size() > 1) { // work out how many offset levels we need to display the // categories without overlapping Point2D p0 = tickData.get(0).getAnchorPt(); Point2D pN = tickData.get(tickData.size() - 1).getAnchorPt(); double availableWidth = pN.distance(p0) * tickData.size() / (tickData.size() - 1); int labelsPerLevel = (int) Math.floor(availableWidth / (maxTickLabelWidth * tickLabelFactor)); int levelsRequired = this.maxTickLabelLevels; if (labelsPerLevel > 0) { levelsRequired = this.categories.size() / labelsPerLevel + 1; } levels = Math.min(levelsRequired, this.maxTickLabelLevels); } int index = 0; for (TickData t : tickData) { int level = index % levels; double adj = height * (level + 0.5); Line2D perpLine = Utils2D.createPerpendicularLine(axisLine, t.getAnchorPt(), this.tickMarkLength + this.tickLabelOffset + adj, opposingPt); double axisTheta = Utils2D.calculateTheta(axisLine); TextAnchor textAnchor = TextAnchor.CENTER; if (axisTheta >= Math.PI / 2.0) { axisTheta = axisTheta - Math.PI; } else if (axisTheta <= -Math.PI / 2) { axisTheta = axisTheta + Math.PI; } String tickLabel = t.getKeyLabel(); if (hinting) { Map<String, String> m = new HashMap<String, String>(); m.put("ref", "{\"type\": \"categoryTickLabel\", \"axis\": \"" + axisStr() + "\", \"key\": \"" + t.getKey() + "\"}"); g2.setRenderingHint(Chart3DHints.KEY_BEGIN_ELEMENT, m); } Shape bounds = TextUtils.drawRotatedString(tickLabel, g2, (float) perpLine.getX2(), (float) perpLine.getY2(), textAnchor, axisTheta, textAnchor); if (hinting) { g2.setRenderingHint(Chart3DHints.KEY_END_ELEMENT, true); } if (info != null) { RenderedElement tickLabelElement = new RenderedElement( InteractiveElementType.CATEGORY_AXIS_TICK_LABEL, bounds); tickLabelElement.setProperty("label", tickLabel); tickLabelElement.setProperty("axis", axisStr()); info.addOffsetElement(tickLabelElement); } index++; } return height * levels; } /** * Draws the category labels perpendicular to the axis. * * @param g2 the graphics target. * @param axisLine the axis line. * @param opposingPt an opposing point (used to indicate which side the * labels will appear on). * @param tickData the tick data. * @param info if not {@code null} this will be populated with * {@link RenderedElement} instances for the tick labels. * @param hinting */ @SuppressWarnings("unchecked") private void drawPerpendicularTickLabels(Graphics2D g2, Line2D axisLine, Point2D opposingPt, List<TickData> tickData, RenderingInfo info, boolean hinting) { for (TickData t : tickData) { Line2D perpLine = Utils2D.createPerpendicularLine(axisLine, t.getAnchorPt(), this.tickMarkLength + this.tickLabelOffset, opposingPt); double perpTheta = Utils2D.calculateTheta(perpLine); TextAnchor textAnchor = TextAnchor.CENTER_LEFT; if (perpTheta >= Math.PI / 2.0) { perpTheta = perpTheta - Math.PI; textAnchor = TextAnchor.CENTER_RIGHT; } else if (perpTheta <= -Math.PI / 2) { perpTheta = perpTheta + Math.PI; textAnchor = TextAnchor.CENTER_RIGHT; } String tickLabel = t.getKeyLabel(); if (hinting) { Map m = new HashMap<String, String>(); m.put("ref", "{\"type\": \"categoryAxisLabel\", \"axis\": \"" + axisStr() + "\", \"key\": \"" + t.getKey() + "\"}"); g2.setRenderingHint(Chart3DHints.KEY_BEGIN_ELEMENT, m); } Shape bounds = TextUtils.drawRotatedString(tickLabel, g2, (float) perpLine.getX2(), (float) perpLine.getY2(), textAnchor, perpTheta, textAnchor); if (hinting) { g2.setRenderingHint(Chart3DHints.KEY_END_ELEMENT, true); } if (info != null) { RenderedElement tickLabelElement = new RenderedElement( InteractiveElementType.CATEGORY_AXIS_TICK_LABEL, bounds); tickLabelElement.setProperty("label", tickLabel); tickLabelElement.setProperty("axis", axisStr()); info.addOffsetElement(tickLabelElement); } } } /** * Generates the tick data for the axis (assumes the axis is being used * as the row axis). The dataset is passed as an argument to provide the * opportunity to incorporate dataset-specific info into tick labels (for * example, a row label might show the total for that row in the dataset) * ---whether or not this is used depends on the axis implementation. * * @param dataset the dataset ({@code null} not permitted). * * @return The tick data. * * @since 1.2 */ @Override @SuppressWarnings("unchecked") public List<TickData> generateTickDataForRows(CategoryDataset3D dataset) { ArgChecks.nullNotPermitted(dataset, "dataset"); List<TickData> result = new ArrayList<TickData>(this.categories.size()); for (Comparable<?> key : this.categories) { double pos = this.range.percent(getCategoryValue(key)); String label = this.tickLabelGenerator.generateRowLabel(dataset, key); result.add(new TickData(pos, key, label)); } return result; } /** * Generates the tick data for the axis (assumes the axis is being used * as the row axis). The dataset is passed as an argument to provide the * opportunity to incorporate dataset-specific info into tick labels (for * example, a row label might show the total for that row in the dataset) * ---whether or not this is used depends on the axis implementation. * * @param dataset the dataset ({@code null} not permitted). * * @return The tick data. * * @since 1.2 */ @Override @SuppressWarnings("unchecked") public List<TickData> generateTickDataForColumns( CategoryDataset3D dataset) { ArgChecks.nullNotPermitted(dataset, "dataset"); List<TickData> result = new ArrayList<TickData>(this.categories.size()); for (Comparable<?> key : this.categories) { double pos = this.range.percent(getCategoryValue(key)); String label = this.tickLabelGenerator.generateColumnLabel(dataset, key); result.add(new TickData(pos, key, label)); } return result; } /** * Generates and returns a list of marker data items for the axis. * * @return A list of marker data items (never {@code null}). */ @Override public List<MarkerData> generateMarkerData() { List<MarkerData> result = new ArrayList<MarkerData>(); for (Map.Entry<String, CategoryMarker> entry : this.markers.entrySet()) { CategoryMarker cm = entry.getValue(); if (cm == null) { continue; } MarkerData markerData; if (cm.getType().equals(CategoryMarkerType.LINE)) { double pos = getCategoryValue(cm.getCategory()); markerData = new MarkerData(entry.getKey(), pos); markerData.setLabelAnchor(cm.getLabel() != null ? cm.getLabelAnchor() : null); } else if (cm.getType().equals(CategoryMarkerType.BAND)) { double pos = getCategoryValue(cm.getCategory()); double width = getCategoryWidth(); markerData = new MarkerData(entry.getKey(), pos - width / 2, false, pos + width / 2, false); markerData.setLabelAnchor(cm.getLabel() != null ? cm.getLabelAnchor() : null); } else { throw new RuntimeException("Unrecognised marker: " + cm.getType()); } result.add(markerData); } return result; } /** * Receives a {@link ChartElementVisitor}. This method is part of a general * mechanism for traversing the chart structure and performing operations * on each element in the chart. You will not normally call this method * directly. * * @param visitor the visitor ({@code null} not permitted). * * @since 1.2 */ @Override public void receive(ChartElementVisitor visitor) { for (Marker marker : this.markers.values()) { marker.receive(visitor); } visitor.visit(this); } /** * Tests this instance for equality with an arbitrary object. * * @param obj the object to test against ({@code null} not permitted). * * @return A boolean. */ @Override public boolean equals(Object obj) { if (obj == this) { return true; } if (!(obj instanceof StandardCategoryAxis3D)) { return false; } StandardCategoryAxis3D that = (StandardCategoryAxis3D) obj; if (this.lowerMargin != that.lowerMargin) { return false; } if (this.upperMargin != that.upperMargin) { return false; } if (this.firstCategoryHalfWidth != that.firstCategoryHalfWidth) { return false; } if (this.lastCategoryHalfWidth != that.lastCategoryHalfWidth) { return false; } if (this.tickMarkLength != that.tickMarkLength) { return false; } if (!ObjectUtils.equalsPaint(this.tickMarkPaint, that.tickMarkPaint)) { return false; } if (!this.tickMarkStroke.equals(that.tickMarkStroke)) { return false; } if (!this.tickLabelGenerator.equals(that.tickLabelGenerator)) { return false; } if (this.tickLabelOffset != that.tickLabelOffset) { return false; } if (!this.tickLabelOrientation.equals(that.tickLabelOrientation)) { return false; } if (this.tickLabelFactor != that.tickLabelFactor) { return false; } if (this.maxTickLabelLevels != that.maxTickLabelLevels) { return false; } if (!this.markers.equals(that.markers)) { return false; } return super.equals(obj); } /** * Provides serialization support. * * @param stream the output stream. * * @throws IOException if there is an I/O error. */ private void writeObject(ObjectOutputStream stream) throws IOException { stream.defaultWriteObject(); SerialUtils.writePaint(this.tickMarkPaint, stream); SerialUtils.writeStroke(this.tickMarkStroke, stream); } /** * Provides serialization support. * * @param stream the input stream. * * @throws IOException if there is an I/O error. * @throws ClassNotFoundException if there is a classpath problem. */ private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException { stream.defaultReadObject(); this.tickMarkPaint = SerialUtils.readPaint(stream); this.tickMarkStroke = SerialUtils.readStroke(stream); } /** * Returns {@code true} if the axis inverts the order of the data items, * and {@code false} otherwise. * * @return A boolean. * * @since 1.5 */ @Override public boolean isInverted() { return this.inverted; } /** * Sets the flag that controls whether or not the axis inverts the order * of the data items and sends an {@link Axis3DChangeEvent} to all * registered listeners. * * @param inverted the new flag value. * * @since 1.5 */ public void setInverted(boolean inverted) { this.inverted = inverted; fireChangeEvent(true); } }