123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528 |
- /*
- Copyright (c) 2004-2012, 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);
- }
- });
- })();
- }
|