/* =========================================================== * 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.Paint; import java.awt.Stroke; 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.Map; import com.orsoncharts.ChartElementVisitor; import com.orsoncharts.Range; import com.orsoncharts.data.category.CategoryDataset3D; import com.orsoncharts.marker.MarkerData; import com.orsoncharts.marker.NumberMarker; import com.orsoncharts.marker.RangeMarker; import com.orsoncharts.marker.ValueMarker; import com.orsoncharts.plot.CategoryPlot3D; import com.orsoncharts.plot.XYZPlot; import com.orsoncharts.util.ArgChecks; import com.orsoncharts.util.ObjectUtils; import com.orsoncharts.util.SerialUtils; /** * A base class for implementing numerical axes. * <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 abstract class AbstractValueAxis3D extends AbstractAxis3D implements ValueAxis3D, Serializable{ /** The type of use for which the axis has been configured. */ private ValueAxis3DType configuredType; /** The axis range. */ protected Range range; private boolean inverted; /** * A flag that controls whether or not the axis range is automatically * adjusted to display all of the data items in the dataset. */ private boolean autoAdjustRange; /** 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; /** * The default range to apply when there is no data in the dataset and the * autoAdjustRange flag is true. A sensible default is going to depend on * the context, so the user should change it as necessary. */ private Range defaultAutoRange; /** * The minimum length for the axis range when auto-calculated. This will * be applied, for example, when the dataset contains just a single value. */ private double minAutoRangeLength; /** The tick label offset (number of Java2D units). */ private double tickLabelOffset; /** The length of tick marks (in Java2D units). Can be set to 0.0. */ 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 orientation for the tick labels. */ private LabelOrientation tickLabelOrientation; /** The tick label factor (defaults to 1.4). */ private double tickLabelFactor; /** Storage for value markers for the axis (empty by default). */ private Map<String, ValueMarker> valueMarkers; /** * Creates a new axis instance. * * @param label the axis label ({@code null} permitted). * @param range the axis range ({@code null} not permitted). */ public AbstractValueAxis3D(String label, Range range) { super(label); ArgChecks.nullNotPermitted(range, "range"); this.configuredType = null; this.range = range; this.autoAdjustRange = true; this.lowerMargin = 0.05; this.upperMargin = 0.05; this.defaultAutoRange = new Range(0.0, 1.0); this.minAutoRangeLength = 0.001; this.tickLabelOffset = 5.0; this.tickLabelOrientation = LabelOrientation.PARALLEL; this.tickLabelFactor = 1.4; this.tickMarkLength = 3.0; this.tickMarkStroke = new BasicStroke(0.5f); this.tickMarkPaint = Color.GRAY; this.valueMarkers = new LinkedHashMap<String, ValueMarker>(); } /** * Returns the configured type for the axis. * * @return The configured type ({@code null} if the axis has not yet * been assigned to a plot). * * @since 1.3 */ @Override public ValueAxis3DType getConfiguredType() { return this.configuredType; } /** * Returns a string representing the configured type of the axis. * * @return A string. */ @Override protected String axisStr() { if (this.configuredType.equals(ValueAxis3DType.VALUE)) { return "value"; } if (this.configuredType.equals(ValueAxis3DType.X)) { return "x"; } if (this.configuredType.equals(ValueAxis3DType.Y)) { return "y"; } if (this.configuredType.equals(ValueAxis3DType.Z)) { return "z"; } return ""; } /** * Returns the axis range. You can set the axis range manually or you can * rely on the autoAdjustRange feature to set the axis range to match * the data being plotted. * * @return the axis range (never {@code null}). */ @Override public Range getRange() { return this.range; } /** * Sets the axis range (bounds) and sends an {@link Axis3DChangeEvent} to * all registered listeners. * * @param range the new range (must have positive length and * {@code null} is not permitted). */ @Override public void setRange(Range range) { ArgChecks.nullNotPermitted(range, "range"); if (range.getLength() <= 0.0) { throw new IllegalArgumentException( "Requires a range with length > 0"); } this.range = range; this.autoAdjustRange = false; fireChangeEvent(true); } /** * Updates the axis range (used by the auto-range calculation) without * notifying listeners. * * @param range the new range. */ protected void updateRange(Range range) { this.range = range; } /** * Sets the axis range and sends an {@link Axis3DChangeEvent} to all * registered listeners. * * @param min the lower bound for the range (requires min < max). * @param max the upper bound for the range (requires max > min). */ @Override public void setRange(double min, double max) { setRange(new Range(min, max)); } /** * Returns the flag that controls whether or not the axis range is * automatically updated in response to dataset changes. The default * value is {@code true}. * * @return A boolean. */ public boolean isAutoAdjustRange() { return this.autoAdjustRange; } /** * Sets the flag that controls whether or not the axis range is * automatically updated in response to dataset changes, and sends an * {@link Axis3DChangeEvent} to all registered listeners. * * @param autoAdjust the new flag value. */ public void setAutoAdjustRange(boolean autoAdjust) { this.autoAdjustRange = autoAdjust; fireChangeEvent(true); } /** * Returns the size of the lower margin that is added by the auto-range * calculation, as a percentage of the data range. This margin is used to * prevent data items from being plotted right at the edges of the chart. * The default value is {@code 0.05} (five percent). * * @return The lower margin. */ public double getLowerMargin() { return this.lowerMargin; } /** * Sets the size of the lower margin that will be added by the auto-range * calculation and sends an {@link Axis3DChangeEvent} to all registered * listeners. * * @param margin the margin as a percentage of the data range * (0.05 = five percent). * * @see #setUpperMargin(double) */ public void setLowerMargin(double margin) { this.lowerMargin = margin; fireChangeEvent(true); } /** * Returns the size of the upper margin that is added by the auto-range * calculation, as a percentage of the data range. This margin is used to * prevent data items from being plotted right at the edges of the chart. * The default value is {@code 0.05} (five percent). * * @return The upper margin. */ public double getUpperMargin() { return this.upperMargin; } /** * Sets the size of the upper margin that will be added by the auto-range * calculation and sends an {@link Axis3DChangeEvent} to all registered * listeners. * * @param margin the margin as a percentage of the data range * (0.05 = five percent). * * @see #setLowerMargin(double) */ public void setUpperMargin(double margin) { this.upperMargin = margin; fireChangeEvent(true); } /** * Returns the default range used when the {@code autoAdjustRange} * flag is {@code true} but the dataset contains no values. The * default range is {@code (0.0 to 1.0)}, depending on the context * you may want to change this. * * @return The default range (never {@code null}). * * @see #setDefaultAutoRange(com.orsoncharts.Range) */ public Range getDefaultAutoRange() { return this.defaultAutoRange; } /** * Sets the default range used when the {@code autoAdjustRange} * flag is {@code true} but the dataset contains no values, and sends * an {@link Axis3DChangeEvent} to all registered listeners. * * @param range the range ({@code null} not permitted). * * @see #getDefaultAutoRange() */ public void setDefaultAutoRange(Range range) { ArgChecks.nullNotPermitted(range, "range"); this.defaultAutoRange = range; fireChangeEvent(true); } /** * Returns the minimum length for the axis range when auto-calculated. * The default value is 0.001. * * @return The minimum length. * * @since 1.4 */ public double getMinAutoRangeLength() { return this.minAutoRangeLength; } /** * Sets the minimum length for the axis range when it is auto-calculated * and sends a change event to all registered listeners. * * @param length the new minimum length. * * @since 1.4 */ public void setMinAutoRangeLength(double length) { ArgChecks.positiveRequired(length, "length"); this.minAutoRangeLength = length; fireChangeEvent(this.range.getLength() < length); } /** * Returns the flag that determines whether or not the order of values on * the axis is inverted. The default value is {@code false}. * * @return A boolean. * * @since 1.5 */ @Override public boolean isInverted() { return this.inverted; } /** * Sets the flag that determines whether or not the order of values on the * axis is inverted, 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); } /** * 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. In general, {@code PARALLEL} is the * best setting for X and Z axes, and {@code PERPENDICULAR} is the * best setting for Y axes. * * @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 tick label factor, a multiplier for the label height to * determine the maximum number of tick labels that can be displayed. * The default value is {@code 1.4}. * * @return The tick label factor. */ public double getTickLabelFactor() { return this.tickLabelFactor; } /** * Sets the tick label factor and sends an {@link Axis3DChangeEvent} * to all registered listeners. This should be at least 1.0, higher values * will result in larger gaps between the tick marks. * * @param factor the factor. */ public void setTickLabelFactor(double factor) { this.tickLabelFactor = factor; fireChangeEvent(false); } /** * Returns the tick label offset, the gap between the tick marks and the * tick labels (in Java2D units). The default value is {@code 5.0}. * * @return The tick label offset. */ public double getTickLabelOffset() { return this.tickLabelOffset; } /** * Sets the tick label offset and sends an {@link Axis3DChangeEvent} to * all registered listeners. * * @param offset the offset. */ public void setTickLabelOffset(double offset) { this.tickLabelOffset = offset; } /** * Returns the length of the tick marks (in Java2D units). The default * value is {@code 3.0}. * * @return The length of the tick marks. */ public double getTickMarkLength() { return this.tickMarkLength; } /** * Sets the length of the tick marks and sends an {@link Axis3DChangeEvent} * to all registered listeners. You can set this to {@code 0.0} if * you prefer no tick marks to be displayed on the axis. * * @param length the length (in Java2D units). */ public void setTickMarkLength(double length) { this.tickMarkLength = length; fireChangeEvent(false); } /** * Returns the stroke used to draw the tick marks. The default value is * {@code BasicStroke(0.5f)}. * * @return The tick mark stroke (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 paint used to draw the tick marks. The default value is * {@code Color.GRAY}. * * @return The tick mark paint (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); } /** * Configures the axis to be used as the value axis for the specified * plot. This method is used internally, you should not need to call it * directly. * * @param plot the plot ({@code null} not permitted). */ @Override @SuppressWarnings("unchecked") public void configureAsValueAxis(CategoryPlot3D plot) { this.configuredType = ValueAxis3DType.VALUE; if (this.autoAdjustRange) { CategoryDataset3D dataset = plot.getDataset(); Range valueRange = plot.getRenderer().findValueRange(dataset); if (valueRange != null) { updateRange(adjustedDataRange(valueRange)); } else { updateRange(this.defaultAutoRange); } } } /** * Configures the axis to be used as the x-axis for the specified plot. * This method is used internally, you should not need to call it * directly. * * @param plot the plot ({@code null} not permitted). */ @Override public void configureAsXAxis(XYZPlot plot) { this.configuredType = ValueAxis3DType.X; if (this.autoAdjustRange) { Range xRange = plot.getRenderer().findXRange(plot.getDataset()); if (xRange != null) { updateRange(adjustedDataRange(xRange)); } else { updateRange(this.defaultAutoRange); } } } /** * Configures the axis to be used as the y-axis for the specified plot. * This method is used internally, you should not need to call it * directly. * * @param plot the plot ({@code null} not permitted). */ @Override public void configureAsYAxis(XYZPlot plot) { this.configuredType = ValueAxis3DType.Y; if (this.autoAdjustRange) { Range yRange = plot.getRenderer().findYRange(plot.getDataset()); if (yRange != null) { updateRange(adjustedDataRange(yRange)); } else { updateRange(this.defaultAutoRange); } } } /** * Configures the axis to be used as the z-axis for the specified plot. * This method is used internally, you should not need to call it * directly. * * @param plot the plot ({@code null} not permitted). */ @Override public void configureAsZAxis(XYZPlot plot) { this.configuredType = ValueAxis3DType.Z; if (this.autoAdjustRange) { Range zRange = plot.getRenderer().findZRange(plot.getDataset()); if (zRange != null) { updateRange(adjustedDataRange(zRange)); } else { updateRange(this.defaultAutoRange); } } } /** * Adjusts the range by adding the lower and upper margins and taking into * account any other settings. * * @param range the range ({@code null} not permitted). * * @return The adjusted range. */ protected abstract Range adjustedDataRange(Range range); /** * 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 ValueMarker getMarker(String key) { return this.valueMarkers.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 * (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, ValueMarker marker) { ValueMarker existing = this.valueMarkers.get(key); if (existing != null) { existing.removeChangeListener(this); } this.valueMarkers.put(key, marker); marker.addChangeListener(this); fireChangeEvent(false); } /** * Returns a new map containing the markers assigned to this axis. * * @return A map. * * @since 1.2 */ public Map<String, ValueMarker> getMarkers() { return new LinkedHashMap<String, ValueMarker>(this.valueMarkers); } /** * 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>(); Range range = getRange(); for (Map.Entry<String, ValueMarker> entry : this.valueMarkers.entrySet()) { ValueMarker vm = entry.getValue(); if (range.intersects(vm.getRange())) { MarkerData markerData; if (vm instanceof NumberMarker) { NumberMarker nm = (NumberMarker) vm; markerData = new MarkerData(entry.getKey(), range.percent(nm.getValue())); markerData.setLabelAnchor(nm.getLabel() != null ? nm.getLabelAnchor() : null); } else if (vm instanceof RangeMarker) { RangeMarker rm = (RangeMarker) vm; double startValue = rm.getStart().getValue(); boolean startPegged = false; if (!range.contains(startValue)) { startValue = range.peggedValue(startValue); startPegged = true; } double startPos = range.percent(startValue); double endValue = rm.getEnd().getValue(); boolean endPegged = false; if (!range.contains(endValue)) { endValue = range.peggedValue(endValue); endPegged = true; } double endPos = range.percent(endValue); markerData = new MarkerData(entry.getKey(), startPos, startPegged, endPos, endPegged); markerData.setLabelAnchor(rm.getLabel() != null ? rm.getLabelAnchor() : null); } else { throw new RuntimeException("Unrecognised marker."); } 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 (ValueMarker marker : this.valueMarkers.values()) { marker.receive(visitor); } visitor.visit(this); } @Override public boolean equals(Object obj) { if (obj == this) { return true; } if (!(obj instanceof AbstractValueAxis3D)) { return false; } AbstractValueAxis3D that = (AbstractValueAxis3D) obj; if (!this.range.equals(that.range)) { return false; } if (this.autoAdjustRange != that.autoAdjustRange) { return false; } if (this.lowerMargin != that.lowerMargin) { return false; } if (this.upperMargin != that.upperMargin) { return false; } if (!this.defaultAutoRange.equals(that.defaultAutoRange)) { return false; } if (this.tickLabelOffset != that.tickLabelOffset) { return false; } if (this.tickLabelFactor != that.tickLabelFactor) { return false; } if (!this.tickLabelOrientation.equals(that.tickLabelOrientation)) { 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; } 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); } }