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