DataChart.js 15 KB

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