/*
* 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));