480 lines
17 KiB
JavaScript
480 lines
17 KiB
JavaScript
/* *********************************************************************
|
|
rrdGraphPng - make rrdcharts interactive
|
|
|
|
Copyright:
|
|
2015 OETIKER+PARTNER AG http://www.oetiker.ch
|
|
|
|
License:
|
|
Gnu GPL Version 2
|
|
|
|
Version: #VERSION#, #DATE#
|
|
|
|
Authors:
|
|
* Tobias Oetiker (oetiker)
|
|
|
|
* **********************************************************************/
|
|
|
|
/**
|
|
* The rrdGraphPng control turns a static RRD chart into an interactive one
|
|
* all that is required for this to work, is the ability to configure the
|
|
* start and end and possible with and height parameters in the chart URL.
|
|
*
|
|
* See index.html in this directory for inspiration on how to use this library.
|
|
*
|
|
*
|
|
*/
|
|
|
|
|
|
qxWeb.define('rrdGraphPng',{
|
|
extend: qxWeb.$$qx.ui.website.Widget,
|
|
statics: {
|
|
_config : {
|
|
canvasPadding: 100,
|
|
initialStart : (new Date()).getTime() / 1000 - 24*3600,
|
|
initialRange: 24*3600,
|
|
moveZoom: 1,
|
|
autoUpdate: true,
|
|
gridFillStyleA: 'rgba(0,0,0,0.08)',
|
|
gridFillStyleB: 'rgba(255,255,255,0.08)'
|
|
},
|
|
rrdGraphPng: function(cfg){
|
|
var png = new rrdGraphPng(this);
|
|
png.init(cfg);
|
|
return png;
|
|
}
|
|
},
|
|
|
|
construct : function(selector, context) {
|
|
this.base(arguments, selector, context);
|
|
},
|
|
|
|
members : {
|
|
__start: null,
|
|
__range: null,
|
|
__syncJob: null,
|
|
init: function(cfg){
|
|
if (!this.base(arguments)) {
|
|
return false;
|
|
};
|
|
var that = this;
|
|
try {
|
|
throw new Error('');
|
|
} catch (e){
|
|
// this is voodoo, but it does often detect where the javascript file
|
|
// lives and thus we can hope to find the cursor files there too
|
|
if (e.stack){
|
|
that.setConfig('cursorUrl',e.stack.replace(/[^$]*http/,'http').replace(/[^\/]*\.js[^$]*/,''));
|
|
}
|
|
};
|
|
|
|
// update the grid no more then 30 times a second
|
|
this.__paintGrid = qxWeb.func.throttle(this.__paintGridReal,32,{trailing: false});
|
|
|
|
if (cfg){
|
|
for (var key in cfg){
|
|
this.setConfig(key,cfg[key])
|
|
}
|
|
}
|
|
var qxWindow = qxWeb(window);
|
|
this.__start = parseInt(this.getConfig('initialStart'));
|
|
this.__range = parseInt(this.getConfig('initialRange'));
|
|
|
|
qxWindow.on('resize',this.update,this);
|
|
this._forEachElementWrapped(function(img,idx) {
|
|
img.setStyle('display','inline-block');
|
|
img.setAttributes({
|
|
unselectable: true,
|
|
draggable: false
|
|
});
|
|
this.__addLoader(img);
|
|
this.__addCanvas(img);
|
|
this.__addTrack(img);
|
|
this.__addRoll(img);
|
|
img.emit('update');
|
|
},this);
|
|
if (this.getConfig('autoUpdate')){
|
|
this.__addSyncCharts();
|
|
}
|
|
return true;
|
|
},
|
|
|
|
|
|
setStart: function(start){
|
|
this.__start = start;
|
|
this.update();
|
|
},
|
|
getStart: function(){
|
|
return this.__start;
|
|
},
|
|
getRange: function(){
|
|
return this.__range;
|
|
},
|
|
setRange: function(range){
|
|
this.__range = range;
|
|
this.update();
|
|
},
|
|
|
|
setStartRange: function(start,range){
|
|
this.__start = start;
|
|
this.__range = range;
|
|
this.update();
|
|
},
|
|
|
|
update: function(){
|
|
this._forEachElementWrapped(function(img,idx) {
|
|
img.emit('update');
|
|
},this);
|
|
},
|
|
|
|
__addSyncCharts: function(){
|
|
var lastEnd = this.__start + this.__range;
|
|
var lastNow = false;
|
|
var that = this;
|
|
var syncCharts = function(){
|
|
var currentEnd = that.__start + that.__range;
|
|
var now = Math.round((new Date).getTime()/1000);
|
|
if (now < currentEnd && now > that.__start){
|
|
if (!lastNow) {
|
|
lastNow = now;
|
|
lastEnd = currentEnd;
|
|
return;
|
|
}
|
|
var increment = now - lastNow;
|
|
var go = false;
|
|
that._forEachElementWrapped(function(img,idx) {
|
|
if (that.__range / img.getWidth() < increment){
|
|
go = true;
|
|
}
|
|
});
|
|
if (go){
|
|
lastNow = now;
|
|
that.__start += increment;
|
|
lastEnd = that.__start + that.__range;
|
|
that.update();
|
|
that.emit('change',that.__start,that.__range);
|
|
}
|
|
}
|
|
else {
|
|
lastNow = false;
|
|
}
|
|
};
|
|
if (this.getConfig('autoUpdate')){
|
|
this.__syncJob = window.setInterval(syncCharts,1000);
|
|
}
|
|
},
|
|
|
|
__buildUrl: function(img,zoom){
|
|
var template = img.getData('src-template');
|
|
var start = this.__start;
|
|
if (start == null || isNaN(start)) return '';
|
|
return qxWeb.template.render(template,{
|
|
width: img.getWidth(),
|
|
height: img.getHeight(),
|
|
start: start,
|
|
end: start + this.__range,
|
|
zoom: zoom ? zoom : 1,
|
|
random: Math.round(Math.random()*1000000000).toString(36)
|
|
});
|
|
},
|
|
|
|
__addCanvas: function(img){
|
|
var offset = img.getOffset();
|
|
var pos = img.getPosition();
|
|
// console.log(img,offset,pos);
|
|
var canvas = qxWeb.create('<canvas></canvas>');
|
|
canvas.setStyles({
|
|
position: 'absolute'
|
|
})
|
|
.setAttributes({
|
|
draggable: "false",
|
|
unselectable: "true"
|
|
})
|
|
.insertBefore(img);
|
|
|
|
canvas.setStyle('cursor','url(' + this.getConfig('cursorUrl') + '/MoveCursor.cur), move');
|
|
|
|
var resize = function(){
|
|
var width = img.getWidth();
|
|
var height = img.getHeight();
|
|
canvas.setStyles({width: width+'px', height: height+'px'});
|
|
canvas.setProperties({width: width.toString(), height: height.toString()});
|
|
//canvas.width = width;
|
|
//canvas.height = height;
|
|
};
|
|
qxWeb(window).on('resize',resize);
|
|
img.__canvas = canvas;
|
|
if (canvas[0].getContext){
|
|
img.__ctx = canvas[0].getContext("2d");
|
|
}
|
|
resize();
|
|
},
|
|
|
|
__rangeCap: function(range){
|
|
return Math.round(Math.min(Math.max(10,range),24*3600*366*20));
|
|
},
|
|
|
|
__addLoader: function(img){
|
|
var loading = false;
|
|
var skipped = false;
|
|
var lastSrc = null;
|
|
var start;
|
|
var retry = 0;
|
|
var onError = function(){
|
|
loading = false;
|
|
//if (retry < 3){
|
|
// retry++;
|
|
// img.emit('update');
|
|
//}
|
|
};
|
|
img.on('error',onError,this);
|
|
var onLoad = function(){
|
|
loading = false;
|
|
if (skipped){
|
|
skipped = false;
|
|
img.emit('update');
|
|
}
|
|
retry = 0;
|
|
};
|
|
img.on('load',onLoad,this);
|
|
var onUpdate = function(zoom){
|
|
var url = this.__buildUrl(img,zoom);
|
|
if (!url) return;
|
|
if (! loading){
|
|
loading = true;
|
|
img.setProperty('src',url);
|
|
}
|
|
else {
|
|
skipped = true;
|
|
}
|
|
};
|
|
|
|
var onUpdateThrottled = qxWeb.func.throttle(onUpdate,120);
|
|
img.on('update',onUpdateThrottled,this);
|
|
|
|
img.once('qxRrdDispose',function(){
|
|
img.off('load',onLoad,this);
|
|
img.off('update',onUpdateThrottled,this);
|
|
img.off('error',onError,this);
|
|
},this);
|
|
|
|
},
|
|
|
|
__addRoll: function(img){
|
|
var that = this;
|
|
var syncUp = qxWeb.func.debounce(function(){
|
|
that.update();
|
|
that.emit('changeStartRange',{start:that.__start,range:that._range});
|
|
},200);
|
|
var xPos = img.getWidth()/2;
|
|
var onMove = function(e){
|
|
var newXPos = e.pageX - img.getOffset().left;
|
|
if (! isNaN(newXPos)){
|
|
xPos = newXPos;
|
|
}
|
|
};
|
|
|
|
var initialDotRange;
|
|
var initialDotStart;
|
|
var dotOff = true;
|
|
var killerId;
|
|
var onRoll = function(e){
|
|
if (e.pointerType != "wheel" || !e._original.ctrlKey ) return;
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
var delta = e.delta.y;
|
|
var initialRange = this.__range;
|
|
var xOrigin = xPos / img.getWidth();
|
|
if (dotOff){
|
|
initialDotRange = this.__range;
|
|
initialDotStart = this.__start;
|
|
dotOff = false;
|
|
}
|
|
this.__paintGrid(img,initialDotRange,initialDotStart);
|
|
this.__range = this.__rangeCap(this.__range*(1+(delta/10000)));
|
|
this.__start = Math.round(this.__start + (initialRange - this.__range)*xOrigin);
|
|
var that = this;
|
|
killerId = window.setTimeout(function(){
|
|
window.clearTimeout(killerId);
|
|
that.__clearGrid(img);
|
|
dotOff=true
|
|
},1000);
|
|
img.emit('update',this.getConfig('moveZoom'));
|
|
syncUp();
|
|
};
|
|
img.__canvas.on('pointermove',onMove,this);
|
|
img.__canvas.on('roll',onRoll,this);
|
|
|
|
//img.once('qxRrdDispose',function(){
|
|
// img.__canvas.off('pointermove',onMove,this);
|
|
// img.__canvas.off('roll',onRoll,this);
|
|
//},this);
|
|
},
|
|
__paintGridReal: function(img,initialRange,initialStart){
|
|
var ctx = img.__ctx;
|
|
if (!ctx){
|
|
return;
|
|
}
|
|
var width = img.getWidth();
|
|
var height = img.getHeight();
|
|
var skip = 100;
|
|
var xIncr = Math.round(initialRange / this.__range * skip);
|
|
var xOff = Math.round((width / this.__range * (initialStart - this.__start)) % xIncr);
|
|
var xWidth = Math.round(xIncr/2);
|
|
var gridStyleA = this.getConfig('gridFillStyleA');
|
|
var gridStyleB = this.getConfig('gridFillStyleB');
|
|
ctx.clearRect(0,0,width,height);
|
|
for (var x=-xIncr+xOff;x<width;x+=xIncr){
|
|
ctx.fillStyle = gridStyleA,
|
|
ctx.fillRect(x,0,xWidth,height);
|
|
ctx.fillStyle = gridStyleB,
|
|
ctx.fillRect(x+xWidth,0,xWidth,height);
|
|
};
|
|
},
|
|
__clearGrid: function(img){
|
|
var ctx = img.__ctx;
|
|
if (!ctx){
|
|
return;
|
|
}
|
|
var width = img.getWidth();
|
|
var height = img.getHeight();
|
|
ctx.clearRect(0,0,width,height);
|
|
},
|
|
__addTrack: function(img){
|
|
var qxDocument = q(document);
|
|
var initialStart = this.__start;
|
|
var initialRange = this.__range;
|
|
var xOrigin;
|
|
var pointerOrigin;
|
|
var imgWidth = img.getWidth() - this.getConfig('canvasPadding');
|
|
var active = false;
|
|
var trackLock = false;
|
|
var vertical;
|
|
|
|
var onPointerMove = function(e){
|
|
if (!active) return;
|
|
if (e.pointerType == 'touch' && e._original.touches.length > 1) return;
|
|
var delta = {
|
|
x: e.pageX - pointerOrigin.x,
|
|
y: e.pageY - pointerOrigin.y
|
|
};
|
|
if (!trackLock){
|
|
if (Math.abs(delta.x) > 10 || Math.abs(delta.y) > 10){
|
|
vertical = Math.abs(delta.x) < Math.abs(delta.y)
|
|
trackLock = true;
|
|
}
|
|
else {
|
|
return;
|
|
}
|
|
}
|
|
if (vertical){
|
|
if (e.pointerType == 'touch') return;
|
|
if (! isNaN(xOrigin)){
|
|
this.__range = this.__rangeCap(initialRange*Math.pow(1.02,delta.y));
|
|
this.__start = Math.round(initialStart + (initialRange - this.__range)*xOrigin);
|
|
}
|
|
}
|
|
else {
|
|
this.__start = initialStart-Math.round(this.__range/imgWidth*delta.x);
|
|
}
|
|
this.__paintGrid(img,initialRange,initialStart);
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
img.emit('update',this.getConfig('moveZoom'));
|
|
};
|
|
|
|
var onPinch = function(e){
|
|
if (!active) return;
|
|
var scale = e.getScale();
|
|
if (!scale) return;
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
|
|
this.__range = this.__rangeCap(initialRange/scale);
|
|
this.__start = Math.round(initialStart + (initialRange - this.__range)/2);
|
|
this.__paintGrid(img,initialRange,initialStart);
|
|
img.emit('update',this.getConfig('moveZoom'));
|
|
};
|
|
|
|
var canvas = img.__canvas;
|
|
|
|
var qxDoc = qxWeb(window);
|
|
var onPointerUp = function(e){
|
|
if (!active) return;
|
|
//e.stopPropagation();
|
|
//e.preventDefault();
|
|
active = false;
|
|
this.__clearGrid(img);
|
|
this.update();
|
|
if (initialRange != this.__range || initialStart != this.__start){
|
|
this.emit('changeStartRange',{start:this.__start,range:this.__range});
|
|
}
|
|
trackLock = false;
|
|
canvas.setStyle('cursor','url(' + this.getConfig('cursorUrl') + '/MoveCursor.cur), move');
|
|
qxDoc.off("pointermove",onPointerMove,this);
|
|
};
|
|
|
|
var onPointerDown = function(e){
|
|
//e.preventDefault();
|
|
//e.stopPropagation();
|
|
if (active) return;
|
|
active = true;
|
|
initialStart = this.__start;
|
|
initialRange = this.__range;
|
|
imgWidth = img.getWidth() - this.getConfig('canvasPadding');
|
|
canvas.setStyle('cursor','url(' + this.getConfig('cursorUrl') + '/DragCursor.cur), move');
|
|
var newXPos = e.pageX - img.getOffset().left;
|
|
pointerOrigin = {
|
|
x: e.pageX, y: e.pageY
|
|
};
|
|
if (! isNaN(newXPos)){
|
|
xOrigin = newXPos / img.getWidth();
|
|
}
|
|
qxDoc.on("pointermove",onPointerMove,this);
|
|
// on mobile devices we do not kill 'touch' because this could
|
|
// spell the start of a vertical scroll
|
|
if (e.pointerType != 'touch') {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
}
|
|
};
|
|
|
|
var onPointerOut = function(e){
|
|
if (!active) return;
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
};
|
|
|
|
qxDoc.on("pointerup",onPointerUp,this,true);
|
|
qxDoc.on('pointerout',onPointerOut,this,true);
|
|
|
|
canvas.on('pinch',onPinch,this);
|
|
canvas.on('pointerdown',onPointerDown,this);
|
|
|
|
img.once('qxRrdDispose',function(){
|
|
canvas.allOff();
|
|
qxDoc.off("pointerup",onPointerUp,this,true);
|
|
qxDoc.off('pointerout',onPointerOut,this,true);
|
|
qxDoc.off('pointermove',onPointerMove,this);
|
|
canvas.remove();
|
|
});
|
|
},
|
|
dispose: function(){
|
|
if (this.__syncJob){
|
|
window.clearInterval(this.__syncJob);
|
|
}
|
|
this._forEachElementWrapped(function(img) {
|
|
img.emit('qxRrdDispose');
|
|
img.removeAttribute('unselectable');
|
|
img.removeAttribute('draggable');
|
|
});
|
|
return this.base(arguments);
|
|
}
|
|
},
|
|
|
|
defer : function(statics) {
|
|
qxWeb.$attach({rrdGraphPng : statics.rrdGraphPng});
|
|
}
|
|
});
|
|
|
|
|