1 | dojo.provide("dojox.widget.DataPresentation"); |
---|
2 | dojo.experimental("dojox.widget.DataPresentation"); |
---|
3 | |
---|
4 | dojo.require("dojox.grid.DataGrid"); |
---|
5 | dojo.require("dojox.charting.Chart2D"); |
---|
6 | dojo.require("dojox.charting.widget.Legend"); |
---|
7 | dojo.require("dojox.charting.action2d.Tooltip"); |
---|
8 | dojo.require("dojox.charting.action2d.Highlight"); |
---|
9 | dojo.require("dojo.colors"); |
---|
10 | dojo.require("dojo.data.ItemFileWriteStore"); |
---|
11 | |
---|
12 | (function(){ |
---|
13 | |
---|
14 | // sort out the labels for the independent axis of the chart |
---|
15 | var getLabels = function(range, labelMod, charttype, domNode){ |
---|
16 | |
---|
17 | // prepare labels for the independent axis |
---|
18 | var labels = []; |
---|
19 | // add empty label, hack |
---|
20 | labels[0] = {value: 0, text: ''}; |
---|
21 | |
---|
22 | var nlabels = range.length; |
---|
23 | |
---|
24 | // auto-set labelMod for horizontal charts if the labels will otherwise collide |
---|
25 | if((charttype !== "ClusteredBars") && (charttype !== "StackedBars")){ |
---|
26 | var cwid = domNode.offsetWidth; |
---|
27 | var tmp = ("" + range[0]).length * range.length * 7; // *assume* 7 pixels width per character ( was 9 ) |
---|
28 | |
---|
29 | if(labelMod == 1){ |
---|
30 | for(var z = 1; z < 500; ++z){ |
---|
31 | if((tmp / z) < cwid){ |
---|
32 | break; |
---|
33 | } |
---|
34 | ++labelMod; |
---|
35 | } |
---|
36 | } |
---|
37 | } |
---|
38 | |
---|
39 | // now set the labels |
---|
40 | for(var i = 0; i < nlabels; i++){ |
---|
41 | //sparse labels |
---|
42 | labels.push({ |
---|
43 | value: i + 1, |
---|
44 | text: (!labelMod || i % labelMod) ? "" : range[i] |
---|
45 | }); |
---|
46 | } |
---|
47 | |
---|
48 | // add empty label again, hack |
---|
49 | labels.push({value: nlabels + 1, text:''}); |
---|
50 | |
---|
51 | return labels; |
---|
52 | }; |
---|
53 | |
---|
54 | // get the configuration of an independent axis for the chart |
---|
55 | var getIndependentAxisArgs = function(charttype, labels){ |
---|
56 | |
---|
57 | var args = { vertical: false, labels: labels, min: 0, max: labels.length-1, majorTickStep: 1, minorTickStep: 1 }; |
---|
58 | |
---|
59 | // clustered or stacked bars have a vertical independent axis |
---|
60 | if((charttype === "ClusteredBars") || (charttype === "StackedBars")){ |
---|
61 | args.vertical = true; |
---|
62 | } |
---|
63 | |
---|
64 | // lines, areas and stacked areas don't need the extra slots at each end |
---|
65 | if((charttype === "Lines") || (charttype === "Areas") || (charttype === "StackedAreas")){ |
---|
66 | args.min++; |
---|
67 | args.max--; |
---|
68 | } |
---|
69 | |
---|
70 | return args; |
---|
71 | }; |
---|
72 | |
---|
73 | // get the configuration of a dependent axis for the chart |
---|
74 | var getDependentAxisArgs = function(charttype, axistype, minval, maxval){ |
---|
75 | |
---|
76 | var args = { vertical: true, fixLower: "major", fixUpper: "major", natural: true }; |
---|
77 | |
---|
78 | // secondary dependent axis is not left-bottom |
---|
79 | if(axistype === "secondary"){ |
---|
80 | args.leftBottom = false; |
---|
81 | } |
---|
82 | |
---|
83 | // clustered or stacked bars have horizontal dependent axes |
---|
84 | if((charttype === "ClusteredBars") || (charttype === "StackedBars")){ |
---|
85 | args.vertical = false; |
---|
86 | } |
---|
87 | |
---|
88 | // ensure axis does not "collapse" for flat series |
---|
89 | if(minval == maxval){ |
---|
90 | args.min = minval - 1; |
---|
91 | args.max = maxval + 1; |
---|
92 | } |
---|
93 | |
---|
94 | return args; |
---|
95 | }; |
---|
96 | |
---|
97 | // get the configuration of a plot for the chart |
---|
98 | var getPlotArgs = function(charttype, axistype, animate){ |
---|
99 | |
---|
100 | var args = { type: charttype, hAxis: "independent", vAxis: "dependent-" + axistype, gap: 4, lines: false, areas: false, markers: false }; |
---|
101 | |
---|
102 | // clustered or stacked bars have horizontal dependent axes |
---|
103 | if((charttype === "ClusteredBars") || (charttype === "StackedBars")){ |
---|
104 | args.hAxis = args.vAxis; |
---|
105 | args.vAxis = "independent"; |
---|
106 | } |
---|
107 | |
---|
108 | // turn on lines for Lines, Areas and StackedAreas |
---|
109 | if((charttype === "Lines") || (charttype === "Hybrid-Lines") || (charttype === "Areas") || (charttype === "StackedAreas")){ |
---|
110 | args.lines = true; |
---|
111 | } |
---|
112 | |
---|
113 | // turn on areas for Areas and StackedAreas |
---|
114 | if((charttype === "Areas") || (charttype === "StackedAreas")){ |
---|
115 | args.areas = true; |
---|
116 | } |
---|
117 | |
---|
118 | // turn on markers and shadow for Lines |
---|
119 | if(charttype === "Lines"){ |
---|
120 | args.markers = true; |
---|
121 | } |
---|
122 | |
---|
123 | // turn on shadow for Hybrid-Lines |
---|
124 | // also, Hybrid-Lines is not a true chart type: use Lines for the actual plot |
---|
125 | if(charttype === "Hybrid-Lines"){ |
---|
126 | args.shadows = {dx: 2, dy: 2, dw: 2}; |
---|
127 | args.type = "Lines"; |
---|
128 | } |
---|
129 | |
---|
130 | // also, Hybrid-ClusteredColumns is not a true chart type: use ClusteredColumns for the actual plot |
---|
131 | if(charttype === "Hybrid-ClusteredColumns"){ |
---|
132 | args.type = "ClusteredColumns"; |
---|
133 | } |
---|
134 | |
---|
135 | // enable animation on the plot if animation is requested |
---|
136 | if(animate){ |
---|
137 | args.animate = animate; |
---|
138 | } |
---|
139 | |
---|
140 | return args; |
---|
141 | }; |
---|
142 | |
---|
143 | // set up a chart presentation |
---|
144 | var setupChart = function(/*DomNode*/domNode, /*Object?*/chart, /*String*/type, /*Boolean*/reverse, /*Object*/animate, /*Integer*/labelMod, /*String*/theme, /*String*/tooltip, /*Object?*/store, /*String?*/query, /*String?*/queryOptions){ |
---|
145 | var _chart = chart; |
---|
146 | |
---|
147 | if(!_chart){ |
---|
148 | domNode.innerHTML = ""; // any other content in the node disrupts the chart rendering |
---|
149 | _chart = new dojox.charting.Chart2D(domNode); |
---|
150 | } |
---|
151 | |
---|
152 | // set the theme |
---|
153 | if(theme){ |
---|
154 | |
---|
155 | // workaround for a theme bug: its _clone method |
---|
156 | // does not transfer the markers, so we repair |
---|
157 | // that omission here |
---|
158 | // FIXME this should be removed once the theme bug is fixed |
---|
159 | theme._clone = function(){ |
---|
160 | var result = new dojox.charting.Theme({ |
---|
161 | chart: this.chart, |
---|
162 | plotarea: this.plotarea, |
---|
163 | axis: this.axis, |
---|
164 | series: this.series, |
---|
165 | marker: this.marker, |
---|
166 | antiAlias: this.antiAlias, |
---|
167 | assignColors: this.assignColors, |
---|
168 | assignMarkers: this.assigneMarkers, |
---|
169 | colors: dojo.delegate(this.colors) |
---|
170 | }); |
---|
171 | |
---|
172 | result.markers = this.markers; |
---|
173 | result._buildMarkerArray(); |
---|
174 | |
---|
175 | return result; |
---|
176 | }; |
---|
177 | |
---|
178 | _chart.setTheme(theme); |
---|
179 | } |
---|
180 | |
---|
181 | var range = store.series_data[0].slice(0); |
---|
182 | |
---|
183 | // reverse the labels if requested |
---|
184 | if(reverse){ |
---|
185 | range.reverse(); |
---|
186 | } |
---|
187 | |
---|
188 | var labels = getLabels(range, labelMod, type, domNode); |
---|
189 | |
---|
190 | // collect details of whether primary and/or secondary axes are required |
---|
191 | // and what plots we have instantiated using each type of axis |
---|
192 | var plots = {}; |
---|
193 | |
---|
194 | // collect maximum and minimum data values |
---|
195 | var maxval = null; |
---|
196 | var minval = null; |
---|
197 | |
---|
198 | var seriestoremove = {}; |
---|
199 | for(var sname in _chart.runs){ |
---|
200 | seriestoremove[sname] = true; |
---|
201 | } |
---|
202 | |
---|
203 | // set x values & max data value |
---|
204 | var nseries = store.series_name.length; |
---|
205 | for(var i = 0; i < nseries; i++){ |
---|
206 | // only include series with chart=true and with some data values in |
---|
207 | if(store.series_chart[i] && (store.series_data[i].length > 0)){ |
---|
208 | |
---|
209 | var charttype = type; |
---|
210 | var axistype = store.series_axis[i]; |
---|
211 | |
---|
212 | if(charttype == "Hybrid"){ |
---|
213 | if(store.series_charttype[i] == 'line'){ |
---|
214 | charttype = "Hybrid-Lines"; |
---|
215 | }else{ |
---|
216 | charttype = "Hybrid-ClusteredColumns"; |
---|
217 | } |
---|
218 | } |
---|
219 | |
---|
220 | // ensure we have recorded that we are using this axis type |
---|
221 | if(!plots[axistype]){ |
---|
222 | plots[axistype] = {}; |
---|
223 | } |
---|
224 | |
---|
225 | // ensure we have the correct type of plot for this series |
---|
226 | if(!plots[axistype][charttype]){ |
---|
227 | var axisname = axistype + "-" + charttype; |
---|
228 | |
---|
229 | // create the plot and enable tooltips |
---|
230 | _chart.addPlot(axisname, getPlotArgs(charttype, axistype, animate)); |
---|
231 | |
---|
232 | var tooltipArgs = {}; |
---|
233 | if(typeof tooltip == 'string'){ |
---|
234 | tooltipArgs.text = function(o){ |
---|
235 | var substitutions = [o.element, o.run.name, range[o.index], ((charttype === "ClusteredBars") || (charttype === "StackedBars")) ? o.x : o.y]; |
---|
236 | return dojo.replace(tooltip, substitutions); // from Dojo 1.4 onward |
---|
237 | //return tooltip.replace(/\{([^\}]+)\}/g, function(_, token){ return dojo.getObject(token, false, substitutions); }); // prior to Dojo 1.4 |
---|
238 | } |
---|
239 | }else if(typeof tooltip == 'function'){ |
---|
240 | tooltipArgs.text = tooltip; |
---|
241 | } |
---|
242 | new dojox.charting.action2d.Tooltip(_chart, axisname, tooltipArgs); |
---|
243 | |
---|
244 | // add highlighting, except for lines |
---|
245 | if(charttype !== "Lines" && charttype !== "Hybrid-Lines"){ |
---|
246 | new dojox.charting.action2d.Highlight(_chart, axisname); |
---|
247 | } |
---|
248 | |
---|
249 | // record that this plot type is now created |
---|
250 | plots[axistype][charttype] = true; |
---|
251 | } |
---|
252 | |
---|
253 | // extract the series values |
---|
254 | var xvals = []; |
---|
255 | var valen = store.series_data[i].length; |
---|
256 | for(var j = 0; j < valen; j++){ |
---|
257 | var val = store.series_data[i][j]; |
---|
258 | xvals.push(val); |
---|
259 | if(maxval === null || val > maxval){ |
---|
260 | maxval = val; |
---|
261 | } |
---|
262 | if(minval === null || val < minval){ |
---|
263 | minval = val; |
---|
264 | } |
---|
265 | } |
---|
266 | |
---|
267 | // reverse the values if requested |
---|
268 | if(reverse){ |
---|
269 | xvals.reverse(); |
---|
270 | } |
---|
271 | |
---|
272 | var seriesargs = { plot: axistype + "-" + charttype }; |
---|
273 | if(store.series_linestyle[i]){ |
---|
274 | seriesargs.stroke = { style: store.series_linestyle[i] }; |
---|
275 | } |
---|
276 | |
---|
277 | _chart.addSeries(store.series_name[i], xvals, seriesargs); |
---|
278 | delete seriestoremove[store.series_name[i]]; |
---|
279 | } |
---|
280 | } |
---|
281 | |
---|
282 | // remove any series that are no longer needed |
---|
283 | for(sname in seriestoremove){ |
---|
284 | _chart.removeSeries(sname); |
---|
285 | } |
---|
286 | |
---|
287 | // create axes |
---|
288 | _chart.addAxis("independent", getIndependentAxisArgs(type, labels)); |
---|
289 | _chart.addAxis("dependent-primary", getDependentAxisArgs(type, "primary", minval, maxval)); |
---|
290 | _chart.addAxis("dependent-secondary", getDependentAxisArgs(type, "secondary", minval, maxval)); |
---|
291 | |
---|
292 | return _chart; |
---|
293 | }; |
---|
294 | |
---|
295 | // set up a legend presentation |
---|
296 | var setupLegend = function(/*DomNode*/domNode, /*Legend*/legend, /*Chart2D*/chart, /*Boolean*/horizontal){ |
---|
297 | // destroy any existing legend and recreate |
---|
298 | var _legend = legend; |
---|
299 | |
---|
300 | if(!_legend){ |
---|
301 | _legend = new dojox.charting.widget.Legend({ chart: chart, horizontal: horizontal }, domNode); |
---|
302 | }else{ |
---|
303 | _legend.refresh(); |
---|
304 | } |
---|
305 | |
---|
306 | return _legend; |
---|
307 | }; |
---|
308 | |
---|
309 | // set up a grid presentation |
---|
310 | var setupGrid = function(/*DomNode*/domNode, /*Object?*/grid, /*Object?*/store, /*String?*/query, /*String?*/queryOptions){ |
---|
311 | var _grid = grid || new dojox.grid.DataGrid({}, domNode); |
---|
312 | _grid.startup(); |
---|
313 | _grid.setStore(store, query, queryOptions); |
---|
314 | |
---|
315 | var structure = []; |
---|
316 | for(var ser = 0; ser < store.series_name.length; ser++){ |
---|
317 | // only include series with grid=true and with some data values in |
---|
318 | if(store.series_grid[ser] && (store.series_data[ser].length > 0)){ |
---|
319 | structure.push({ field: "data." + ser, name: store.series_name[ser], width: "auto", formatter: store.series_gridformatter[ser] }); |
---|
320 | } |
---|
321 | } |
---|
322 | |
---|
323 | _grid.setStructure(structure); |
---|
324 | |
---|
325 | return _grid; |
---|
326 | }; |
---|
327 | |
---|
328 | // set up a title presentation |
---|
329 | var setupTitle = function(/*DomNode*/domNode, /*object*/store){ |
---|
330 | if(store.title){ |
---|
331 | domNode.innerHTML = store.title; |
---|
332 | } |
---|
333 | }; |
---|
334 | |
---|
335 | // set up a footer presentation |
---|
336 | var setupFooter = function(/*DomNode*/domNode, /*object*/store){ |
---|
337 | if(store.footer){ |
---|
338 | domNode.innerHTML = store.footer; |
---|
339 | } |
---|
340 | }; |
---|
341 | |
---|
342 | // obtain a subfield from a field specifier which may contain |
---|
343 | // multiple levels (eg, "child.foo[36].manacle") |
---|
344 | var getSubfield = function(/*Object*/object, /*String*/field){ |
---|
345 | var result = object; |
---|
346 | |
---|
347 | if(field){ |
---|
348 | var fragments = field.split(/[.\[\]]+/); |
---|
349 | for(var frag = 0, l = fragments.length; frag < l; frag++){ |
---|
350 | if(result){ |
---|
351 | result = result[fragments[frag]]; |
---|
352 | } |
---|
353 | } |
---|
354 | } |
---|
355 | |
---|
356 | return result; |
---|
357 | }; |
---|
358 | |
---|
359 | dojo.declare("dojox.widget.DataPresentation", null, { |
---|
360 | // summary: |
---|
361 | // |
---|
362 | // DataPresentation |
---|
363 | // |
---|
364 | // A widget that connects to a data store in a simple manner, |
---|
365 | // and also provides some additional convenience mechanisms |
---|
366 | // for connecting to common data sources without needing to |
---|
367 | // explicitly construct a Dojo data store. The widget can then |
---|
368 | // present the data in several forms: as a graphical chart, |
---|
369 | // as a tabular grid, or as display panels presenting meta-data |
---|
370 | // (title, creation information, etc) from the data. The |
---|
371 | // widget can also create and manage several of these forms |
---|
372 | // in one simple construction. |
---|
373 | // |
---|
374 | // Note: this is a first experimental draft and any/all details |
---|
375 | // are subject to substantial change in later drafts. |
---|
376 | // |
---|
377 | // example: |
---|
378 | // |
---|
379 | // var pres = new dojox.data.DataPresentation("myChartNode", { |
---|
380 | // type: "chart", |
---|
381 | // url: "/data/mydata", |
---|
382 | // gridNode: "myGridNode" |
---|
383 | // }); |
---|
384 | // |
---|
385 | // properties: |
---|
386 | // |
---|
387 | // store: Object |
---|
388 | // Dojo data store used to supply data to be presented. This may |
---|
389 | // be supplied on construction or created implicitly based on |
---|
390 | // other construction parameters ('data', 'url'). |
---|
391 | // |
---|
392 | // query: String |
---|
393 | // Query to apply to the Dojo data store used to supply data to |
---|
394 | // be presented. |
---|
395 | // |
---|
396 | // queryOptions: String |
---|
397 | // Query options to apply to the Dojo data store used to supply |
---|
398 | // data to be presented. |
---|
399 | // |
---|
400 | // data: Object |
---|
401 | // Data to be presented. If supplied on construction this property |
---|
402 | // will override any value supplied for the 'store' property. |
---|
403 | // |
---|
404 | // url: String |
---|
405 | // URL to fetch data from in JSON format. If supplied on |
---|
406 | // construction this property will override any values supplied |
---|
407 | // for the 'store' and/or 'data' properties. Note that the data |
---|
408 | // can also be comment-filtered JSON, although this will trigger |
---|
409 | // a warning message in the console unless djConfig.useCommentedJson |
---|
410 | // has been set to true. |
---|
411 | // |
---|
412 | // urlContent: Object |
---|
413 | // Content to be passed to the URL when fetching data. If a URL has |
---|
414 | // not been supplied, this value is ignored. |
---|
415 | // |
---|
416 | // urlError: function |
---|
417 | // A function to be called if an error is encountered when fetching |
---|
418 | // data from the supplied URL. This function will be supplied with |
---|
419 | // two parameters exactly as the error function supplied to the |
---|
420 | // dojo.xhrGet function. This function may be called multiple times |
---|
421 | // if a refresh interval has been supplied. |
---|
422 | // |
---|
423 | // refreshInterval: Number |
---|
424 | // the time interval in milliseconds after which the data supplied |
---|
425 | // via the 'data' property or fetched from a URL via the 'url' |
---|
426 | // property should be regularly refreshed. This property is |
---|
427 | // ignored if neither the 'data' nor 'url' property has been |
---|
428 | // supplied. If the refresh interval is zero, no regular refresh is done. |
---|
429 | // |
---|
430 | // refreshIntervalPending: |
---|
431 | // the JavaScript set interval currently in progress, if any |
---|
432 | // |
---|
433 | // series: Array |
---|
434 | // an array of objects describing the data series to be included |
---|
435 | // in the data presentation. Each object may contain the |
---|
436 | // following fields: |
---|
437 | // datapoints: the name of the field from the source data which |
---|
438 | // contains an array of the data points for this data series. |
---|
439 | // If not supplied, the source data is assumed to be an array |
---|
440 | // of data points to be used. |
---|
441 | // field: the name of the field within each data point which |
---|
442 | // contains the data for this data series. If not supplied, |
---|
443 | // each data point is assumed to be the value for the series. |
---|
444 | // name: a name for the series, used in the legend and grid headings |
---|
445 | // namefield: the name of the field from the source data which |
---|
446 | // contains the name the series, used in the legend and grid |
---|
447 | // headings. If both name and namefield are supplied, name takes |
---|
448 | // precedence. If neither are supplied, a default name is used. |
---|
449 | // chart: true if the series should be included in a chart presentation (default: true) |
---|
450 | // charttype: the type of presentation of the series in the chart, which can be |
---|
451 | // "range", "line", "bar" (default: "bar") |
---|
452 | // linestyle: the stroke style for lines (if applicable) (default: "Solid") |
---|
453 | // axis: the dependant axis to which the series will be attached in the chart, |
---|
454 | // which can be "primary" or "secondary" |
---|
455 | // grid: true if the series should be included in a data grid presentation (default: true) |
---|
456 | // gridformatter: an optional formatter to use for this series in the data grid |
---|
457 | // |
---|
458 | // a call-back function may alternatively be supplied. The function takes |
---|
459 | // a single parameter, which will be the data (from the 'data' field or |
---|
460 | // loaded from the value in the 'url' field), and should return the array |
---|
461 | // of objects describing the data series to be included in the data |
---|
462 | // presentation. This enables the series structures to be built dynamically |
---|
463 | // after data load, and rebuilt if necessary on data refresh. The call-back |
---|
464 | // function will be called each time new data is set, loaded or refreshed. |
---|
465 | // A call-back function cannot be used if the data is supplied directly |
---|
466 | // from a Dojo data store. |
---|
467 | // |
---|
468 | // type: String |
---|
469 | // the type of presentation to be applied at the DOM attach point. |
---|
470 | // This can be 'chart', 'legend', 'grid', 'title', 'footer'. The |
---|
471 | // default type is 'chart'. |
---|
472 | type: "chart", |
---|
473 | // |
---|
474 | // chartType: String |
---|
475 | // the type of chart to display. This can be 'clusteredbars', |
---|
476 | // 'areas', 'stackedcolumns', 'stackedbars', 'stackedareas', |
---|
477 | // 'lines', 'hybrid'. The default type is 'bar'. |
---|
478 | chartType: "clusteredBars", |
---|
479 | // |
---|
480 | // reverse: Boolean |
---|
481 | // true if the chart independant axis should be reversed. |
---|
482 | reverse: false, |
---|
483 | // |
---|
484 | // animate: Object |
---|
485 | // if an object is supplied, then the chart bars or columns will animate |
---|
486 | // into place. If the object contains a field 'duration' then the value |
---|
487 | // supplied is the duration of the animation in milliseconds, otherwise |
---|
488 | // a default duration is used. A boolean value true can alternatively be |
---|
489 | // supplied to enable animation with the default duration. |
---|
490 | // The default is null (no animation). |
---|
491 | animate: null, |
---|
492 | // |
---|
493 | // labelMod: Integer |
---|
494 | // the frequency of label annotations to be included on the |
---|
495 | // independent axis. 1=every label. 0=no labels. The default is 1. |
---|
496 | labelMod: 1, |
---|
497 | // |
---|
498 | // tooltip: String | Function |
---|
499 | // a string pattern defining the tooltip text to be applied to chart |
---|
500 | // data points, or a function which takes a single parameter and returns |
---|
501 | // the tooltip text to be applied to chart data points. The string pattern |
---|
502 | // will have the following substitutions applied: |
---|
503 | // {0} - the type of chart element ('bar', 'surface', etc) |
---|
504 | // {1} - the name of the data series |
---|
505 | // {2} - the independent axis value at the tooltip data point |
---|
506 | // {3} - the series value at the tooltip data point point |
---|
507 | // The function, if supplied, will receive a single parameter exactly |
---|
508 | // as per the dojox.charting.action2D.Tooltip class. The default value |
---|
509 | // is to apply the default tooltip as defined by the |
---|
510 | // dojox.charting.action2D.Tooltip class. |
---|
511 | // |
---|
512 | // legendHorizontal: Boolean | Number |
---|
513 | // true if the legend should be rendered horizontally, or a number if |
---|
514 | // the legend should be rendered as horizontal rows with that number of |
---|
515 | // items in each row, or false if the legend should be rendered |
---|
516 | // vertically (same as specifying 1). The default is true (legend |
---|
517 | // rendered horizontally). |
---|
518 | legendHorizontal: true, |
---|
519 | // |
---|
520 | // theme: String|Theme |
---|
521 | // a theme to use for the chart, or the name of a theme. |
---|
522 | // |
---|
523 | // chartNode: String|DomNode |
---|
524 | // an optional DOM node or the id of a DOM node to receive a |
---|
525 | // chart presentation of the data. Supply only when a chart is |
---|
526 | // required and the type is not 'chart'; when the type is |
---|
527 | // 'chart' this property will be set to the widget attach point. |
---|
528 | // |
---|
529 | // legendNode: String|DomNode |
---|
530 | // an optional DOM node or the id of a DOM node to receive a |
---|
531 | // chart legend for the data. Supply only when a legend is |
---|
532 | // required and the type is not 'legend'; when the type is |
---|
533 | // 'legend' this property will be set to the widget attach point. |
---|
534 | // |
---|
535 | // gridNode: String|DomNode |
---|
536 | // an optional DOM node or the id of a DOM node to receive a |
---|
537 | // grid presentation of the data. Supply only when a grid is |
---|
538 | // required and the type is not 'grid'; when the type is |
---|
539 | // 'grid' this property will be set to the widget attach point. |
---|
540 | // |
---|
541 | // titleNode: String|DomNode |
---|
542 | // an optional DOM node or the id of a DOM node to receive a |
---|
543 | // title for the data. Supply only when a title is |
---|
544 | // required and the type is not 'title'; when the type is |
---|
545 | // 'title' this property will be set to the widget attach point. |
---|
546 | // |
---|
547 | // footerNode: String|DomNode |
---|
548 | // an optional DOM node or the id of a DOM node to receive a |
---|
549 | // footer presentation of the data. Supply only when a footer is |
---|
550 | // required and the type is not 'footer'; when the type is |
---|
551 | // 'footer' this property will be set to the widget attach point. |
---|
552 | // |
---|
553 | // chartWidget: Object |
---|
554 | // the chart widget, if any |
---|
555 | // |
---|
556 | // legendWidget: Object |
---|
557 | // the legend widget, if any |
---|
558 | // |
---|
559 | // gridWidget: Object |
---|
560 | // the grid widget, if any |
---|
561 | |
---|
562 | constructor: function(node, args){ |
---|
563 | // summary: |
---|
564 | // Set up properties and initialize. |
---|
565 | // |
---|
566 | // arguments: |
---|
567 | // node: DomNode |
---|
568 | // The node to attach the data presentation to. |
---|
569 | // kwArgs: Object (see above) |
---|
570 | |
---|
571 | // apply arguments directly |
---|
572 | dojo.mixin(this, args); |
---|
573 | |
---|
574 | // store our DOM attach point |
---|
575 | this.domNode = dojo.byId(node); |
---|
576 | |
---|
577 | // also apply the DOM attach point as the node for the presentation type |
---|
578 | this[this.type + "Node"] = this.domNode; |
---|
579 | |
---|
580 | // load the theme if provided by name |
---|
581 | if(typeof this.theme == 'string'){ |
---|
582 | this.theme = dojo.getObject(this.theme); |
---|
583 | } |
---|
584 | |
---|
585 | // resolve any the nodes that were supplied as ids |
---|
586 | this.chartNode = dojo.byId(this.chartNode); |
---|
587 | this.legendNode = dojo.byId(this.legendNode); |
---|
588 | this.gridNode = dojo.byId(this.gridNode); |
---|
589 | this.titleNode = dojo.byId(this.titleNode); |
---|
590 | this.footerNode = dojo.byId(this.footerNode); |
---|
591 | |
---|
592 | // we used to support a 'legendVertical' so for now |
---|
593 | // at least maintain backward compatibility |
---|
594 | if(this.legendVertical){ |
---|
595 | this.legendHorizontal = !this.legendVertical; |
---|
596 | } |
---|
597 | |
---|
598 | if(this.url){ |
---|
599 | this.setURL(null, null, this.refreshInterval); |
---|
600 | } |
---|
601 | else{ |
---|
602 | if(this.data){ |
---|
603 | this.setData(null, this.refreshInterval); |
---|
604 | } |
---|
605 | else{ |
---|
606 | this.setStore(); |
---|
607 | } |
---|
608 | } |
---|
609 | }, |
---|
610 | |
---|
611 | setURL: function(/*String?*/url, /*Object?*/ urlContent, /*Number?*/refreshInterval){ |
---|
612 | // summary: |
---|
613 | // Sets the URL to fetch data from, with optional content |
---|
614 | // supplied with the request, and an optional |
---|
615 | // refresh interval in milliseconds (0=no refresh) |
---|
616 | |
---|
617 | // if a refresh interval is supplied we will start a fresh |
---|
618 | // refresh after storing the supplied url |
---|
619 | if(refreshInterval){ |
---|
620 | this.cancelRefresh(); |
---|
621 | } |
---|
622 | |
---|
623 | this.url = url || this.url; |
---|
624 | this.urlContent = urlContent || this.urlContent; |
---|
625 | this.refreshInterval = refreshInterval || this.refreshInterval; |
---|
626 | |
---|
627 | var me = this; |
---|
628 | |
---|
629 | dojo.xhrGet({ |
---|
630 | url: this.url, |
---|
631 | content: this.urlContent, |
---|
632 | handleAs: 'json-comment-optional', |
---|
633 | load: function(response, ioArgs){ |
---|
634 | me.setData(response); |
---|
635 | }, |
---|
636 | error: function(xhr, ioArgs){ |
---|
637 | if(me.urlError && (typeof me.urlError == "function")){ |
---|
638 | me.urlError(xhr, ioArgs); |
---|
639 | } |
---|
640 | } |
---|
641 | }); |
---|
642 | |
---|
643 | if(refreshInterval && (this.refreshInterval > 0)){ |
---|
644 | this.refreshIntervalPending = setInterval(function(){ |
---|
645 | me.setURL(); |
---|
646 | }, this.refreshInterval); |
---|
647 | } |
---|
648 | }, |
---|
649 | |
---|
650 | setData: function(/*Object?*/data, /*Number?*/refreshInterval){ |
---|
651 | // summary: |
---|
652 | // Sets the data to be presented, and an optional |
---|
653 | // refresh interval in milliseconds (0=no refresh) |
---|
654 | |
---|
655 | // if a refresh interval is supplied we will start a fresh |
---|
656 | // refresh after storing the supplied data reference |
---|
657 | if(refreshInterval){ |
---|
658 | this.cancelRefresh(); |
---|
659 | } |
---|
660 | |
---|
661 | this.data = data || this.data; |
---|
662 | this.refreshInterval = refreshInterval || this.refreshInterval; |
---|
663 | |
---|
664 | // TODO if no 'series' property was provided, build one intelligently here |
---|
665 | // (until that is done, a 'series' property must be supplied) |
---|
666 | |
---|
667 | var _series = (typeof this.series == 'function') ? this.series(this.data) : this.series; |
---|
668 | |
---|
669 | var datasets = [], |
---|
670 | series_data = [], |
---|
671 | series_name = [], |
---|
672 | series_chart = [], |
---|
673 | series_charttype = [], |
---|
674 | series_linestyle = [], |
---|
675 | series_axis = [], |
---|
676 | series_grid = [], |
---|
677 | series_gridformatter = [], |
---|
678 | maxlen = 0; |
---|
679 | |
---|
680 | // identify the dataset arrays in which series values can be found |
---|
681 | for(var ser = 0; ser < _series.length; ser++){ |
---|
682 | datasets[ser] = getSubfield(this.data, _series[ser].datapoints); |
---|
683 | if(datasets[ser] && (datasets[ser].length > maxlen)){ |
---|
684 | maxlen = datasets[ser].length; |
---|
685 | } |
---|
686 | |
---|
687 | series_data[ser] = []; |
---|
688 | // name can be specified in series structure, or by field in series structure, otherwise use a default |
---|
689 | series_name[ser] = _series[ser].name || (_series[ser].namefield ? getSubfield(this.data, _series[ser].namefield) : null) || ("series " + ser); |
---|
690 | series_chart[ser] = (_series[ser].chart !== false); |
---|
691 | series_charttype[ser] = _series[ser].charttype || "bar"; |
---|
692 | series_linestyle[ser] = _series[ser].linestyle; |
---|
693 | series_axis[ser] = _series[ser].axis || "primary"; |
---|
694 | series_grid[ser] = (_series[ser].grid !== false); |
---|
695 | series_gridformatter[ser] = _series[ser].gridformatter; |
---|
696 | } |
---|
697 | |
---|
698 | // create an array of data points by sampling the series |
---|
699 | // and an array of series arrays by collecting the series |
---|
700 | // each data point has an 'index' item containing a sequence number |
---|
701 | // and items named "data.0", "data.1", ... containing the series samples |
---|
702 | // and the first data point also has items named "name.0", "name.1", ... containing the series names |
---|
703 | // and items named "series.0", "series.1", ... containing arrays with the complete series in |
---|
704 | var point, datapoint, datavalue, fdatavalue; |
---|
705 | var datapoints = []; |
---|
706 | |
---|
707 | for(point = 0; point < maxlen; point++){ |
---|
708 | datapoint = { index: point }; |
---|
709 | for(ser = 0; ser < _series.length; ser++){ |
---|
710 | if(datasets[ser] && (datasets[ser].length > point)){ |
---|
711 | datavalue = getSubfield(datasets[ser][point], _series[ser].field); |
---|
712 | |
---|
713 | if(series_chart[ser]){ |
---|
714 | // convert the data value to a float if possible |
---|
715 | fdatavalue = parseFloat(datavalue); |
---|
716 | if(!isNaN(fdatavalue)){ |
---|
717 | datavalue = fdatavalue; |
---|
718 | } |
---|
719 | } |
---|
720 | |
---|
721 | datapoint["data." + ser] = datavalue; |
---|
722 | series_data[ser].push(datavalue); |
---|
723 | } |
---|
724 | } |
---|
725 | datapoints.push(datapoint); |
---|
726 | } |
---|
727 | |
---|
728 | if(maxlen <= 0){ |
---|
729 | datapoints.push({index: 0}); |
---|
730 | } |
---|
731 | |
---|
732 | // now build a prepared store from the data points we've constructed |
---|
733 | var store = new dojo.data.ItemFileWriteStore({ data: { identifier: 'index', items: datapoints }}); |
---|
734 | if(this.data.title){ |
---|
735 | store.title = this.data.title; |
---|
736 | } |
---|
737 | if(this.data.footer){ |
---|
738 | store.footer = this.data.footer; |
---|
739 | } |
---|
740 | |
---|
741 | store.series_data = series_data; |
---|
742 | store.series_name = series_name; |
---|
743 | store.series_chart = series_chart; |
---|
744 | store.series_charttype = series_charttype; |
---|
745 | store.series_linestyle = series_linestyle; |
---|
746 | store.series_axis = series_axis; |
---|
747 | store.series_grid = series_grid; |
---|
748 | store.series_gridformatter = series_gridformatter; |
---|
749 | |
---|
750 | this.setPreparedStore(store); |
---|
751 | |
---|
752 | if(refreshInterval && (this.refreshInterval > 0)){ |
---|
753 | var me = this; |
---|
754 | this.refreshIntervalPending = setInterval(function(){ |
---|
755 | me.setData(); |
---|
756 | }, this.refreshInterval); |
---|
757 | } |
---|
758 | }, |
---|
759 | |
---|
760 | refresh: function(){ |
---|
761 | // summary: |
---|
762 | // If a URL or data has been supplied, refreshes the |
---|
763 | // presented data from the URL or data. If a refresh |
---|
764 | // interval is also set, the periodic refresh is |
---|
765 | // restarted. If a URL or data was not supplied, this |
---|
766 | // method has no effect. |
---|
767 | if(this.url){ |
---|
768 | this.setURL(this.url, this.urlContent, this.refreshInterval); |
---|
769 | }else if(this.data){ |
---|
770 | this.setData(this.data, this.refreshInterval); |
---|
771 | } |
---|
772 | }, |
---|
773 | |
---|
774 | cancelRefresh: function(){ |
---|
775 | // summary: |
---|
776 | // Cancels any and all outstanding data refreshes |
---|
777 | if(this.refreshIntervalPending){ |
---|
778 | // cancel existing refresh |
---|
779 | clearInterval(this.refreshIntervalPending); |
---|
780 | this.refreshIntervalPending = undefined; |
---|
781 | } |
---|
782 | }, |
---|
783 | |
---|
784 | setStore: function(/*Object?*/store, /*String?*/query, /*Object?*/queryOptions){ |
---|
785 | // FIXME build a prepared store properly -- this requires too tight a convention to be followed to be useful |
---|
786 | this.setPreparedStore(store, query, queryOptions); |
---|
787 | }, |
---|
788 | |
---|
789 | setPreparedStore: function(/*Object?*/store, /*String?*/query, /*Object?*/queryOptions){ |
---|
790 | // summary: |
---|
791 | // Sets the store and query. |
---|
792 | // |
---|
793 | this.preparedstore = store || this.store; |
---|
794 | this.query = query || this.query; |
---|
795 | this.queryOptions = queryOptions || this.queryOptions; |
---|
796 | |
---|
797 | if(this.preparedstore){ |
---|
798 | if(this.chartNode){ |
---|
799 | this.chartWidget = setupChart(this.chartNode, this.chartWidget, this.chartType, this.reverse, this.animate, this.labelMod, this.theme, this.tooltip, this.preparedstore, this.query, this,queryOptions); |
---|
800 | this.renderChartWidget(); |
---|
801 | } |
---|
802 | if(this.legendNode){ |
---|
803 | this.legendWidget = setupLegend(this.legendNode, this.legendWidget, this.chartWidget, this.legendHorizontal); |
---|
804 | } |
---|
805 | if(this.gridNode){ |
---|
806 | this.gridWidget = setupGrid(this.gridNode, this.gridWidget, this.preparedstore, this.query, this.queryOptions); |
---|
807 | this.renderGridWidget(); |
---|
808 | } |
---|
809 | if(this.titleNode){ |
---|
810 | setupTitle(this.titleNode, this.preparedstore); |
---|
811 | } |
---|
812 | if(this.footerNode){ |
---|
813 | setupFooter(this.footerNode, this.preparedstore); |
---|
814 | } |
---|
815 | } |
---|
816 | }, |
---|
817 | |
---|
818 | renderChartWidget: function(){ |
---|
819 | // summary: |
---|
820 | // Renders the chart widget (if any). This method is |
---|
821 | // called whenever a chart widget is created or |
---|
822 | // configured, and may be connected to. |
---|
823 | if(this.chartWidget){ |
---|
824 | this.chartWidget.render(); |
---|
825 | } |
---|
826 | }, |
---|
827 | |
---|
828 | renderGridWidget: function(){ |
---|
829 | // summary: |
---|
830 | // Renders the grid widget (if any). This method is |
---|
831 | // called whenever a grid widget is created or |
---|
832 | // configured, and may be connected to. |
---|
833 | if(this.gridWidget){ |
---|
834 | this.gridWidget.render(); |
---|
835 | } |
---|
836 | }, |
---|
837 | |
---|
838 | getChartWidget: function(){ |
---|
839 | // summary: |
---|
840 | // Returns the chart widget (if any) created if the type |
---|
841 | // is "chart" or the "chartNode" property was supplied. |
---|
842 | return this.chartWidget; |
---|
843 | }, |
---|
844 | |
---|
845 | getGridWidget: function(){ |
---|
846 | // summary: |
---|
847 | // Returns the grid widget (if any) created if the type |
---|
848 | // is "grid" or the "gridNode" property was supplied. |
---|
849 | return this.gridWidget; |
---|
850 | }, |
---|
851 | |
---|
852 | destroy: function(){ |
---|
853 | // summary: |
---|
854 | // Destroys the widget and all components and resources. |
---|
855 | |
---|
856 | // cancel any outstanding refresh requests |
---|
857 | this.cancelRefresh(); |
---|
858 | |
---|
859 | if(this.chartWidget){ |
---|
860 | this.chartWidget.destroy(); |
---|
861 | delete this.chartWidget; |
---|
862 | } |
---|
863 | |
---|
864 | if(this.legendWidget){ |
---|
865 | // no legend.destroy() |
---|
866 | delete this.legendWidget; |
---|
867 | } |
---|
868 | |
---|
869 | if(this.gridWidget){ |
---|
870 | // no grid.destroy() |
---|
871 | delete this.gridWidget; |
---|
872 | } |
---|
873 | |
---|
874 | if(this.chartNode){ |
---|
875 | this.chartNode.innerHTML = ""; |
---|
876 | } |
---|
877 | |
---|
878 | if(this.legendNode){ |
---|
879 | this.legendNode.innerHTML = ""; |
---|
880 | } |
---|
881 | |
---|
882 | if(this.gridNode){ |
---|
883 | this.gridNode.innerHTML = ""; |
---|
884 | } |
---|
885 | |
---|
886 | if(this.titleNode){ |
---|
887 | this.titleNode.innerHTML = ""; |
---|
888 | } |
---|
889 | |
---|
890 | if(this.footerNode){ |
---|
891 | this.footerNode.innerHTML = ""; |
---|
892 | } |
---|
893 | } |
---|
894 | |
---|
895 | }); |
---|
896 | |
---|
897 | })(); |
---|