LineGraph.js 36 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103
  1. var util = require('../../util');
  2. var DOMutil = require('../../DOMutil');
  3. var DataSet = require('../../DataSet');
  4. var DataView = require('../../DataView');
  5. var Component = require('./Component');
  6. var DataAxis = require('./DataAxis');
  7. var GraphGroup = require('./GraphGroup');
  8. var Legend = require('./Legend');
  9. var Bars = require('./graph2d_types/bar');
  10. var Lines = require('./graph2d_types/line');
  11. var Points = require('./graph2d_types/points');
  12. var UNGROUPED = '__ungrouped__'; // reserved group id for ungrouped items
  13. /**
  14. * This is the constructor of the LineGraph. It requires a Timeline body and options.
  15. *
  16. * @param {vis.Timeline.body} body
  17. * @param {Object} options
  18. * @constructor LineGraph
  19. * @extends Component
  20. */
  21. function LineGraph(body, options) {
  22. this.id = util.randomUUID();
  23. this.body = body;
  24. this.defaultOptions = {
  25. yAxisOrientation: 'left',
  26. defaultGroup: 'default',
  27. sort: true,
  28. sampling: true,
  29. stack: false,
  30. graphHeight: '400px',
  31. shaded: {
  32. enabled: false,
  33. orientation: 'bottom' // top, bottom, zero
  34. },
  35. style: 'line', // line, bar
  36. barChart: {
  37. width: 50,
  38. sideBySide: false,
  39. align: 'center' // left, center, right
  40. },
  41. interpolation: {
  42. enabled: true,
  43. parametrization: 'centripetal', // uniform (alpha = 0.0), chordal (alpha = 1.0), centripetal (alpha = 0.5)
  44. alpha: 0.5
  45. },
  46. drawPoints: {
  47. enabled: true,
  48. size: 6,
  49. style: 'square' // square, circle
  50. },
  51. dataAxis: {}, //Defaults are done on DataAxis level
  52. legend: {}, //Defaults are done on Legend level
  53. groups: {
  54. visibility: {}
  55. }
  56. };
  57. // options is shared by this lineGraph and all its items
  58. this.options = util.extend({}, this.defaultOptions);
  59. this.dom = {};
  60. this.props = {};
  61. this.hammer = null;
  62. this.groups = {};
  63. this.abortedGraphUpdate = false;
  64. this.updateSVGheight = false;
  65. this.updateSVGheightOnResize = false;
  66. this.forceGraphUpdate = true;
  67. var me = this;
  68. this.itemsData = null; // DataSet
  69. this.groupsData = null; // DataSet
  70. // listeners for the DataSet of the items
  71. this.itemListeners = {
  72. 'add': function (event, params, senderId) { // eslint-disable-line no-unused-vars
  73. me._onAdd(params.items);
  74. },
  75. 'update': function (event, params, senderId) { // eslint-disable-line no-unused-vars
  76. me._onUpdate(params.items);
  77. },
  78. 'remove': function (event, params, senderId) { // eslint-disable-line no-unused-vars
  79. me._onRemove(params.items);
  80. }
  81. };
  82. // listeners for the DataSet of the groups
  83. this.groupListeners = {
  84. 'add': function (event, params, senderId) { // eslint-disable-line no-unused-vars
  85. me._onAddGroups(params.items);
  86. },
  87. 'update': function (event, params, senderId) { // eslint-disable-line no-unused-vars
  88. me._onUpdateGroups(params.items);
  89. },
  90. 'remove': function (event, params, senderId) { // eslint-disable-line no-unused-vars
  91. me._onRemoveGroups(params.items);
  92. }
  93. };
  94. this.items = {}; // object with an Item for every data item
  95. this.selection = []; // list with the ids of all selected nodes
  96. this.lastStart = this.body.range.start;
  97. this.touchParams = {}; // stores properties while dragging
  98. this.svgElements = {};
  99. this.setOptions(options);
  100. this.groupsUsingDefaultStyles = [0];
  101. this.body.emitter.on('rangechanged', function () {
  102. me.lastStart = me.body.range.start;
  103. me.svg.style.left = util.option.asSize(-me.props.width);
  104. me.forceGraphUpdate = true;
  105. //Is this local redraw necessary? (Core also does a change event!)
  106. me.redraw.call(me);
  107. });
  108. // create the HTML DOM
  109. this._create();
  110. this.framework = {svg: this.svg, svgElements: this.svgElements, options: this.options, groups: this.groups};
  111. }
  112. LineGraph.prototype = new Component();
  113. /**
  114. * Create the HTML DOM for the ItemSet
  115. */
  116. LineGraph.prototype._create = function () {
  117. var frame = document.createElement('div');
  118. frame.className = 'vis-line-graph';
  119. this.dom.frame = frame;
  120. // create svg element for graph drawing.
  121. this.svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
  122. this.svg.style.position = 'relative';
  123. this.svg.style.height = ('' + this.options.graphHeight).replace('px', '') + 'px';
  124. this.svg.style.display = 'block';
  125. frame.appendChild(this.svg);
  126. // data axis
  127. this.options.dataAxis.orientation = 'left';
  128. this.yAxisLeft = new DataAxis(this.body, this.options.dataAxis, this.svg, this.options.groups);
  129. this.options.dataAxis.orientation = 'right';
  130. this.yAxisRight = new DataAxis(this.body, this.options.dataAxis, this.svg, this.options.groups);
  131. delete this.options.dataAxis.orientation;
  132. // legends
  133. this.legendLeft = new Legend(this.body, this.options.legend, 'left', this.options.groups);
  134. this.legendRight = new Legend(this.body, this.options.legend, 'right', this.options.groups);
  135. this.show();
  136. };
  137. /**
  138. * set the options of the LineGraph. the mergeOptions is used for subObjects that have an enabled element.
  139. * @param {object} options
  140. */
  141. LineGraph.prototype.setOptions = function (options) {
  142. if (options) {
  143. var fields = ['sampling', 'defaultGroup', 'stack', 'height', 'graphHeight', 'yAxisOrientation', 'style', 'barChart', 'dataAxis', 'sort', 'groups'];
  144. if (options.graphHeight === undefined && options.height !== undefined) {
  145. this.updateSVGheight = true;
  146. this.updateSVGheightOnResize = true;
  147. }
  148. else if (this.body.domProps.centerContainer.height !== undefined && options.graphHeight !== undefined) {
  149. if (parseInt((options.graphHeight + '').replace("px", '')) < this.body.domProps.centerContainer.height) {
  150. this.updateSVGheight = true;
  151. }
  152. }
  153. util.selectiveDeepExtend(fields, this.options, options);
  154. util.mergeOptions(this.options, options, 'interpolation');
  155. util.mergeOptions(this.options, options, 'drawPoints');
  156. util.mergeOptions(this.options, options, 'shaded');
  157. util.mergeOptions(this.options, options, 'legend');
  158. if (options.interpolation) {
  159. if (typeof options.interpolation == 'object') {
  160. if (options.interpolation.parametrization) {
  161. if (options.interpolation.parametrization == 'uniform') {
  162. this.options.interpolation.alpha = 0;
  163. }
  164. else if (options.interpolation.parametrization == 'chordal') {
  165. this.options.interpolation.alpha = 1.0;
  166. }
  167. else {
  168. this.options.interpolation.parametrization = 'centripetal';
  169. this.options.interpolation.alpha = 0.5;
  170. }
  171. }
  172. }
  173. }
  174. if (this.yAxisLeft) {
  175. if (options.dataAxis !== undefined) {
  176. this.yAxisLeft.setOptions(this.options.dataAxis);
  177. this.yAxisRight.setOptions(this.options.dataAxis);
  178. }
  179. }
  180. if (this.legendLeft) {
  181. if (options.legend !== undefined) {
  182. this.legendLeft.setOptions(this.options.legend);
  183. this.legendRight.setOptions(this.options.legend);
  184. }
  185. }
  186. if (this.groups.hasOwnProperty(UNGROUPED)) {
  187. this.groups[UNGROUPED].setOptions(options);
  188. }
  189. }
  190. // this is used to redraw the graph if the visibility of the groups is changed.
  191. if (this.dom.frame) { //not on initial run?
  192. this.forceGraphUpdate=true;
  193. this.body.emitter.emit("_change",{queue: true});
  194. }
  195. };
  196. /**
  197. * Hide the component from the DOM
  198. */
  199. LineGraph.prototype.hide = function () {
  200. // remove the frame containing the items
  201. if (this.dom.frame.parentNode) {
  202. this.dom.frame.parentNode.removeChild(this.dom.frame);
  203. }
  204. };
  205. /**
  206. * Show the component in the DOM (when not already visible).
  207. */
  208. LineGraph.prototype.show = function () {
  209. // show frame containing the items
  210. if (!this.dom.frame.parentNode) {
  211. this.body.dom.center.appendChild(this.dom.frame);
  212. }
  213. };
  214. /**
  215. * Set items
  216. * @param {vis.DataSet | null} items
  217. */
  218. LineGraph.prototype.setItems = function (items) {
  219. var me = this,
  220. ids,
  221. oldItemsData = this.itemsData;
  222. // replace the dataset
  223. if (!items) {
  224. this.itemsData = null;
  225. }
  226. else if (items instanceof DataSet || items instanceof DataView) {
  227. this.itemsData = items;
  228. }
  229. else {
  230. throw new TypeError('Data must be an instance of DataSet or DataView');
  231. }
  232. if (oldItemsData) {
  233. // unsubscribe from old dataset
  234. util.forEach(this.itemListeners, function (callback, event) {
  235. oldItemsData.off(event, callback);
  236. });
  237. // remove all drawn items
  238. ids = oldItemsData.getIds();
  239. this._onRemove(ids);
  240. }
  241. if (this.itemsData) {
  242. // subscribe to new dataset
  243. var id = this.id;
  244. util.forEach(this.itemListeners, function (callback, event) {
  245. me.itemsData.on(event, callback, id);
  246. });
  247. // add all new items
  248. ids = this.itemsData.getIds();
  249. this._onAdd(ids);
  250. }
  251. };
  252. /**
  253. * Set groups
  254. * @param {vis.DataSet} groups
  255. */
  256. LineGraph.prototype.setGroups = function (groups) {
  257. var me = this;
  258. var ids;
  259. // unsubscribe from current dataset
  260. if (this.groupsData) {
  261. util.forEach(this.groupListeners, function (callback, event) {
  262. me.groupsData.off(event, callback);
  263. });
  264. // remove all drawn groups
  265. ids = this.groupsData.getIds();
  266. this.groupsData = null;
  267. for (var i = 0; i < ids.length; i++) {
  268. this._removeGroup(ids[i]);
  269. }
  270. }
  271. // replace the dataset
  272. if (!groups) {
  273. this.groupsData = null;
  274. }
  275. else if (groups instanceof DataSet || groups instanceof DataView) {
  276. this.groupsData = groups;
  277. }
  278. else {
  279. throw new TypeError('Data must be an instance of DataSet or DataView');
  280. }
  281. if (this.groupsData) {
  282. // subscribe to new dataset
  283. var id = this.id;
  284. util.forEach(this.groupListeners, function (callback, event) {
  285. me.groupsData.on(event, callback, id);
  286. });
  287. // draw all ms
  288. ids = this.groupsData.getIds();
  289. this._onAddGroups(ids);
  290. }
  291. };
  292. LineGraph.prototype._onUpdate = function (ids) {
  293. this._updateAllGroupData(ids);
  294. };
  295. LineGraph.prototype._onAdd = function (ids) {
  296. this._onUpdate(ids);
  297. };
  298. LineGraph.prototype._onRemove = function (ids) {
  299. this._onUpdate(ids);
  300. };
  301. LineGraph.prototype._onUpdateGroups = function (groupIds) {
  302. this._updateAllGroupData(null, groupIds);
  303. };
  304. LineGraph.prototype._onAddGroups = function (groupIds) {
  305. this._onUpdateGroups(groupIds);
  306. };
  307. /**
  308. * this cleans the group out off the legends and the dataaxis, updates the ungrouped and updates the graph
  309. * @param {Array} groupIds
  310. * @private
  311. */
  312. LineGraph.prototype._onRemoveGroups = function (groupIds) {
  313. for (var i = 0; i < groupIds.length; i++) {
  314. this._removeGroup(groupIds[i]);
  315. }
  316. this.forceGraphUpdate = true;
  317. this.body.emitter.emit("_change",{queue: true});
  318. };
  319. /**
  320. * this cleans the group out off the legends and the dataaxis
  321. * @param {vis.GraphGroup.id} groupId
  322. * @private
  323. */
  324. LineGraph.prototype._removeGroup = function (groupId) {
  325. if (this.groups.hasOwnProperty(groupId)) {
  326. if (this.groups[groupId].options.yAxisOrientation == 'right') {
  327. this.yAxisRight.removeGroup(groupId);
  328. this.legendRight.removeGroup(groupId);
  329. this.legendRight.redraw();
  330. }
  331. else {
  332. this.yAxisLeft.removeGroup(groupId);
  333. this.legendLeft.removeGroup(groupId);
  334. this.legendLeft.redraw();
  335. }
  336. delete this.groups[groupId];
  337. }
  338. };
  339. /**
  340. * update a group object with the group dataset entree
  341. *
  342. * @param {vis.GraphGroup} group
  343. * @param {vis.GraphGroup.id} groupId
  344. * @private
  345. */
  346. LineGraph.prototype._updateGroup = function (group, groupId) {
  347. if (!this.groups.hasOwnProperty(groupId)) {
  348. this.groups[groupId] = new GraphGroup(group, groupId, this.options, this.groupsUsingDefaultStyles);
  349. if (this.groups[groupId].options.yAxisOrientation == 'right') {
  350. this.yAxisRight.addGroup(groupId, this.groups[groupId]);
  351. this.legendRight.addGroup(groupId, this.groups[groupId]);
  352. }
  353. else {
  354. this.yAxisLeft.addGroup(groupId, this.groups[groupId]);
  355. this.legendLeft.addGroup(groupId, this.groups[groupId]);
  356. }
  357. }
  358. else {
  359. this.groups[groupId].update(group);
  360. if (this.groups[groupId].options.yAxisOrientation == 'right') {
  361. this.yAxisRight.updateGroup(groupId, this.groups[groupId]);
  362. this.legendRight.updateGroup(groupId, this.groups[groupId]);
  363. //If yAxisOrientation changed, clean out the group from the other axis.
  364. this.yAxisLeft.removeGroup(groupId);
  365. this.legendLeft.removeGroup(groupId);
  366. }
  367. else {
  368. this.yAxisLeft.updateGroup(groupId, this.groups[groupId]);
  369. this.legendLeft.updateGroup(groupId, this.groups[groupId]);
  370. //If yAxisOrientation changed, clean out the group from the other axis.
  371. this.yAxisRight.removeGroup(groupId);
  372. this.legendRight.removeGroup(groupId);
  373. }
  374. }
  375. this.legendLeft.redraw();
  376. this.legendRight.redraw();
  377. };
  378. /**
  379. * this updates all groups, it is used when there is an update the the itemset.
  380. *
  381. * @param {Array} ids
  382. * @param {Array} groupIds
  383. * @private
  384. */
  385. LineGraph.prototype._updateAllGroupData = function (ids, groupIds) {
  386. if (this.itemsData != null) {
  387. var groupsContent = {};
  388. var items = this.itemsData.get();
  389. var fieldId = this.itemsData._fieldId;
  390. var idMap = {};
  391. if (ids){
  392. ids.map(function (id) {
  393. idMap[id] = id;
  394. });
  395. }
  396. //pre-Determine array sizes, for more efficient memory claim
  397. var groupCounts = {};
  398. for (var i = 0; i < items.length; i++) {
  399. var item = items[i];
  400. var groupId = item.group;
  401. if (groupId === null || groupId === undefined) {
  402. groupId = UNGROUPED;
  403. }
  404. groupCounts.hasOwnProperty(groupId) ? groupCounts[groupId]++ : groupCounts[groupId] = 1;
  405. }
  406. //Pre-load arrays from existing groups if items are not changed (not in ids)
  407. var existingItemsMap = {};
  408. if (!groupIds && ids) {
  409. for (groupId in this.groups) {
  410. if (this.groups.hasOwnProperty(groupId)) {
  411. group = this.groups[groupId];
  412. var existing_items = group.getItems();
  413. groupsContent[groupId] = existing_items.filter(function (item) {
  414. existingItemsMap[item[fieldId]] = item[fieldId];
  415. return (item[fieldId] !== idMap[item[fieldId]]);
  416. });
  417. var newLength = groupCounts[groupId];
  418. groupCounts[groupId] -= groupsContent[groupId].length;
  419. if (groupsContent[groupId].length < newLength) {
  420. groupsContent[groupId][newLength - 1] = {};
  421. }
  422. }
  423. }
  424. }
  425. //Now insert data into the arrays.
  426. for (i = 0; i < items.length; i++) {
  427. item = items[i];
  428. groupId = item.group;
  429. if (groupId === null || groupId === undefined) {
  430. groupId = UNGROUPED;
  431. }
  432. if (!groupIds && ids && (item[fieldId] !== idMap[item[fieldId]]) && existingItemsMap.hasOwnProperty(item[fieldId])) {
  433. continue;
  434. }
  435. if (!groupsContent.hasOwnProperty(groupId)) {
  436. groupsContent[groupId] = new Array(groupCounts[groupId]);
  437. }
  438. //Copy data (because of unmodifiable DataView input.
  439. var extended = util.bridgeObject(item);
  440. extended.x = util.convert(item.x, 'Date');
  441. extended.end = util.convert(item.end, 'Date');
  442. extended.orginalY = item.y; //real Y
  443. extended.y = Number(item.y);
  444. extended[fieldId] = item[fieldId];
  445. var index= groupsContent[groupId].length - groupCounts[groupId]--;
  446. groupsContent[groupId][index] = extended;
  447. }
  448. //Make sure all groups are present, to allow removal of old groups
  449. for (groupId in this.groups){
  450. if (this.groups.hasOwnProperty(groupId)){
  451. if (!groupsContent.hasOwnProperty(groupId)) {
  452. groupsContent[groupId] = new Array(0);
  453. }
  454. }
  455. }
  456. //Update legendas, style and axis
  457. for (groupId in groupsContent) {
  458. if (groupsContent.hasOwnProperty(groupId)) {
  459. if (groupsContent[groupId].length == 0) {
  460. if (this.groups.hasOwnProperty(groupId)) {
  461. this._removeGroup(groupId);
  462. }
  463. } else {
  464. var group = undefined;
  465. if (this.groupsData != undefined) {
  466. group = this.groupsData.get(groupId);
  467. }
  468. if (group == undefined) {
  469. group = {id: groupId, content: this.options.defaultGroup + groupId};
  470. }
  471. this._updateGroup(group, groupId);
  472. this.groups[groupId].setItems(groupsContent[groupId]);
  473. }
  474. }
  475. }
  476. this.forceGraphUpdate = true;
  477. this.body.emitter.emit("_change",{queue: true});
  478. }
  479. };
  480. /**
  481. * Redraw the component, mandatory function
  482. * @return {boolean} Returns true if the component is resized
  483. */
  484. LineGraph.prototype.redraw = function () {
  485. var resized = false;
  486. // calculate actual size and position
  487. this.props.width = this.dom.frame.offsetWidth;
  488. this.props.height = this.body.domProps.centerContainer.height
  489. - this.body.domProps.border.top
  490. - this.body.domProps.border.bottom;
  491. // check if this component is resized
  492. resized = this._isResized() || resized;
  493. // check whether zoomed (in that case we need to re-stack everything)
  494. var visibleInterval = this.body.range.end - this.body.range.start;
  495. var zoomed = (visibleInterval != this.lastVisibleInterval);
  496. this.lastVisibleInterval = visibleInterval;
  497. // the svg element is three times as big as the width, this allows for fully dragging left and right
  498. // without reloading the graph. the controls for this are bound to events in the constructor
  499. if (resized == true) {
  500. this.svg.style.width = util.option.asSize(3 * this.props.width);
  501. this.svg.style.left = util.option.asSize(-this.props.width);
  502. // if the height of the graph is set as proportional, change the height of the svg
  503. if ((this.options.height + '').indexOf("%") != -1 || this.updateSVGheightOnResize == true) {
  504. this.updateSVGheight = true;
  505. }
  506. }
  507. // update the height of the graph on each redraw of the graph.
  508. if (this.updateSVGheight == true) {
  509. if (this.options.graphHeight != this.props.height + 'px') {
  510. this.options.graphHeight = this.props.height + 'px';
  511. this.svg.style.height = this.props.height + 'px';
  512. }
  513. this.updateSVGheight = false;
  514. }
  515. else {
  516. this.svg.style.height = ('' + this.options.graphHeight).replace('px', '') + 'px';
  517. }
  518. // zoomed is here to ensure that animations are shown correctly.
  519. if (resized == true || zoomed == true || this.abortedGraphUpdate == true || this.forceGraphUpdate == true) {
  520. resized = this._updateGraph() || resized;
  521. this.forceGraphUpdate = false;
  522. }
  523. else {
  524. // move the whole svg while dragging
  525. if (this.lastStart != 0) {
  526. var offset = this.body.range.start - this.lastStart;
  527. var range = this.body.range.end - this.body.range.start;
  528. if (this.props.width != 0) {
  529. var rangePerPixelInv = this.props.width / range;
  530. var xOffset = offset * rangePerPixelInv;
  531. this.svg.style.left = (-this.props.width - xOffset) + 'px';
  532. }
  533. }
  534. }
  535. this.legendLeft.redraw();
  536. this.legendRight.redraw();
  537. return resized;
  538. };
  539. LineGraph.prototype._getSortedGroupIds = function(){
  540. // getting group Ids
  541. var grouplist = [];
  542. for (var groupId in this.groups) {
  543. if (this.groups.hasOwnProperty(groupId)) {
  544. var group = this.groups[groupId];
  545. if (group.visible == true && (this.options.groups.visibility[groupId] === undefined || this.options.groups.visibility[groupId] == true)) {
  546. grouplist.push({id:groupId,zIndex:group.options.zIndex});
  547. }
  548. }
  549. }
  550. util.insertSort(grouplist,function(a,b){
  551. var az = a.zIndex;
  552. var bz = b.zIndex;
  553. if (az === undefined) az=0;
  554. if (bz === undefined) bz=0;
  555. return az==bz? 0: (az<bz ? -1: 1);
  556. });
  557. var groupIds = new Array(grouplist.length);
  558. for (var i=0; i< grouplist.length; i++){
  559. groupIds[i] = grouplist[i].id;
  560. }
  561. return groupIds;
  562. };
  563. /**
  564. * Update and redraw the graph.
  565. *
  566. * @returns {boolean}
  567. * @private
  568. */
  569. LineGraph.prototype._updateGraph = function () {
  570. // reset the svg elements
  571. DOMutil.prepareElements(this.svgElements);
  572. if (this.props.width != 0 && this.itemsData != null) {
  573. var group, i;
  574. var groupRanges = {};
  575. var changeCalled = false;
  576. // this is the range of the SVG canvas
  577. var minDate = this.body.util.toGlobalTime(-this.body.domProps.root.width);
  578. var maxDate = this.body.util.toGlobalTime(2 * this.body.domProps.root.width);
  579. // getting group Ids
  580. var groupIds = this._getSortedGroupIds();
  581. if (groupIds.length > 0) {
  582. var groupsData = {};
  583. // fill groups data, this only loads the data we require based on the timewindow
  584. this._getRelevantData(groupIds, groupsData, minDate, maxDate);
  585. // apply sampling, if disabled, it will pass through this function.
  586. this._applySampling(groupIds, groupsData);
  587. // we transform the X coordinates to detect collisions
  588. for (i = 0; i < groupIds.length; i++) {
  589. this._convertXcoordinates(groupsData[groupIds[i]]);
  590. }
  591. // now all needed data has been collected we start the processing.
  592. this._getYRanges(groupIds, groupsData, groupRanges);
  593. // update the Y axis first, we use this data to draw at the correct Y points
  594. changeCalled = this._updateYAxis(groupIds, groupRanges);
  595. // at changeCalled, abort this update cycle as the graph needs another update with new Width input from the Redraw container.
  596. // Cleanup SVG elements on abort.
  597. if (changeCalled == true) {
  598. DOMutil.cleanupElements(this.svgElements);
  599. this.abortedGraphUpdate = true;
  600. return true;
  601. }
  602. this.abortedGraphUpdate = false;
  603. // With the yAxis scaled correctly, use this to get the Y values of the points.
  604. var below = undefined;
  605. for (i = 0; i < groupIds.length; i++) {
  606. group = this.groups[groupIds[i]];
  607. if (this.options.stack === true && this.options.style === 'line') {
  608. if (group.options.excludeFromStacking == undefined || !group.options.excludeFromStacking) {
  609. if (below != undefined) {
  610. this._stack(groupsData[group.id], groupsData[below.id]);
  611. if (group.options.shaded.enabled == true && group.options.shaded.orientation !== "group"){
  612. if (group.options.shaded.orientation == "top" && below.options.shaded.orientation !== "group"){
  613. below.options.shaded.orientation="group";
  614. below.options.shaded.groupId=group.id;
  615. } else {
  616. group.options.shaded.orientation="group";
  617. group.options.shaded.groupId=below.id;
  618. }
  619. }
  620. }
  621. below = group;
  622. }
  623. }
  624. this._convertYcoordinates(groupsData[groupIds[i]], group);
  625. }
  626. //Precalculate paths and draw shading if appropriate. This will make sure the shading is always behind any lines.
  627. var paths = {};
  628. for (i = 0; i < groupIds.length; i++) {
  629. group = this.groups[groupIds[i]];
  630. if (group.options.style === 'line' && group.options.shaded.enabled == true) {
  631. var dataset = groupsData[groupIds[i]];
  632. if (dataset == null || dataset.length == 0) {
  633. continue;
  634. }
  635. if (!paths.hasOwnProperty(groupIds[i])) {
  636. paths[groupIds[i]] = Lines.calcPath(dataset, group);
  637. }
  638. if (group.options.shaded.orientation === "group") {
  639. var subGroupId = group.options.shaded.groupId;
  640. if (groupIds.indexOf(subGroupId) === -1) {
  641. console.log(group.id + ": Unknown shading group target given:" + subGroupId);
  642. continue;
  643. }
  644. if (!paths.hasOwnProperty(subGroupId)) {
  645. paths[subGroupId] = Lines.calcPath(groupsData[subGroupId], this.groups[subGroupId]);
  646. }
  647. Lines.drawShading(paths[groupIds[i]], group, paths[subGroupId], this.framework);
  648. }
  649. else {
  650. Lines.drawShading(paths[groupIds[i]], group, undefined, this.framework);
  651. }
  652. }
  653. }
  654. // draw the groups, calculating paths if still necessary.
  655. Bars.draw(groupIds, groupsData, this.framework);
  656. for (i = 0; i < groupIds.length; i++) {
  657. group = this.groups[groupIds[i]];
  658. if (groupsData[groupIds[i]].length > 0) {
  659. switch (group.options.style) {
  660. case "line":
  661. if (!paths.hasOwnProperty(groupIds[i])) {
  662. paths[groupIds[i]] = Lines.calcPath(groupsData[groupIds[i]], group);
  663. }
  664. Lines.draw(paths[groupIds[i]], group, this.framework);
  665. // eslint-disable-line no-fallthrough
  666. case "point":
  667. // eslint-disable-line no-fallthrough
  668. case "points":
  669. if (group.options.style == "point" || group.options.style == "points" || group.options.drawPoints.enabled == true) {
  670. Points.draw(groupsData[groupIds[i]], group, this.framework);
  671. }
  672. break;
  673. case "bar":
  674. // bar needs to be drawn enmasse
  675. // eslint-disable-line no-fallthrough
  676. default:
  677. //do nothing...
  678. }
  679. }
  680. }
  681. }
  682. }
  683. // cleanup unused svg elements
  684. DOMutil.cleanupElements(this.svgElements);
  685. return false;
  686. };
  687. LineGraph.prototype._stack = function (data, subData) {
  688. var index, dx, dy, subPrevPoint, subNextPoint;
  689. index = 0;
  690. // for each data point we look for a matching on in the set below
  691. for (var j = 0; j < data.length; j++) {
  692. subPrevPoint = undefined;
  693. subNextPoint = undefined;
  694. // we look for time matches or a before-after point
  695. for (var k = index; k < subData.length; k++) {
  696. // if times match exactly
  697. if (subData[k].x === data[j].x) {
  698. subPrevPoint = subData[k];
  699. subNextPoint = subData[k];
  700. index = k;
  701. break;
  702. }
  703. else if (subData[k].x > data[j].x) { // overshoot
  704. subNextPoint = subData[k];
  705. if (k == 0) {
  706. subPrevPoint = subNextPoint;
  707. }
  708. else {
  709. subPrevPoint = subData[k - 1];
  710. }
  711. index = k;
  712. break;
  713. }
  714. }
  715. // in case the last data point has been used, we assume it stays like this.
  716. if (subNextPoint === undefined) {
  717. subPrevPoint = subData[subData.length - 1];
  718. subNextPoint = subData[subData.length - 1];
  719. }
  720. // linear interpolation
  721. dx = subNextPoint.x - subPrevPoint.x;
  722. dy = subNextPoint.y - subPrevPoint.y;
  723. if (dx == 0) {
  724. data[j].y = data[j].orginalY + subNextPoint.y;
  725. }
  726. else {
  727. data[j].y = data[j].orginalY + (dy / dx) * (data[j].x - subPrevPoint.x) + subPrevPoint.y; // ax + b where b is data[j].y
  728. }
  729. }
  730. }
  731. /**
  732. * first select and preprocess the data from the datasets.
  733. * the groups have their preselection of data, we now loop over this data to see
  734. * what data we need to draw. Sorted data is much faster.
  735. * more optimization is possible by doing the sampling before and using the binary search
  736. * to find the end date to determine the increment.
  737. *
  738. * @param {array} groupIds
  739. * @param {object} groupsData
  740. * @param {date} minDate
  741. * @param {date} maxDate
  742. * @private
  743. */
  744. LineGraph.prototype._getRelevantData = function (groupIds, groupsData, minDate, maxDate) {
  745. var group, i, j, item;
  746. if (groupIds.length > 0) {
  747. for (i = 0; i < groupIds.length; i++) {
  748. group = this.groups[groupIds[i]];
  749. var itemsData = group.getItems();
  750. // optimization for sorted data
  751. if (group.options.sort == true) {
  752. var dateComparator = function (a, b) {
  753. return a.getTime() == b.getTime() ? 0 : a < b ? -1 : 1
  754. };
  755. var first = Math.max(0, util.binarySearchValue(itemsData, minDate, 'x', 'before', dateComparator));
  756. var last = Math.min(itemsData.length, util.binarySearchValue(itemsData, maxDate, 'x', 'after', dateComparator) + 1);
  757. if (last <= 0) {
  758. last = itemsData.length;
  759. }
  760. var dataContainer = new Array(last-first);
  761. for (j = first; j < last; j++) {
  762. item = group.itemsData[j];
  763. dataContainer[j-first] = item;
  764. }
  765. groupsData[groupIds[i]] = dataContainer;
  766. }
  767. else {
  768. // If unsorted data, all data is relevant, just returning entire structure
  769. groupsData[groupIds[i]] = group.itemsData;
  770. }
  771. }
  772. }
  773. };
  774. /**
  775. *
  776. * @param {Array.<vis.GraphGroup.id>} groupIds
  777. * @param {vis.DataSet} groupsData
  778. * @private
  779. */
  780. LineGraph.prototype._applySampling = function (groupIds, groupsData) {
  781. var group;
  782. if (groupIds.length > 0) {
  783. for (var i = 0; i < groupIds.length; i++) {
  784. group = this.groups[groupIds[i]];
  785. if (group.options.sampling == true) {
  786. var dataContainer = groupsData[groupIds[i]];
  787. if (dataContainer.length > 0) {
  788. var increment = 1;
  789. var amountOfPoints = dataContainer.length;
  790. // the global screen is used because changing the width of the yAxis may affect the increment, resulting in an endless loop
  791. // of width changing of the yAxis.
  792. //TODO: This assumes sorted data, but that's not guaranteed!
  793. var xDistance = this.body.util.toGlobalScreen(dataContainer[dataContainer.length - 1].x) - this.body.util.toGlobalScreen(dataContainer[0].x);
  794. var pointsPerPixel = amountOfPoints / xDistance;
  795. increment = Math.min(Math.ceil(0.2 * amountOfPoints), Math.max(1, Math.round(pointsPerPixel)));
  796. var sampledData = new Array(amountOfPoints);
  797. for (var j = 0; j < amountOfPoints; j += increment) {
  798. var idx = Math.round(j/increment);
  799. sampledData[idx]=dataContainer[j];
  800. }
  801. groupsData[groupIds[i]] = sampledData.splice(0,Math.round(amountOfPoints/increment));
  802. }
  803. }
  804. }
  805. }
  806. };
  807. /**
  808. *
  809. * @param {Array.<vis.GraphGroup.id>} groupIds
  810. * @param {vis.DataSet} groupsData
  811. * @param {object} groupRanges | this is being filled here
  812. * @private
  813. */
  814. LineGraph.prototype._getYRanges = function (groupIds, groupsData, groupRanges) {
  815. var groupData, group, i;
  816. var combinedDataLeft = [];
  817. var combinedDataRight = [];
  818. var options;
  819. if (groupIds.length > 0) {
  820. for (i = 0; i < groupIds.length; i++) {
  821. groupData = groupsData[groupIds[i]];
  822. options = this.groups[groupIds[i]].options;
  823. if (groupData.length > 0) {
  824. group = this.groups[groupIds[i]];
  825. // if bar graphs are stacked, their range need to be handled differently and accumulated over all groups.
  826. if (options.stack === true && options.style === 'bar') {
  827. if (options.yAxisOrientation === 'left') {
  828. combinedDataLeft = combinedDataLeft.concat(groupData);
  829. }
  830. else {
  831. combinedDataRight = combinedDataRight.concat(groupData);
  832. }
  833. }
  834. else {
  835. groupRanges[groupIds[i]] = group.getYRange(groupData, groupIds[i]);
  836. }
  837. }
  838. }
  839. // if bar graphs are stacked, their range need to be handled differently and accumulated over all groups.
  840. Bars.getStackedYRange(combinedDataLeft, groupRanges, groupIds, '__barStackLeft', 'left');
  841. Bars.getStackedYRange(combinedDataRight, groupRanges, groupIds, '__barStackRight', 'right');
  842. }
  843. };
  844. /**
  845. * this sets the Y ranges for the Y axis. It also determines which of the axis should be shown or hidden.
  846. * @param {Array.<vis.GraphGroup.id>} groupIds
  847. * @param {Object} groupRanges
  848. * @returns {boolean} resized
  849. * @private
  850. */
  851. LineGraph.prototype._updateYAxis = function (groupIds, groupRanges) {
  852. var resized = false;
  853. var yAxisLeftUsed = false;
  854. var yAxisRightUsed = false;
  855. var minLeft = 1e9, minRight = 1e9, maxLeft = -1e9, maxRight = -1e9, minVal, maxVal;
  856. // if groups are present
  857. if (groupIds.length > 0) {
  858. // this is here to make sure that if there are no items in the axis but there are groups, that there is no infinite draw/redraw loop.
  859. for (var i = 0; i < groupIds.length; i++) {
  860. var group = this.groups[groupIds[i]];
  861. if (group && group.options.yAxisOrientation != 'right') {
  862. yAxisLeftUsed = true;
  863. minLeft = 1e9;
  864. maxLeft = -1e9;
  865. }
  866. else if (group && group.options.yAxisOrientation) {
  867. yAxisRightUsed = true;
  868. minRight = 1e9;
  869. maxRight = -1e9;
  870. }
  871. }
  872. // if there are items:
  873. for (i = 0; i < groupIds.length; i++) {
  874. if (groupRanges.hasOwnProperty(groupIds[i])) {
  875. if (groupRanges[groupIds[i]].ignore !== true) {
  876. minVal = groupRanges[groupIds[i]].min;
  877. maxVal = groupRanges[groupIds[i]].max;
  878. if (groupRanges[groupIds[i]].yAxisOrientation != 'right') {
  879. yAxisLeftUsed = true;
  880. minLeft = minLeft > minVal ? minVal : minLeft;
  881. maxLeft = maxLeft < maxVal ? maxVal : maxLeft;
  882. }
  883. else {
  884. yAxisRightUsed = true;
  885. minRight = minRight > minVal ? minVal : minRight;
  886. maxRight = maxRight < maxVal ? maxVal : maxRight;
  887. }
  888. }
  889. }
  890. }
  891. if (yAxisLeftUsed == true) {
  892. this.yAxisLeft.setRange(minLeft, maxLeft);
  893. }
  894. if (yAxisRightUsed == true) {
  895. this.yAxisRight.setRange(minRight, maxRight);
  896. }
  897. }
  898. resized = this._toggleAxisVisiblity(yAxisLeftUsed, this.yAxisLeft) || resized;
  899. resized = this._toggleAxisVisiblity(yAxisRightUsed, this.yAxisRight) || resized;
  900. if (yAxisRightUsed == true && yAxisLeftUsed == true) {
  901. this.yAxisLeft.drawIcons = true;
  902. this.yAxisRight.drawIcons = true;
  903. }
  904. else {
  905. this.yAxisLeft.drawIcons = false;
  906. this.yAxisRight.drawIcons = false;
  907. }
  908. this.yAxisRight.master = !yAxisLeftUsed;
  909. this.yAxisRight.masterAxis = this.yAxisLeft;
  910. if (this.yAxisRight.master == false) {
  911. if (yAxisRightUsed == true) {
  912. this.yAxisLeft.lineOffset = this.yAxisRight.width;
  913. }
  914. else {
  915. this.yAxisLeft.lineOffset = 0;
  916. }
  917. resized = this.yAxisLeft.redraw() || resized;
  918. resized = this.yAxisRight.redraw() || resized;
  919. }
  920. else {
  921. resized = this.yAxisRight.redraw() || resized;
  922. }
  923. // clean the accumulated lists
  924. var tempGroups = ['__barStackLeft', '__barStackRight', '__lineStackLeft', '__lineStackRight'];
  925. for (i = 0; i < tempGroups.length; i++) {
  926. if (groupIds.indexOf(tempGroups[i]) != -1) {
  927. groupIds.splice(groupIds.indexOf(tempGroups[i]), 1);
  928. }
  929. }
  930. return resized;
  931. };
  932. /**
  933. * This shows or hides the Y axis if needed. If there is a change, the changed event is emitted by the updateYAxis function
  934. *
  935. * @param {boolean} axisUsed
  936. * @param {vis.DataAxis} axis
  937. * @returns {boolean}
  938. * @private
  939. */
  940. LineGraph.prototype._toggleAxisVisiblity = function (axisUsed, axis) {
  941. var changed = false;
  942. if (axisUsed == false) {
  943. if (axis.dom.frame.parentNode && axis.hidden == false) {
  944. axis.hide();
  945. changed = true;
  946. }
  947. }
  948. else {
  949. if (!axis.dom.frame.parentNode && axis.hidden == true) {
  950. axis.show();
  951. changed = true;
  952. }
  953. }
  954. return changed;
  955. };
  956. /**
  957. * This uses the DataAxis object to generate the correct X coordinate on the SVG window. It uses the
  958. * util function toScreen to get the x coordinate from the timestamp. It also pre-filters the data and get the minMax ranges for
  959. * the yAxis.
  960. *
  961. * @param {Array.<Object>} datapoints
  962. * @private
  963. */
  964. LineGraph.prototype._convertXcoordinates = function (datapoints) {
  965. var toScreen = this.body.util.toScreen;
  966. for (var i = 0; i < datapoints.length; i++) {
  967. datapoints[i].screen_x = toScreen(datapoints[i].x) + this.props.width;
  968. datapoints[i].screen_y = datapoints[i].y; //starting point for range calculations
  969. if (datapoints[i].end != undefined) {
  970. datapoints[i].screen_end = toScreen(datapoints[i].end) + this.props.width;
  971. }
  972. else {
  973. datapoints[i].screen_end = undefined;
  974. }
  975. }
  976. };
  977. /**
  978. * This uses the DataAxis object to generate the correct X coordinate on the SVG window. It uses the
  979. * util function toScreen to get the x coordinate from the timestamp. It also pre-filters the data and get the minMax ranges for
  980. * the yAxis.
  981. *
  982. * @param {Array.<Object>} datapoints
  983. * @param {vis.GraphGroup} group
  984. * @private
  985. */
  986. LineGraph.prototype._convertYcoordinates = function (datapoints, group) {
  987. var axis = this.yAxisLeft;
  988. var svgHeight = Number(this.svg.style.height.replace('px', ''));
  989. if (group.options.yAxisOrientation == 'right') {
  990. axis = this.yAxisRight;
  991. }
  992. for (var i = 0; i < datapoints.length; i++) {
  993. datapoints[i].screen_y = Math.round(axis.convertValue(datapoints[i].y));
  994. }
  995. group.setZeroPosition(Math.min(svgHeight, axis.convertValue(0)));
  996. };
  997. module.exports = LineGraph;