
org.jfree.chart3d.axis.LogAxis3D Maven / Gradle / Ivy
/* ===========================================================
* Orson Charts : a 3D chart library for the Java(tm) platform
* ===========================================================
*
* (C)opyright 2013-2022, by David Gilbert. All rights reserved.
*
* https://github.com/jfree/orson-charts
*
* 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 .
*
* [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 org.jfree.chart3d.axis;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.font.TextAttribute;
import java.awt.font.TextLayout;
import java.awt.geom.Line2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.text.AttributedString;
import java.text.DecimalFormat;
import java.text.Format;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.HashMap;
import java.util.Map;
import org.jfree.chart3d.Chart3DHints;
import org.jfree.chart3d.data.Range;
import org.jfree.chart3d.graphics2d.TextAnchor;
import org.jfree.chart3d.graphics3d.RenderingInfo;
import org.jfree.chart3d.graphics3d.internal.Utils2D;
import org.jfree.chart3d.internal.Args;
import org.jfree.chart3d.internal.ObjectUtils;
import org.jfree.chart3d.internal.TextUtils;
/**
* A numerical axis with a logarithmic scale.
*
* 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.
*
* @since 1.2
*/
@SuppressWarnings("serial")
public class LogAxis3D extends AbstractValueAxis3D implements ValueAxis3D {
/** The default value for the smallest value attribute. */
public static final double DEFAULT_SMALLEST_VALUE = 1E-100;
/** The logarithm base. */
private double base = 10.0;
/** The logarithm of the base value - cached for performance. */
private double baseLog;
/** The logarithms of the current axis range. */
private Range logRange;
/**
* The smallest value for the axis. In general, only positive values
* can be plotted against a log axis but to simplify the generation of
* bar charts (where the base of the bars is typically at 0.0) the axis
* will return {@code smallestValue} as the translated value for 0.0.
* It is important to make sure there are no real data values smaller
* than this value.
*/
private double smallestValue;
/**
* The symbol used to represent the log base on the tick labels. If this
* is {@code null} the numerical value will be displayed.
*/
private String baseSymbol;
/**
* The number formatter for the base value.
*/
private NumberFormat baseFormatter = new DecimalFormat("0");
/**
* The tick selector (if not {@code null}, then auto-tick selection is
* used).
*/
private TickSelector tickSelector = new NumberTickSelector();
/**
* The tick size. If the tickSelector is not {@code null} then it is
* used to auto-select an appropriate tick size and format.
*/
private double tickSize = 1.0;
/** The tick formatter (never {@code null}). */
private Format tickLabelFormatter = new DecimalFormat("0.0");
/**
* Creates a new log axis with a default base of 10.
*
* @param label the axis label ({@code null} permitted).
*/
public LogAxis3D(String label) {
super(label, new Range(DEFAULT_SMALLEST_VALUE, 1.0));
this.base = 10.0;
this.baseLog = Math.log(this.base);
this.logRange = new Range(calculateLog(DEFAULT_SMALLEST_VALUE),
calculateLog(1.0));
this.smallestValue = DEFAULT_SMALLEST_VALUE;
}
/**
* Returns the logarithmic base value. The default value is {@code 10}.
*
* @return The logarithmic base value.
*/
public double getBase() {
return this.base;
}
/**
* Sets the logarithmic base value and sends an {@code Axis3DChangeEvent}
* to all registered listeners.
*
* @param base the base value.
*/
public void setBase(double base) {
this.base = base;
this.baseLog = Math.log(base);
fireChangeEvent(true);
}
/**
* Returns the base symbol, used in tick labels for the axis. A typical
* value would be "e" when using a natural logarithm scale. If this is
* {@code null}, the tick labels will display the numerical base value.
* The default value is {@code null}.
*
* @return The base symbol (possibly {@code null}).
*/
public String getBaseSymbol() {
return this.baseSymbol;
}
/**
* Sets the base symbol and sends an {@code Axis3DChangeEvent} to all
* registered listeners. If you set this to {@code null}, the tick labels
* will display a numerical representation of the base value.
*
* @param symbol the base symbol ({@code null} permitted).
*/
public void setBaseSymbol(String symbol) {
this.baseSymbol = symbol;
fireChangeEvent(false);
}
/**
* Returns the formatter used for the log base value when it is displayed
* in tick labels. The default value is {@code NumberFormat("0")}.
*
* @return The base formatter (never {@code null}).
*/
public NumberFormat getBaseFormatter() {
return this.baseFormatter;
}
/**
* Sets the formatter for the log base value and sends an
* {@code Axis3DChangeEvent} to all registered listeners.
*
* @param formatter the formatter ({@code null} not permitted).
*/
public void setBaseFormatter(NumberFormat formatter) {
Args.nullNotPermitted(formatter, "formatter");
this.baseFormatter = formatter;
fireChangeEvent(false);
}
/**
* Returns the smallest positive data value that will be represented on
* the axis. This will be used as the lower bound for the axis if the
* data range contains any value from {@code 0.0} up to this value.
*
* @return The smallest value.
*/
public double getSmallestValue() {
return this.smallestValue;
}
/**
* Sets the smallest positive data value that will be represented on the
* axis and sends an {@code Axis3DChangeEvent} to all registered listeners.
*
* @param smallestValue the value (must be positive).
*/
public void setSmallestValue(double smallestValue) {
Args.positiveRequired(smallestValue, "smallestValue");
this.smallestValue = smallestValue;
fireChangeEvent(true);
}
/**
* Returns the tick selector for the axis.
*
* @return The tick selector (possibly {@code null}).
*/
public TickSelector getTickSelector() {
return this.tickSelector;
}
/**
* Sets the tick selector and sends an {@code Axis3DChangeEvent} to all
* registered listeners.
*
* @param selector the selector ({@code null} permitted).
*/
public void setTickSelector(TickSelector selector) {
this.tickSelector = selector;
fireChangeEvent(false);
}
/**
* Returns the tick size to be used when the tick selector is
* {@code null}.
*
* @return The tick size.
*/
public double getTickSize() {
return this.tickSize;
}
/**
* Sets the tick size and sends an {@code Axis3DChangeEvent} to all
* registered listeners.
*
* @param tickSize the new tick size.
*/
public void setTickSize(double tickSize) {
this.tickSize = tickSize;
fireChangeEvent(false);
}
/**
* Returns the tick label formatter. The default value is
* {@code DecimalFormat("0.0")}.
*
* @return The tick label formatter (never {@code null}).
*/
public Format getTickLabelFormatter() {
return this.tickLabelFormatter;
}
/**
* Sets the formatter for the tick labels and sends an
* {@code Axis3DChangeEvent} to all registered listeners.
*
* @param formatter the formatter ({@code null} not permitted).
*/
public void setTickLabelFormatter(Format formatter) {
Args.nullNotPermitted(formatter, "formatter");
this.tickLabelFormatter = formatter;
fireChangeEvent(false);
}
/**
* Sets the range for the axis. This method is overridden to check that
* the range does not contain negative values, and to update the log values
* for the range.
*
* @param range the range ({@code nul} not permitted).
*/
@Override
public void setRange(Range range) {
Args.nullNotPermitted(range, "range");
this.range = new Range(Math.max(range.getMin(), this.smallestValue),
range.getMax());
this.logRange = new Range(calculateLog(this.range.getMin()),
calculateLog(this.range.getMax()));
fireChangeEvent(true);
}
/**
* Sets the range for the axis. This method is overridden to check that
* the range does not contain negative values, and to update the log values
* for the range.
*
* @param min the lower bound for the range.
* @param max the upper bound for the range.
*/
@Override
public void setRange(double min, double max) {
Args.negativeNotPermitted(min, "min");
this.range = new Range(Math.max(min, this.smallestValue), max);
this.logRange = new Range(calculateLog(this.range.getMin()),
calculateLog(this.range.getMax()));
fireChangeEvent(true);
}
@Override
protected void updateRange(Range range) {
this.range = range;
this.logRange = new Range(calculateLog(this.range.getMin()),
calculateLog(this.range.getMax()));
}
/**
* Calculates the log of the given {@code value}, using the current base.
*
* @param value the value (negatives not permitted).
*
* @return The log of the given value.
*
* @see #calculateValue(double)
* @see #getBase()
*/
public final double calculateLog(double value) {
return Math.log(value) / this.baseLog;
}
/**
* Calculates the value from a given log value.
*
* @param log the log value.
*
* @return The value with the given log.
*
* @see #calculateLog(double)
* @see #getBase()
*/
public final double calculateValue(double log) {
return Math.pow(this.base, log);
}
/**
* Translates a data value to a world coordinate, assuming that the axis
* begins at the origin and has the specified length.
*
* @param value the data value.
* @param length the axis length in world coordinates.
*
* @return The world coordinate of this data value on the axis.
*/
@Override
public double translateToWorld(double value, double length) {
double logv = calculateLog(value);
double percent = this.logRange.percent(logv);
if (isInverted()) {
percent = 1.0 - percent;
}
return percent * length;
}
/**
* Draws the axis.
*
* @param g2 the graphics target ({@code null} not permitted).
* @param startPt the starting point.
* @param endPt the ending point.
* @param opposingPt an opposing point (labels will be on the other side
* of the line).
* @param tickData the tick data (including anchor points calculated by
* the 3D engine).
* @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 startPt, Point2D endPt,
Point2D opposingPt, List tickData, RenderingInfo info,
boolean hinting) {
if (!isVisible()) {
return;
}
// draw a line for the axis
g2.setStroke(getLineStroke());
g2.setPaint(getLineColor());
Line2D axisLine = new Line2D.Float(startPt, endPt);
g2.draw(axisLine);
// draw the tick marks and labels
double tickMarkLength = getTickMarkLength();
double tickLabelOffset = getTickLabelOffset();
g2.setPaint(getTickMarkPaint());
g2.setStroke(getTickMarkStroke());
for (TickData t : tickData) {
if (tickMarkLength > 0.0) {
Line2D tickLine = Utils2D.createPerpendicularLine(axisLine,
t.getAnchorPt(), tickMarkLength, opposingPt);
g2.draw(tickLine);
}
}
double maxTickLabelDim = 0.0;
if (getTickLabelsVisible()) {
g2.setFont(getTickLabelFont());
g2.setPaint(getTickLabelColor());
LabelOrientation orientation = getTickLabelOrientation();
if (orientation.equals(LabelOrientation.PERPENDICULAR)) {
maxTickLabelDim = drawPerpendicularTickLabels(g2, axisLine,
opposingPt, tickData, hinting);
} else if (orientation.equals(LabelOrientation.PARALLEL)) {
maxTickLabelDim = g2.getFontMetrics().getHeight();
double adj = g2.getFontMetrics().getAscent() / 2.0;
drawParallelTickLabels(g2, axisLine, opposingPt, tickData, adj,
hinting);
}
}
// draw the axis label (if any)...
if (getLabel() != null) {
/* Shape labelBounds = */drawAxisLabel(getLabel(), g2, axisLine,
opposingPt, maxTickLabelDim + tickMarkLength
+ tickLabelOffset + getLabelOffset(), info, hinting);
}
}
private double drawPerpendicularTickLabels(Graphics2D g2, Line2D axisLine,
Point2D opposingPt, List tickData, boolean hinting) {
double result = 0.0;
for (TickData t : tickData) {
double theta = Utils2D.calculateTheta(axisLine);
double thetaAdj = theta + Math.PI / 2.0;
if (thetaAdj < -Math.PI / 2.0) {
thetaAdj = thetaAdj + Math.PI;
}
if (thetaAdj > Math.PI / 2.0) {
thetaAdj = thetaAdj - Math.PI;
}
Line2D perpLine = Utils2D.createPerpendicularLine(axisLine,
t.getAnchorPt(), getTickMarkLength()
+ getTickLabelOffset(), opposingPt);
double perpTheta = Utils2D.calculateTheta(perpLine);
TextAnchor textAnchor = TextAnchor.CENTER_LEFT;
if (Math.abs(perpTheta) > Math.PI / 2.0) {
textAnchor = TextAnchor.CENTER_RIGHT;
}
double logy = calculateLog(t.getDataValue());
AttributedString as = createTickLabelAttributedString(logy,
this.tickLabelFormatter);
Rectangle2D nonRotatedBounds = new Rectangle2D.Double();
if (hinting) {
Map m = new HashMap<>();
m.put("ref", "{\"type\": \"valueTickLabel\", \"axis\": "
+ axisStr() + ", \"value\": \""
+ t.getDataValue() + "\"}");
g2.setRenderingHint(Chart3DHints.KEY_BEGIN_ELEMENT, m);
}
TextUtils.drawRotatedString(as, g2,
(float) perpLine.getX2(), (float) perpLine.getY2(),
textAnchor, thetaAdj, textAnchor, nonRotatedBounds);
if (hinting) {
g2.setRenderingHint(Chart3DHints.KEY_END_ELEMENT, true);
}
result = Math.max(result, nonRotatedBounds.getWidth());
}
return result;
}
private void drawParallelTickLabels(Graphics2D g2, Line2D axisLine,
Point2D opposingPt, List tickData, double adj,
boolean hinting) {
for (TickData t : tickData) {
double theta = Utils2D.calculateTheta(axisLine);
TextAnchor anchor = TextAnchor.CENTER;
if (theta < -Math.PI / 2.0) {
theta = theta + Math.PI;
anchor = TextAnchor.CENTER;
}
if (theta > Math.PI / 2.0) {
theta = theta - Math.PI;
anchor = TextAnchor.CENTER;
}
Line2D perpLine = Utils2D.createPerpendicularLine(axisLine,
t.getAnchorPt(), getTickMarkLength()
+ getTickLabelOffset() + adj, opposingPt);
double logy = calculateLog(t.getDataValue());
AttributedString as = createTickLabelAttributedString(logy,
this.tickSelector.getCurrentTickLabelFormat());
if (hinting) {
Map m = new HashMap<>();
m.put("ref", "{\"type\": \"valueTickLabel\", \"axis\": "
+ axisStr() + ", \"value\": \""
+ t.getDataValue() + "\"}");
g2.setRenderingHint(Chart3DHints.KEY_BEGIN_ELEMENT, m);
}
TextUtils.drawRotatedString(as, g2,
(float) perpLine.getX2(), (float) perpLine.getY2(),
anchor, theta, anchor, null);
if (hinting) {
g2.setRenderingHint(Chart3DHints.KEY_END_ELEMENT, true);
}
}
}
private AttributedString createTickLabelAttributedString(double logy,
Format exponentFormatter) {
String baseStr = this.baseSymbol;
if (baseStr == null) {
baseStr = this.baseFormatter.format(this.base);
}
String exponentStr = exponentFormatter.format(logy);
AttributedString as = new AttributedString(baseStr + exponentStr);
as.addAttributes(getTickLabelFont().getAttributes(), 0, (baseStr
+ exponentStr).length());
as.addAttribute(TextAttribute.SUPERSCRIPT,
TextAttribute.SUPERSCRIPT_SUPER, baseStr.length(),
baseStr.length() + exponentStr.length());
return as;
}
/**
* Adjusts the range by adding the lower and upper margins on the
* logarithmic range.
*
* @param range the range ({@code nul} not permitted).
*
* @return The adjusted range.
*/
@Override
protected Range adjustedDataRange(Range range) {
Args.nullNotPermitted(range, "range");
double logmin = calculateLog(Math.max(range.getMin(),
this.smallestValue));
double logmax = calculateLog(range.getMax());
double length = logmax - logmin;
double lm = length * getLowerMargin();
double um = length * getUpperMargin();
double lowerBound = calculateValue(logmin - lm);
double upperBound = calculateValue(logmax + um);
return new Range(lowerBound, upperBound);
}
/**
* Selects a standard tick unit on the logarithmic range.
*
* @param g2 the graphics target ({@code null} not permitted).
* @param pt0 the starting point.
* @param pt1 the ending point.
* @param opposingPt an opposing point.
*
* @return The tick unit (log increment).
*/
@Override
public double selectTick(Graphics2D g2, Point2D pt0, Point2D pt1,
Point2D opposingPt) {
if (this.tickSelector == null) {
return this.tickSize;
}
g2.setFont(getTickLabelFont());
FontMetrics fm = g2.getFontMetrics();
double length = pt0.distance(pt1);
double rangeLength = this.logRange.getLength();
LabelOrientation orientation = getTickLabelOrientation();
if (orientation.equals(LabelOrientation.PERPENDICULAR)) {
// based on the font height, we can determine roughly how many tick
// labels will fit in the length available
int height = fm.getHeight();
// the tickLabelFactor allows some control over how dense the labels
// will be
int maxTicks = (int) (length / (height * getTickLabelFactor()));
if (maxTicks > 2 && this.tickSelector != null) {
this.tickSelector.select(rangeLength / 2.0);
// step through until we have too many ticks OR we run out of
// tick sizes
int tickCount = (int) (rangeLength
/ this.tickSelector.getCurrentTickSize());
while (tickCount < maxTicks) {
this.tickSelector.previous();
tickCount = (int) (rangeLength
/ this.tickSelector.getCurrentTickSize());
}
this.tickSelector.next();
this.tickSize = this.tickSelector.getCurrentTickSize();
this.tickLabelFormatter
= this.tickSelector.getCurrentTickLabelFormat();
} else {
this.tickSize = Double.NaN;
}
} else if (orientation.equals(LabelOrientation.PARALLEL)) {
// choose a unit that is at least as large as the length of the axis
this.tickSelector.select(rangeLength);
boolean done = false;
while (!done) {
if (this.tickSelector.previous()) {
// estimate the label widths, and do they overlap?
AttributedString s0 = createTickLabelAttributedString(
this.logRange.getMax() + this.logRange.getMin(),
this.tickSelector.getCurrentTickLabelFormat());
TextLayout layout0 = new TextLayout(s0.getIterator(),
g2.getFontRenderContext());
double w0 = layout0.getAdvance();
AttributedString s1 = createTickLabelAttributedString(
this.logRange.getMax() + this.logRange.getMin(),
this.tickSelector.getCurrentTickLabelFormat());
TextLayout layout1 = new TextLayout(s1.getIterator(),
g2.getFontRenderContext());
double w1 = layout1.getAdvance();
double w = Math.max(w0, w1);
int n = (int) (length / (w * this.getTickLabelFactor()));
if (n < rangeLength
/ tickSelector.getCurrentTickSize()) {
tickSelector.next();
done = true;
}
} else {
done = true;
}
}
this.tickSize = this.tickSelector.getCurrentTickSize();
this.tickLabelFormatter
= this.tickSelector.getCurrentTickLabelFormat();
}
return this.tickSize;
}
/**
* Generates tick data for the axis, assuming the specified tick unit
* (a log increment in this case). If the tick unit is Double.NaN then
* ticks will be added for the bounds of the axis only.
*
* @param tickUnit the tick unit.
*
* @return A list of tick data items.
*/
@Override
public List generateTickData(double tickUnit) {
List result = new ArrayList<>();
if (Double.isNaN(tickUnit)) {
result.add(new TickData(0, getRange().getMin()));
result.add(new TickData(1, getRange().getMax()));
} else {
double logx = tickUnit
* Math.ceil(this.logRange.getMin() / tickUnit);
while (logx <= this.logRange.getMax()) {
result.add(new TickData(this.logRange.percent(logx),
calculateValue(logx)));
logx += tickUnit;
}
}
return result;
}
@Override
public int hashCode() {
int hash = 5;
hash = 59 * hash + (int) (Double.doubleToLongBits(this.base)
^ (Double.doubleToLongBits(this.base) >>> 32));
hash = 59 * hash + (int) (Double.doubleToLongBits(this.smallestValue)
^ (Double.doubleToLongBits(this.smallestValue) >>> 32));
hash = 59 * hash + ObjectUtils.hashCode(this.baseSymbol);
hash = 59 * hash + ObjectUtils.hashCode(this.baseFormatter);
hash = 59 * hash + ObjectUtils.hashCode(this.tickSelector);
hash = 59 * hash + (int) (Double.doubleToLongBits(this.tickSize)
^ (Double.doubleToLongBits(this.tickSize) >>> 32));
hash = 59 * hash + ObjectUtils.hashCode(this.tickLabelFormatter);
return hash;
}
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final LogAxis3D other = (LogAxis3D) obj;
if (Double.doubleToLongBits(this.base)
!= Double.doubleToLongBits(other.base)) {
return false;
}
if (Double.doubleToLongBits(this.smallestValue)
!= Double.doubleToLongBits(other.smallestValue)) {
return false;
}
if (!ObjectUtils.equals(this.baseSymbol, other.baseSymbol)) {
return false;
}
if (!ObjectUtils.equals(this.baseFormatter, other.baseFormatter)) {
return false;
}
if (!ObjectUtils.equals(this.tickSelector, other.tickSelector)) {
return false;
}
if (Double.doubleToLongBits(this.tickSize)
!= Double.doubleToLongBits(other.tickSize)) {
return false;
}
if (!ObjectUtils.equals(this.tickLabelFormatter,
other.tickLabelFormatter)) {
return false;
}
return super.equals(obj);
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy