Home Manual Reference Source Repository

src/utils/scale-tick-intervals.js


/**
 * Code to calculate which values to label in a scale between two
 * endpoints, and how they should be labelled. Based on
 * ScaleTickIntervals from Sonic Visualiser, relicensed for this
 * library. Copyright 2017 QMUL.
 */

export default class ScaleTickIntervals {

  constructor() { }
  
  /**
   * Return an array of objects describing tick locations and labels,
   * each object having "value" (number) and "label" (string)
   * properties. All ticks will be within the range [min, max] and
   * there will be approximately n+1 of them, dividing the range up
   * into n divisions, although this number may vary based on which
   * tick values seem best suited to labelling.
   */
  linear(min, max, n) {
    let instruction = this._linearInstruction(min, max, n);
    return this._explode(instruction);
  }

  _linearInstruction(min, max, n) {
    let display = "auto";
    if (max < min) {
      return this._linearInstruction(max, min, n);
    }
    if (n < 1 || max === min) {
      return {
	initial: min, limit: min, spacing: 1.0,
	roundTo: min, display, precision: 1, logUnmap: false
      };
    }
    if (min !== min || max !== max) {
      // NaNs must be involved
      console.log("ScaleTickIntervals: WARNING: min = " + min + ", max = " + max);
      return [];
    }

    let inc = (max - min) / n;

    const digInc = Math.log10(inc);
    const digMax = Math.log10(Math.abs(max));
    const digMin = Math.log10(Math.abs(min));
    
    const precInc = Math.floor(digInc);
    const roundTo = Math.pow(10.0, precInc);

    if (precInc > -4 && precInc < 4) {
      display = "fixed";
    } else if ((digMax >= -2.0 && digMax <= 3.0) &&
               (digMin >= -3.0 && digMin <= 3.0)) {
      display = "fixed";
    } else {
      display = "scientific";
    }
        
    const precRange = Math.ceil(digMax - digInc);

    let prec = 1;
        
    if (display === "fixed") {
      if (digInc < 0) {
        prec = -precInc;
      } else {
        prec = 0;
      }
    } else {
      prec = precRange;
    }

    let minTick = min;
        
    if (roundTo !== 0.0) {
      inc = Math.round(inc / roundTo) * roundTo;
      if (inc < roundTo) inc = roundTo;
      minTick = Math.ceil(minTick / roundTo) * roundTo;
      if (minTick > max) minTick = max;
    }

    if (display === "scientific" && minTick !== 0.0) {
      const digNewMin = Math.log10(Math.abs(minTick));
      if (digNewMin < digInc) {
        prec = Math.ceil(digMax - digNewMin);
      }
    }

    return {
      initial: minTick, limit: max, spacing: inc,
      roundTo, display, precision: prec, logUnmap: false
    };
  }

  _makeTick(display, precision, value) {
    if (display === "scientific") {
      return { value, label: value.toExponential(precision) };
    } else if (display === "fixed") {
      return { value, label: value.toFixed(precision) };
    } else {
      return { value, label: value.toPrecision(precision) };
    }
  }

  _explode(instruction) {

    if (instruction.spacing === 0.0) {
      return [];
    }

    let eps = 1e-7;
    if (instruction.spacing < eps * 10.0) {
      eps = instruction.spacing / 10.0;
    }

    const max = instruction.limit;
    let n = 0;

    let ticks = [];
        
    while (true) {
      let value = instruction.initial + n * instruction.spacing;
      if (value >= max + eps) {
        break;
      }
      if (instruction.logUnmap) {
        value = Math.pow(10.0, value);
      }
      if (instruction.roundTo !== 0.0) {
        value = instruction.roundTo * Math.round(value / instruction.roundTo);
      }
      ticks.push(this._makeTick(instruction.display,
                                instruction.precision,
                                value));
      ++n;
    }

    return ticks;
  }
}