/*
* Copyright 2012 Diamond Light Source Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.csstudio.swt.xygraph.linearscale;
import java.math.BigDecimal;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* Tick factory produces the different axis ticks. When specifying a format and
* given the screen size parameters and range it will return a list of Ticks
*/
public class TickFactory {
public enum TickFormatting {
/**
* Automatically adjust precision
*/
autoMode,
/**
* Rounded or chopped to the nearest decimal
*/
roundAndChopMode,
/**
* Use Exponent
*/
useExponent,
/**
* Use SI units (k,M,G,etc.)
*/
useSIunits,
/**
* Use external scale provider
*/
useCustom;
}
private TickFormatting formatOfTicks;
private final static BigDecimal EPSILON = new BigDecimal("1.0E-20");
private static final int DIGITS_UPPER_LIMIT = 6; // limit for number of digits to display left of decimal point
private static final int DIGITS_LOWER_LIMIT = -6; // limit for number of zeros to display right of decimal point
private static final double ROUND_FRACTION = 2e-6; // fraction of denominator to round to
private static final BigDecimal BREL_ERROR = new BigDecimal("1e-15");
private static final double REL_ERROR = BREL_ERROR.doubleValue();
private double graphMin;
private double graphMax;
private String tickFormat;
private IScaleProvider scale;
private int intervals; // number of intervals
private boolean isReversed;
/**
* @param format
*/
public TickFactory(IScaleProvider scale) {
this(TickFormatting.useCustom, scale);
}
/**
* @param format
*/
public TickFactory(TickFormatting format, IScaleProvider scale) {
formatOfTicks = format;
this.scale = scale;
}
private String getTickString(double value) {
if (scale!=null) value = scale.getLabel(value);
String returnString = "";
if (Double.isNaN(value))
return returnString;
switch (formatOfTicks) {
case autoMode:
returnString = String.format(tickFormat, value);
break;
case useExponent:
returnString = String.format(tickFormat, value);
break;
case roundAndChopMode:
returnString = String.format("%d", Math.round(value));
break;
case useSIunits:
double absValue = Math.abs(value);
if (absValue == 0.0) {
returnString = String.format("%6.2f", value);
} else if (absValue <= 1E-15) {
returnString = String.format("%6.2ff", value * 1E15);
} else if (absValue <= 1E-12) {
returnString = String.format("%6.2fp", value * 1E12);
} else if (absValue <= 1E-9) {
returnString = String.format("%6.2fn", value * 1E9);
} else if (absValue <= 1E-6) {
returnString = String.format("%6.2fµ", value * 1E6);
} else if (absValue <= 1E-3) {
returnString = String.format("%6.2fm", value * 1E3);
} else if (absValue < 1E3) {
returnString = String.format("%6.2f", value);
} else if (absValue < 1E6) {
returnString = String.format("%6.2fk", value * 1E-3);
} else if (absValue < 1E9) {
returnString = String.format("%6.2fM", value * 1E-6);
} else if (absValue < 1E12) {
returnString = String.format("%6.2fG", value * 1E-9);
} else if (absValue < 1E15) {
returnString = String.format("%6.2fT", value * 1E-12);
} else if (absValue < 1E18)
returnString = String.format("%6.2fP", value * 1E-15);
break;
case useCustom:
returnString = scale.format(value);
break;
}
return returnString;
}
private void createFormatString(final int precision, final boolean b) {
switch (formatOfTicks) {
case autoMode:
tickFormat = b ? String.format("%%.%de", precision) : String.format("%%.%df", precision);
break;
case useExponent:
tickFormat = String.format("%%.%de", precision);
break;
default:
tickFormat = null;
break;
}
}
/**
* Round numerator down to multiples of denominators
* @param n numerator
* @param d denominator
* @return
*/
protected static double roundDown(BigDecimal n, BigDecimal d) {
final int ns = n.signum();
if (ns == 0)
return 0;
final int ds = d.signum();
if (ds == 0)
throw new IllegalArgumentException("Zero denominator is not allowed");
n = n.abs();
d = d.abs();
final BigDecimal[] x = n.divideAndRemainder(d);
double rx = x[1].doubleValue();
if (rx > (1-ROUND_FRACTION)*d.doubleValue()) {
// trim up if close to denominator
x[1] = BigDecimal.ZERO;
x[0] = x[0].add(BigDecimal.ONE);
} else if (rx < ROUND_FRACTION*d.doubleValue()) {
x[1] = BigDecimal.ZERO;
}
final int xs = x[1].signum();
if (xs == 0) {
return ns != ds ? -x[0].multiply(d).doubleValue() : x[0].multiply(d).doubleValue();
} else if (xs < 0) {
throw new IllegalStateException("Cannot happen!");
}
if (ns != ds)
return x[0].signum() == 0 ? -d.doubleValue() : -x[0].add(BigDecimal.ONE).multiply(d).doubleValue();
return x[0].multiply(d).doubleValue();
}
/**
* Round numerator up to multiples of denominators
* @param n numerator
* @param d denominator
* @return
*/
protected static double roundUp(BigDecimal n, BigDecimal d) {
final int ns = n.signum();
if (ns == 0)
return 0;
final int ds = d.signum();
if (ds == 0)
throw new IllegalArgumentException("Zero denominator is not allowed");
n = n.abs();
d = d.abs();
final BigDecimal[] x = n.divideAndRemainder(d);
double rx = x[1].doubleValue();
if (rx != 0) {
if (rx < ROUND_FRACTION*d.doubleValue()) {
// trim down if close to zero
x[1] = BigDecimal.ZERO;
} else if (rx > (1-ROUND_FRACTION)*d.doubleValue()) {
x[1] = BigDecimal.ZERO;
x[0] = x[0].add(BigDecimal.ONE);
}
}
final int xs = x[1].signum();
if (xs == 0) {
return ns != ds ? -x[0].multiply(d).doubleValue() : x[0].multiply(d).doubleValue();
} else if (xs < 0) {
throw new IllegalStateException("Cannot happen!");
}
if (ns != ds)
return x[0].signum() == 0 ? 0 : -x[0].multiply(d).doubleValue();
return x[0].add(BigDecimal.ONE).multiply(d).doubleValue();
}
/**
* @param x
* @return floor of log 10
*/
private static int log10(BigDecimal x) {
int c = x.compareTo(BigDecimal.ONE);
int e = 0;
while (c < 0) {
e--;
x = x.scaleByPowerOfTen(1);
c = x.compareTo(BigDecimal.ONE);
}
c = x.compareTo(BigDecimal.TEN);
while (c >= 0) {
e++;
x = x.scaleByPowerOfTen(-1);
c = x.compareTo(BigDecimal.TEN);
}
return e;
}
/**
* @param x
* @param round if true, then round else take ceiling
* @return a nice number
*/
protected static BigDecimal nicenum(BigDecimal x, boolean round) {
int expv; /* exponent of x */
double f; /* fractional part of x */
double nf; /* nice, rounded number */
BigDecimal bf;
expv = log10(x);
bf = x.scaleByPowerOfTen(-expv);
f = bf.doubleValue(); /* between 1 and 10 */
if (round) {
if (f < 1.5)
nf = 1;
else if (f < 2.25)
nf = 2;
else if (f < 7.5)
nf = 5;
else
nf = 10;
}
else if (f <= 1.)
nf = 1;
else if (f <= 2.)
nf = 2;
else if (f <= 5.)
nf = 5;
else
nf = 10;
return BigDecimal.valueOf(BigDecimal.valueOf(nf).scaleByPowerOfTen(expv).doubleValue());
}
private double determineNumTicks(double min, double max, int maxTicks, boolean allowMinMaxOver) {
BigDecimal bMin = BigDecimal.valueOf(min);
BigDecimal bMax = BigDecimal.valueOf(max);
BigDecimal bRange = bMax.subtract(bMin);
if (bRange.signum() < 0) {
BigDecimal bt = bMin;
bMin = bMax;
bMax = bt;
bRange = bRange.negate();
isReversed = true;
} else {
isReversed = false;
}
BigDecimal magnitude = BigDecimal.valueOf(Math.max(Math.abs(min), Math.abs(max)));
// tick points too dense to do anything
if (bRange.compareTo(EPSILON.multiply(magnitude)) < 0) {
return 0;
}
// Important fix: This avoids tick labeller entering an infinite loop
// for some plotting cases.
try {
if (magnitude.doubleValue()<=Double.MIN_VALUE) {
return 0;
}
} catch (Throwable ne) {
// Might be a big number that doubleValue() does not work on - carry on!
}
bRange = nicenum(bRange, false);
BigDecimal bUnit;
int nTicks = maxTicks - 1;
if (Math.signum(min)*Math.signum(max) < 0) {
// straddle case
nTicks++;
}
do {
long n;
do { // ensure number of ticks is less or equal to number requested
bUnit = nicenum(BigDecimal.valueOf(bRange.doubleValue() / nTicks), true);
n = bRange.divideToIntegralValue(bUnit).longValue();
} while (n > maxTicks && --nTicks > 0);
if (allowMinMaxOver) {
graphMin = roundDown(bMin, bUnit);
if (graphMin == 0) // ensure positive zero
graphMin = 0;
graphMax = roundUp(bMax, bUnit);
if (graphMax == 0)
graphMax = 0;
} else {
if (isReversed) {
graphMin = max;
graphMax = min;
} else {
graphMin = min;
graphMax = max;
}
}
if (bUnit.compareTo(BREL_ERROR.multiply(magnitude)) <= 0) {
intervals = -1; // signal that we hit the limit of precision
} else {
intervals = (int) Math.round((graphMax - graphMin) / bUnit.doubleValue());
}
} while (intervals > maxTicks && --nTicks > 0);
if (isReversed) {
double t = graphMin;
graphMin = graphMax;
graphMax = t;
}
double tickUnit = isReversed ? -bUnit.doubleValue() : bUnit.doubleValue();
/**
* We get the labelled max and min for determining the precision which
* the ticks should be shown at.
*/
int d = bUnit.scale() == bUnit.precision() ? -bUnit.scale() : bUnit.precision() - bUnit.scale() - 1;
int p = (int) Math.max(Math.floor(Math.log10(Math.abs(graphMin))), Math.floor(Math.log10(Math.abs(graphMax))));
// System.err.println("P: " + bUnit.precision() + ", S: " +
// bUnit.scale() + " => " + d + ", " + p);
if (p <= DIGITS_LOWER_LIMIT || p >= DIGITS_UPPER_LIMIT) {
createFormatString(Math.max(p - d, 0), true);
} else {
createFormatString(Math.max(-d, 0), false);
}
return tickUnit;
}
private boolean inRange(double x, double min, double max) {
if (isReversed) {
return x >= max && x <= min;
}
return x >= min && x <= max;
}
/**
* Generate a list of ticks that span range given by min and max. The maximum number of
* ticks is exceed by one in the case where the range straddles zero.
* @param min
* @param max
* @param maxTicks
* @param allowMinMaxOver allow min/maximum overwrite
* @param tight if true then remove ticks outside range
* @return a list of the ticks for the axis
*/
public List<Tick> generateTicks(double min, double max, int maxTicks,
boolean allowMinMaxOver, final boolean tight) {
List<Tick> ticks = new ArrayList<Tick>();
double tickUnit = determineNumTicks(min, max, maxTicks, allowMinMaxOver);
if (tickUnit == 0)
return ticks;
for (int i = 0; i <= intervals; i++) {
double p = graphMin + i * tickUnit;
if (Math.abs(p/tickUnit) < REL_ERROR)
p = 0; // ensure positive zero
boolean r = inRange(p, min, max);
if (!tight || r) {
Tick newTick = new Tick();
newTick.setValue(p);
newTick.setText(getTickString(p));
ticks.add(newTick);
}
}
int imax = ticks.size();
if (imax > 1) {
if (!tight && allowMinMaxOver) {
Tick t = ticks.get(imax - 1);
if (!isReversed && t.getValue() < max) { // last is >= max
t.setValue(graphMax);
t.setText(getTickString(graphMax));
}
}
} else if (maxTicks > 1) {
if (imax == 0) {
imax++;
Tick newTick = new Tick();
newTick.setValue(graphMin);
newTick.setText(getTickString(graphMin));
ticks.add(newTick);
}
if (imax == 1) {
Tick t = ticks.get(0);
Tick newTick = new Tick();
if (t.getText().equals(getTickString(graphMax))) {
newTick.setValue(graphMin);
newTick.setText(getTickString(graphMin));
ticks.add(0, newTick);
} else {
newTick.setValue(graphMax);
newTick.setText(getTickString(graphMax));
ticks.add(newTick);
}
imax++;
}
}
double lo = tight ? min : ticks.get(0).getValue();
double hi = tight ? max : (imax > 1 ? ticks.get(imax - 1).getValue() : lo);
double range = imax > 1 ? hi - lo : 1;
if (isReversed) {
for (Tick t : ticks) {
t.setPosition(1 - (t.getValue() - lo) / range);
}
} else {
for (Tick t : ticks) {
t.setPosition((t.getValue() - lo) / range);
}
}
return ticks;
}
private static final DecimalFormat CUSTOM_FORMAT = new DecimalFormat("#####0.000");
private static final DecimalFormat INDEX_FORMAT = new DecimalFormat("0");
/**
* Generate a list of ticks that span range given by min and max.
* @param min
* @param max
* @param maxTicks
* @param tight if true then remove ticks outside range (ignored)
* @return a list of the ticks for the axis
*/
public List<Tick> generateIndexBasedTicks(double min, double max, int maxTicks, boolean tight) {
isReversed = min > max;
if (isReversed) {
double t = max;
max = min;
min = t;
}
List<Tick> ticks = new ArrayList<Tick>();
double gRange = nicenum(BigDecimal.valueOf(max - min), false).doubleValue();
double tickUnit = 1;
intervals = 0;
int it = maxTicks - 1;
while (intervals < 1) {
tickUnit = Math.max(1, nicenum(BigDecimal.valueOf(gRange / it++), true).doubleValue());
tickUnit = Math.floor(tickUnit); // make integer
graphMin = Math.ceil(Math.ceil(min / tickUnit) * tickUnit);
graphMax = Math.floor(Math.floor(max / tickUnit) * tickUnit);
intervals = (int) Math.floor((graphMax - graphMin) / tickUnit);
if (tickUnit == 1) {
break;
}
}
switch (formatOfTicks) {
case autoMode:
tickFormat = "%g";
break;
case useExponent:
tickFormat = "%e";
break;
default:
tickFormat = null;
break;
}
for (int i = 0; i <= intervals; i++) {
double p = graphMin + i * tickUnit;
Tick newTick = new Tick();
newTick.setValue(p);
newTick.setText(getTickString(p));
ticks.add(newTick);
}
int imax = ticks.size();
double range = imax > 1 ? max - min : 1;
for (Tick t : ticks) {
t.setPosition((t.getValue() - min) / range);
}
if (isReversed) {
Collections.reverse(ticks);
}
if (formatOfTicks == TickFormatting.autoMode) { // override labels
if (scale != null && scale.areLabelCustomised()) {
double vmin = Double.POSITIVE_INFINITY;
double vmax = Double.NEGATIVE_INFINITY;
boolean allInts = true;
for (Tick t : ticks) {
double v = Math.abs(scale.getLabel(t.getValue()));
if (Double.isNaN(v))
continue;
if (allInts) {
allInts = Math.abs(v - Math.floor(v)) == 0;
}
v = Math.abs(v);
if (v < vmin && v > 0)
vmin = v;
if (v > vmax)
vmax = v;
}
if (allInts) {
for (Tick t : ticks) {
double v = scale.getLabel(t.getValue());
if (!Double.isNaN(v))
t.setText(INDEX_FORMAT.format(v));
}
} else if (Math.log10(vmin) >= DIGITS_LOWER_LIMIT || Math.log10(vmax) <= DIGITS_UPPER_LIMIT) {
for (Tick t : ticks) {
double v = scale.getLabel(t.getValue());
if (!Double.isNaN(v))
t.setText(CUSTOM_FORMAT.format(v));
}
}
} else {
for (Tick t : ticks) {
t.setText(INDEX_FORMAT.format(t.getValue()));
}
}
}
return ticks;
}
private double determineNumLogTicks(double min, double max, int maxTicks,
boolean allowMinMaxOver) {
isReversed = min > max;
if (isReversed) {
double t = min;
min = max;
max = t;
}
int loDecade = (int) Math.floor(Math.log10(min)); // lowest decade (or power of ten)
int hiDecade = (int) Math.ceil(Math.log10(max));
int decades = hiDecade - loDecade;
int unit;
int n = maxTicks-1;
do {
unit = decades/n--;
} while (unit == 0);
if (allowMinMaxOver) {
graphMin = Math.pow(10, loDecade);
graphMax = Math.pow(10, (n+1)*unit + loDecade);
} else {
graphMin = min;
graphMax = max;
}
intervals = (int) Math.floor(Math.log10(graphMax/graphMin)/unit);
if (isReversed) {
double t = graphMin;
graphMin = graphMax;
graphMax = t;
}
double tickUnit = isReversed ? Math.pow(10, -unit) : Math.pow(10, unit);
if (loDecade < -3 || hiDecade > 3 || decades > 6) {
createFormatString(0, true);
} else {
createFormatString(Math.max(-loDecade, 0), false);
}
return tickUnit;
}
private boolean inRangeLog(double x, double min, double max) {
if (isReversed) {
max -= BREL_ERROR.doubleValue();
return x >= max && x <= min;
}
min -= BREL_ERROR.doubleValue();
return x >= min && x <= max;
}
/**
* @param min (must be >0)
* @param max (must be >0)
* @param maxTicks
* @param allowMinMaxOver allow min/maximum overwrite
* @param tight if true then remove ticks outside range
* @return a list of the ticks for the axis
*/
public List<Tick> generateLogTicks(double min, double max, int maxTicks,
boolean allowMinMaxOver, final boolean tight) {
if (min <= 0 || max <= 0) {
throw new IllegalArgumentException("Non-positive minimum and maximum values are not allowed");
}
List<Tick> ticks = new ArrayList<Tick>();
double tickUnit = determineNumLogTicks(min, max, maxTicks, allowMinMaxOver);
double p = graphMin;
for (int i = 0; i <= intervals; i++) {
boolean r = inRangeLog(p, min, max);
if (!tight || r) {
Tick newTick = new Tick();
newTick.setValue(p);
newTick.setText(getTickString(p));
ticks.add(newTick);
}
p *= tickUnit;
}
int imax = ticks.size();
if (imax < intervals) {
boolean r = inRangeLog(p, min, max);
if (!tight || r) {
Tick newTick = new Tick();
newTick.setValue(p);
newTick.setText(getTickString(p));
ticks.add(newTick);
imax++;
}
}
if (imax > 1) {
if (!tight && allowMinMaxOver) {
Tick t = ticks.get(imax - 1);
if (!isReversed && t.getValue() < max) { // last is >= max
t.setValue(graphMax);
t.setText(getTickString(graphMax));
}
}
} else if (maxTicks > 1) {
if (imax == 0) {
imax++;
Tick newTick = new Tick();
if (isReversed) {
newTick.setValue(graphMax);
newTick.setText(getTickString(graphMax));
} else {
newTick.setValue(graphMin);
newTick.setText(getTickString(graphMin));
}
ticks.add(newTick);
}
if (imax == 1) {
if (!tight && allowMinMaxOver) {
Tick t = ticks.get(0);
Tick newTick = new Tick();
if (t.getText().equals(getTickString(graphMax))) {
double value = graphMin;
newTick.setValue(value);
newTick.setText(getTickString(value));
ticks.add(0, newTick);
} else {
double value = graphMax;
newTick.setValue(value);
newTick.setText(getTickString(value));
ticks.add(newTick);
}
imax++;
}
}
}
if (tickUnit > 1) {
double lo = Math.log(tight ? min : ticks.get(0).getValue());
double hi = Math.log(tight ? max : ticks.get(imax - 1).getValue());
double range = hi - lo;
for (Tick t : ticks) {
t.setPosition((Math.log(t.getValue()) - lo) / range);
}
} else {
double lo = Math.log(tight ? max : ticks.get(0).getValue());
double hi = Math.log(tight ? min : ticks.get(imax - 1).getValue());
double range = hi - lo;
for (Tick t : ticks) {
t.setPosition(1 - (Math.log(t.getValue()) - lo) / range);
}
}
return ticks;
}
}