vis.js is a dynamic, browser-based visualization library
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1103 lines
36 KiB

10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
9 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
9 years ago
10 years ago
10 years ago
  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;