chart
Version:
event based time series charting API
430 lines (415 loc) • 15.5 kB
JavaScript
var Hash = require('hashish');
var mr = require('mrcolor');
var getSpacing = function(windowsize,canvaswidth) {
return Math.floor(canvaswidth / (windowsize-1));
}
exports.getSpacing = getSpacing;
exports.getStartX = function(length,windowsize,canvaswidth) {
var x = undefined;
var spacing = getSpacing(windowsize,canvaswidth);
if (length <= windowsize) {
x = canvaswidth - (spacing * (length-1));
} else
x = 0;
return x;
};
exports.cropData = function(list,windowsize) {
if (list.length < windowsize)
return list
else return list.slice(list.length - windowsize)
};
var colorToString = function(colorobj,alpha) {
var color = colorobj.rgb();
if (alpha !== undefined)
return 'rgba('+color[0]+','+color[1]+','+color[2]+','+alpha+')';
else
return 'rgb('+color[0]+','+color[1]+','+color[2]+')';
};
exports.colorToString = colorToString;
var drawDot = function(params) {
params.ctx.beginPath();
params.ctx.strokeStyle = colorToString(params.color);
params.ctx.arc(params.x, params.y, params.radius, 0, Math.PI*2, false);
params.ctx.stroke();
};
exports.drawDot = drawDot;
exports.drawLine = function(params) {
params.ctx.beginPath();
params.ctx.arc(params.x, params.y, params.radius, 0, Math.PI*2, false);
params.ctx.strokeStyle = params.color;
params.ctx.stroke();
};
exports.drawHorizontalGrid = function(width,height,ctx,color){
var heightchunks = Math.floor(height / 10);
for (var i = 0; i < heightchunks; i++) {
ctx.strokeStyle = color;
ctx.beginPath();
ctx.moveTo(0,i*heightchunks);
ctx.lineTo(width,i*heightchunks);
ctx.stroke();
}
}
var getDateString = function(ms) {
var date = new Date(ms);
var pad = function(str) {
if (str.length == 1)
return '0'.concat(str)
if (str.length === 0)
return '00'
else
return str
};
var hours = date.getHours() % 12;
if (hours === 0)
hours = '12';
var seconds = pad(date.getSeconds());
var minutes = pad(date.getMinutes());
var meridian = date.getHours() >= 12 ? 'pm' : 'am';
return hours +':'.concat(minutes) + ':'.concat(seconds) + meridian;
};
// if specialkey is defined, then we only look at members of list are specialkey
// i.e. list = [{foo:3,bar:9},{foo:4,bar:19}] rangeY(list,'foo'), gets range for just foo.
exports.rangeY = function(list,specialkey) {
// % to top pad so the "peak" isn't at the top of the viewport, but we allow some extra space for better visualization
// var padding = 0.10; // 0.10 = 10%;
var padding = 0;
var minY = undefined;
var maxY = undefined;
for (var i = 0; i < list.length; i++) {
Hash(list[i])
.filter(function(val,key) {
if (specialkey !== undefined)
return (key == specialkey)
return (key !== 'date')
})
.forEach(function(val,key) {
if (minY == undefined)
minY = val;
if (maxY == undefined)
maxY = val;
if (val < minY)
minY = val;
if (val > maxY)
maxY = val;
});
}
maxY = (1 + padding)*maxY;
var spread = undefined;
if ((minY!== undefined) && (maxY !== undefined)) {
spread = maxY - minY;
}
// shift is the amount any value in the interval needs to be shifted by to fall with the interval [0,spread]
var shift = undefined;
if ((minY < 0) && (maxY >= 0)) {
shift = Math.abs(minY);
}
if ((minY < 0) && (maxY < 0)) {
shift = Math.abs(maxY) + Math.abs(minY);
}
if (minY > 0) {
shift = -minY;
}
if (minY == 0)
shift = 0;
return {min:minY,max:maxY,spread:spread,shift:shift}
};
var tick = function() {
var dash = function(ctx,x,y,offset,value,linecolor,labelcolor) {
ctx.fillStyle = labelcolor;
ctx.strokeStyle = linecolor;
ctx.beginPath()
ctx.moveTo(x-offset,y)
ctx.lineTo(x+offset,y);
ctx.stroke();
ctx.fillText(value.toFixed(2),x-40,y+3);
}
var large = function(ctx,x,y,value,linecolor,labelcolor) {
dash(ctx,x,y,6,value,linecolor,labelcolor);
}
var small = function(ctx,x,y,value,linecolor,labelcolor) {
dash(ctx,x,y,2,value,linecolor,labelcolor);
}
return {
large: large,
small: small
}
};
exports.drawYaxis = function(params) {
var canvas = params.canvas;
var ctx = params.ctx;
var range = params.range;
var config = params.config;
var yline = params.yline;
var ylabel = params.ylabel;
var availableHeight = canvas.height - config.padding.top - config.padding.bottom;
ctx.strokeStyle = yline;
ctx.beginPath();
ctx.moveTo(config.axispadding.left,canvas.height-config.padding.bottom);
ctx.lineTo(config.axispadding.left,config.padding.top);
ctx.stroke();
var majordivisions = 4;
var step = range.spread / majordivisions;
for (var i = 0; i <= majordivisions; i++) {
var ticky = (availableHeight) - ((i / majordivisions) * availableHeight);
ticky += config.padding.top;
var value = range.min + (i*step);
tick().large(ctx,config.axispadding.left,ticky,value,yline,ylabel);
}
};
exports.drawYaxisMultiple = function(canvas,ctx,yaxises) {
var idx = 0;
Hash(yaxises).forEach(function(axis,key) {
var x = 5 + (35*idx);
ctx.fillStyle = '#FFF';
ctx.font = '10px sans-serif';
ctx.fillText(axis.range.min.toFixed(2),x,canvas.height);
ctx.fillText(axis.range.max.toFixed(2),x,10);
ctx.strokeStyle = colorToString(axis.color);
ctx.beginPath();
ctx.moveTo(x,canvas.height);
ctx.lineTo(x,0);
ctx.stroke();
var majordivisions = 4;
var step = axis.range.spread / majordivisions;
for (var i = 0; i < majordivisions; i++) {
var ticky = (canvas.height) - ((i / majordivisions) * canvas.height);
var value = axis.range.min + (i*step);
tick().large(ctx,x,ticky,value);
}
idx++;
});
};
exports.clip = function(params) {
var ctx = params.ctx;
var height = params.height;
var config = params.config;
var clipcolor = params.clipcolor;
if (params.type == 'clear')
ctx.clearRect(0,0,config.axispadding.left,height);
if (params.type == 'fill') {
ctx.fillStyle = clipcolor;
ctx.fillRect(0,0,config.axispadding.left,height);
}
};
exports.drawXaxis = function(params) {
var datatodisplay = params.datatodisplay;
var ctx = params.ctx;
var spacing = params.spacing;
var startx = params.startx;
var height = params.height;
var width = params.width;
var config = params.config;
var gridcolor = params.gridcolor;
var xlabel = params.xlabel;
var xline = params.xline;
var doVertGrid = params.doVertGrid || false;
// draw x-axis
ctx.strokeStyle = params.xline;
ctx.beginPath();
ctx.moveTo(0,height - config.padding.bottom);
ctx.lineTo(width,height - config.padding.bottom);
ctx.stroke();
// draw vertical grid
if (doVertGrid === true) {
ctx.fillStyle = xlabel;
ctx.lineWidth = 1;
for (var i = 0; i < datatodisplay.length;i++) {
ctx.strokeStyle = gridcolor;
ctx.beginPath();
var x = startx+i*spacing;
x += 0.5;
ctx.moveTo(x,0);
ctx.lineTo(x,height);
ctx.stroke();
var datestring = getDateString(datatodisplay[i].date);
ctx.fillText(datestring,startx+i*spacing,height-5);
}
}
};
var lastsavedparams = {};
exports.getDisplayPoints = function(params) {
var datatodisplay = params.datatodisplay;
var startx = params.startx;
var spacing = params.spacing;
var height = params.height;
var yaxises = params.yaxises;
var range = params.range;
var config = params.config;
var displayPoints = {};
Hash(yaxises)
.filter(function(obj) {
return (obj.display && obj.display === true)
})
.forEach(function(yaxis,key) {
displayPoints[key] = {};
displayPoints[key].yaxis = yaxis;
displayPoints[key].list = [];
datatodisplay.forEach(function(data,idx) {
var yval = 0;
var ratio = (data[key] + range.shift) / range.spread;
var availableHeight = height - config.padding.top - config.padding.bottom;
if (range.spread !== 0) {
yval = ratio * availableHeight;
}
var displayY = height - yval - config.padding.bottom;
displayPoints[key].list.push({x:startx+(idx*spacing),y:displayY});
},this);
})
;
return displayPoints;
};
// filters datatodisplay for dyanmic ranges based on legend select/deselect
exports.filterDynamicRangeY = function(datatodisplay,yaxises) {
var filtered_list = []; // specifically for dynamic ranges
for (var i = 0; i < datatodisplay.length; i++) {
var item = Hash(datatodisplay[i])
.filter(function(val,key) {
return (key == 'date') || (yaxises[key].display == true)
})
.end;
filtered_list.push(item);
}
var range = exports.rangeY(filtered_list);
return range;
}
exports.draw = function (params) {
lastsavedparams = params;
var datatodisplay = params.datatodisplay;
var startx = params.startx;
var spacing = params.spacing;
var buffer = params.buffer;
var bufferctx = params.bufferctx;
var yaxises = params.yaxises;
var config = params.config;
var rendermode = params.rendermode;
bufferctx.clearRect(0,0,buffer.width,buffer.height);
var range = params.range;
Hash(yaxises)
.filter(function(obj) {
return (obj.display && obj.display === true)
})
.forEach(function(yaxis,key) {
// draw lines
bufferctx.strokeStyle = colorToString(yaxis.color);
bufferctx.fillStyle = colorToString(mr.lighten(yaxis.color),0.5);
datatodisplay.forEach(function(data,idx) {
var yval = 0;
var ratio = (data[key] + range.shift) / range.spread;
var availableHeight = buffer.height - config.padding.top - config.padding.bottom;
if (range.spread !== 0) {
yval = ratio * availableHeight;
}
var displayY = buffer.height - yval - config.padding.bottom;
if (rendermode == 'line' || rendermode == 'linefill') {
if (idx === 0) {
bufferctx.beginPath();
bufferctx.moveTo(startx+idx*spacing,displayY);
} else {
bufferctx.lineTo(startx+(idx*spacing),displayY);
}
if (idx == (datatodisplay.length -1)) {
if (rendermode == 'linefill') {
bufferctx.lineTo(startx+(idx*spacing),buffer.height-config.padding.bottom);
bufferctx.lineTo(startx,buffer.height-config.padding.bottom);
bufferctx.fill();
}
bufferctx.stroke();
}
}
if (rendermode == 'bar') {
bufferctx.beginPath();
var centerx = startx + idx*spacing;
bufferctx.moveTo(centerx-10,displayY);
bufferctx.lineTo(centerx+10,displayY);
bufferctx.lineTo(centerx+10,buffer.height-config.padding.bottom);
bufferctx.lineTo(centerx-10,buffer.height-config.padding.bottom);
bufferctx.lineTo(centerx-10,displayY);
bufferctx.stroke();
bufferctx.fill();
}
},this);
// draw dots
datatodisplay.forEach(function(data,idx) {
var yval = 0;
var ratio = (data[key] + range.shift) / range.spread;
var availableHeight = buffer.height - config.padding.top - config.padding.bottom;
if (range.spread !== 0) {
yval = ratio * availableHeight;
}
var displayY = buffer.height - yval - config.padding.bottom;
drawDot({
x:startx+(idx*spacing),
y:displayY,
radius:3,
ctx:bufferctx,
color:yaxis.color
});
},this);
})
;
};
exports.redraw = function(params) {
lastsavedparams.yaxises = params.yaxises;
exports.draw(lastsavedparams);
};
// completely parallel implementation for multiple y-axises.
// diff log
// changed functions/variables to _multiple
// commented out portions of code are there to indicate the strikethrus from the single axis
var lastsavedparams_multiple = {};
exports.draw_multiple = function (params) {
lastsavedparams_multiple = params;
var datatodisplay = params.datatodisplay;
var startx = params.startx;
var spacing = params.spacing;
var buffer = params.buffer;
var bufferctx = params.bufferctx;
var yaxises = params.yaxises;
bufferctx.clearRect(0,0,buffer.width,buffer.height);
// commmented out because range now comes on the axis
// var range = exports.rangeY(datatodisplay);
Hash(yaxises)
.filter(function(obj) {
return (obj.display && obj.display === true)
})
.forEach(function(yaxis,key) {
// draw lines
bufferctx.strokeStyle = colorToString(yaxis.color);
datatodisplay.forEach(function(data,idx) {
var yval = 0;
// var ratio = (data[key] + range.shift) / range.spread;
var ratio = (data[key] + yaxis.range.shift) / yaxis.range.spread;
if (yaxis.range.spread !== 0) {
yval = ratio * buffer.height;
}
if (idx === 0) {
bufferctx.beginPath();
bufferctx.moveTo(startx+idx*spacing,buffer.height - yval);
} else {
bufferctx.lineTo(startx+(idx*spacing),buffer.height - yval);
}
if (idx == (datatodisplay.length -1)) {
bufferctx.stroke();
}
},this);
// draw dots
datatodisplay.forEach(function(data,idx) {
var yval = 0;
if (yaxis.range.spread !== 0) {
yval = ((data[key] + yaxis.range.shift) / yaxis.range.spread) * buffer.height;
}
drawDot({
x:startx+(idx*spacing),
y:buffer.height - yval,
radius:3,
ctx:bufferctx,
color:yaxis.color
});
},this);
})
;
};
exports.redraw_multiple = function(params) {
lastsavedparams_multiple.yaxises = params.yaxises;
exports.draw_multiple(lastsavedparams_multiple);
};