API Docs for: 0.0.1
Show:

File: lib/graphs/grid.js

/*
 * niViz -- snow profiles visualization
 * Copyright (C) 2015 WSL/SLF - Fluelastrasse 11 - 7260 Davos Dorf - Switzerland.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero 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 Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 */

(function (niviz) {
  'use strict';

  // --- Module Dependencies ---
  var Common = niviz.Common;
  var round  = niviz.util.round;
  var log10  = niviz.util.log10;

  /** @module niviz */

  /**
   * The static Grid class encompasses a few static methods to split up an axis
   * into intervals of points in such a way that number aesthetics are preserved.
   *
   * @class Grid
   * @static
   */
  function Grid () {}

  /**
   * This function returns the number of points that should devide an axis
   * of length 'pixels'.
   *
   * @static
   * @method points
   * @param {Number} pixels
   * @return {Number}
   */
  Grid.points = function (pixels) {
    if (pixels < 200) return 5;
    else return 10;
  };

  /**
   * Split up the snow height uniformly into intervals.
   *
   * @method hsgrid
   * @static
   * @param {Station} station
   * @param {Number} [itop] Pixel value of the top
   * @param {Number} [ibottom] Pixel value of the bottom
   * @param {Boolean} [autoscale] Whether to use autoscale (default: no)
   * @return {Object} Object with max, min, divisions, height and heights properties
   */
  Grid.hsgrid = function (station, itop, ibottom, autoscale) {
    var top = station.top, bottom = station.bottom,
      increment, heights = [], i, points = 0;

    if (!autoscale) {
      if (itop !== null && ibottom !== null) {
        top = itop;
        bottom = ibottom;
      } else if (top === 0) { // for profiles without snow height
        bottom = bottom ? Math.ceil((bottom - 20) / 20) * 20 : 0;
      } else {
        top = Math.max(Math.floor((top + 50) / 50) * 50, 200);
        bottom = bottom ? Math.ceil((bottom - 50) / 50) * 50 : 0;
      }

      if (Common.defaults.max_snowheight) top = Common.defaults.max_snowheight;
    } else if (top === 0) { // for profiles without snow height
      bottom = bottom ? Math.ceil((bottom - 20) / 20) * 20 : 0;
    }

    var heights = Grid.smartLegend(bottom, top);

    return {
      max: heights[heights.length - 1],
      min: heights[0],
      divisions: heights.length,
      height: heights[heights.length - 1] - heights[0],
      heights: heights
    };
  };

  /**
   * Split up a given snow height interval uniformly into intervals.
   *
   * @method hsgrid
   * @static
   * @param {Number} [bottom] Bottom height
   * @param {Number} [top] Top height
   * @return {Object} Object with max, min, divisions, height and heights properties
   */
  Grid.hsfixed = function (bottom, top) {
    var increment, heights = [], i, points = 5;

    increment = Math.round((top - bottom) / points);
    for (i = 0; i < points; ++i)
      heights.push(Math.round(bottom + i * increment));

    heights.push(Math.round(top));

    return {
      max: top,
      min: bottom,
      divisions: points + 1,
      height: top - bottom,
      heights: heights
    };
  };

  /**
   * This function returns the min and max to be used when drawing the axis
   * with values ranging from min to max, a pixel span of length pixels is to be
   * utilized.
   *
   * @static
   * @method points
   * @param {Number} min Min value that needs to be on axis
   * @param {Number} max Max value that needs to be on axis
   * @param {Number} pixels Length of axis in pixels
   * @return {Object} Object with min and max property
   */
  Grid.minmax = function (min, max, pixels, points, mag) {
    if (min === undefined || max === undefined) return { min: 0, max: 1 }; //HACK

    var height = Math.max(max - min, 0.1), mag = mag || Math.floor(log10(Math.abs(height))),
        pts = points || Grid.points(pixels), newmax, newmin;

    newmax = Math.floor((max + Math.pow(10, mag)) / Math.pow(10, mag)) * Math.pow(10, mag);
    if (min >= 0)
      newmin = Math.ceil((min - Math.pow(10, mag)) / Math.pow(10, mag)) * Math.pow(10, mag);
    else
      newmin = Math.floor((min - Math.pow(10, mag)) / Math.pow(10, mag)) * Math.pow(10, mag);

    if (max >= 0 && min >= 0) {
      newmin = Math.max(0, newmin);
    } else if (max <= 0 && min <= 0) {
      newmax = Math.min(0, newmax);
    } else { // make sure zero is displayed
      var inc = (newmax - newmin) / pts, top = round(newmax / inc, 2),
          bottom = round(newmin / inc, 2);

      if (parseInt(top) !== top || (parseInt(bottom) !== bottom)) {
        newmax = Math.round(top) * inc;
        newmin = Math.round(bottom) * inc;

        if (newmax < max || newmin > min) {
          var log = Math.floor(log10(inc));
          inc = Math.floor((inc + Math.pow(10, log)) / Math.pow(10, log)) * Math.pow(10, log);

          newmax = Math.round(top) * inc;
          newmin = Math.round(bottom) * inc;
        }
      }
    }

    return { min: newmin, max: newmax };
  };

  /**
   * This function returns the steps to be used when drawing the axis
   * with values ranging from minimum to maximum. Optionally a desired
   * number of points can be handed over as third parameter.
   *
   * NOTE: This function has been copied from trunk/meteoio/Graphics.cc
   *       of the MeteoIO project: see https://models.slf.ch/p/meteoio
   *
   * @static
   * @method smartLegend
   * @param {Number} min Min value that needs to be on axis
   * @param {Number} max Max value that needs to be on axis
   * @param {Number} nb_labels Number of labels desired
   * @return Array<Number> The points that include min max and guarantee a nice display
   */
  Grid.smartLegend = function (minimum, maximum, nb_labels) {
    var range = maximum - minimum, min_norm = minimum, max_norm = maximum,
        decade_mult = null, step_norm, nb_labels_norm, height = 2000;

    nb_labels = nb_labels || 7;

    if (range > 0.) {
      var decade  = Math.floor(log10(range));
      decade_mult = Math.pow(10., decade);
      min_norm    = Math.floor(minimum / decade_mult * 10.) / 10.;
      max_norm    = Math.ceil(maximum / decade_mult * 10.) / 10.; //between 0 & 10
      var range_norm  = max_norm - min_norm; //between 0 & 10
      var step        = range_norm / nb_labels; //between 0 & 10 / number of labels -> between 0 & 1

      // for precision issues, it is necessary to keep step_norm
      // as an interger -> we will always use step_norm/10
      if (step <= 0.1) step_norm = 1;
      else if (step <= 0.2) step_norm=2;
      else if (step <= 0.5) step_norm=5;
      else step_norm = 10;

      if (max_norm >= 0) {
        min_norm = Math.floor(min_norm / step_norm * 10) * step_norm / 10.;
      } else {
        min_norm = Math.ceil(min_norm / step_norm * 10) * step_norm / 10.;
      }

      if (max_norm >= 0) {
        max_norm = Math.ceil(max_norm / step_norm * 10) * step_norm / 10.;
      } else {
        max_norm = Math.floor(max_norm / step_norm * 10) * step_norm / 10.;
      }

      //because min_norm might have been tweaked:
      nb_labels_norm = Math.ceil((max_norm - min_norm) / step_norm * 10) + 1;
    } else {
      step_norm      = 0;
      decade_mult    = 1.;
      nb_labels_norm = 1;
    }

    var heights = [];

    for (var l = 0; l < nb_labels_norm; l++) {
      var level_val = (step_norm * l / 10. + min_norm) * decade_mult;
      if (Math.abs(level_val) < (range * 1e-6)) level_val = 0.; //to get a nice 0 at zero

      if (!Common.defaults.autoscale && Common.defaults.max_snowheight
          && level_val > Common.defaults.max_snowheight) {
        heights.push(Common.defaults.max_snowheight);
        break;
      }

      heights.push(level_val);
    }

    return heights;
  }

  /**
   * This function returns the points for axis labels for a given axis
   * with values ranging from axis.min to axis.max. Both logarithmic and
   * non-logarithmic axes are fully supported.
   *
   * @static
   * @method gridify
   * @param {Axis} axis An axis object representing the ordinate of a paramter
   * @return {Object} Object with Array<Number> heights and min, max, height properties
   */
  Grid.gridify = function (axis) {
    var pts = Grid.points(Math.abs(axis.pxmax - axis.pxmin)),
        height = round(axis.max - axis.min, 2), inc = height / pts, heights = [], i, ii;

    if (axis.log && axis.min === 0) // HACK
      axis.min = Math.pow(10, Math.floor(log10(axis.max)) - 2);

    if (axis.log && axis.max > 0 && axis.min > 0) {
      var expmin = log10(axis.min), expmax = log10(axis.max),
          mag = expmax - expmin, expinc = Math.ceil(mag), tmp = [], h;

      heights.push(axis.min);
      heights.push(axis.max);

      if (mag > 1) {
        for (i = 0, ii = expinc; i <= ii; ++i) {
          if (mag < 3 && i) {
            h = Math.pow(10, Math.ceil(expmin) + i) / 2;
            if (heights.indexOf(height) === -1 ) tmp.push(h);
          }

          h = Math.pow(10, Math.ceil(expmin) + i);
          if (heights.indexOf(h) === -1 ) tmp.push(h);
        }

        heights.splice.apply(heights, [1, 0].concat(tmp));
      } else { // within one magnitude
        while (heights.length < 8) {
          if (heights.length * 2 >= 8) break;

          tmp = [];
          for (i = 1, ii = heights.length; i < ii; ++i) {
            h = round((heights[i] - heights[i - 1]) / 2 + heights[i - 1], 2);
            tmp.push(h);
          }

          for (i = 0, ii = tmp.length; i < ii; ++i)
            heights.splice(2 * i + 1, 0, tmp[i]);
        }
      }
    } else {
      for (i = 0, ii = pts; i <= ii; ++i)
        heights.push(round(axis.min + i * inc, 2));
    }

    return {
      max: axis.max,
      min: axis.min,
      height: height,
      heights: heights
    };
  };

  // --- Module Exports ---
  niviz.Grid = Grid;

}(niviz));