/* ************************************************************************ Copyrigtht: OETIKER+PARTNER AG License: GPLv3 or later Authors: Tobias Oetiker Utf8Check: äöü ************************************************************************ */ /** * Create a D3.js based, interactive chart. * */ var ID; qx.Class.define("ep.visualizer.chart.BrowserChart", { extend : qx.ui.core.Widget, /** * @param instance {String} identifying the visualizer instance * @param recId {Number} stable identifier for the extopus node * @param chartDef {Array} chart description array * *
     * { [
     *       { cmd: 'LINE', width: 1, color, '#ff00ff', legend: 'text' },
     *       { cmd: 'AREA', stack: 1, color, '#df33ff', legend: 'more text' },
     *        ...
     *    ]
     * }
     * 
*/ construct : function(instance,recId,chartDef) { this.base(arguments); this._setLayout(new qx.ui.layout.VBox(10)); this.set({ instance: instance, recId: recId }); // the chart var d3Obj = this.__d3Obj = new qxd3.Svg(); var margin = this.self(arguments).MARGIN; this.__chart = d3Obj.getD3SvgNode() .append("g") .attr("transform", "translate("+margin.left+","+margin.top+")"); this.__d3 = d3Obj.getD3(); // add the svg object into the LoadingBox this._add(d3Obj,{flex: 1}); if (chartDef){ this.setChartDef(chartDef); } // insert our global CSS once only var CSS = this.self(arguments).BASECSS; var selector; for (selector in CSS){ d3Obj.addCssRule(selector, CSS[selector]); } this.__dataNode = []; d3Obj.addListener('appear',this.setSize,this); d3Obj.addListener('resize',this.setSize,this); var timer = this.__timer = new qx.event.Timer(5 * 1000); d3Obj.addListener('disappear', function() { timer.stop() }, this); d3Obj.addListener('appear', function() { timer.start() }, this); timer.addListener('interval', function() { this.trackingRedraw(); }, this); timer.start(); }, statics : { /** * Some basic css for the D3 chart browserchart */ BASECSS: { 'svg': { "font": "10px sans-serif" }, '.axis': { "shape-rendering": 'crispEdges' }, '.axis path, .axis line': { 'fill': 'none', 'stroke-width': '1px' }, '.x.axis path': { 'stroke': '#000' }, '.x.axis line': { 'stroke': '#000', 'stroke-opacity': '.2', 'stroke-dasharray': '1,1' }, '.y.axis line': { 'stroke': '#000', 'stroke-opacity': '.2', 'stroke-dasharray': '1,1' } }, MARGIN: { left: 50, top: 10, bottom: 15, right: 10 } }, properties : { /** * inbytes, outbytes ... */ view : { init : null, nullable : true }, /** * amount of time in the chart (in seconds) */ timeWidth : { init : null, check : 'Integer', nullable : true }, /** * at what point in time is the end of the chart, if no time is give the chart will update automatically */ endTime : { init : null, check : 'Integer', nullable : true }, /** * track current time */ trackCurrentTime: { init : null, check : 'Boolean', nullable : true }, /** * instance name of the table. This lets us identify ourselves when requesting data from the server */ instance : { init : null, nullable : true }, /** * extopus Id aka recId ... to identify the chart we are talking about */ recId: { init : null, check : 'Integer', nullable : true }, /** * chart defs (chart definitions) */ chartDef: { init : [], check : 'Array', nullable : true } }, members : { __timer : null, __d3: null, __chart: null, __xAxisPainter: null, __yAxisPainter: null, __linePainter: null, __areaPainter: null, __xScale: null, __yScale: null, __zoomRectNode: null, __xAxisNode: null, __yAxisNode: null, __dataNode: null, __clipPath: null, __chartWidth: null, __fetchWait: null, __fetchAgain: null, __legendContainer: null, __d3Obj: null, __data: null, getDataArea: function(){ if (this.__dataArea) return this.__dataArea; return this.__dataArea = this.__chart.insert("g",':first-child'); }, getXAxisPainter: function(){ if (this.__xAxisPainter) return this.__xAxisPainter; var customTimeFormat = this.__d3.time.format.multi([ [".%L", function(d) { return d.getMilliseconds(); }], [":%S", function(d) { return d.getSeconds(); }], ["%H:%M", function(d) { return d.getMinutes(); }], ["%H:%M", function(d) { return d.getHours(); }], ["%m-%d", function(d) { return d.getDay() && d.getDate() != 1; }], ["%Y-%m-%d", function(d) { return true }] ]); this.__xAxisPainter = this.__d3.svg.axis() .scale(this.getXScale()) .orient("bottom") .tickPadding(6) .tickFormat(customTimeFormat); return this.__xAxisPainter; }, getYAxisPainter: function(){ if (this.__yAxisPainter) return this.__yAxisPainter; var si = ['p','n','y','m','','k','M','G','T','P']; var d3 = this.__d3; var commasFormatter = d3.format(",.1f"); this.__yAxisPainter = d3.svg.axis() .scale(this.getYScale()) .orient("left") .tickPadding(6) .tickFormat(function(d){ if (d == 0){ return d; } var log = Math.log(d)/Math.LN10; var idx = log < 0 ? Math.ceil(log/3) : Math.floor(log/3); var factor = Math.pow(10,idx*3); return commasFormatter(d/factor) + si[idx+4]; }); return this.__yAxisPainter; }, getXScale: function(){ if (this.__xScale) return this.__xScale; this.__xScale = this.__d3.time.scale(); this.__xScale.domain([new Date(new Date().getTime() - 24*3600*1000),new Date()]); return this.__xScale; }, getYScale: function(){ if (this.__yScale) return this.__yScale; this.__yScale = this.__d3.scale.linear(); return this.__yScale; }, getDataPainter: function(id){ switch(this.getChartDef()[id].cmd){ case 'LINE': return this.getLinePainter(); break; case 'AREA': return this.getAreaPainter(); break; default: this.debug("invalid cmd"); break; } }, getLinePainter: function(){ if (this.__linePainter) return this.__linePainter; var xScale = this.getXScale(); var yScale = this.getYScale(); this.__linePainter = this.__d3.svg.line() .interpolate("step-before") .x(function(d){ return xScale(d.date); }) .y(function(d){ return yScale(d.y); }) .defined(function(d){ return d.d }); return this.__linePainter; }, getAreaPainter: function(){ if (this.__areaPainter) return this.__areaPainter; var xScale = this.getXScale(); var yScale = this.getYScale(); this.__areaPainter = this.__d3.svg.area() .interpolate("step-before") .x(function(d){ return xScale(d.date); }) .y0(function(d){ return yScale(d.y0);}) .y1(function(d){ return yScale(d.y); }) .defined(function(d){ return d.d }); return this.__areaPainter; }, getZoomRectNode: function(){ if (this.__zoomRectNode) return this.__zoomRectNode; var that = this; var zoomer = this.__zoomer = this.__d3.behavior.zoom() .scaleExtent([0.0001, 1000]) .on("zoom", function (){that.redraw()}); zoomer.x(this.getXScale()); this.__zoomRectNode = this.__chart.append("rect") .style({ cursor: 'move', fill: 'none', 'pointer-events': 'all' }) .call(zoomer); return this.__zoomRectNode; }, getXAxisNode: function(){ if (this.__xAxisNode) return this.__xAxisNode; this.__xAxisNode = this.__chart.append("g") .attr("class", "x axis"); return this.__xAxisNode; }, getYAxisNode: function(){ if (this.__yAxisNode) return this.__yAxisNode; this.__yAxisNode = this.__chart.append("g") .attr("class", "y axis"); return this.__yAxisNode; }, getClipPath: function(){ if (this.__clipPath) return this.__clipPath; ID++; this.__clipPathID = this.getInstance() + '-clipPath-' + ID; this.__clipPath = this.__chart.append("clipPath") .attr('id',this.__clipPathID) .append("rect"); return this.__clipPath; }, getDataNode: function(id){ if (this.__dataNode[id]) return this.__dataNode[id]; var chartDef = this.getChartDef()[id]; var dataArea = this.getDataArea(); switch(chartDef.cmd){ case 'LINE': this.__dataNode[id] = dataArea.append("path") .attr("clip-path", "url(#"+this.__clipPathID+")") .attr('stroke', chartDef.color) .attr('stroke-width', 2) .attr("fill", "none") .attr("shape-rendering", 'crispEdges'); break; case 'AREA': this.__dataNode[id] = dataArea.append("path") .attr("clip-path", "url(#"+this.__clipPathID+")") .attr("fill",chartDef.color); break; default: this.debug("unsupported cmd:" + chartDef.cmd); break; } return this.__dataNode[id]; }, resetChart: function(newValue, oldValue, propertyName){ if (this.__dataNode){ for (var i=0;i maxValue && item.date >= dates[0] && item.date <= dates[1]){ maxValue = item.y; } }) } this.getYScale().domain([0,maxValue]).nice(); this.getYAxisNode().call(this.getYAxisPainter()); for(var i=0;i 0){ node.attr("d",this.getDataPainter(i)); } } } }, redraw: function(){ var dates = this.getXScale().domain(); var start = Math.round(dates[0].getTime()/1000); var end = Math.round(dates[1].getTime()/1000); var dataStep = Math.round((end-start)/ this.__chartWidth); var extra = Math.round(end - start ); var d3 = this.__d3; start -= extra; end += extra; this.yScaleRedraw(); this.getXAxisNode().call(this.getXAxisPainter()); if (this.__fetchWait){ this.__fetchAgain = 1; return; } var existingData = this.dataSlicer(start,end,dataStep); if (existingData.missingStart == existingData.missingEnd){ return; } var needChartDef = this.getChartDef().length == 0; var rpc = ep.data.Server.getInstance(); var that = this; this.__fetchWait = 1; rpc.callAsyncSmart(function(ret){ var d3Data = that.__data = that.d3DataTransformer(ret,dataStep); var maxValue = 0; for(var i=0;i= start; var appendMode = oldEnd >= end && oldStart <= end; for (var i=0;i= start ) { prepend[i].push(item); missingStart = date+dataStep; } if (appendMode && date <= end ){ append[i].push(item); if ( date < missingEnd ) { missingEnd = date-dataStep; } } } } /* lets make sure don't trip over ourselves */ if (missingStart > missingEnd){ this.debug("missingStart:"+missingStart+" missingEnd:"+missingEnd); missingEnd = missingStart; } return { missingStart: missingStart, missingEnd: missingEnd, append: append, prepend: prepend } }, d3DataTransformer: function(data,dataStep){ var d3 = this.__d3; if (data == null || typeof(data) != 'object' || data[0] == null){ return null; } var minStep = 24*3600; var minStart = (new Date).getTime()/1000; data.forEach(function(d){ if (d.status != 'ok') return; if (minStep == null || minStep > d.step){ minStep = d.step; minStart = d.start; } }); var d3Data = []; for (var i=0; i 0 && d3Data[i-1][ii] != null && typeof(d3Data[i-1][ii]) == 'object'){ y0 = d3Data[i-1][ii].y; } var yval = parseFloat(data[i].values[Math.round(ii*st)]); d3Data[i][ii] = { y: (isNaN(yval) ? 0 : yval) +y0, y0: y0, d: !isNaN(yval), date: new Date((minStart+ii*minStep)*1000) } } } return {data:d3Data,dataStep: dataStep} } }, destruct : function() { this.__timer.stop(); } });