[483] | 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 | })(); |
---|