501 lines
15 KiB
JavaScript
501 lines
15 KiB
JavaScript
|
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;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);
|
||
|
}
|
||
|
});
|
||
|
|
||
|
})();
|