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 | // A widget that connects to a data store in a simple manner, |
---|
362 | // and also provides some additional convenience mechanisms |
---|
363 | // for connecting to common data sources without needing to |
---|
364 | // explicitly construct a Dojo data store. The widget can then |
---|
365 | // present the data in several forms: as a graphical chart, |
---|
366 | // as a tabular grid, or as display panels presenting meta-data |
---|
367 | // (title, creation information, etc) from the data. The |
---|
368 | // widget can also create and manage several of these forms |
---|
369 | // in one simple construction. |
---|
370 | // |
---|
371 | // Note: this is a first experimental draft and any/all details |
---|
372 | // are subject to substantial change in later drafts. |
---|
373 | // example: |
---|
374 | // | var pres = new dojox.data.DataPresentation("myChartNode", { |
---|
375 | // | type: "chart", |
---|
376 | // | url: "/data/mydata", |
---|
377 | // | gridNode: "myGridNode" |
---|
378 | // | }); |
---|
379 | |
---|
380 | // store: Object |
---|
381 | // Dojo data store used to supply data to be presented. This may |
---|
382 | // be supplied on construction or created implicitly based on |
---|
383 | // other construction parameters ('data', 'url'). |
---|
384 | |
---|
385 | // query: String |
---|
386 | // Query to apply to the Dojo data store used to supply data to |
---|
387 | // be presented. |
---|
388 | |
---|
389 | // queryOptions: String |
---|
390 | // Query options to apply to the Dojo data store used to supply |
---|
391 | // data to be presented. |
---|
392 | |
---|
393 | // data: Object |
---|
394 | // Data to be presented. If supplied on construction this property |
---|
395 | // will override any value supplied for the 'store' property. |
---|
396 | |
---|
397 | // url: String |
---|
398 | // URL to fetch data from in JSON format. If supplied on |
---|
399 | // construction this property will override any values supplied |
---|
400 | // for the 'store' and/or 'data' properties. Note that the data |
---|
401 | // can also be comment-filtered JSON, although this will trigger |
---|
402 | // a warning message in the console unless djConfig.useCommentedJson |
---|
403 | // has been set to true. |
---|
404 | |
---|
405 | // urlContent: Object |
---|
406 | // Content to be passed to the URL when fetching data. If a URL has |
---|
407 | // not been supplied, this value is ignored. |
---|
408 | |
---|
409 | // urlError: function |
---|
410 | // A function to be called if an error is encountered when fetching |
---|
411 | // data from the supplied URL. This function will be supplied with |
---|
412 | // two parameters exactly as the error function supplied to the |
---|
413 | // dojo.xhrGet function. This function may be called multiple times |
---|
414 | // if a refresh interval has been supplied. |
---|
415 | |
---|
416 | // refreshInterval: Number |
---|
417 | // the time interval in milliseconds after which the data supplied |
---|
418 | // via the 'data' property or fetched from a URL via the 'url' |
---|
419 | // property should be regularly refreshed. This property is |
---|
420 | // ignored if neither the 'data' nor 'url' property has been |
---|
421 | // supplied. If the refresh interval is zero, no regular refresh is done. |
---|
422 | |
---|
423 | // refreshIntervalPending: |
---|
424 | // the JavaScript set interval currently in progress, if any |
---|
425 | |
---|
426 | // series: Array |
---|
427 | // an array of objects describing the data series to be included |
---|
428 | // in the data presentation. Each object may contain the |
---|
429 | // following fields: |
---|
430 | // |
---|
431 | // - datapoints: the name of the field from the source data which |
---|
432 | // contains an array of the data points for this data series. |
---|
433 | // If not supplied, the source data is assumed to be an array |
---|
434 | // of data points to be used. |
---|
435 | // - field: the name of the field within each data point which |
---|
436 | // contains the data for this data series. If not supplied, |
---|
437 | // each data point is assumed to be the value for the series. |
---|
438 | // - name: a name for the series, used in the legend and grid headings |
---|
439 | // - namefield: |
---|
440 | // the name of the field from the source data which |
---|
441 | // contains the name the series, used in the legend and grid |
---|
442 | // headings. If both name and namefield are supplied, name takes |
---|
443 | // precedence. If neither are supplied, a default name is used. |
---|
444 | // - chart: true if the series should be included in a chart presentation (default: true) |
---|
445 | // - charttype: the type of presentation of the series in the chart, which can be |
---|
446 | // "range", "line", "bar" (default: "bar") |
---|
447 | // - linestyle: the stroke style for lines (if applicable) (default: "Solid") |
---|
448 | // - axis: the dependant axis to which the series will be attached in the chart, |
---|
449 | // which can be "primary" or "secondary" |
---|
450 | // - grid: true if the series should be included in a data grid presentation (default: true) |
---|
451 | // - gridformatter: an optional formatter to use for this series in the data grid |
---|
452 | // |
---|
453 | // a call-back function may alternatively be supplied. The function takes |
---|
454 | // a single parameter, which will be the data (from the 'data' field or |
---|
455 | // loaded from the value in the 'url' field), and should return the array |
---|
456 | // of objects describing the data series to be included in the data |
---|
457 | // presentation. This enables the series structures to be built dynamically |
---|
458 | // after data load, and rebuilt if necessary on data refresh. The call-back |
---|
459 | // function will be called each time new data is set, loaded or refreshed. |
---|
460 | // A call-back function cannot be used if the data is supplied directly |
---|
461 | // from a Dojo data store. |
---|
462 | |
---|
463 | // type: String |
---|
464 | // the type of presentation to be applied at the DOM attach point. |
---|
465 | // This can be 'chart', 'legend', 'grid', 'title', 'footer'. The |
---|
466 | // default type is 'chart'. |
---|
467 | type: "chart", |
---|
468 | |
---|
469 | // chartType: String |
---|
470 | // the type of chart to display. This can be 'clusteredbars', |
---|
471 | // 'areas', 'stackedcolumns', 'stackedbars', 'stackedareas', |
---|
472 | // 'lines', 'hybrid'. The default type is 'bar'. |
---|
473 | chartType: "clusteredBars", |
---|
474 | |
---|
475 | // reverse: Boolean |
---|
476 | // true if the chart independent axis should be reversed. |
---|
477 | reverse: false, |
---|
478 | |
---|
479 | // animate: Object |
---|
480 | // if an object is supplied, then the chart bars or columns will animate |
---|
481 | // into place. If the object contains a field 'duration' then the value |
---|
482 | // supplied is the duration of the animation in milliseconds, otherwise |
---|
483 | // a default duration is used. A boolean value true can alternatively be |
---|
484 | // supplied to enable animation with the default duration. |
---|
485 | // The default is null (no animation). |
---|
486 | animate: null, |
---|
487 | |
---|
488 | // labelMod: Integer |
---|
489 | // the frequency of label annotations to be included on the |
---|
490 | // independent axis. 1=every label. 0=no labels. The default is 1. |
---|
491 | labelMod: 1, |
---|
492 | |
---|
493 | // tooltip: String|Function |
---|
494 | // a string pattern defining the tooltip text to be applied to chart |
---|
495 | // data points, or a function which takes a single parameter and returns |
---|
496 | // the tooltip text to be applied to chart data points. The string pattern |
---|
497 | // will have the following substitutions applied: |
---|
498 | // |
---|
499 | // - {0} - the type of chart element ('bar', 'surface', etc) |
---|
500 | // - {1} - the name of the data series |
---|
501 | // - {2} - the independent axis value at the tooltip data point |
---|
502 | // - {3} - the series value at the tooltip data point point |
---|
503 | // |
---|
504 | // The function, if supplied, will receive a single parameter exactly |
---|
505 | // as per the dojox.charting.action2D.Tooltip class. The default value |
---|
506 | // is to apply the default tooltip as defined by the |
---|
507 | // dojox.charting.action2D.Tooltip class. |
---|
508 | |
---|
509 | // legendHorizontal: Boolean|Number |
---|
510 | // true if the legend should be rendered horizontally, or a number if |
---|
511 | // the legend should be rendered as horizontal rows with that number of |
---|
512 | // items in each row, or false if the legend should be rendered |
---|
513 | // vertically (same as specifying 1). The default is true (legend |
---|
514 | // rendered horizontally). |
---|
515 | legendHorizontal: true, |
---|
516 | |
---|
517 | // theme: String|Theme |
---|
518 | // a theme to use for the chart, or the name of a theme. |
---|
519 | |
---|
520 | // chartNode: String|DomNode |
---|
521 | // an optional DOM node or the id of a DOM node to receive a |
---|
522 | // chart presentation of the data. Supply only when a chart is |
---|
523 | // required and the type is not 'chart'; when the type is |
---|
524 | // 'chart' this property will be set to the widget attach point. |
---|
525 | |
---|
526 | // legendNode: String|DomNode |
---|
527 | // an optional DOM node or the id of a DOM node to receive a |
---|
528 | // chart legend for the data. Supply only when a legend is |
---|
529 | // required and the type is not 'legend'; when the type is |
---|
530 | // 'legend' this property will be set to the widget attach point. |
---|
531 | |
---|
532 | // gridNode: String|DomNode |
---|
533 | // an optional DOM node or the id of a DOM node to receive a |
---|
534 | // grid presentation of the data. Supply only when a grid is |
---|
535 | // required and the type is not 'grid'; when the type is |
---|
536 | // 'grid' this property will be set to the widget attach point. |
---|
537 | |
---|
538 | // titleNode: String|DomNode |
---|
539 | // an optional DOM node or the id of a DOM node to receive a |
---|
540 | // title for the data. Supply only when a title is |
---|
541 | // required and the type is not 'title'; when the type is |
---|
542 | // 'title' this property will be set to the widget attach point. |
---|
543 | |
---|
544 | // footerNode: String|DomNode |
---|
545 | // an optional DOM node or the id of a DOM node to receive a |
---|
546 | // footer presentation of the data. Supply only when a footer is |
---|
547 | // required and the type is not 'footer'; when the type is |
---|
548 | // 'footer' this property will be set to the widget attach point. |
---|
549 | |
---|
550 | // chartWidget: Object |
---|
551 | // the chart widget, if any |
---|
552 | |
---|
553 | // legendWidget: Object |
---|
554 | // the legend widget, if any |
---|
555 | |
---|
556 | // gridWidget: Object |
---|
557 | // the grid widget, if any |
---|
558 | |
---|
559 | constructor: function(node, args){ |
---|
560 | // summary: |
---|
561 | // Set up properties and initialize. |
---|
562 | // node: DomNode |
---|
563 | // The node to attach the data presentation to. |
---|
564 | // args: Object |
---|
565 | // (see above) |
---|
566 | |
---|
567 | // apply arguments directly |
---|
568 | dojo.mixin(this, args); |
---|
569 | |
---|
570 | // store our DOM attach point |
---|
571 | this.domNode = dojo.byId(node); |
---|
572 | |
---|
573 | // also apply the DOM attach point as the node for the presentation type |
---|
574 | this[this.type + "Node"] = this.domNode; |
---|
575 | |
---|
576 | // load the theme if provided by name |
---|
577 | if(typeof this.theme == 'string'){ |
---|
578 | this.theme = dojo.getObject(this.theme); |
---|
579 | } |
---|
580 | |
---|
581 | // resolve any the nodes that were supplied as ids |
---|
582 | this.chartNode = dojo.byId(this.chartNode); |
---|
583 | this.legendNode = dojo.byId(this.legendNode); |
---|
584 | this.gridNode = dojo.byId(this.gridNode); |
---|
585 | this.titleNode = dojo.byId(this.titleNode); |
---|
586 | this.footerNode = dojo.byId(this.footerNode); |
---|
587 | |
---|
588 | // we used to support a 'legendVertical' so for now |
---|
589 | // at least maintain backward compatibility |
---|
590 | if(this.legendVertical){ |
---|
591 | this.legendHorizontal = !this.legendVertical; |
---|
592 | } |
---|
593 | |
---|
594 | if(this.url){ |
---|
595 | this.setURL(null, null, this.refreshInterval); |
---|
596 | } |
---|
597 | else{ |
---|
598 | if(this.data){ |
---|
599 | this.setData(null, this.refreshInterval); |
---|
600 | } |
---|
601 | else{ |
---|
602 | this.setStore(); |
---|
603 | } |
---|
604 | } |
---|
605 | }, |
---|
606 | |
---|
607 | setURL: function(/*String?*/url, /*Object?*/ urlContent, /*Number?*/refreshInterval){ |
---|
608 | // summary: |
---|
609 | // Sets the URL to fetch data from, with optional content |
---|
610 | // supplied with the request, and an optional |
---|
611 | // refresh interval in milliseconds (0=no refresh) |
---|
612 | |
---|
613 | // if a refresh interval is supplied we will start a fresh |
---|
614 | // refresh after storing the supplied url |
---|
615 | if(refreshInterval){ |
---|
616 | this.cancelRefresh(); |
---|
617 | } |
---|
618 | |
---|
619 | this.url = url || this.url; |
---|
620 | this.urlContent = urlContent || this.urlContent; |
---|
621 | this.refreshInterval = refreshInterval || this.refreshInterval; |
---|
622 | |
---|
623 | var me = this; |
---|
624 | |
---|
625 | dojo.xhrGet({ |
---|
626 | url: this.url, |
---|
627 | content: this.urlContent, |
---|
628 | handleAs: 'json-comment-optional', |
---|
629 | load: function(response, ioArgs){ |
---|
630 | me.setData(response); |
---|
631 | }, |
---|
632 | error: function(xhr, ioArgs){ |
---|
633 | if(me.urlError && (typeof me.urlError == "function")){ |
---|
634 | me.urlError(xhr, ioArgs); |
---|
635 | } |
---|
636 | } |
---|
637 | }); |
---|
638 | |
---|
639 | if(refreshInterval && (this.refreshInterval > 0)){ |
---|
640 | this.refreshIntervalPending = setInterval(function(){ |
---|
641 | me.setURL(); |
---|
642 | }, this.refreshInterval); |
---|
643 | } |
---|
644 | }, |
---|
645 | |
---|
646 | setData: function(/*Object?*/data, /*Number?*/refreshInterval){ |
---|
647 | // summary: |
---|
648 | // Sets the data to be presented, and an optional |
---|
649 | // refresh interval in milliseconds (0=no refresh) |
---|
650 | |
---|
651 | // if a refresh interval is supplied we will start a fresh |
---|
652 | // refresh after storing the supplied data reference |
---|
653 | if(refreshInterval){ |
---|
654 | this.cancelRefresh(); |
---|
655 | } |
---|
656 | |
---|
657 | this.data = data || this.data; |
---|
658 | this.refreshInterval = refreshInterval || this.refreshInterval; |
---|
659 | |
---|
660 | // TODO if no 'series' property was provided, build one intelligently here |
---|
661 | // (until that is done, a 'series' property must be supplied) |
---|
662 | |
---|
663 | var _series = (typeof this.series == 'function') ? this.series(this.data) : this.series; |
---|
664 | |
---|
665 | var datasets = [], |
---|
666 | series_data = [], |
---|
667 | series_name = [], |
---|
668 | series_chart = [], |
---|
669 | series_charttype = [], |
---|
670 | series_linestyle = [], |
---|
671 | series_axis = [], |
---|
672 | series_grid = [], |
---|
673 | series_gridformatter = [], |
---|
674 | maxlen = 0; |
---|
675 | |
---|
676 | // identify the dataset arrays in which series values can be found |
---|
677 | for(var ser = 0; ser < _series.length; ser++){ |
---|
678 | datasets[ser] = getSubfield(this.data, _series[ser].datapoints); |
---|
679 | if(datasets[ser] && (datasets[ser].length > maxlen)){ |
---|
680 | maxlen = datasets[ser].length; |
---|
681 | } |
---|
682 | |
---|
683 | series_data[ser] = []; |
---|
684 | // name can be specified in series structure, or by field in series structure, otherwise use a default |
---|
685 | series_name[ser] = _series[ser].name || (_series[ser].namefield ? getSubfield(this.data, _series[ser].namefield) : null) || ("series " + ser); |
---|
686 | series_chart[ser] = (_series[ser].chart !== false); |
---|
687 | series_charttype[ser] = _series[ser].charttype || "bar"; |
---|
688 | series_linestyle[ser] = _series[ser].linestyle; |
---|
689 | series_axis[ser] = _series[ser].axis || "primary"; |
---|
690 | series_grid[ser] = (_series[ser].grid !== false); |
---|
691 | series_gridformatter[ser] = _series[ser].gridformatter; |
---|
692 | } |
---|
693 | |
---|
694 | // create an array of data points by sampling the series |
---|
695 | // and an array of series arrays by collecting the series |
---|
696 | // each data point has an 'index' item containing a sequence number |
---|
697 | // and items named "data.0", "data.1", ... containing the series samples |
---|
698 | // and the first data point also has items named "name.0", "name.1", ... containing the series names |
---|
699 | // and items named "series.0", "series.1", ... containing arrays with the complete series in |
---|
700 | var point, datapoint, datavalue, fdatavalue; |
---|
701 | var datapoints = []; |
---|
702 | |
---|
703 | for(point = 0; point < maxlen; point++){ |
---|
704 | datapoint = { index: point }; |
---|
705 | for(ser = 0; ser < _series.length; ser++){ |
---|
706 | if(datasets[ser] && (datasets[ser].length > point)){ |
---|
707 | datavalue = getSubfield(datasets[ser][point], _series[ser].field); |
---|
708 | |
---|
709 | if(series_chart[ser]){ |
---|
710 | // convert the data value to a float if possible |
---|
711 | fdatavalue = parseFloat(datavalue); |
---|
712 | if(!isNaN(fdatavalue)){ |
---|
713 | datavalue = fdatavalue; |
---|
714 | } |
---|
715 | } |
---|
716 | |
---|
717 | datapoint["data." + ser] = datavalue; |
---|
718 | series_data[ser].push(datavalue); |
---|
719 | } |
---|
720 | } |
---|
721 | datapoints.push(datapoint); |
---|
722 | } |
---|
723 | |
---|
724 | if(maxlen <= 0){ |
---|
725 | datapoints.push({index: 0}); |
---|
726 | } |
---|
727 | |
---|
728 | // now build a prepared store from the data points we've constructed |
---|
729 | var store = new dojo.data.ItemFileWriteStore({ data: { identifier: 'index', items: datapoints }}); |
---|
730 | if(this.data.title){ |
---|
731 | store.title = this.data.title; |
---|
732 | } |
---|
733 | if(this.data.footer){ |
---|
734 | store.footer = this.data.footer; |
---|
735 | } |
---|
736 | |
---|
737 | store.series_data = series_data; |
---|
738 | store.series_name = series_name; |
---|
739 | store.series_chart = series_chart; |
---|
740 | store.series_charttype = series_charttype; |
---|
741 | store.series_linestyle = series_linestyle; |
---|
742 | store.series_axis = series_axis; |
---|
743 | store.series_grid = series_grid; |
---|
744 | store.series_gridformatter = series_gridformatter; |
---|
745 | |
---|
746 | this.setPreparedStore(store); |
---|
747 | |
---|
748 | if(refreshInterval && (this.refreshInterval > 0)){ |
---|
749 | var me = this; |
---|
750 | this.refreshIntervalPending = setInterval(function(){ |
---|
751 | me.setData(); |
---|
752 | }, this.refreshInterval); |
---|
753 | } |
---|
754 | }, |
---|
755 | |
---|
756 | refresh: function(){ |
---|
757 | // summary: |
---|
758 | // If a URL or data has been supplied, refreshes the |
---|
759 | // presented data from the URL or data. If a refresh |
---|
760 | // interval is also set, the periodic refresh is |
---|
761 | // restarted. If a URL or data was not supplied, this |
---|
762 | // method has no effect. |
---|
763 | if(this.url){ |
---|
764 | this.setURL(this.url, this.urlContent, this.refreshInterval); |
---|
765 | }else if(this.data){ |
---|
766 | this.setData(this.data, this.refreshInterval); |
---|
767 | } |
---|
768 | }, |
---|
769 | |
---|
770 | cancelRefresh: function(){ |
---|
771 | // summary: |
---|
772 | // Cancels any and all outstanding data refreshes |
---|
773 | if(this.refreshIntervalPending){ |
---|
774 | // cancel existing refresh |
---|
775 | clearInterval(this.refreshIntervalPending); |
---|
776 | this.refreshIntervalPending = undefined; |
---|
777 | } |
---|
778 | }, |
---|
779 | |
---|
780 | setStore: function(/*Object?*/store, /*String?*/query, /*Object?*/queryOptions){ |
---|
781 | // FIXME build a prepared store properly -- this requires too tight a convention to be followed to be useful |
---|
782 | this.setPreparedStore(store, query, queryOptions); |
---|
783 | }, |
---|
784 | |
---|
785 | setPreparedStore: function(/*Object?*/store, /*String?*/query, /*Object?*/queryOptions){ |
---|
786 | // summary: |
---|
787 | // Sets the store and query. |
---|
788 | |
---|
789 | this.preparedstore = store || this.store; |
---|
790 | this.query = query || this.query; |
---|
791 | this.queryOptions = queryOptions || this.queryOptions; |
---|
792 | |
---|
793 | if(this.preparedstore){ |
---|
794 | if(this.chartNode){ |
---|
795 | 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); |
---|
796 | this.renderChartWidget(); |
---|
797 | } |
---|
798 | if(this.legendNode){ |
---|
799 | this.legendWidget = setupLegend(this.legendNode, this.legendWidget, this.chartWidget, this.legendHorizontal); |
---|
800 | } |
---|
801 | if(this.gridNode){ |
---|
802 | this.gridWidget = setupGrid(this.gridNode, this.gridWidget, this.preparedstore, this.query, this.queryOptions); |
---|
803 | this.renderGridWidget(); |
---|
804 | } |
---|
805 | if(this.titleNode){ |
---|
806 | setupTitle(this.titleNode, this.preparedstore); |
---|
807 | } |
---|
808 | if(this.footerNode){ |
---|
809 | setupFooter(this.footerNode, this.preparedstore); |
---|
810 | } |
---|
811 | } |
---|
812 | }, |
---|
813 | |
---|
814 | renderChartWidget: function(){ |
---|
815 | // summary: |
---|
816 | // Renders the chart widget (if any). This method is |
---|
817 | // called whenever a chart widget is created or |
---|
818 | // configured, and may be connected to. |
---|
819 | if(this.chartWidget){ |
---|
820 | this.chartWidget.render(); |
---|
821 | } |
---|
822 | }, |
---|
823 | |
---|
824 | renderGridWidget: function(){ |
---|
825 | // summary: |
---|
826 | // Renders the grid widget (if any). This method is |
---|
827 | // called whenever a grid widget is created or |
---|
828 | // configured, and may be connected to. |
---|
829 | if(this.gridWidget){ |
---|
830 | this.gridWidget.render(); |
---|
831 | } |
---|
832 | }, |
---|
833 | |
---|
834 | getChartWidget: function(){ |
---|
835 | // summary: |
---|
836 | // Returns the chart widget (if any) created if the type |
---|
837 | // is "chart" or the "chartNode" property was supplied. |
---|
838 | return this.chartWidget; |
---|
839 | }, |
---|
840 | |
---|
841 | getGridWidget: function(){ |
---|
842 | // summary: |
---|
843 | // Returns the grid widget (if any) created if the type |
---|
844 | // is "grid" or the "gridNode" property was supplied. |
---|
845 | return this.gridWidget; |
---|
846 | }, |
---|
847 | |
---|
848 | destroy: function(){ |
---|
849 | // summary: |
---|
850 | // Destroys the widget and all components and resources. |
---|
851 | |
---|
852 | // cancel any outstanding refresh requests |
---|
853 | this.cancelRefresh(); |
---|
854 | |
---|
855 | if(this.chartWidget){ |
---|
856 | this.chartWidget.destroy(); |
---|
857 | delete this.chartWidget; |
---|
858 | } |
---|
859 | |
---|
860 | if(this.legendWidget){ |
---|
861 | // no legend.destroy() |
---|
862 | delete this.legendWidget; |
---|
863 | } |
---|
864 | |
---|
865 | if(this.gridWidget){ |
---|
866 | // no grid.destroy() |
---|
867 | delete this.gridWidget; |
---|
868 | } |
---|
869 | |
---|
870 | if(this.chartNode){ |
---|
871 | this.chartNode.innerHTML = ""; |
---|
872 | } |
---|
873 | |
---|
874 | if(this.legendNode){ |
---|
875 | this.legendNode.innerHTML = ""; |
---|
876 | } |
---|
877 | |
---|
878 | if(this.gridNode){ |
---|
879 | this.gridNode.innerHTML = ""; |
---|
880 | } |
---|
881 | |
---|
882 | if(this.titleNode){ |
---|
883 | this.titleNode.innerHTML = ""; |
---|
884 | } |
---|
885 | |
---|
886 | if(this.footerNode){ |
---|
887 | this.footerNode.innerHTML = ""; |
---|
888 | } |
---|
889 | } |
---|
890 | |
---|
891 | }); |
---|
892 | |
---|
893 | })(); |
---|