dojo.provide("dojox.charting.DataChart"); dojo.require("dojox.charting.Chart2D"); dojo.require("dojox.charting.themes.PlotKit.blue"); dojo.experimental("dojox.charting.DataChart"); (function(){ // Defaults for axes // to be mixed in with xaxis/yaxis custom properties // see dojox.charting.axis2d.Default for details. var _yaxis = { vertical: true, min: 0, max: 10, majorTickStep: 5, minorTickStep: 1, natural:false, stroke: "black", majorTick: {stroke: "black", length: 8}, minorTick: {stroke: "gray", length: 2}, majorLabels:true }; var _xaxis = { natural: true, // true - no fractions majorLabels: true, //show labels on major ticks includeZero: false, // do not change on upating chart majorTickStep: 1, majorTick: {stroke: "black", length: 8}, fixUpper:"major", stroke: "black", htmlLabels: true, from:1 }; // default for chart elements var chartPlot = { markers: true, tension:2, gap:2 }; dojo.declare("dojox.charting.DataChart", [dojox.charting.Chart2D], { // summary: // DataChart // Extension to the 2D chart that connects to a data store in // a simple manner. Convenience methods have been added for // connecting store item labels to the chart labels. // // description: // This code should be considered very experimental and the APIs subject // to change. This is currently an alpha version and will need some testing // and review. // // The main reason for this extension is to create animated charts, generally // available with scroll=true, and a property field that gets continually updated. // The previous property settings are kept in memory and displayed until scrolled // off the chart. // // Although great effort was made to maintain the integrity of the current // charting APIs, some things have been added or modified in order to get // the store to connect and also to get the data to scroll/animate. // "displayRange" in particular is used to force the xaxis to a specific // size and keep the chart from stretching or squashing to fit the data. // // Currently, plot lines can only be set at initialization. Setting // a new store query will have no effect (although using setStore // may work but its untested). // // example: // // | var chart = new dojox.charting.DataChart("myNode", { // | displayRange:8, // | store:dataStore, // | query:{symbol:"*"}, // | fieldName:"price" // | type: dojox.charting.plot2d.Columns // | }); // // properties: // // scroll: Boolean // Whether live data updates and changes display, like columns moving // up and down, or whether it scrolls to the left as data is added scroll:true, // // comparative: Boolean // If false, all items are each their own series. // If true, the items are combined into one series // so that their charted properties can be compared. comparative:false, // // query: String // Used for fetching items. Will vary depending upon store. query: "*", // // queryOptions: String // Option used for fetching items queryOptions: "", // /*===== // start:Number // first item to fetch from store // count:Number // Total amount of items to fetch from store // sort:Object // Paramaters to sort the fetched items from store =====*/ // // fieldName: String // The field in the store item that is getting charted fieldName: "value", // // chartTheme: dojox.charting.themes.* // The theme to style the chart. Defaults to PlotKit.blue. chartTheme: dojox.charting.themes.PlotKit.blue, // // displayRange: Number // The number of major ticks to show on the xaxis displayRange:0, // // stretchToFit: Boolean // If true, chart is sized to data. If false, chart is a // fixed size. Note, is overridden by displayRange. // TODO: Stretch for the y-axis? stretchToFit:true, // // minWidth: Number // The the smallest the chart width can be minWidth:200, // // minHeight: Number // The the smallest the chart height can be minHeight:100, // // showing: Boolean // Whether the chart is showing (default) on // initialization or hidden. showing: true, // // label: String // The name field of the store item // DO NOT SET: Set from store.labelAttribute label: "name", constructor: function(node, kwArgs){ // summary: // Set up properties and initialize chart build. // // arguments: // node: DomNode // The node to attach the chart to. // kwArgs: Object // xaxis: Object // optional parameters for xaxis (see above) // yaxis: Object // optional parameters for yaxis (see above) // store: Object // dojo.data store (currently nly supports Persevere) // xaxis: Object // First query for store // grid: Object // Options for the grid plot // chartPlot: Object // Options for chart elements (lines, bars, etc) this.domNode = dojo.byId(node); dojo.mixin(this, kwArgs); this.xaxis = dojo.mixin(dojo.mixin({}, _xaxis), kwArgs.xaxis); if(this.xaxis.labelFunc == "seriesLabels"){ this.xaxis.labelFunc = dojo.hitch(this, "seriesLabels"); } this.yaxis = dojo.mixin(dojo.mixin({}, _yaxis), kwArgs.yaxis); if(this.yaxis.labelFunc == "seriesLabels"){ this.yaxis.labelFunc = dojo.hitch(this, "seriesLabels"); } this.convertLabels(this.yaxis); this.convertLabels(this.xaxis); this.onSetItems = {}; this.onSetInterval = 0; this.dataLength = 0; this.seriesData = {}; this.seriesDataBk = {}; this.firstRun = true; this.dataOffset = 0; // FIXME: looks better with this, but it's custom this.chartTheme.plotarea.stroke = {color: "gray", width: 3}; this.setTheme(this.chartTheme); // displayRange overrides stretchToFit if(this.displayRange){ this.stretchToFit = false; } if(!this.stretchToFit){ this.xaxis.to = this.displayRange; } this.addAxis("x", this.xaxis); this.addAxis("y", this.yaxis); chartPlot.type = kwArgs.type || "Markers" this.addPlot("default", dojo.mixin(chartPlot, kwArgs.chartPlot)); this.addPlot("grid", dojo.mixin(kwArgs.grid || {}, {type: "Grid", hMinorLines: true})); if(this.showing){ this.render(); } if(kwArgs.store){ this.setStore(kwArgs.store, kwArgs.query, kwArgs.fieldName, kwArgs.queryOptions); } }, setStore: function(/*Object*/store, /* ? String*/query, /* ? String*/fieldName, /* ? Object */queryOptions){ // summary: // Sets the chart store and query // then does the first fetch and // connects to subsequent changes. // // TODO: Not handling resetting store // this.firstRun = true; this.store = store || this.store; this.query = query || this.query; this.fieldName = fieldName || this.fieldName; this.label = this.store.getLabelAttributes(); this.queryOptions = queryOptions || queryOptions; this.fetch(); dojo.connect(this.store, "onSet", this, "onSet"); dojo.connect(this.store, "onError", this, "onError"); }, show: function(){ // summary: // If chart is hidden, show it if(!this.showing){ dojo.style(this.domNode, "display", ""); this.showing = true; this.render(); } }, hide: function(){ // summary: // If chart is showing, hide it // Prevents rendering while hidden if(this.showing){ dojo.style(this.domNode, "display", "none"); this.showing = false; } }, onSet: function(/*storeObject*/item){ // summary: // Fired when a store item changes. // Collects the item calls and when // done (after 200ms), sends item // array to onData(). // // FIXME: Using labels instead of IDs for item // identifiers here and in the chart series. This // is obviously short sighted, but currently used // for seriesLabels. Workaround for potential bugs // is to assign a label for which all items are unique. var nm = this.getProperty(item, this.label); // FIXME: why the check for if-in-runs? if(nm in this.runs || this.comparative){ clearTimeout(this.onSetInterval); if(!this.onSetItems[nm]){ this.onSetItems[nm] = item; } this.onSetInterval = setTimeout(dojo.hitch(this, function(){ clearTimeout(this.onSetInterval); var items = []; for(var nm in this.onSetItems){ items.push(this.onSetItems[nm]); } this.onData(items); this.onSetItems = {}; }),200); } }, onError: function(/*Error*/err){ // stub // Fires on fetch error console.error(err); }, onDataReceived: function(/*Array*/items){ // summary: // stub. Fires after data is received but // before data is parsed and rendered }, getProperty: function(/*storeObject*/item, prop){ // summary: // The main use of this function is to determine // between a single value and an array of values. // Other property types included for convenience. // if(prop==this.label){ return this.store.getLabel(item); } if(prop=="id"){ return this.store.getIdentity(item); } var value = this.store.getValues(item, prop); if(value.length < 2){ value = this.store.getValue(item, prop); } return value; }, onData: function(/*Array*/items){ // summary: // Called after a completed fetch // or when store items change. // On first run, sets the chart data, // then updates chart and legends. // //console.log("Store:", store);console.log("items: (", items.length+")", items);console.log("Chart:", this); if(!items.length){ return; } if(this.items && this.items.length != items.length){ dojo.forEach(items, function(m){ var id = this.getProperty(m, "id"); dojo.forEach(this.items, function(m2, i){ if(this.getProperty(m2, "id") == id){ this.items[i] = m2; } },this); }, this); items = this.items; } if(this.stretchToFit){ this.displayRange = items.length; } this.onDataReceived(items); this.items = items; if(this.comparative){ // all items are gathered together and used as one // series so their properties can be compared. var nm = "default"; this.seriesData[nm] = []; this.seriesDataBk[nm] = []; dojo.forEach(items, function(m, i){ var field = this.getProperty(m, this.fieldName); this.seriesData[nm].push(field); }, this); }else{ // each item is a seperate series. dojo.forEach(items, function(m, i){ var nm = this.store.getLabel(m); if(!this.seriesData[nm]){ this.seriesData[nm] = []; this.seriesDataBk[nm] = []; } // the property in the item we are using var field = this.getProperty(m, this.fieldName); if(dojo.isArray(field)){ // Data is an array, so it's a snapshot, and not // live, updating data // this.seriesData[nm] = field; }else{ if(!this.scroll){ // Data updates, and "moves in place". Columns and // line markers go up and down // // create empty chart elements by starting an array // with zeros until we reach our relevant data var ar = dojo.map(new Array(i+1), function(){ return 0; }); ar.push(Number(field)); this.seriesData[nm] = ar; }else{ // Data updates and scrolls to the left if(this.seriesDataBk[nm].length > this.seriesData[nm].length){ this.seriesData[nm] = this.seriesDataBk[nm]; } // Collecting and storing series data. The items come in // only one at a time, but we need to display historical // data, so it is kept in memory. this.seriesData[nm].push(Number(field)); } this.seriesDataBk[nm].push(Number(field)); } }, this); } // displayData is the segment of the data array that is within // the chart boundaries var displayData; if(this.firstRun){ // First time around we need to add the series (chart lines) // to the chart. this.firstRun = false; for(nm in this.seriesData){ this.addSeries(nm, this.seriesData[nm]); displayData = this.seriesData[nm]; } }else{ // update existing series for(nm in this.seriesData){ displayData = this.seriesData[nm]; if(this.scroll && displayData.length > this.displayRange){ // chart lines have gone beyond the right boundary. this.dataOffset = displayData.length-this.displayRange - 1; displayData = displayData.slice(displayData.length-this.displayRange, displayData.length); } this.updateSeries(nm, displayData); } } this.dataLength = displayData.length; if(this.showing){ this.render(); } }, fetch: function(){ // summary: // Fetches initial data. Subsequent changes // are received via onSet in data store. // if(!this.store){ return; } this.store.fetch({query:this.query, queryOptions:this.queryOptions, start:this.start, count:this.count, sort:this.sort, onComplete:dojo.hitch(this, "onData"), onError:dojo.hitch(this, "onError")}); }, convertLabels: function(axis){ // summary: // Convenience method to convert a label array of strings // into an array of objects // if(!axis.labels || dojo.isObject(axis.labels[0])){ return null; } axis.labels = dojo.map(axis.labels, function(ele, i){ return {value:i, text:ele}; }); return null; // null }, seriesLabels: function(/*Number*/val){ // summary: // Convenience method that sets series labels based on item labels. val--; if(this.series.length<1 || (!this.comparative && val>this.series.length)){ return "-"; } if(this.comparative){ return this.store.getLabel(this.items[val]);// String }else{ // FIXME: // Here we are setting the label base on if there is data in the array slot. // A typical series may look like: [0,0,3.1,0,0,0] which mean the data is populated in the // 3rd row or column. This works well and keeps the labels aligned but has a side effect // of not showing the label is the data is zero. Work around is to not go lower than // 0.01 or something. for(var i=0;i0){ return this.series[i].name; // String } } } return "-"; // String }, resizeChart: function(/*Object*/dim){ // summary: // Call this function to change the chart size. // Can be connected to a layout widget that calls // resize. // var w = Math.max(dim.w, this.minWidth); var h = Math.max(dim.h, this.minHeight); this.resize(w, h); } }); })();