dojo.provide("dojox.charting.Chart2D"); dojo.require("dojox.gfx"); dojo.require("dojox.lang.functional"); dojo.require("dojox.lang.functional.fold"); dojo.require("dojox.lang.functional.reversed"); dojo.require("dojox.charting.Theme"); dojo.require("dojox.charting.Series"); // require all axes to support references by name dojo.require("dojox.charting.axis2d.Default"); // require all plots to support references by name dojo.require("dojox.charting.plot2d.Default"); dojo.require("dojox.charting.plot2d.Lines"); dojo.require("dojox.charting.plot2d.Areas"); dojo.require("dojox.charting.plot2d.Markers"); dojo.require("dojox.charting.plot2d.MarkersOnly"); dojo.require("dojox.charting.plot2d.Scatter"); dojo.require("dojox.charting.plot2d.Stacked"); dojo.require("dojox.charting.plot2d.StackedLines"); dojo.require("dojox.charting.plot2d.StackedAreas"); dojo.require("dojox.charting.plot2d.Columns"); dojo.require("dojox.charting.plot2d.StackedColumns"); dojo.require("dojox.charting.plot2d.ClusteredColumns"); dojo.require("dojox.charting.plot2d.Bars"); dojo.require("dojox.charting.plot2d.StackedBars"); dojo.require("dojox.charting.plot2d.ClusteredBars"); dojo.require("dojox.charting.plot2d.Grid"); dojo.require("dojox.charting.plot2d.Pie"); dojo.require("dojox.charting.plot2d.Bubble"); (function(){ var df = dojox.lang.functional, dc = dojox.charting, clear = df.lambda("item.clear()"), purge = df.lambda("item.purgeGroup()"), destroy = df.lambda("item.destroy()"), makeClean = df.lambda("item.dirty = false"), makeDirty = df.lambda("item.dirty = true"); dojo.declare("dojox.charting.Chart2D", null, { constructor: function(node, kwArgs){ // initialize parameters if(!kwArgs){ kwArgs = {}; } this.margins = kwArgs.margins ? kwArgs.margins : {l: 10, t: 10, r: 10, b: 10}; this.stroke = kwArgs.stroke; this.fill = kwArgs.fill; // default initialization this.theme = null; this.axes = {}; // map of axes this.stack = []; // stack of plotters this.plots = {}; // map of plotter indices this.series = []; // stack of data runs this.runs = {}; // map of data run indices this.dirty = true; this.coords = null; // create a surface this.node = dojo.byId(node); var box = dojo.marginBox(node); this.surface = dojox.gfx.createSurface(this.node, box.w, box.h); }, destroy: function(){ dojo.forEach(this.series, destroy); dojo.forEach(this.stack, destroy); df.forIn(this.axes, destroy); this.surface.destroy(); }, getCoords: function(){ if(!this.coords){ this.coords = dojo.coords(this.node, true); } return this.coords; }, setTheme: function(theme){ this.theme = theme._clone(); this.dirty = true; return this; }, addAxis: function(name, kwArgs){ var axis; if(!kwArgs || !("type" in kwArgs)){ axis = new dc.axis2d.Default(this, kwArgs); }else{ axis = typeof kwArgs.type == "string" ? new dc.axis2d[kwArgs.type](this, kwArgs) : new kwArgs.type(this, kwArgs); } axis.name = name; axis.dirty = true; if(name in this.axes){ this.axes[name].destroy(); } this.axes[name] = axis; this.dirty = true; return this; }, getAxis: function(name){ return this.axes[name]; }, removeAxis: function(name){ if(name in this.axes){ // destroy the axis this.axes[name].destroy(); delete this.axes[name]; // mark the chart as dirty this.dirty = true; } return this; // self }, addPlot: function(name, kwArgs){ var plot; if(!kwArgs || !("type" in kwArgs)){ plot = new dc.plot2d.Default(this, kwArgs); }else{ plot = typeof kwArgs.type == "string" ? new dc.plot2d[kwArgs.type](this, kwArgs) : new kwArgs.type(this, kwArgs); } plot.name = name; plot.dirty = true; if(name in this.plots){ this.stack[this.plots[name]].destroy(); this.stack[this.plots[name]] = plot; }else{ this.plots[name] = this.stack.length; this.stack.push(plot); } this.dirty = true; return this; }, removePlot: function(name){ if(name in this.plots){ // get the index and remove the name var index = this.plots[name]; delete this.plots[name]; // destroy the plot this.stack[index].destroy(); // remove the plot from the stack this.stack.splice(index, 1); // update indices to reflect the shift df.forIn(this.plots, function(idx, name, plots){ if(idx > index){ plots[name] = idx - 1; } }); // mark the chart as dirty this.dirty = true; } return this; // self }, addSeries: function(name, data, kwArgs){ var run = new dc.Series(this, data, kwArgs); if(name in this.runs){ this.series[this.runs[name]].destroy(); this.series[this.runs[name]] = run; }else{ this.runs[name] = this.series.length; this.series.push(run); } run.name = name; this.dirty = true; // fix min/max if(!("ymin" in run) && "min" in run){ run.ymin = run.min; } if(!("ymax" in run) && "max" in run){ run.ymax = run.max; } return this; }, removeSeries: function(name){ if(name in this.runs){ // get the index and remove the name var index = this.runs[name], plotName = this.series[index].plot; delete this.runs[name]; // destroy the run this.series[index].destroy(); // remove the run from the stack of series this.series.splice(index, 1); // update indices to reflect the shift df.forIn(this.runs, function(idx, name, runs){ if(idx > index){ runs[name] = idx - 1; } }); this.dirty = true; } return this; // self }, updateSeries: function(name, data){ if(name in this.runs){ var run = this.series[this.runs[name]]; run.data = data; run.dirty = true; this._invalidateDependentPlots(run.plot, false); this._invalidateDependentPlots(run.plot, true); } return this; }, resize: function(width, height){ var box; switch(arguments.length){ case 0: box = dojo.marginBox(this.node); break; case 1: box = width; break; default: box = {w: width, h: height}; break; } dojo.marginBox(this.node, box); this.surface.setDimensions(box.w, box.h); this.dirty = true; this.coords = null; return this.render(); }, getGeometry: function(){ var ret = {}; df.forIn(this.axes, function(axis){ if(axis.initialized()){ ret[axis.name] = { name: axis.name, vertical: axis.vertical, scaler: axis.scaler, ticks: axis.ticks }; } }); return ret; }, setAxisWindow: function(name, scale, offset){ var axis = this.axes[name]; if(axis){ axis.setWindow(scale, offset); } return this; }, setWindow: function(sx, sy, dx, dy){ if(!("plotArea" in this)){ this.calculateGeometry(); } df.forIn(this.axes, function(axis){ var scale, offset, bounds = axis.getScaler().bounds, s = bounds.span / (bounds.upper - bounds.lower); if(axis.vertical){ scale = sy; offset = dy / s / scale; }else{ scale = sx; offset = dx / s / scale; } axis.setWindow(scale, offset); }); return this; }, calculateGeometry: function(){ if(this.dirty){ return this.fullGeometry(); } // calculate geometry dojo.forEach(this.stack, function(plot){ if(plot.dirty || (plot.hAxis && this.axes[plot.hAxis].dirty) || (plot.vAxis && this.axes[plot.vAxis].dirty)){ plot.calculateAxes(this.plotArea); } }, this); return this; }, fullGeometry: function(){ this._makeDirty(); // clear old values dojo.forEach(this.stack, clear); // rebuild new connections, and add defaults // set up a theme if(!this.theme){ this.setTheme(new dojox.charting.Theme(dojox.charting._def)); } // assign series dojo.forEach(this.series, function(run){ if(!(run.plot in this.plots)){ var plot = new dc.plot2d.Default(this, {}); plot.name = run.plot; this.plots[run.plot] = this.stack.length; this.stack.push(plot); } this.stack[this.plots[run.plot]].addSeries(run); }, this); // assign axes dojo.forEach(this.stack, function(plot){ if(plot.hAxis){ plot.setAxis(this.axes[plot.hAxis]); } if(plot.vAxis){ plot.setAxis(this.axes[plot.vAxis]); } }, this); // calculate geometry // 1st pass var dim = this.dim = this.surface.getDimensions(); dim.width = dojox.gfx.normalizedLength(dim.width); dim.height = dojox.gfx.normalizedLength(dim.height); df.forIn(this.axes, clear); dojo.forEach(this.stack, function(plot){ plot.calculateAxes(dim); }); // assumption: we don't have stacked axes yet var offsets = this.offsets = {l: 0, r: 0, t: 0, b: 0}; df.forIn(this.axes, function(axis){ df.forIn(axis.getOffsets(), function(o, i){ offsets[i] += o; }); }); // add margins df.forIn(this.margins, function(o, i){ offsets[i] += o; }); // 2nd pass with realistic dimensions this.plotArea = {width: dim.width - offsets.l - offsets.r, height: dim.height - offsets.t - offsets.b}; df.forIn(this.axes, clear); dojo.forEach(this.stack, function(plot){ plot.calculateAxes(this.plotArea); }, this); return this; }, render: function(){ if(this.theme){ this.theme.clear(); } if(this.dirty){ return this.fullRender(); } this.calculateGeometry(); // go over the stack backwards df.forEachRev(this.stack, function(plot){ plot.render(this.dim, this.offsets); }, this); // go over axes df.forIn(this.axes, function(axis){ axis.render(this.dim, this.offsets); }, this); this._makeClean(); // BEGIN FOR HTML CANVAS if(this.surface.render){ this.surface.render(); }; // END FOR HTML CANVAS return this; }, fullRender: function(){ // calculate geometry this.fullGeometry(); var offsets = this.offsets, dim = this.dim; // get required colors var requiredColors = df.foldl(this.stack, "z + plot.getRequiredColors()", 0); this.theme.defineColors({num: requiredColors, cache: false}); // clear old shapes dojo.forEach(this.series, purge); df.forIn(this.axes, purge); dojo.forEach(this.stack, purge); this.surface.clear(); // generate shapes // draw a plot background var t = this.theme, fill = t.plotarea && t.plotarea.fill, stroke = t.plotarea && t.plotarea.stroke; if(fill){ this.surface.createRect({ x: offsets.l, y: offsets.t, width: dim.width - offsets.l - offsets.r, height: dim.height - offsets.t - offsets.b }).setFill(fill); } if(stroke){ this.surface.createRect({ x: offsets.l, y: offsets.t, width: dim.width - offsets.l - offsets.r - 1, height: dim.height - offsets.t - offsets.b - 1 }).setStroke(stroke); } // go over the stack backwards df.foldr(this.stack, function(z, plot){ return plot.render(dim, offsets), 0; }, 0); // pseudo-clipping: matting fill = this.fill ? this.fill : (t.chart && t.chart.fill); stroke = this.stroke ? this.stroke : (t.chart && t.chart.stroke); // TRT: support for "inherit" as a named value in a theme. if(fill == "inherit"){ // find the background color of the nearest ancestor node, and use that explicitly. var node = this.node, fill = new dojo.Color(dojo.style(node, "backgroundColor")); while(fill.a==0 && node!=document.documentElement){ fill = new dojo.Color(dojo.style(node, "backgroundColor")); node = node.parentNode; } } if(fill){ if(offsets.l){ // left this.surface.createRect({ width: offsets.l, height: dim.height + 1 }).setFill(fill); } if(offsets.r){ // right this.surface.createRect({ x: dim.width - offsets.r, width: offsets.r + 1, height: dim.height + 1 }).setFill(fill); } if(offsets.t){ // top this.surface.createRect({ width: dim.width + 1, height: offsets.t }).setFill(fill); } if(offsets.b){ // bottom this.surface.createRect({ y: dim.height - offsets.b, width: dim.width + 1, height: offsets.b + 2 }).setFill(fill); } } if(stroke){ this.surface.createRect({ width: dim.width - 1, height: dim.height - 1 }).setStroke(stroke); } // go over axes df.forIn(this.axes, function(axis){ axis.render(dim, offsets); }); this._makeClean(); // BEGIN FOR HTML CANVAS if(this.surface.render){ this.surface.render(); }; // END FOR HTML CANVAS return this; }, connectToPlot: function(name, object, method){ return name in this.plots ? this.stack[this.plots[name]].connect(object, method) : null; }, _makeClean: function(){ // reset dirty flags dojo.forEach(this.axes, makeClean); dojo.forEach(this.stack, makeClean); dojo.forEach(this.series, makeClean); this.dirty = false; }, _makeDirty: function(){ // reset dirty flags dojo.forEach(this.axes, makeDirty); dojo.forEach(this.stack, makeDirty); dojo.forEach(this.series, makeDirty); this.dirty = true; }, _invalidateDependentPlots: function(plotName, /* Boolean */ verticalAxis){ if(plotName in this.plots){ var plot = this.stack[this.plots[plotName]], axis, axisName = verticalAxis ? "vAxis" : "hAxis"; if(plot[axisName]){ axis = this.axes[plot[axisName]]; if(axis.dependOnData()){ axis.dirty = true; // find all plots and mark them dirty dojo.forEach(this.stack, function(p){ if(p[axisName] && p[axisName] == plot[axisName]){ p.dirty = true; } }); } }else{ plot.dirty = true; } } } }); })();