API Docs for: 0.0.1
Show:

File: lib/graphs/bargraph.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/>.
 */

/*eslint complexity: [2, 24]*/
(function (niviz, moment) {
  'use strict';

  // --- Module Dependencies ---
  var Config   = niviz.Config;
  var Axis     = niviz.Axis;
  var Button   = niviz.Visuals.Button;
  var Gradient = niviz.Gradient;
  var round    = niviz.util.round;
  var t        = niviz.Translate.gettext;
  var extend   = niviz.util.extend;
  var Grid     = niviz.Grid;
  var Value    = niviz.Value;

  /** @module niviz */

  /**
   * Visualization of a singular snow profile as bar graph with optional curves.
   *
   * @class BarGraph
   * @constructor
   *
   * @param {Object} paper SnapSVG paper object
   * @param {Object} properties
   */
  function BarGraph (paper, properties) {
    this.paper = paper;
    this.elements = {};
    this.pngcanvas = document.createElement('canvas');
    this.ctx = this.pngcanvas.getContext('2d');
    this.pngmode = false;
    this.pngtimer = null;

    this.properties = extend({}, BarGraph.defaults.values);
    extend(this.properties, properties);
  }

  BarGraph.defaults = new Config('BarGraph', [
    { name: 'fontsize',   type: 'number', default: 11 },
    { name: 'symbolsize', type: 'number', default: 18 },
    { name: 'grid_color', type: 'color',  default: '#707070' }
  ]);

  BarGraph.defaults.load();

  /**
   * Check if the object with identifier name exists in this.elements and
   * try to remove it from the paper and then delete the object itself.
   *
   * @method remove
   * @private
   * @param {String} name Object identifier
   */
  BarGraph.prototype.remove = function (name, ctx) {
    ctx = ctx || this.elements;
    if (ctx[name] && ctx[name].remove) ctx[name].remove();
    delete ctx[name];
  };

  /**
   * Cleanup the graph by deleting some visual elements and then call
   * configure() to set up valid config again.
   *
   * @method reconfigure
   * @param {Object} properties
   */
  BarGraph.prototype.reconfigure = function (properties) {
    extend(this.properties, properties);
    this.store = {};
    this.range = null;

    ['date', 'labels', 'grid'].forEach(function (e) {
      this.remove(e);
    }, this);

    this.configure();
  };

  /**
   * Calculate important corner points of this BarGraph instance and initialize
   * properties.
   * @method configure
   * @private
   */
  BarGraph.prototype.configure = function () {
    var p = this.properties, primary = p.primary,
      c1 = p.cartesian.pixel(primary.axis, 'hs', primary.min, p.hs.min),
      c2 = p.cartesian.pixel(primary.axis, 'hs', primary.max, p.hs.max);

    p.xleft = c2.x;
    p.origin = { x: c1.x, y: p.top + p.height };
    p.tl = { x: c2.x, y: p.top };
    p.xright = c1.x + p.cart;
    p.cliprect = [c2.x, p.top + 1, c1.x - c2.x, p.height - 1];

    var axis = null;
    if (p.inverted) {
      axis = new Axis(p.hs.min, p.hs.max, p.top, p.top + p.height);
    } else {
      axis = new Axis(p.hs.min, p.hs.max, p.top + p.height, p.top);
    }

    if (!this.range) this.range = {
      min: p.hs.min, max: p.hs.max,
      heights: Grid.smartLegend(p.hs.min, p.hs.max),
      axis: axis
    };

    this.coord = this.coordfunc();
    if (!this.store) this.store = {};
  };

  /**
   * Draw the green bar if necessary, i. e. if the profile height is less than the
   * snow height or if the snow height is zero.
   *
   * @method greenbar
   * @private
   * @param {Object} svg group container to append to
   */
  BarGraph.prototype.greenbar = function (set) {
    var range = this.range, profile = this.profile, paper = this.paper, p = this.properties, y;

    if (range.min < profile.bottom) {
      if (profile.hs === 0 || profile.height < profile.hs) { // green bar
        y = Math.round(range.axis.pixel(profile.bottom));
        set.add(paper.rect(p.xleft - 10, y, p.xright - p.xleft + 20, 20).
                attr({stroke: 'none', fill: '#060', opacity: 0.4}));
      }
    }
  };

  /**
   * Draw grid paths, either on the grid (light and dotted) or as solid lines.
   *
   * @method path
   * @private
   * @param {Boolean} grid true if dotted, false if solid
   * @param {Array<String>} path The path to draw
   * @param {Object} svg group container to append to
   */
  BarGraph.prototype.path = function (grid, path, set) {
    var p = this.properties;

    if (grid) {
      set.add(path.attr({
        'stroke-dasharray': '1,1',
        'stroke': '#bdbdbd',
        'opacity': 0.9,
        fill: 'none'
      }).transform('t0.5,0.5'));
    } else {
      set.add(path.attr({
        'stroke': p.grid_color || '#707070',
        'opacity': 0.9,
        fill: 'none'
      }).transform('t0.5,0.5'));
    }
  };

  /**
   * Draw the grid of the ordinate (snow height) including annotating texts.
   *
   * @method gridy
   * @private
   * @return {Object} svg group container
   */
  BarGraph.prototype.gridy = function () {
    var axis = this.range.axis, paper = this.paper, legend = paper.g(), path = paper.g(),
      set = paper.g(), p = this.properties,
      lpos = (p.hslabel === 'left' ? p.left + p.fontsize : p.right + 8 * p.fontsize / 2);

    this.remove('gridy');

    // HS Legend
    this.range.heights.forEach(function (height, ii) {
      var y = Math.round(axis.pixel(height));
      if (isNaN(y)) return; // safe guard

      if (ii && ii < (this.range.heights.length - 1)) {
        if (height === 0 ) {
          legend.add(paper.line(p.xright - 5, y, p.xleft + 5, y));
        } else {
          path.add(paper.line(p.xright - 5, y, p.xleft + 5, y));
        }
        legend.add(paper.line(p.xright, y, p.xright - 5, y));
        legend.add(paper.line(p.xleft, y, p.xleft + 5, y));
      }

      set.add(paper.text(p.xright + 5, y + p.fontsize / 2 - 2, round(height, 4) + '')
              .attr(p.font).attr({'text-anchor': 'start'}).attr({ opacity: 0.9 }));

    }, this);

    set.add(paper.text(lpos, p.top + p.height / 2, t('Height') + ' [cm]')
            .attr(p.font).transform('r270,' + lpos +','+ (p.top + p.height / 2)).attr({ opacity: 0.9 }));

    this.path(true, path, set);
    this.path(false, legend, set);
    this.greenbar(set); // draw the green bar if necessary
    this.elements['gridy'] = set;

    return set;//.toBack();
  };

  /**
   * Check whether the max and min snow height or the span of the ordinate has changed.
   *
   * @method changed
   * @private
   * @return {Boolean} true if there was a change, false otherwise
   */
  BarGraph.prototype.changed = function () {
    var store = this.store;

    if (store.range && // check if redraw is necessary
        (store.range.min === this.range.min) &&
        (store.range.max === this.range.max) &&
        (store.range.pxmin === this.range.axis.pxmin) &&
        (store.range.pxmax === this.range.axis.pxmax)) return false;

    return true;
  };

  /**
   * Draw the top and bottom grid of the abscissa (user configured)
   * including annotating texts.
   *
   * @method grid
   * @private
   */
  BarGraph.prototype.grid = function () {
    var p = this.properties, cartesian = p.cartesian, paper = this.paper,
      ii, set = paper.g(), o = p.origin, tl = p.tl;

    if (!this.changed()) return;
    this.remove('grid');

    set.add(this.gridy());

    // Outer frame, cart on the right
    var xright = p.xright;
    var legend = paper.g(), path = paper.g();
    legend.add(paper.polyline(o.x, o.y, o.x, tl.y, tl.x, tl.y, tl.x, o.y,
                              o.x, o.y, xright, o.y, xright, tl.y, o.x, tl.y));

    // Legend, top and bottom
    var primary = p.primary, /*secondary = p.secondary,*/
      primepos = primary.pos === 'top' ? tl.y - 7 : o.y + 7 + p.fontsize;

    for (ii = primary.min; ii <= primary.max; ii += primary.inc) {
      var x = Math.round(cartesian.px(primary.axis, ii));
      if (ii > primary.min && ii < primary.max) {

        if (p.use_hhindex_as_primary_axis) { // dotted line to the bottom
          //path.add(paper.line(x, tl.y + 5, x, o.y));
          legend.add(paper.line(x, p.top + .5, x, p.top + 5));
        } else {
          path.add(paper.line(x, tl.y + 5, x, o.y - 5));
          legend.add(paper.line(x, o.y - .5, x, o.y - 5));
          legend.add(paper.line(x, p.top + .5, x, p.top + 5));
        }
      }

      if (!p.nobars && !p.use_hhindex_as_primary_axis) {
        set.add(paper.text(x, primepos, ii + '').attr(p.font).attr({ opacity: 0.9 }));
      } else if (!p.nobars && p.use_hhindex_as_primary_axis && ii) {
        set.add(paper.text(x+1, primepos, Value.Hardness.codes[ii - 1] + '')
           .attr(p.font).attr({ opacity: 0.9 }));
      }
    }

    if (!p.nobars) set.add(paper.text(tl.x + (o.x - tl.x) / 2,
      primary.pos === 'top' ? primepos - 1.5 * p.fontsize : primepos + 1.5 * p.fontsize,
      p.primary.lbl).attr(p.font).attr({ opacity: 0.9 }));

    this.path(true, path, set);
    this.path(false, legend, set);

    this.mouseevents(set);
    this.button();
    this.elements['grid'] = set;//.toBack();

    this.store.range = { min: this.range.min, max: this.range.max,
                         pxmin: this.range.axis.pxmin, pxmax: this.range.axis.pxmax };
  };

  /**
   * Create a cover for the graph area and turn the drag events on
   *
   * @method mouseevents
   * @private
   * @param {Object} svg group container to append the cover to
   */
  BarGraph.prototype.mouseevents = function (set) {
    var p = this.properties, o = p.origin, tl = p.tl;

    if (this.cover) {
      this.mouseon(false);
      this.cover.undrag();
    }

    this.remove('cover', this);

    this.cover = this.paper
      .rect(tl.x, tl.y, o.x - tl.x, o.y - tl.y)
      .attr({ 'opacity': 0.0, 'fill': '#F0F' });
    set.add(this.cover);
    this.draggable(this.cover);
  };

  /**
   * Set width and height for the png canvas, which is an SVG image
   * element
   *
   * @method setupPNG
   * @param {Number} width
   * @param {Number} height
   * @private
   */
  BarGraph.prototype.setupPNG = function (width, height) {
    this.pngcanvas.width = width;
    this.pngcanvas.height = height;
  };

  /**
   * Draw a PNG polygon onto the png canvas
   *
   * @method drawPNGPolygon
   * @param {Array<Number>} path
   * @param {Object} options
   * @private
   */
  BarGraph.prototype.drawPNGPolygon = function (path, options) {
    this.ctx.fillStyle = options.fill;

    this.ctx.beginPath();
    this.ctx.moveTo(path[0], path[1]);
    this.ctx.lineTo(path[2], path[3]);
    this.ctx.lineTo(path[4], path[5]);
    this.ctx.lineTo(path[6], path[7]);
    this.ctx.fill();
    this.ctx.closePath();
  };

  /**
   * Converts the image on the canvas into a base64 encoded dataURI and embeds it in
   * a SVG image tag.
   *
   * @method pastePNG
   * @param {Object} origin has a x and y property
   * @private
   */
  BarGraph.prototype.pastePNG = function (origin) {
    var image = this.elements['image'], p = this.properties;

    if (!image) {
      this.elements['image'] = this.paper.image(this.pngcanvas.toDataURL('image/png'),
                                                0, 0, p.origin.x, p.origin.y);
      this.elements['image'].prependTo(this.paper);

      // The PNG image renders blurry -
      // this style option is there to make the edges crisper:
      this.elements['image'].node.style['image-rendering'] = 'pixelated';
    } else {
      image.attr({ href: this.pngcanvas.toDataURL('image/png'), x: 0, y: 0,
                   width: p.origin.x, height: p.origin.y });
    }
  };

  /**
   * Draw bars into the BarGraph for a certain data with a certain value function.
   * This function takes a value from the profile feature (passed as data) and
   * returns a single number that determines the length of the bar denoted by the
   * axis this.range.axis.
   *
   * @method bar
   * @private
   * @param {Feature} data A niViz feature such as Hardness
   * @param {Function} valuefunc
   * @param {Boolean} drawaspng Whether to draw bars on png canvas or as svg elements
   */
  BarGraph.prototype.bar = function (data, valuefunc, drawaspng) {
    clearTimeout(this.pngtimer);
    this.remove('bars');
    if (this.bars) this.bars.length = 0;

    if (!data) return; // an empty profile

    var paper = this.paper, ii, origin = this.coord(0, this.range.min),
      y = origin.y, h = this.range.min, current, profile = this.profile,
      set = paper.g(), layers = data.layers.length, lbottom = 0, fill,
      minh = this.range.min, maxh = this.range.max, p = this.properties,
      pxmin = this.range.axis.pxmin, pxmax = this.range.axis.pxmax + 1,
      form, gs, value, currenth, layer, r, color, height, path = [], self = this;

    if (drawaspng) this.setupPNG(origin.x, origin.y);
    else this.setupPNG(0, 0);

    this.bars = new Array(layers);
    var mfcrPattern = this.ctx.createPattern(Gradient.patternMFcr, 'repeat');

    for (ii = 0; ii < layers; ++ii) {
      path.length = 0;
      layer = data.layers[ii];
      value = valuefunc(layer);
      current = this.coord(value, layer.top);
      currenth = layer.top;
      current.y = Math.round(current.y * 2) / 2;

      form = profile.grainshape && profile.grainshape.layers[ii] || {};
      gs = profile.grainsize && profile.grainsize.layers[ii] || undefined;

      if (!p.monochrome) {
        fill = color = form.color || '#333';
        if (form.value.primary === 'MFcr' && !drawaspng)
          fill = paper.verticalHatching('#333', color);
        else if (form.value.primary === 'MFcr' && drawaspng)
          fill = mfcrPattern;
      } else {
        fill = color = '#5e97f6';
      }

      if (layer.bottom !== undefined) {
        lbottom = layer.bottom;
        y = this.coord(0, lbottom).y;
      }

      if ((currenth >= maxh && h >= maxh) || (currenth <= minh && h <= minh)) {
        h = layer.top;
        y = current.y;
        continue;
      }

      if (layer.bottom !== undefined) y = this.coord(0, layer.bottom).y;

      y = Math.round(y * 2) / 2;

      if (p.inverted) {
        if (current.y < pxmin) current.y = pxmin;
        if (current.y > pxmax) current.y = pxmax;
        if (y < pxmin) y = pxmin;
        if (y > pxmax) y = pxmax;

        height = current.y - y;
      } else {
        if (current.y > pxmin) current.y = pxmin;
        if (current.y < pxmax) current.y = pxmax;
        if (y > pxmin) y = pxmin;
        if (y < pxmax) y = pxmax;

        height = y - current.y;
      }

      if (height <= 0) height = 0.5;

      if (isNaN(current.x)) current.x = origin.x;

      var bottomLeftX = current.x;

      if (p.hardnessBottom) {
        var hBottom = profile.hardnessBottom && profile.hardnessBottom.layers[ii] || undefined;
        if (hBottom && hBottom.code) {
          bottomLeftX = this.coord(valuefunc(hBottom), hBottom.top).x;
        }
      }

      var ypx = current.y;
      if (p.inverted) {
        ypx -= height;
      } else {
        ypx += height;
      }

      path.push(origin.x, Math.round(current.y),
                current.x, Math.round(current.y),
                bottomLeftX, Math.round(ypx),
                origin.x, Math.round(ypx));

      if (drawaspng) {
        this.pngmode = true;
        this.drawPNGPolygon(path, { fill: fill });
      } else {
        this.pngmode = false;
        r = paper.polyline(path)//rect(current.x, current.y, origin.x - current.x, height)
          .attr({
            //'stroke-width': 0.0,
            //'stroke-opacity': 0.9,
            //stroke: color,
            fill: fill,
            //opacity: 0.9,
            clip: paper.rect(p.cliprect[0], p.cliprect[1], p.cliprect[2], p.cliprect[3]),
            pointerEvents: 'none'
          });

        this.bars[ii] = {
          rect: r,
          color: color,
          layertop: layer.top,
          index: ii,
          layerbottom: layer.bottom !== undefined ?
            layer.bottom : ii ? data.layers[ii - 1].top : lbottom,
          gs: gs ? round(gs.avg, 2) +
            (gs.max && gs.max !== gs.avg ? ' - ' + round(gs.max, 2) : '') : undefined,
          top: current.y,
          center: p.inverted ? y + height / 2 : y - height / 2,
          right: origin.x,
          text: form.code || ''
        };

        h = layer.top;
        y = current.y;
        set.add(r);
      }
    }

    if (drawaspng) this.pastePNG(origin);
    else this.remove('image');

    if (this.pngmode) {
      this.pngtimer = setTimeout(function () {
        self.bar(data, valuefunc, false);
        self.elements['bars'].after(self.elements['curve']);//.after(self.elements['grid']);
      }, 500);
    }
    set.after(self.elements['grid'])
    this.elements['bars'] = set;
  };

  /**
   * Create a coordinate function for the bars.
   *
   * @method coordfunc
   * @private
   * @return {Function} The coordinate calculation function
   */
  BarGraph.prototype.coordfunc = function () {
    var p = this.properties, bars = p.bars;

    return function (x, y) {
      var obj = p.cartesian.pixel(bars.x, bars.y, x, y);
      return { x: obj.x, y: this.range.axis.pixel(y) };
    };
  };

  /**
   * This method enables/disables the mousemove event on the element spanning
   * the graph (this.cover).
   *
   * @method mouseon
   * @private
   * @param {Boolean} on Whether to turn the mousemove event on or off
   */
  BarGraph.prototype.mouseon = function (on) {
    var self = this, p = this.properties, canvas = p.canvas;

    if (on) {
      this.cover.mousemove(function (e) {
        self.mousemove(e, canvas.offset().top);
      });

      this.cover.mouseout(function () {
        clearTimeout(self.timer);
        setTimeout( function () { //remove last highlights
          if (self.rect) self.rect.attr({ 'stroke-width': 0.5,
                                          'opacity': .9, stroke: self.rectcolor });
          self.cleanup();
          self.remove('legend');
        }, 30);
      });

      this.cover.dblclick(function (e) {
        self.mousedblclick.call(self, e, canvas.offset().top);
      });

    } else {
      clearTimeout(self.timer);
      if (self.cover) self.cover.unmousemove();
      if (self.cover) self.cover.unmouseout();
      if (self.cover) self.cover.undblclick();
    }
  };

  /**
   * Method that emits a layer event to the emitter
   * specified in the properties
   *
   * @method mousedblclick
   * @private
   */
  BarGraph.prototype.mousedblclick = function (e, top) {
    var offset = e.pageY - top, coord, current;

    coord = this.range.axis.coord(offset);
    current = this.index(coord);

    if (current && this.properties.emitter)
      this.properties.emitter.emit('layer', current);
  };

  /**
   * Method that generates a function in a closure to deal with
   * the mousemove event. It calls the method BarGraph:highlight and
   * BarGraph:showdot in turn.
   *
   * @method mousemove
   * @private
   * @return {Function} mousemove handler
   */
  BarGraph.prototype.mousemove = function () {
    var last = 0, current, lastdot;

    return function (e, top) {
      clearTimeout(this.timer);
      var offset = e.pageY - top, self = this, coord;

      if (!this.dot) lastdot = undefined;

      this.timer = setTimeout(function () {
        coord = self.range.axis.coord(offset);
        current = self.index(coord);
        last = current ?
          self.highlight.call(self, last, current) :
          self.unhighlight.call(self, last);

        lastdot = self.showdot(coord, lastdot);
      }, 3);
    };
  }();

  /**
   * Remove the highlight on the currently highlighted bar, including the popup.
   * @method unhighlight
   * @private
   */
  BarGraph.prototype.unhighlight = function (last) {
    if (last && last.rect) {
      last.rect.attr({ 'stroke-width': 0.0, 'opacity': 1, stroke: last.color });
      last = undefined;
      this.remove('popup', this);
    }

    return last;
  };

  /**
   * Highlight the current cursor position by showing a popup to the right of
   * the bar currently active. Remove any opacity on active bar.
   *
   * @method highlight
   * @private
   * @param {Object} last Last active bar
   * @param {Object} current Bar to activate
   * @return {Object} Reference to currently active bar
   */
  BarGraph.prototype.highlight = function (last, current) {
    var font = {
      fontSize: this.properties.symbolsize + 'px',
      fill: '#000',
      fontFamily: 'snowsymbolsiacs',
      textAnchor: 'middle'
    }, xpos = this.coord(0, 0).x + 2, p = this.properties;

    if (last && last.rect) {
      last.rect.attr({'stroke-width': 0.0, 'opacity': 1, stroke: last.color });
    }

    this.remove('popup', this);

    this.popup = this.paper.g();

    var lbl = this.paper.text(0, 0, current.text).attr(font);
    var lblgs = current.gs ?
        this.paper.text(0, this.properties.symbolsize,
                        '\u2300 ' + current.gs + ' mm').attr(p.font) : null;
    var lbl2 = this.paper.text(0,
        this.properties.symbolsize + this.properties.fontsize + 2,
          round(current.layertop, 2) + ' / ' + round(current.layerbottom, 2) + ' cm')
        .attr(p.font);

    var set = this.paper.g().add(lbl).add(lblgs).add(lbl2);
    this.popup.add(this.paper.popup(xpos, current.center, set, 'right', 10)
        .attr({
          fill: '#fff', stroke: '#333', 'stroke-width': 1, 'fill-opacity': .9
        }));
    this.popup.add(set);

    current.rect.attr({'stroke-width': 1.0, 'opacity': 1.0, stroke: '#666'});
    this.rect = current.rect;
    this.rectcolor = current.color;
    last = current;

    return last;
  };

  /**
   * Highlight the current cursor position by adding a dot on top of the
   * currently active curve (if activated). Calls BarGraph:showlegend.
   *
   * @method showdot
   * @private
   * @param {Number} coord x-coordinate of dot
   * @param {Number} lastindex Last index of layer array of curve feature
   * @return {Number} Current index of layer array of highlighted curve feature
   */
  BarGraph.prototype.showdot = function (coord, lastindex) {
    if (!this.curvedata || !this.curvedata.index) return undefined;
    var index = this.curvedata.index(coord), current = this.curvedata.layers[index], c,
      p = this.properties;

    if (lastindex === index || index === undefined) return index;

    var value = current.numeric || (current.value && (current.value.avg || current.value));
    if (typeof value !== 'number') return undefined;
    c = {
      x: this.curveaxis.pixel(value),
      y: this.range.axis.pixel(current.top)
    };

    var text = round(value, 6) + ' ' + this.curvedata.unit +
        ' @ ' + round(current.top, 6) + ' cm';
    this.showlegend(this.curvecolor, text);

    if (!this.dot) {
      this.dot = this.paper.circle(c.x, c.y, 2).attr({
        'stroke': this.curvecolor, 'fill': this.curvecolor, 'stroke-width': 1
      });
      this.dot.node.style['pointer-events'] = 'none';
    } else {
      this.dot.attr({ cx: c.x, cy: c.y });
    }

    this.dot.attr({
      clip: this.paper.rect(p.cliprect[0], p.cliprect[1], p.cliprect[2], p.cliprect[3])
    });

    return index;
  };

  /**
   * Highlight the current cursor position by adding a legend denoting the
   * current data value at the position of the dot.
   *
   * @method showlegend
   * @private
   * @param {String} color Color of the legend
   * @param {String} text Actual test to display
   */
  BarGraph.prototype.showlegend = function (color, text) {
    var p = this.properties, tl = p.tl, paper = this.paper,
      legend = this.elements['legend'];

    if (!legend) {
      var set = paper.g(), center = { x: tl.x + 9, y: tl.y + 2.3 * p.fontsize };

      set.add(paper.circle(center.x, center.y, 4).attr({
        'stroke': color, 'fill': color, 'stroke-width': 0
      }));

      var txtval = paper.text(center.x + 10, center.y + p.fontsize / 4, text)
          .attr(p.font).attr({ 'text-anchor': 'start', 'fill': color || '#000' });

      set.add(txtval);
      this.elements['legend'] = set;
      this.legend = txtval;
      txtval.node.style['pointer-events'] = 'none';
    } else {
      this.legend.attr({ text: text });
    }
  };

  /**
   * Calculate the index of the bar that spans the y value passed.
   *
   * @method index
   * @private
   * @param {Number} y y-coordinate (height)
   * @return {Number} Index in this.bars or undefined
   */
  BarGraph.prototype.index = function (y) {
    if (!this.bars || this.bars.length === 0) return undefined;
    var i, ii = this.bars.length;

    if (ii && this.bars[ii - 1] && (y > this.bars[ii - 1].layertop)) return undefined;

    for (i = 0; i < ii; ++i) {
      if (this.bars[i]) {
        if (y > this.bars[i].layerbottom  &&
            y <= this.bars[i].layertop) return this.bars[i];
      }
    }
  };

  /**
   * Create 'Reset zoom' button.
   * @method button
   * @private
   */
  BarGraph.prototype.button = function () {
    var p = this.properties;

    this.remove('resetzoom');
    this.elements['resetzoom'] =
      new Button(this.paper, p.xright - 80, p.top - 35, 'Reset zoom', this, this.reset);
    if (!this.zoomed) this.elements['resetzoom'].attr('display', 'none');
  };

  /**
   * Reset the zoom, redraw ordinate grid, redraw bars.
   * @method reset
   * @private
   */
  BarGraph.prototype.reset = function () {
    var start = Math.round(start), end = Math.round(end), p = this.properties;
    this.mouseon(false);
    this.elements['resetzoom'].attr('display', 'none');
    this.range.min = p.hs.min;
    this.range.max = p.hs.max;
    this.range.points = null;
    this.range.heights = Grid.smartLegend(p.hs.min, p.hs.max);
    this.range.axis.range(p.hs.min, p.hs.max);
    this.zoomed = false;

    this.gridy();
    if (!p.nobars) this.bar(this.profile[p.bars.x], p.valuefunc, false);
    this.callback();
    this.mouseon(true);
  };

  /**
   * Drag functions used for zooming when mouse goes down over this.cover,
   * start drawing selection box.
   *
   * @method draggable
   * @private
   * @param {Element} element element to set up with drag functionality
   */
  BarGraph.prototype.draggable = function (element) {
    var self = this, lastdy = 0, offset = this.properties.canvas.offset(),
      bbox = element.getBBox(), top = offset.top + bbox.y,
      end = offset.top + bbox.y2, clickstart, startpos, endpos,
      left = bbox.x, width = bbox.width;

    element.undrag(); // cleanup
    element.drag(
      function (dx, dy, x, y, event) { //mousedrag
        var my = this;
        y = event.pageY || y;

        this.dragtimer = setTimeout( function () {
          if (lastdy === dy || !my.box) return;
          lastdy = dy;

          if (y >= top && y <= end) {
            my.box.transform('T0,' + Math.min(0, dy));
            my.box.attr('height', Math.abs(dy));

            endpos = y + 1 - top;
          } else if (y > end) {
            my.box.attr('height', Math.abs(bbox.height - startpos));
            endpos = bbox.height;
          } else if (y < top) {
            endpos = 0;
          }
        }, 10);
      },
      function (x, y, event) { //mousestart
        self.remove('box', this);
        y = event.pageY || y;

        offset = self.properties.canvas.offset();
        bbox = element.getBBox();
        top = offset.top + bbox.y;
        end = offset.top + bbox.y2;

        clickstart = +new Date();
        lastdy = 0;

        startpos = y + 1 - top; // the +1 is for index calculation
        this.box = self.paper.rect(left, y - offset.top, width, 0)
          .attr({ stroke: '#9999FF', fill: '#9999FF', opacity: 0.3, pointerEvents: 'none' });
      },
      function (event) { //mouseend
        clearTimeout(this.dragtimer);
        self.remove('box', this);
        if ((+new Date()) - clickstart > 200) { // ignore clicks <= 200ms long
          startpos = self.height(startpos / bbox.height);
          endpos = self.height(endpos / bbox.height);
          if (startpos > endpos) startpos = [endpos, endpos = startpos][0];
          self.zoom(startpos, endpos);
        }
      }
    );
  };

  /**
   * Given a percentage, calculate a corresponding value within
   * this.range.min (0%) and this.range.max (100%).
   *
   * @method height
   * @private
   * @param {Number} reatio A percentage
   * @return {Number}
   */
  BarGraph.prototype.height = function (ratio) {
    if (this.properties.inverted) {
      var min = this.range.min, max = this.range.max, span = min - max;
      return min - ratio * span;
    }

    var min = this.range.min, max = this.range.max, span = max - min;
    return max - ratio * span;
  };

  /**
   * Display date and time of current profile.
   *
   * @method datetime
   * @private
   * @param {Moment} datetime
   */
  BarGraph.prototype.date = function (datetime) {
    var element = this.elements['date'], text = datetime.format('YYYY-MM-DD HH:mm');

    if (element) {
      element.attr('text', text);
    } else {
      var p = this.properties,
        label = this.paper.text(p.xleft, p.top + p.height + 4 * p.fontsize, text)
          .attr(p.font).attr({'text-anchor': 'start'});

      this.elements['date'] = label;
    }
  };

  /**
   * Draw grid for abscissa axis - if curve should be displayed.
   *
   * @method gridx
   * @private
   * @param {Feature} data
   * @param {Axis} axis
   * @param {String} color
   * @param {Function} callback Function that is called on label click
   */
  BarGraph.prototype.gridx = function (data, counter, axis, color, callback) {
    var gridx = this.store.gridx;
    if (gridx) {
      if ((gridx.min === axis.min) &&
          (gridx.max === axis.max) &&
          (gridx.pxmin === axis.pxmin) &&
          (gridx.pxmax === axis.pxmax) &&
          (gridx.type === data.type) &&
          (gridx.counter === counter)) return;
    }

    this.remove('gridx');

    console.dir('redrawing gridx - parameter ' + data. name + ' (' + counter + ')');
    var paper = this.paper, p = this.properties, primary = p.primary,
      o = p.origin, tl = p.tl, set = paper.g(), ii, path = paper.g(),
      primepos = primary.pos === 'top' ? o.y + 7 + p.fontsize / 2 : tl.y - 7 - p.fontsize / 2,
      steps = Math.round(Math.abs(primary.max - primary.min) / primary.inc);

    if (p.use_hhindex_as_primary_axis) {
      var curvegrid = Grid.smartLegend(axis.min <= axis.max ? axis.min : axis.max,
                                       axis.min <= axis.max ? axis.max : axis.min);
      var grid = paper.g();
      steps = curvegrid.length;

      for (ii = 0; ii < steps; ++ii) {
        var x = curvegrid[ii];
        var coord = round(axis.pixel(x), 2);

        if (ii && ii !== steps - 1) {
          path.add(paper.line(coord, tl.y + .5, coord, o.y - 5));
          grid.add(paper.line(coord, p.origin.y, coord, p.origin.y - 5));
        }
        set.add(paper.text(coord + 1, primepos, x + '').attr(p.font).attr({ opacity: 0.9 }));
      }
      this.path(true, path, set);
      grid.attr({ 'stroke': p.grid_color || '#707070' }).transform('t0.5,0.5');
      set.add(grid);
    } else {
      for (ii = 0; ii <= steps; ++ii) {
        var x = p.cartesian.px(primary.axis, ii * primary.inc + primary.min);
        var coord = round(axis.coord(x), 2);
        set.add(paper.text(x + 2, primepos, coord).attr(p.font).attr({ opacity: 0.9 }));
      }
    }

    var lblpos = {
      x: tl.x + (o.x - tl.x) / 2,
      y: primary.pos === 'top' ? primepos + 1.3 * p.fontsize : primepos - 1.3 * p.fontsize
    };

    var clientHeight = this.paper.node.clientHeight, show_method = true;
    if (clientHeight < lblpos.y + p.fontsize) {
      show_method = false;
    }

    var unit = data.unit ? ' [' + data.unit + ']' : '';
    var name = counter ? t(data.name) + ' (' + (counter + 1) + ')' : t(data.name);
    set.add(paper.text(lblpos.x, lblpos.y, name + unit)
            .attr(p.font).attr({fill: color, cursor: 'pointer'}).attr({ opacity: 0.9 })
            .click(function () { callback(); }));

    if (show_method && data.info && data.info.method) {
      set.add(paper.text(lblpos.x, lblpos.y + 1.2 * p.fontsize, t('Method')+ ': ' + data.info.method)
            .attr(p.font).attr({fill: color, cursor: 'pointer'}).attr({ opacity: 0.9 })
            .click(function () { callback(); }));
    }

    this.store.gridx = {
      min: axis.min, max: axis.max, pxmin: axis.pxmin, pxmax: axis.pxmax,
      type: data.type, counter: counter
    };
    this.elements['gridx'] = set;
    //console.dir(this.store.gridx);
  };

  /**
   * Remove the currently displayed curve.
   * @method clearcurve
   */
  BarGraph.prototype.clearcurve = function () {
    if (this.elements['curve']) {
      this.remove('curve');
      this.curvedata = this.curvecolor = this.curveaxis = undefined;
    }
  };

  /**
   * Draw curve for a certin parameter, either as stair case graph or curve.
   *
   * @method curve
   * @param {Feature} data
   * @param {Axis} axis
   * @param {String} color
   * @param {Function} callback Function that is called on label click
   * @param {Boolean} stairs true = display the curve as stair case graph
   */
  BarGraph.prototype.curve = function (data, axis, color, callback, stairs, counter) {
    var profile = this.profile, path = [],
        paper = this.paper, i, ii, current, c, p = this.properties;

    this.curvedata = data;
    this.curvecolor = color;
    this.curveaxis = axis;

    this.remove('curve');

    if (data && data.layers) {

      var last = null, top = null;
      for (i = 0, ii = data.layers.length; i < ii; ++i) {
        current = data.layers[i];

        var val = current.numeric || (current.value && current.value.avg) || current.value;
        if (typeof val !== 'number') continue;
        if (axis.log && val <= 0) continue;
        c = {
          x: axis.pixel(val),
          y: this.range.axis.pixel(current.top)
        };

        if (stairs) {
          if (!path.length) {
            val = (current.bottom || current.bottom === 0) ? current.bottom : profile.bottom;
            var bottom = this.range.axis.pixel(val);
            last = { x: p.origin.x, y: bottom };
            path.push(p.origin.x, bottom);
          }

          if (top && current.bottom && top !== current.bottom) {
            var by = this.range.axis.pixel(current.bottom);
            path.push(p.origin.x, last.y, p.origin.x, by);
            path.push(c.x, by, c.x, c.y);
          } else {
            path.push(c.x, last.y, c.x, c.y);
          }

          if (i === (ii - 1)) path.push(p.origin.x, c.y);

          last = c;
          top = current.top;
        } else {
          if (!path.length) path.push(c.x, c.y);
          else path.push(c.x, c.y);
        }
      }

      var line = paper.polyline(path).attr({
        stroke: color,
        fill: 'none',
        clip: paper.rect(p.cliprect[0], p.cliprect[1], p.cliprect[2], p.cliprect[3]),
        pointerEvents: 'none'
      });
    }

    this.gridx(data, counter, axis, color, callback);
    this.elements['curve'] = line;
  };

  /**
   * Zoom into the range denoted by the start and end parameters.
   *
   * @method zoom
   * @private
   * @param {Number} start Snow height
   * @param {Number} end Snow height
   */
  BarGraph.prototype.zoom = function (start, end) {
    var p = this.properties;

    start = Math.max(Math.floor(start), p.hs.min);
    end = Math.ceil(end);

    if (start === end) return;

    var heights = Grid.smartLegend(start, end);

    if (heights[0] === this.range.min && heights[heights.length - 1] === this.range.max)
      return;

    this.mouseon(false);
    this.range.heights = heights;
    this.range.min = heights[0];
    this.range.max = heights[heights.length - 1];
    this.range.points = heights.length;
    this.range.axis.range(this.range.min, this.range.max);
    this.gridy();

    this.elements['resetzoom'].attr('display', '');
    if (!p.nobars) this.bar(this.profile[p.bars.x], p.valuefunc, false);
    this.callback();

    this.zoomed = true;
    this.mouseon(true);
  };

  /**
   * Call all callbacks registered.
   *
   * @method callback
   * @private
   * @chainable
   */
  BarGraph.prototype.callback = function () {
    this.properties.callbacks.forEach(function (callback) {
      callback.f.apply(this, callback.param);
    }, this);

    return this;
  };

  /**
   * Remove layer popup and curve dot
   *
   * @method draw
   * @chainable
   */
  BarGraph.prototype.cleanup = function () {
    this.remove('popup', this);
    this.dot && this.dot.remove && this.dot.remove();
    this.dot = null;

    return this;
  };

  /**
   * Draw the complete BarGraph.
   *
   * @method draw
   * @param {Profile} profile
   */
  BarGraph.prototype.draw = function (profile, renderaspng) {
    var p = this.properties;

    this.mouseon(false);

    if (profile) this.profile = profile;
    this.cleanup().configure(this.profile);

    this.grid();
    if (!p.nobars) this.bar(this.profile[p.bars.x], p.valuefunc, renderaspng);
    this.callback();
    this.mouseon(true);
    if (this.profile && p.showdate) this.date(this.profile.date);
  };

  /**
   * An object representing a bar, i. e. a layer of  stratographic parameters
   *
   * @class Bar
   * @constructor
   */
  // function Bar () {}


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

}(niviz, moment));