source: Dev/branches/rest-dojo-ui/client/dojox/charting/DataChart.js @ 256

Last change on this file since 256 was 256, checked in by hendrikvanantwerpen, 13 years ago

Reworked project structure based on REST interaction and Dojo library. As
soon as this is stable, the old jQueryUI branch can be removed (it's
kept for reference).

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