source: Dev/trunk/src/client/dojox/charting/DataChart.js @ 532

Last change on this file since 532 was 483, checked in by hendrikvanantwerpen, 11 years ago

Added Dojo 1.9.3 release.

File size: 14.9 KB
Line 
1define(["dojo/_base/kernel", "dojo/_base/lang", "dojo/_base/declare", "dojo/_base/html", "dojo/_base/connect",
2         "dojo/_base/array", "./Chart2D", "./themes/PlotKit/blue", "dojo/dom"],
3         function(kernel, lang, declare, html, hub, arr, Chart, blue, dom){
4        // FIXME: This module drags in all Charting modules because of the Chart2D dependency...it is VERY heavy
5        kernel.experimental("dojox.charting.DataChart");
6
7        // Defaults for axes
8        //      to be mixed in with xaxis/yaxis custom properties
9        // see dojox.charting.axis2d.Default for details.
10        var _yaxis = {
11                vertical: true,
12                min: 0,
13                max: 10,
14                majorTickStep: 5,
15                minorTickStep: 1,
16                natural:false,
17                stroke: "black",
18                majorTick: {stroke: "black", length: 8},
19                minorTick: {stroke: "gray", length: 2},
20                majorLabels:true
21        };
22
23        var _xaxis = {
24                natural: true,          // true - no fractions
25                majorLabels: true,      //show labels on major ticks
26                includeZero: false, // do not change on upating chart
27                majorTickStep: 1,
28                majorTick: {stroke: "black", length: 8},
29                fixUpper:"major",
30                stroke: "black",
31                htmlLabels: true,
32                from:1
33        };
34
35        // default for chart elements
36        var chartPlot = {
37                markers: true,
38                tension:2,
39                gap:2
40        };
41
42        return declare("dojox.charting.DataChart", Chart, {
43                // summary:
44                //              Extension to the 2D chart that connects to a data store in
45                //              a simple manner. Convenience methods have been added for
46                //              connecting store item labels to the chart labels.
47                // description:
48                //              This code should be considered very experimental and the APIs subject
49                //              to change. This is currently an alpha version and will need some testing
50                //              and review.
51                //
52                //              The main reason for this extension is to create animated charts, generally
53                //              available with scroll=true, and a property field that gets continually updated.
54                //              The previous property settings are kept in memory and displayed until scrolled
55                //              off the chart.
56                //
57                //              Although great effort was made to maintain the integrity of the current
58                //              charting APIs, some things have been added or modified in order to get
59                //              the store to connect and also to get the data to scroll/animate.
60                //              "displayRange" in particular is used to force the xaxis to a specific
61                //              size and keep the chart from stretching or squashing to fit the data.
62                //
63                //              Currently, plot lines can only be set at initialization. Setting
64                //              a new store query will have no effect (although using setStore
65                //              may work but its untested).
66                // example:
67                //      |       var chart = new dojox.charting.DataChart("myNode", {
68                //      |               displayRange:8,
69                //      |               store:dataStore,
70                //      |               query:{symbol:"*"},
71                //      |               fieldName:"price"
72                //      |               type: dojox.charting.plot2d.Columns
73                //      |       });
74
75                // scroll: Boolean
76                //              Whether live data updates and changes display, like columns moving
77                //              up and down, or whether it scrolls to the left as data is added
78                scroll:true,
79
80                // comparative: Boolean
81                //              If false, all items are each their own series.
82                //              If true, the items are combined into one series
83                //              so that their charted properties can be compared.
84                comparative:false,
85
86                // query: String
87                //              Used for fetching items. Will vary depending upon store.
88                query: "*",
89
90                // queryOptions: String
91                //              Option used for fetching items
92                queryOptions: "",
93
94                /*=====
95                        // start:Number
96                        //              first item to fetch from store
97                        // count:Number
98                        //              Total amount of items to fetch from store
99                        // sort:Object
100                        //              Parameters to sort the fetched items from store
101                =====*/
102
103                // fieldName: String
104                //              The field in the store item that is getting charted
105                fieldName: "value",
106
107                // chartTheme: dojox.charting.themes.*
108                //              The theme to style the chart. Defaults to PlotKit.blue.
109                chartTheme: blue,
110
111                // displayRange: Number
112                //              The number of major ticks to show on the xaxis
113                displayRange:0,
114
115                // stretchToFit: Boolean
116                //              If true, chart is sized to data. If false, chart is a
117                //              fixed size. Note, is overridden by displayRange.
118                //              TODO: Stretch for the y-axis?
119                stretchToFit:true,
120
121                // minWidth: Number
122                //              The the smallest the chart width can be
123                minWidth:200,
124
125                // minHeight: Number
126                //              The the smallest the chart height can be
127                minHeight:100,
128
129                // showing: Boolean
130                //              Whether the chart is showing (default) on
131                //              initialization or hidden.
132                showing: true,
133
134                // label: String
135                //              The name field of the store item
136                //              DO NOT SET: Set from store.labelAttribute
137                label: "name",
138
139                constructor: function(node, kwArgs){
140                        // summary:
141                        //              Set up properties and initialize chart build.
142                        // node: DomNode
143                        //              The node to attach the chart to.
144                        // kwArgs: Object
145                        //              - xaxis: Object: optional parameters for xaxis (see above)
146                        //              - yaxis: Object: optional parameters for yaxis (see above)
147                        //              - store: Object: dojo.data store (currently nly supports Persevere)
148                        //              - xaxis: Object: First query for store
149                        //              - grid: Object: Options for the grid plot
150                        //              - chartPlot: Object: Options for chart elements (lines, bars, etc)
151
152                        this.domNode = dom.byId(node);
153
154                        lang.mixin(this, kwArgs);
155
156                        this.xaxis = lang.mixin(lang.mixin({}, _xaxis), kwArgs.xaxis);
157                        if(this.xaxis.labelFunc == "seriesLabels"){
158                                this.xaxis.labelFunc = lang.hitch(this, "seriesLabels");
159                        }
160
161                        this.yaxis = lang.mixin(lang.mixin({}, _yaxis), kwArgs.yaxis);
162                        if(this.yaxis.labelFunc == "seriesLabels"){
163                                this.yaxis.labelFunc = lang.hitch(this, "seriesLabels");
164                        }
165
166                        // potential event's collector
167                        this._events = [];
168
169                        this.convertLabels(this.yaxis);
170                        this.convertLabels(this.xaxis);
171
172                        this.onSetItems = {};
173                        this.onSetInterval = 0;
174                        this.dataLength = 0;
175                        this.seriesData = {};
176                        this.seriesDataBk = {};
177                        this.firstRun =  true;
178
179                        this.dataOffset = 0;
180
181                        // FIXME: looks better with this, but it's custom
182                        this.chartTheme.plotarea.stroke = {color: "gray", width: 3};
183
184                        this.setTheme(this.chartTheme);
185
186                        // displayRange overrides stretchToFit
187                        if(this.displayRange){
188                                this.stretchToFit = false;
189                        }
190                        if(!this.stretchToFit){
191                                this.xaxis.to = this.displayRange;
192                        }
193                        // we don't want axis on Pie
194                        var cartesian = kwArgs.type && kwArgs.type != "Pie" && kwArgs.type.prototype.declaredClass != "dojox.charting.plot2d.Pie";
195                        if(cartesian){
196                                this.addAxis("x", this.xaxis);
197                                this.addAxis("y", this.yaxis);
198                        }
199                        chartPlot.type = kwArgs.type || "Markers";
200                        this.addPlot("default", lang.mixin(chartPlot, kwArgs.chartPlot));
201                        if(cartesian){
202                                this.addPlot("grid", lang.mixin(kwArgs.grid || {}, {type: "Grid", hMinorLines: true}));
203                        }
204                       
205                        if(this.showing){
206                                this.render();
207                        }
208
209                        if(kwArgs.store){
210                                this.setStore(kwArgs.store, kwArgs.query, kwArgs.fieldName, kwArgs.queryOptions);
211                        }
212                },
213
214                destroy: function(){
215                        arr.forEach(this._events, hub.disconnect);
216                        this.inherited(arguments);
217                },
218
219                setStore: function(/*Object*/store, /* ? String*/query, /* ? String*/fieldName, /* ? Object */queryOptions){
220                        // summary:
221                        //              Sets the chart store and query
222                        //              then does the first fetch and
223                        //              connects to subsequent changes.
224
225                        // TODO: Not handling resetting store
226
227                        this.firstRun = true;
228                        this.store = store || this.store;
229                        this.query = query || this.query;
230                        this.fieldName = fieldName || this.fieldName;
231                        this.label = this.store.getLabelAttributes();
232                        this.queryOptions = queryOptions || queryOptions;
233
234                        arr.forEach(this._events, hub.disconnect);
235                        this._events = [
236                                hub.connect(this.store, "onSet", this, "onSet"),
237                                hub.connect(this.store, "onError", this, "onError")
238                        ];
239                        this.fetch();
240                },
241
242                show: function(){
243                        // summary:
244                        //              If chart is hidden, show it
245                        if(!this.showing){
246                                html.style(this.domNode, "display", "");
247                                this.showing = true;
248                                this.render();
249                        }
250                },
251                hide: function(){
252                        // summary:
253                        //              If chart is showing, hide it
254                        //              Prevents rendering while hidden
255                        if(this.showing){
256                                html.style(this.domNode, "display", "none");
257                                this.showing = false;
258                        }
259                },
260
261                onSet: function(/*storeObject*/item){
262                        // summary:
263                        //              Fired when a store item changes.
264                        //              Collects the item calls and when
265                        //              done (after 200ms), sends item
266                        //              array to onData().
267
268                        // FIXME: Using labels instead of IDs for item
269                        //      identifiers here and in the chart series. This
270                        //      is obviously short sighted, but currently used
271                        //      for seriesLabels. Workaround for potential bugs
272                        //      is to assign a label for which all items are unique.
273
274                        var nm = this.getProperty(item, this.label);
275
276                        // FIXME: why the check for if-in-runs?
277                        if(nm in this.runs || this.comparative){
278                                clearTimeout(this.onSetInterval);
279                                if(!this.onSetItems[nm]){
280                                        this.onSetItems[nm] = item;
281                                }
282                                this.onSetInterval = setTimeout(lang.hitch(this, function(){
283                                        clearTimeout(this.onSetInterval);
284                                        var items = [];
285                                        for(var nm in this.onSetItems){
286                                                items.push(this.onSetItems[nm]);
287                                        }
288                                        this.onData(items);
289                                        this.onSetItems = {};
290                                }),200);
291                        }
292                },
293
294                onError: function(/*Error*/err){
295                        // stub
296                        //      Fires on fetch error
297                        console.error("DataChart Error:", err);
298                },
299
300                onDataReceived: function(/*Array*/items){
301                        // summary:
302                        //              stub. Fires after data is received but
303                        //              before data is parsed and rendered
304                },
305
306                getProperty: function(/*storeObject*/item, prop){
307                        // summary:
308                        //              The main use of this function is to determine
309                        //              between a single value and an array of values.
310                        //              Other property types included for convenience.
311                        //
312                        if(prop==this.label){
313                                return this.store.getLabel(item);
314                        }
315                        if(prop=="id"){
316                                return this.store.getIdentity(item);
317                        }
318                        var value = this.store.getValues(item, prop);
319                        if(value.length < 2){
320                                value = this.store.getValue(item, prop);
321                        }
322                        return value;
323                },
324                onData: function(/*Array*/items){
325                        // summary:
326                        //              Called after a completed fetch
327                        //              or when store items change.
328                        //              On first run, sets the chart data,
329                        //              then updates chart and legends.
330
331                        //console.log("Store:", store);console.log("items: (", items.length+")", items);console.log("Chart:", this);
332                        if(!items || !items.length){ return; }
333
334                        if(this.items && this.items.length != items.length){
335                                arr.forEach(items, function(m){
336                                        var id = this.getProperty(m, "id");
337                                        arr.forEach(this.items, function(m2, i){
338                                                if(this.getProperty(m2, "id") == id){
339                                                        this.items[i] = m2;
340                                                }
341                                        },this);
342                                }, this);
343                                items = this.items;
344                        }
345                        if(this.stretchToFit){
346                                this.displayRange = items.length;
347                        }
348                        this.onDataReceived(items);
349                        this.items = items;
350
351
352                        if(this.comparative){
353                                // all items are gathered together and used as one
354                                //      series so their properties can be compared.
355                                var nm = "default";
356
357                                this.seriesData[nm] = [];
358                                this.seriesDataBk[nm] = [];
359                                arr.forEach(items, function(m){
360                                        var field = this.getProperty(m, this.fieldName);
361                                        this.seriesData[nm].push(field);
362                                }, this);
363
364                        }else{
365
366                                // each item is a separate series.
367                                arr.forEach(items, function(m, i){
368                                        var nm = this.store.getLabel(m);
369                                        if(!this.seriesData[nm]){
370                                                this.seriesData[nm] = [];
371                                                this.seriesDataBk[nm] = [];
372                                        }
373
374                                        // the property in the item we are using
375                                        var field = this.getProperty(m, this.fieldName);
376                                        if(lang.isArray(field)){
377                                                // Data is an array, so it's a snapshot, and not
378                                                //      live, updating data
379                                                //
380                                                this.seriesData[nm] = field;
381
382                                        }else{
383                                                if(!this.scroll){
384                                                        // Data updates, and "moves in place". Columns and
385                                                        //      line markers go up and down
386                                                        //
387                                                        // create empty chart elements by starting an array
388                                                        //      with zeros until we reach our relevant data
389                                                        var ar = arr.map(new Array(i+1), function(){ return 0; });
390                                                        ar.push(Number(field));
391                                                        this.seriesData[nm] = ar;
392
393                                                }else{
394                                                        // Data updates and scrolls to the left
395                                                        if(this.seriesDataBk[nm].length > this.seriesData[nm].length){
396                                                                this.seriesData[nm] = this.seriesDataBk[nm];
397                                                        }
398                                                        // Collecting and storing series data. The items come in
399                                                        //      only one at a time, but we need to display historical
400                                                        //      data, so it is kept in memory.
401                                                        this.seriesData[nm].push(Number(field));
402                                                }
403                                                this.seriesDataBk[nm].push(Number(field));
404                                        }
405                                }, this);
406                        }
407
408                        // displayData is the segment of the data array that is within
409                        // the chart boundaries
410                        var displayData;
411                        if(this.firstRun){
412                                // First time around we need to add the series (chart lines)
413                                //      to the chart.
414                                this.firstRun = false;
415                                for(nm in this.seriesData){
416                                        this.addSeries(nm, this.seriesData[nm]);
417                                        displayData = this.seriesData[nm];
418                                }
419
420                        }else{
421
422                                // update existing series
423                                for(nm in this.seriesData){
424                                        displayData = this.seriesData[nm];
425
426                                        if(this.scroll && displayData.length > this.displayRange){
427                                                // chart lines have gone beyond the right boundary.
428                                                this.dataOffset = displayData.length-this.displayRange - 1;
429                                                displayData = displayData.slice(displayData.length-this.displayRange, displayData.length);
430                                        }
431                                        this.updateSeries(nm, displayData);
432                                }
433                        }
434                        this.dataLength = displayData.length;
435
436                        if(this.showing){
437                                this.render();
438                        }
439
440                },
441
442                fetch: function(){
443                        // summary:
444                        //              Fetches initial data. Subsequent changes
445                        //              are received via onSet in data store.
446                        //
447                        if(!this.store){ return; }
448                        this.store.fetch({query:this.query, queryOptions:this.queryOptions, start:this.start, count:this.count, sort:this.sort,
449                                onComplete:lang.hitch(this, function(data){
450                                        setTimeout(lang.hitch(this, function(){
451                                                this.onData(data)
452                                        }),0);
453                                }),
454                                onError:lang.hitch(this, "onError")
455                        });
456                },
457
458                convertLabels: function(axis){
459                        // summary:
460                        //              Convenience method to convert a label array of strings
461                        //              into an array of objects
462                        //
463                        if(!axis.labels || lang.isObject(axis.labels[0])){ return null; }
464
465                        axis.labels = arr.map(axis.labels, function(ele, i){
466                                return {value:i, text:ele};
467                        });
468                        return null; // null
469                },
470
471                seriesLabels: function(/*Number*/val){
472                        // summary:
473                        //              Convenience method that sets series labels based on item labels.
474                        val--;
475                        if(this.series.length<1 || (!this.comparative && val>this.series.length)){ return "-"; }
476                        if(this.comparative){
477                                return this.store.getLabel(this.items[val]);// String
478
479                        }else{
480                                // FIXME:
481                                // Here we are setting the label base on if there is data in the array slot.
482                                //      A typical series may look like: [0,0,3.1,0,0,0] which mean the data is populated in the
483                                //      3rd row or column. This works well and keeps the labels aligned but has a side effect
484                                //      of not showing the label is the data is zero. Work around is to not go lower than
485                                //      0.01 or something.
486                                for(var i=0;i<this.series.length; i++){
487                                        if(this.series[i].data[val]>0){
488                                                return this.series[i].name; // String
489                                        }
490                                }
491                        }
492                        return "-"; // String
493
494                },
495
496                resizeChart: function(/*Object*/dim){
497                        // summary:
498                        //              Call this function to change the chart size.
499                        //              Can be connected to a layout widget that calls
500                        //              resize.
501
502                        var w = Math.max(dim.w, this.minWidth);
503                        var h = Math.max(dim.h, this.minHeight);
504                        this.resize(w, h);
505                }
506        });
507});
Note: See TracBrowser for help on using the repository browser.