/* Timeline.js
* Author : Phyks (http://phyks.me)
* http://phyks.github.io/timeline.js
* --------------------------------------------------------------------------------
* "THE NO-ALCOHOL BEER-WARE LICENSE" (Revision 42):
* Phyks (webmaster@phyks.me) wrote this file. As long as you retain this notice you
* can do whatever you want with this stuff (and you can also do whatever you want
* with this stuff without retaining it, but that's not cool...). If we meet some
* day, and you think this stuff is worth it, you can buy me a beer soda
* in return.
* Phyks
* ---------------------------------------------------------------------------------
*/
var SVG = {};
SVG.ns = "http://www.w3.org/2000/svg";
SVG.xlinkns = "http://www.w3.org/1999/xlink";
SVG.marginBottom = 10;
SVG.marginTop = 15;
SVG.marginLeft = 10;
SVG.marginRight = 10;
SVG.rounded = false;
SVG.x_axis = false;
SVG.parent_holder = false;
SVG.holder = false;
SVG.g = false;
SVG.axis = false;
SVG.raw_points = [];
SVG.labels = [];
SVG.x_callback = false;
/* Initialization :
* arg is an object with :
* id = id of the parent block
* height / width = size of the svg
* grid = small / big / both
* x_axis = true / false to show or hide x axis
* rounded = true / false to use splines to smoothen the graph
* x_callback = function(arg) { } or false is called to display the legend on the x axis
*/
SVG.init = function (arg) {
if(!document.implementation.hasFeature("http://www.w3.org/TR/SVG11/feature#Image", "1.1")) {
alert("Your browser does not support embedded SVG.");
}
SVG.parent_holder = document.getElementById(arg.id);
var svg = document.createElementNS(SVG.ns, 'svg:svg');
svg.setAttribute('width', arg.width);
svg.setAttribute('height', arg.height);
svg.setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:xlink', SVG.xlinkns);
SVG.parent_holder.appendChild(svg);
SVG.holder = SVG.parent_holder.querySelector('svg');
var defs = document.createElementNS(SVG.ns, 'defs');
SVG.holder.appendChild(defs);
if(arg.grid === 'small' || arg.grid === 'both') {
var small_grid_pattern = document.createElementNS(SVG.ns, 'pattern');
small_grid_pattern.setAttribute('id', 'smallGrid');
small_grid_pattern.setAttribute('width', 8);
small_grid_pattern.setAttribute('height', 8);
small_grid_pattern.setAttribute('patternUnits', 'userSpaceOnUse');
var small_grid_path = document.createElementNS(SVG.ns, 'path');
small_grid_path.setAttribute('d', 'M 8 0 L 0 0 0 8');
small_grid_path.setAttribute('fill', 'none');
small_grid_path.setAttribute('stroke', 'gray');
small_grid_path.setAttribute('stroke-width', '0.5');
small_grid_pattern.appendChild(small_grid_path);
defs.appendChild(small_grid_pattern);
}
if(arg.grid === 'big' || arg.grid === 'both') {
var grid_pattern = document.createElementNS(SVG.ns, 'pattern');
grid_pattern.setAttribute('id', 'grid');
grid_pattern.setAttribute('width', 80);
grid_pattern.setAttribute('height', 80);
grid_pattern.setAttribute('patternUnits', 'userSpaceOnUse');
if(arg.grid === 'both') {
var grid_rect = document.createElementNS(SVG.ns, 'rect');
grid_rect.setAttribute('width', 80);
grid_rect.setAttribute('height', 80);
grid_rect.setAttribute('fill', 'url(#smallGrid)');
grid_pattern.appendChild(grid_rect);
}
var grid_path = document.createElementNS(SVG.ns, 'path');
grid_path.setAttribute('d', 'M 80 0 L 0 0 0 80');
grid_path.setAttribute('fill', 'none');
grid_path.setAttribute('stroke', 'gray');
grid_path.setAttribute('stroke-width', '1');
grid_pattern.appendChild(grid_path);
defs.appendChild(grid_pattern);
}
SVG.grid = arg.grid;
var marker = document.createElementNS(SVG.ns, 'marker');
marker.setAttribute('id', 'markerArrow');
marker.setAttribute('markerWidth', 13);
marker.setAttribute('markerHeight', 13);
marker.setAttribute('refX', 2);
marker.setAttribute('refY', 6);
marker.setAttribute('orient', 'auto');
var marker_path = document.createElementNS(SVG.ns, 'path');
marker_path.setAttribute('d', 'M2,2 L2,11 L10,6 L2,2');
marker_path.setAttribute('fill', 'gray');
marker.appendChild(marker_path);
defs.appendChild(marker);
SVG.g = document.createElementNS(SVG.ns, 'g');
SVG.g.setAttribute('transform', 'translate(0, ' + SVG.parent_holder.offsetHeight + ') scale(1, -1)');
SVG.holder.appendChild(SVG.g);
if(arg.x_axis === true) {
SVG.axis = document.createElementNS(SVG.ns, 'line');
SVG.axis.setAttribute('x1', SVG.marginLeft);
SVG.axis.setAttribute('x2', SVG.parent_holder.offsetWidth - 13 - SVG.marginRight);
SVG.axis.setAttribute('stroke', 'gray');
SVG.axis.setAttribute('stroke-width', 3);
SVG.axis.setAttribute('marker-end', 'url("#markerArrow")');
SVG.g.appendChild(SVG.axis);
}
if(SVG.grid !== "none") {
var grid = document.createElementNS(SVG.ns, 'rect');
grid.setAttribute('width', "100%");
grid.setAttribute('height', "100%");
if(SVG.grid === 'big' || SVG.grid === 'both') {
grid.setAttribute('fill', 'url(#grid)');
}
else {
grid.setAttribute('fill', 'url(#smallGrid)');
}
SVG.g.appendChild(grid);
}
SVG.rounded = arg.rounded;
SVG.x_axis = arg.x_axis;
SVG.x_callback = arg.x_callback;
SVG.parent_holder.addEventListener('mousemove', function(e) {
var evt = e || window.event;
var rect = false;
// Reinitialize all states
var rects = SVG.holder.querySelectorAll('.over');
for(rect = 0; rect < rects.length; rect ++) {
SVG.holder.getElementById(rects[rect].getAttribute('id').replace('over', 'point')).setAttribute('r', '4');
if(SVG.labels[graph][parseInt(rects[rect].getAttribute('id').replace('over_', ''))] !== '') {
SVG.holder.getElementById(rects[rect].getAttribute('id').replace('over', 'label')).setAttribute('display', 'none');
}
}
SVG.overEffect(evt.clientX, evt.clientY);
});
}
SVG.hasClass = function (element, cls) {
return (' ' + element.getAttribute('class') + ' ').indexOf(' ' + cls + ' ') > -1;
}
SVG.overEffect = function(x, y) {
if(!document.elementFromPoint(x, y)) {
return;
}
// Recursive function to pass event to all superposed rects
var rect = document.elementFromPoint(x, y);
if(!SVG.hasClass(rect, 'over')) {
return;
}
// Handle the event on current rect
SVG.holder.getElementById(rect.getAttribute('id').replace('over', 'point')).setAttribute('r', '6');
if(SVG.labels[graph][parseInt(rect.getAttribute('id').replace('over_', ''))] !== '') {
SVG.holder.getElementById(rect.getAttribute('id').replace('over', 'label')).setAttribute('display', 'block');
}
// Hide it
rect.setAttribute('display', 'none');
// Recursive call
SVG.overEffect(x, y);
// Display again the rect element
rect.setAttribute('display', 'block');
}
SVG.newCoordinates = function(value, min, max, minValue, maxValue) {
var a = (maxValue - minValue) / (max - min);
return a * value - a * min + minValue;
}
SVG.scale = function(data) {
var minX = new Array(), minY = new Array();
var maxX = new Array(), maxY = new Array();
var x = 0, y = 0;
var circle = false, last_point = false, line = false;
var tmp_minX = false;
var tmp_minY = 0;
var tmp_maxX = false;
var tmp_maxY = false;
for(graph in data) {
tmp_minX = false;
tmp_minY = 0;
tmp_maxX = false;
tmp_maxY = false;
for(point = 0; point < data[graph].data.length; point++) {
x = data[graph].data[point][0];
y = data[graph].data[point][1];
if(x < tmp_minX || tmp_minX === false) {
tmp_minX = x;
}
if(x > tmp_maxX || tmp_maxX === false) {
tmp_maxX = x;
}
if(y < tmp_minY) {
tmp_minY = y;
}
if(y > tmp_maxY || tmp_maxY === false) {
tmp_maxY = y;
}
}
minX.push(tmp_minX);
minY.push(tmp_minY);
maxX.push(tmp_maxX);
maxY.push(tmp_maxY);
}
minX = Math.min.apply(null, minX);
minY = Math.min.apply(null, minY);
maxX = Math.max.apply(null, maxX);
maxY = Math.max.apply(null, maxY);
x = SVG.newCoordinates(minX+Math.pow(10, Math.floor(Math.log(maxX - minX) / Math.log(10))), minX, maxX, SVG.marginLeft, SVG.parent_holder.offsetWidth - SVG.marginRight) - SVG.newCoordinates(minX, minX, maxX, SVG.marginLeft, SVG.parent_holder.offsetWidth - SVG.marginRight);
y = SVG.newCoordinates(minY+Math.pow(10, Math.floor(Math.log(maxY - minY) / Math.log(10))), minY, maxY, 2*SVG.marginBottom, SVG.parent_holder.offsetHeight - SVG.marginTop) - SVG.newCoordinates(minY, minY, maxY, 2*SVG.marginBottom, SVG.parent_holder.offsetHeight - SVG.marginTop);
if(SVG.grid === 'big' || SVG.grid === 'both') {
SVG.holder.getElementById('grid').setAttribute('width', x);
SVG.holder.getElementById('grid').setAttribute('height', y);
SVG.holder.getElementById('grid').setAttribute('y', SVG.newCoordinates(Math.floor(minY / Math.pow(10, Math.floor(Math.log(maxY - minY) / Math.log(10)))) * Math.pow(10, Math.floor(Math.log(maxY - minY) / Math.log(10))), minY, maxY, 2*SVG.marginBottom, SVG.parent_holder.offsetHeight - SVG.marginTop));
SVG.holder.getElementById('grid').setAttribute('x', SVG.newCoordinates(Math.floor(minX / Math.pow(10, Math.floor(Math.log(maxX - minX) / Math.log(10)))) * Math.pow(10, Math.floor(Math.log(maxX - minX) / Math.log(10))), minX, maxX, SVG.marginLeft, SVG.parent_holder.offsetWidth - SVG.marginRight));
SVG.holder.getElementById('grid').querySelector('path').setAttribute('d', 'M '+x+' 0 L 0 0 0 '+y);
if(SVG.grid === 'both') {
SVG.holder.getElementById('grid').querySelector('rect').setAttribute('width', x);
SVG.holder.getElementById('grid').querySelector('rect').setAttribute('height', y);
}
}
if(SVG.grid === 'small' || SVG.grid === 'both') {
x = x / 10;
y = y / 10;
SVG.holder.getElementById('smallGrid').setAttribute('width', x);
SVG.holder.getElementById('smallGrid').setAttribute('height', y);
if(SVG.grid === 'small') {
SVG.holder.getElementById('smallGrid').setAttribute('y', SVG.newCoordinates(Math.floor(minY / Math.pow(10, Math.floor(Math.log(maxY - minY) / Math.log(10)))) * Math.pow(10, Math.floor(Math.log(maxY - minY) / Math.log(10))), minY, maxY, 2*SVG.marginBottom, SVG.parent_holder.offsetHeight - SVG.marginTop));
SVG.holder.getElementById('smallGrid').setAttribute('x', SVG.newCoordinates(Math.floor(minX / Math.pow(10, Math.floor(Math.log(maxX - minX) / Math.log(10)))) * Math.pow(10, Math.floor(Math.log(maxX - minX) / Math.log(10))), minX, maxX, SVG.marginLeft, SVG.parent_holder.offsetWidth - SVG.marginRight));
}
SVG.holder.getElementById('smallGrid').querySelector('path').setAttribute('d', 'M '+x+' 0 L 0 0 0 '+y);
}
/* Draw axis */
if(SVG.x_axis === true) {
y = SVG.newCoordinates(0, minY, maxY, 2*SVG.marginBottom, SVG.parent_holder.offsetHeight - SVG.marginTop);
SVG.axis.setAttribute('y1', y);
SVG.axis.setAttribute('y2', y);
}
var returned = new Array();
returned['minX'] = minX;
returned['minY'] = minY;
returned['maxX'] = maxX;
returned['maxY'] = maxY;
return returned;
}
SVG.addGraph = function (graph, color) {
SVG.raw_points[graph] = {};
SVG.raw_points[graph].color = color;
SVG.raw_points[graph].data = new Array();
SVG.labels[graph] = new Array();
};
SVG.addPoints = function (graph, data) {
data.sort(function (a, b) {
if(a.x < b.x) {
return -1;
}
else if(a.x == b.x) {
return 0;
}
else {
return 1;
}
});
for(point = 0; point < data.length; point++) {
SVG.raw_points[graph].data.push([data[point].x, data[point].y]);
if(data[point].label !== 'undefined') {
SVG.labels[graph].push(data[point].label);
}
else {
SVG.labels[graph].push('');
}
}
}
SVG.getControlPoints = function (data) {
// http://www.particleincell.com/wp-content/uploads/2012/06/bezier-spline.js
p1 = new Array();
p2 = new Array();
n = data.length - 1;
/*rhs vector*/
a = new Array();
b = new Array();
c = new Array();
r = new Array();
/*left most segment*/
a[0] = 0;
b[0] = 2;
c[0] = 1;
r[0] = data[0] + 2*data[1];
/*internal segments*/
for (i = 1; i < n - 1; i++) {
a[i] = 1;
b[i] = 4;
c[i] = 1;
r[i] = 4 * data[i] + 2 * data[i+1];
}
/*right segment*/
a[n-1] = 2;
b[n-1] = 7;
c[n-1] = 0;
r[n-1] = 8*data[n-1] + data[n];
/*solves Ax=b with the Thomas algorithm (from Wikipedia)*/
for (i = 1; i < n; i++) {
m = a[i]/b[i-1];
b[i] = b[i] - m * c[i - 1];
r[i] = r[i] - m*r[i-1];
}
p1[n-1] = r[n-1]/b[n-1];
for (i = n - 2; i >= 0; --i) {
p1[i] = (r[i] - c[i] * p1[i+1]) / b[i];
}
/*we have p1, now compute p2*/
for (i=0;i', '').split('');
var i = 0;
var tmp = false;
for(i = 0; i < text.length; i++) {
text[i] = text[i].replace(/(<([^>]+)>)/ig,"").replace('%y', SVG.raw_points[graph].data[point][1]).replace('%x', SVG.raw_points[graph].data[point][0]);
if(i % 2 == 0) {
element.appendChild(document.createTextNode(text[i]));
}
else {
tmp = document.createElementNS(SVG.ns, 'tspan');
tmp.setAttribute('dy', '-5');
tmp.appendChild(document.createTextNode(text[i]));
element.appendChild(tmp);
}
}
var path = document.createElementNS(SVG.ns, 'path');
path.setAttribute('stroke', 'black');
path.setAttribute('stroke-width', 2);
path.setAttribute('fill', 'white');
path.setAttribute('opacity', 0.5);
// Append here to have them with the good z-index, update their attributes later
g.appendChild(path);
g.appendChild(element);
var x_text = x[point] - element.getBoundingClientRect().width / 2;
var y_text = SVG.parent_holder.offsetHeight - y[point] - 20;
var element_width = element.getBoundingClientRect().width;
var element_height = element.getBoundingClientRect().height;
if(x[point] + element_width / 2 > SVG.parent_holder.offsetWidth) {
x_text = x[point] - element_width - 20;
y_text = SVG.parent_holder.offsetHeight - y[point] + 5;
path.setAttribute('d', 'M '+(x_text - 5)+' '+(y_text + 5)+' L '+(x_text - 5)+' '+(y_text - element_height + 5)+' L '+(x_text + element_width + 5)+' '+(y_text - element_height + 5)+' L '+(x_text + element_width + 5)+' '+(y_text - element_height/2 + 2.5)+' L '+(x_text + element_width + 10)+' '+(y_text - element_height/2 + 5)+' L '+(x_text + element_width + 5)+' '+(y_text - element_height/2 + 7.5)+' L '+(x_text + element_width + 5)+' '+(y_text + 5)+' Z');
}
else if(x[point] - element.getBoundingClientRect().width / 2 < 0) {
x_text = x[point] + 20;
y_text = SVG.parent_holder.offsetHeight - y[point] + 5;
path.setAttribute('d', 'M '+(x_text - 5)+' '+(y_text + 5)+' L '+(x_text - 5)+' '+(y_text - element_height/2 + 7.5)+' L '+(x_text - 10)+' '+(y_text - element_height/2 + 5)+' L '+(x_text - 5)+' '+(y_text - element_height/2 + 2.5)+' L '+(x_text - 5)+' '+(y_text - element_height + 5)+' L '+(x_text + element_width + 5)+' '+(y_text - element_height + 5)+' L '+(x_text + element_width + 5)+' '+(y_text + 5)+' Z');
}
else if(y[point] + element.getBoundingClientRect().height > SVG.parent_holder.offsetHeight) {
x_text = x[point] + 20;
y_text = SVG.parent_holder.offsetHeight - y[point] + 5;
path.setAttribute('d', 'M '+(x_text - 5)+' '+(y_text + 5)+' L '+(x_text - 5)+' '+(y_text - element_height/2 + 7.5)+' L '+(x_text - 10)+' '+(y_text - element_height/2 + 5)+' L '+(x_text - 5)+' '+(y_text - element_height/2 + 2.5)+' L '+(x_text - 5)+' '+(y_text - element_height + 5)+' L '+(x_text + element_width + 5)+' '+(y_text - element_height + 5)+' L '+(x_text + element_width + 5)+' '+(y_text + 5)+' Z');
}
else {
path.setAttribute('d', 'M '+(x_text - 5)+' '+(y_text + 5)+' L '+(x_text - 5)+' '+(y_text - element_height + 5)+' L '+(x_text + element_width + 5)+' '+(y_text - element_height + 5)+' L '+(x_text + element_width + 5)+' '+(y_text + 5)+' L '+(x_text + element_width/2 + 2.5)+' '+(y_text + 5)+' L '+(x_text + element_width/2)+' '+(y_text + 10)+' L '+(x_text + element_width/2 - 2.5)+' '+(y_text + 5)+' Z');
}
element.setAttribute('x', x_text);
element.setAttribute('y', y_text);
g.setAttribute('display', 'none');
}
}
for(point = 0; point < SVG.raw_points[graph].data.length; point++) {
rect = document.createElementNS(SVG.ns, 'rect');
rect.setAttribute('class', 'over');
rect.setAttribute('id', 'over_'+point+'_'+graph);
if(point == 0) {
rect.setAttribute('x', 0);
}
else {
rect.setAttribute('x', (x[point] + x[point - 1]) / 2);
}
rect.setAttribute('y', 0);
rect.setAttribute('fill', 'white');
rect.setAttribute('opacity', '0');
if(point == SVG.raw_points[graph].data.length - 1) {
rect.setAttribute('width', SVG.parent_holder.offsetWidth - (x[point] + x[point - 1])/2);
}
else if(point == 0) {
rect.setAttribute('width', (x[1] + x[0])/2 + SVG.marginLeft);
}
else {
rect.setAttribute('width', (x[point + 1] - x[point - 1])/2);
}
rect.setAttribute('height', '100%');
SVG.g.appendChild(rect);
if(SVG.x_callback !== false) {
element = document.createElementNS(SVG.ns, 'text');
element.setAttribute('class', 'legend_x');
element.setAttribute('fill', 'gray');
element.setAttribute('transform', 'translate(0, ' + SVG.parent_holder.offsetHeight + ') scale(1, -1)');
element.appendChild(document.createTextNode(SVG.x_callback(x[point])));
SVG.g.appendChild(element);
element.setAttribute('x', x[point] - element.getBoundingClientRect().width / 2 + 2.5);
element.setAttribute('y', SVG.parent_holder.offsetHeight - SVG.marginBottom - SVG.newCoordinates(0, scale.minY, scale.maxY, 2*SVG.marginBottom, SVG.parent_holder.offsetHeight - SVG.marginTop));
element = document.createElementNS(SVG.ns, 'line');
element.setAttribute('class', 'legend_x');
element.setAttribute('stroke', 'gray');
element.setAttribute('stroke-width', 2);
element.setAttribute('x1', x[point]);
element.setAttribute('x2', x[point]);
element.setAttribute('y1', SVG.newCoordinates(0, scale.minY, scale.maxY, 2*SVG.marginBottom, SVG.parent_holder.offsetHeight - SVG.marginTop) - 5);
element.setAttribute('y2', SVG.newCoordinates(0, scale.minY, scale.maxY, 2*SVG.marginBottom, SVG.parent_holder.offsetHeight - SVG.marginTop) + 5);
SVG.g.appendChild(element);
}
}
}
}
window.onresize = function() {
if(SVG.g !== false) {
SVG.g.setAttribute('transform', 'translate(0, ' + SVG.parent_holder.offsetHeight + ') scale(1, -1)');
if(SVG.x_axis === true) {
SVG.axis.setAttribute('x2', SVG.parent_holder.offsetWidth - 13 - SVG.marginRight);
}
[].forEach.call(SVG.holder.querySelectorAll('.label, .over, .point, .line, .graph, .legend_x'), function(el) {
el.parentNode.removeChild(el);
});
SVG.draw();
}
}