DataChart.js 15 KB

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