/******************************************************************************* * Copyright (c) 2012, 2017 Diamond Light Source Ltd. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html ******************************************************************************/ package org.eclipse.nebula.visualization.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 { /** * tick formatting modes * */ 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"); /** * limit for number of digits to display left of decimal point */ private static final int DIGITS_UPPER_LIMIT = 6; /** * limit for number of zeros to display right of decimal point */ private static final int DIGITS_LOWER_LIMIT = -6; /** * fraction of denominator to round to */ private static final double ROUND_FRACTION = 2e-6; 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 numberOfIntervals; 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 useExponent) { switch (formatOfTicks) { case autoMode: tickFormat = useExponent ? 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 numerator * @param denominator * @return rounded down value */ protected static double roundDown(BigDecimal numerator, BigDecimal denominator) { final int ns = numerator.signum(); if (ns == 0) return 0; final int ds = denominator.signum(); if (ds == 0) { throw new IllegalArgumentException("Zero denominator is not allowed"); } numerator = numerator.abs(); denominator = denominator.abs(); final BigDecimal[] x = numerator.divideAndRemainder(denominator); double rx = x[1].doubleValue(); if (rx > (1 - ROUND_FRACTION) * denominator.doubleValue()) { // trim up if close to denominator x[1] = BigDecimal.ZERO; x[0] = x[0].add(BigDecimal.ONE); } else if (rx < ROUND_FRACTION * denominator.doubleValue()) { x[1] = BigDecimal.ZERO; } final int xs = x[1].signum(); if (xs == 0) { return ns != ds ? -x[0].multiply(denominator).doubleValue() : x[0].multiply(denominator).doubleValue(); } else if (xs < 0) { throw new IllegalStateException("Cannot happen!"); } if (ns != ds) return x[0].signum() == 0 ? -denominator.doubleValue() : -x[0].add(BigDecimal.ONE).multiply(denominator).doubleValue(); return x[0].multiply(denominator).doubleValue(); } /** * Round numerator up to multiples of denominators * * @param numerator * @param denominator * @return rounded up value */ protected static double roundUp(BigDecimal numerator, BigDecimal denominator) { final int ns = numerator.signum(); if (ns == 0) return 0; final int ds = denominator.signum(); if (ds == 0) { throw new IllegalArgumentException("Zero denominator is not allowed"); } numerator = numerator.abs(); denominator = denominator.abs(); final BigDecimal[] x = numerator.divideAndRemainder(denominator); double rx = x[1].doubleValue(); if (rx != 0) { if (rx < ROUND_FRACTION * denominator.doubleValue()) { // trim down if close to zero x[1] = BigDecimal.ZERO; } else if (rx > (1 - ROUND_FRACTION) * denominator.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(denominator).doubleValue() : x[0].multiply(denominator).doubleValue(); } else if (xs < 0) { throw new IllegalStateException("Cannot happen!"); } if (ns != ds) return x[0].signum() == 0 ? 0 : -x[0].multiply(denominator).doubleValue(); return x[0].add(BigDecimal.ONE).multiply(denominator).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 < 3.25) nf = 2.5; 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()).stripTrailingZeros(); } 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) { numberOfIntervals = -1; // signal that we hit the limit of precision } else { numberOfIntervals = (int) Math.round((graphMax - graphMin) / bUnit.doubleValue()); } } while (numberOfIntervals > 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() < 0 ? bUnit.precision() + bUnit.scale() - 1 : bUnit.scale(); int p = (int) Math.max(Math.floor(Math.log10(Math.abs(graphMin))), Math.floor(Math.log10(Math.abs(graphMax)))); if (p <= DIGITS_LOWER_LIMIT || p >= DIGITS_UPPER_LIMIT) { createFormatString(Math.max(d + p, 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 <= numberOfIntervals; 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 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; numberOfIntervals = 0; int it = maxTicks - 1; while (numberOfIntervals < 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); numberOfIntervals = (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 <= numberOfIntervals; 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.isLabelCustomised()) { 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(scale.format(v)); } } } else { for (Tick t : ticks) { t.setText(INDEX_FORMAT.format(t.getValue())); } } } return ticks; } private final static int LOWEST_LOG_10 = -323; // sub-normal value 4.9e-324 private final static int HIGHEST_LOG_10 = 308; // 1.80e308 private int determineNumLogTicks(double min, double max, int maxTicks, boolean allowMinMaxOver) { isReversed = min > max; if (isReversed) { double t = min; min = max; max = t; } graphMin = Math.log10(min); graphMax = Math.log10(max); int loDecade = (int) Math.floor(graphMin); // lowest decade (or power of // ten) if (loDecade < LOWEST_LOG_10) { loDecade = LOWEST_LOG_10; } int hiDecade = (int) Math.ceil(graphMax); if (hiDecade > HIGHEST_LOG_10) { hiDecade = HIGHEST_LOG_10; } int decades = hiDecade - loDecade; int unit = (int) Math.ceil(1 + decades / (maxTicks + 1)); int n = decades / unit; if (allowMinMaxOver) { graphMin = loDecade; graphMax = n * unit + loDecade; numberOfIntervals = n; } else { numberOfIntervals = (int) Math.floor(graphMax - graphMin) / unit; } if (isReversed) { double t = graphMin; graphMin = graphMax; graphMax = t; } if (loDecade < -3 || hiDecade > 3 || decades > 6) { createFormatString(0, true); } else { createFormatString(Math.max(-loDecade, 0), false); } return isReversed ? -unit : unit; } 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>(); int tickUnit = determineNumLogTicks(min, max, maxTicks, allowMinMaxOver); double p = graphMin; for (int i = 0; i <= numberOfIntervals; i++) { double x = Math.pow(10, p); boolean r = inRangeLog(x, min, max); if (!tight || r) { Tick newTick = new Tick(); newTick.setValue(x); newTick.setText(getTickString(x)); ticks.add(newTick); } p += tickUnit; } int imax = ticks.size(); if (imax < numberOfIntervals) { double x = Math.pow(10, p); boolean r = inRangeLog(x, min, max); if (!tight || r) { Tick newTick = new Tick(); newTick.setValue(x); newTick.setText(getTickString(x)); ticks.add(newTick); imax++; } } if (imax > 1) { if (!tight && allowMinMaxOver) { Tick t = ticks.get(imax - 1); if (!isReversed && t.getValue() < max) { // last is >= max double x = Math.pow(10, graphMax); t.setValue(x); t.setText(getTickString(x)); } } } else if (maxTicks > 1) { if (imax == 0) { imax++; Tick newTick = new Tick(); double x = Math.pow(10, isReversed ? graphMax : graphMin); newTick.setValue(x); newTick.setText(getTickString(x)); ticks.add(newTick); } if (imax == 1) { if (!tight && allowMinMaxOver) { Tick t = ticks.get(0); Tick newTick = new Tick(); double x = Math.pow(10, graphMax); if (t.getText().equals(getTickString(x))) { x = Math.pow(10, graphMin); newTick.setValue(x); newTick.setText(getTickString(x)); ticks.add(0, newTick); } else { newTick.setValue(x); newTick.setText(getTickString(x)); ticks.add(newTick); } imax++; } } } if (tickUnit > 0) { 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; } }