You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
507 lines
15 KiB
507 lines
15 KiB
define(["dojo/_base/kernel", "dojo/_base/lang", "dojo/_base/declare", "dojo/_base/html", "dojo/_base/connect", |
|
"dojo/_base/array", "./Chart2D", "./themes/PlotKit/blue", "dojo/dom"], |
|
function(kernel, lang, declare, html, hub, arr, Chart, blue, dom){ |
|
// FIXME: This module drags in all Charting modules because of the Chart2D dependency...it is VERY heavy |
|
kernel.experimental("dojox.charting.DataChart"); |
|
|
|
// 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 |
|
}; |
|
|
|
return declare("dojox.charting.DataChart", Chart, { |
|
// summary: |
|
// 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 |
|
// | }); |
|
|
|
// 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 |
|
// Parameters 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: 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. |
|
// 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 = dom.byId(node); |
|
|
|
lang.mixin(this, kwArgs); |
|
|
|
this.xaxis = lang.mixin(lang.mixin({}, _xaxis), kwArgs.xaxis); |
|
if(this.xaxis.labelFunc == "seriesLabels"){ |
|
this.xaxis.labelFunc = lang.hitch(this, "seriesLabels"); |
|
} |
|
|
|
this.yaxis = lang.mixin(lang.mixin({}, _yaxis), kwArgs.yaxis); |
|
if(this.yaxis.labelFunc == "seriesLabels"){ |
|
this.yaxis.labelFunc = lang.hitch(this, "seriesLabels"); |
|
} |
|
|
|
// potential event's collector |
|
this._events = []; |
|
|
|
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; |
|
} |
|
// we don't want axis on Pie |
|
var cartesian = kwArgs.type && kwArgs.type != "Pie" && kwArgs.type.prototype.declaredClass != "dojox.charting.plot2d.Pie"; |
|
if(cartesian){ |
|
this.addAxis("x", this.xaxis); |
|
this.addAxis("y", this.yaxis); |
|
} |
|
chartPlot.type = kwArgs.type || "Markers"; |
|
this.addPlot("default", lang.mixin(chartPlot, kwArgs.chartPlot)); |
|
if(cartesian){ |
|
this.addPlot("grid", lang.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); |
|
} |
|
}, |
|
|
|
destroy: function(){ |
|
arr.forEach(this._events, hub.disconnect); |
|
this.inherited(arguments); |
|
}, |
|
|
|
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; |
|
|
|
arr.forEach(this._events, hub.disconnect); |
|
this._events = [ |
|
hub.connect(this.store, "onSet", this, "onSet"), |
|
hub.connect(this.store, "onError", this, "onError") |
|
]; |
|
this.fetch(); |
|
}, |
|
|
|
show: function(){ |
|
// summary: |
|
// If chart is hidden, show it |
|
if(!this.showing){ |
|
html.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){ |
|
html.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(lang.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("DataChart 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 || !items.length){ return; } |
|
|
|
if(this.items && this.items.length != items.length){ |
|
arr.forEach(items, function(m){ |
|
var id = this.getProperty(m, "id"); |
|
arr.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] = []; |
|
arr.forEach(items, function(m){ |
|
var field = this.getProperty(m, this.fieldName); |
|
this.seriesData[nm].push(field); |
|
}, this); |
|
|
|
}else{ |
|
|
|
// each item is a separate series. |
|
arr.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(lang.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 = arr.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:lang.hitch(this, function(data){ |
|
setTimeout(lang.hitch(this, function(){ |
|
this.onData(data) |
|
}),0); |
|
}), |
|
onError:lang.hitch(this, "onError") |
|
}); |
|
}, |
|
|
|
convertLabels: function(axis){ |
|
// summary: |
|
// Convenience method to convert a label array of strings |
|
// into an array of objects |
|
// |
|
if(!axis.labels || lang.isObject(axis.labels[0])){ return null; } |
|
|
|
axis.labels = arr.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;i<this.series.length; i++){ |
|
if(this.series[i].data[val]>0){ |
|
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); |
|
} |
|
}); |
|
});
|
|
|