/*
Copyright (c) 2004-2011, The Dojo Foundation All Rights Reserved.
Available via Academic Free License >= 2.1 OR the modified BSD license.
see: http://dojotoolkit.org/license for details
*/
if(!dojo._hasResource["dojox.charting.DataChart"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code.
dojo._hasResource["dojox.charting.DataChart"] = true;
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");
}
// 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;
}
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);
}
},
destroy: function(){
dojo.forEach(this._events, dojo.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;
dojo.forEach(this._events, dojo.disconnect);
this._events = [
dojo.connect(this.store, "onSet", this, "onSet"),
dojo.connect(this.store, "onError", this, "onError")
];
this.fetch();
},
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("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){
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, function(data){
setTimeout(dojo.hitch(this, function(){
this.onData(data)
}),0);
}),
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;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);
}
});
})();
}