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.

960 lines
29 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
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
  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 BarGraphFunctions = require('./graph2d_types/bar');
  10. var UNGROUPED = '__ungrouped__'; // reserved group id for ungrouped items
  11. /**
  12. * This is the constructor of the LineGraph. It requires a Timeline body and options.
  13. *
  14. * @param body
  15. * @param options
  16. * @constructor
  17. */
  18. function LineGraph(body, options) {
  19. this.id = util.randomUUID();
  20. this.body = body;
  21. this.defaultOptions = {
  22. yAxisOrientation: 'left',
  23. defaultGroup: 'default',
  24. sort: true,
  25. sampling: true,
  26. graphHeight: '400px',
  27. shaded: {
  28. enabled: false,
  29. orientation: 'bottom' // top, bottom
  30. },
  31. style: 'line', // line, bar
  32. barChart: {
  33. width: 50,
  34. handleOverlap: 'overlap',
  35. align: 'center' // left, center, right
  36. },
  37. catmullRom: {
  38. enabled: true,
  39. parametrization: 'centripetal', // uniform (alpha = 0.0), chordal (alpha = 1.0), centripetal (alpha = 0.5)
  40. alpha: 0.5
  41. },
  42. drawPoints: {
  43. enabled: true,
  44. size: 6,
  45. style: 'square' // square, circle
  46. },
  47. dataAxis: {
  48. showMinorLabels: true,
  49. showMajorLabels: true,
  50. icons: false,
  51. width: '40px',
  52. visible: true,
  53. alignZeros: true,
  54. customRange: {
  55. left: {min:undefined, max:undefined},
  56. right: {min:undefined, max:undefined}
  57. }
  58. //, these options are not set by default, but this shows the format they will be in
  59. //format: {
  60. // left: {decimals: 2},
  61. // right: {decimals: 2}
  62. //},
  63. //title: {
  64. // left: {
  65. // text: 'left',
  66. // style: 'color:black;'
  67. // },
  68. // right: {
  69. // text: 'right',
  70. // style: 'color:black;'
  71. // }
  72. //}
  73. },
  74. legend: {
  75. enabled: false,
  76. icons: true,
  77. left: {
  78. visible: true,
  79. position: 'top-left' // top/bottom - left,right
  80. },
  81. right: {
  82. visible: true,
  83. position: 'top-right' // top/bottom - left,right
  84. }
  85. },
  86. groups: {
  87. visibility: {}
  88. }
  89. };
  90. // options is shared by this ItemSet and all its items
  91. this.options = util.extend({}, this.defaultOptions);
  92. this.dom = {};
  93. this.props = {};
  94. this.hammer = null;
  95. this.groups = {};
  96. this.abortedGraphUpdate = false;
  97. this.autoSizeSVG = false;
  98. var me = this;
  99. this.itemsData = null; // DataSet
  100. this.groupsData = null; // DataSet
  101. // listeners for the DataSet of the items
  102. this.itemListeners = {
  103. 'add': function (event, params, senderId) {
  104. me._onAdd(params.items);
  105. },
  106. 'update': function (event, params, senderId) {
  107. me._onUpdate(params.items);
  108. },
  109. 'remove': function (event, params, senderId) {
  110. me._onRemove(params.items);
  111. }
  112. };
  113. // listeners for the DataSet of the groups
  114. this.groupListeners = {
  115. 'add': function (event, params, senderId) {
  116. me._onAddGroups(params.items);
  117. },
  118. 'update': function (event, params, senderId) {
  119. me._onUpdateGroups(params.items);
  120. },
  121. 'remove': function (event, params, senderId) {
  122. me._onRemoveGroups(params.items);
  123. }
  124. };
  125. this.items = {}; // object with an Item for every data item
  126. this.selection = []; // list with the ids of all selected nodes
  127. this.lastStart = this.body.range.start;
  128. this.touchParams = {}; // stores properties while dragging
  129. this.svgElements = {};
  130. this.setOptions(options);
  131. this.groupsUsingDefaultStyles = [0];
  132. this.body.emitter.on('rangechanged', function() {
  133. me.lastStart = me.body.range.start;
  134. me.svg.style.left = util.option.asSize(-me.width);
  135. me._updateGraph.apply(me);
  136. });
  137. // create the HTML DOM
  138. this._create();
  139. this.framework = {svg: this.svg, svgElements: this.svgElements, options: this.options, groups: this.groups};
  140. this.body.emitter.emit('change');
  141. }
  142. LineGraph.prototype = new Component();
  143. /**
  144. * Create the HTML DOM for the ItemSet
  145. */
  146. LineGraph.prototype._create = function(){
  147. var frame = document.createElement('div');
  148. frame.className = 'LineGraph';
  149. this.dom.frame = frame;
  150. // create svg element for graph drawing.
  151. this.svg = document.createElementNS('http://www.w3.org/2000/svg','svg');
  152. this.svg.style.position = 'relative';
  153. this.svg.style.height = ('' + this.options.graphHeight).replace('px','') + 'px';
  154. this.svg.style.display = 'block';
  155. frame.appendChild(this.svg);
  156. // data axis
  157. this.options.dataAxis.orientation = 'left';
  158. this.yAxisLeft = new DataAxis(this.body, this.options.dataAxis, this.svg, this.options.groups);
  159. this.options.dataAxis.orientation = 'right';
  160. this.yAxisRight = new DataAxis(this.body, this.options.dataAxis, this.svg, this.options.groups);
  161. delete this.options.dataAxis.orientation;
  162. // legends
  163. this.legendLeft = new Legend(this.body, this.options.legend, 'left', this.options.groups);
  164. this.legendRight = new Legend(this.body, this.options.legend, 'right', this.options.groups);
  165. this.show();
  166. };
  167. /**
  168. * set the options of the LineGraph. the mergeOptions is used for subObjects that have an enabled element.
  169. * @param {object} options
  170. */
  171. LineGraph.prototype.setOptions = function(options) {
  172. if (options) {
  173. var fields = ['sampling','defaultGroup','graphHeight','yAxisOrientation','style','barChart','dataAxis','sort','groups'];
  174. if (options.graphHeight === undefined && options.height !== undefined && this.body.domProps.centerContainer.height !== undefined) {
  175. this.autoSizeSVG = true;
  176. }
  177. else if (this.body.domProps.centerContainer.height !== undefined && options.graphHeight !== undefined) {
  178. if (parseInt((options.graphHeight + '').replace("px",'')) < this.body.domProps.centerContainer.height) {
  179. this.autoSizeSVG = true;
  180. }
  181. }
  182. util.selectiveDeepExtend(fields, this.options, options);
  183. util.mergeOptions(this.options, options,'catmullRom');
  184. util.mergeOptions(this.options, options,'drawPoints');
  185. util.mergeOptions(this.options, options,'shaded');
  186. util.mergeOptions(this.options, options,'legend');
  187. if (options.catmullRom) {
  188. if (typeof options.catmullRom == 'object') {
  189. if (options.catmullRom.parametrization) {
  190. if (options.catmullRom.parametrization == 'uniform') {
  191. this.options.catmullRom.alpha = 0;
  192. }
  193. else if (options.catmullRom.parametrization == 'chordal') {
  194. this.options.catmullRom.alpha = 1.0;
  195. }
  196. else {
  197. this.options.catmullRom.parametrization = 'centripetal';
  198. this.options.catmullRom.alpha = 0.5;
  199. }
  200. }
  201. }
  202. }
  203. if (this.yAxisLeft) {
  204. if (options.dataAxis !== undefined) {
  205. this.yAxisLeft.setOptions(this.options.dataAxis);
  206. this.yAxisRight.setOptions(this.options.dataAxis);
  207. }
  208. }
  209. if (this.legendLeft) {
  210. if (options.legend !== undefined) {
  211. this.legendLeft.setOptions(this.options.legend);
  212. this.legendRight.setOptions(this.options.legend);
  213. }
  214. }
  215. if (this.groups.hasOwnProperty(UNGROUPED)) {
  216. this.groups[UNGROUPED].setOptions(options);
  217. }
  218. }
  219. if (this.dom.frame) {
  220. this._updateGraph();
  221. }
  222. };
  223. /**
  224. * Hide the component from the DOM
  225. */
  226. LineGraph.prototype.hide = function() {
  227. // remove the frame containing the items
  228. if (this.dom.frame.parentNode) {
  229. this.dom.frame.parentNode.removeChild(this.dom.frame);
  230. }
  231. };
  232. /**
  233. * Show the component in the DOM (when not already visible).
  234. * @return {Boolean} changed
  235. */
  236. LineGraph.prototype.show = function() {
  237. // show frame containing the items
  238. if (!this.dom.frame.parentNode) {
  239. this.body.dom.center.appendChild(this.dom.frame);
  240. }
  241. };
  242. /**
  243. * Set items
  244. * @param {vis.DataSet | null} items
  245. */
  246. LineGraph.prototype.setItems = function(items) {
  247. var me = this,
  248. ids,
  249. oldItemsData = this.itemsData;
  250. // replace the dataset
  251. if (!items) {
  252. this.itemsData = null;
  253. }
  254. else if (items instanceof DataSet || items instanceof DataView) {
  255. this.itemsData = items;
  256. }
  257. else {
  258. throw new TypeError('Data must be an instance of DataSet or DataView');
  259. }
  260. if (oldItemsData) {
  261. // unsubscribe from old dataset
  262. util.forEach(this.itemListeners, function (callback, event) {
  263. oldItemsData.off(event, callback);
  264. });
  265. // remove all drawn items
  266. ids = oldItemsData.getIds();
  267. this._onRemove(ids);
  268. }
  269. if (this.itemsData) {
  270. // subscribe to new dataset
  271. var id = this.id;
  272. util.forEach(this.itemListeners, function (callback, event) {
  273. me.itemsData.on(event, callback, id);
  274. });
  275. // add all new items
  276. ids = this.itemsData.getIds();
  277. this._onAdd(ids);
  278. }
  279. this._updateUngrouped();
  280. this._updateGraph();
  281. this.redraw();
  282. };
  283. /**
  284. * Set groups
  285. * @param {vis.DataSet} groups
  286. */
  287. LineGraph.prototype.setGroups = function(groups) {
  288. var me = this;
  289. var ids;
  290. // unsubscribe from current dataset
  291. if (this.groupsData) {
  292. util.forEach(this.groupListeners, function (callback, event) {
  293. me.groupsData.unsubscribe(event, callback);
  294. });
  295. // remove all drawn groups
  296. ids = this.groupsData.getIds();
  297. this.groupsData = null;
  298. this._onRemoveGroups(ids); // note: this will cause a redraw
  299. }
  300. // replace the dataset
  301. if (!groups) {
  302. this.groupsData = null;
  303. }
  304. else if (groups instanceof DataSet || groups instanceof DataView) {
  305. this.groupsData = groups;
  306. }
  307. else {
  308. throw new TypeError('Data must be an instance of DataSet or DataView');
  309. }
  310. if (this.groupsData) {
  311. // subscribe to new dataset
  312. var id = this.id;
  313. util.forEach(this.groupListeners, function (callback, event) {
  314. me.groupsData.on(event, callback, id);
  315. });
  316. // draw all ms
  317. ids = this.groupsData.getIds();
  318. this._onAddGroups(ids);
  319. }
  320. this._onUpdate();
  321. };
  322. /**
  323. * Update the data
  324. * @param [ids]
  325. * @private
  326. */
  327. LineGraph.prototype._onUpdate = function(ids) {
  328. this._updateUngrouped();
  329. this._updateAllGroupData();
  330. this._updateGraph();
  331. this.redraw();
  332. };
  333. LineGraph.prototype._onAdd = function (ids) {this._onUpdate(ids);};
  334. LineGraph.prototype._onRemove = function (ids) {this._onUpdate(ids);};
  335. LineGraph.prototype._onUpdateGroups = function (groupIds) {
  336. for (var i = 0; i < groupIds.length; i++) {
  337. var group = this.groupsData.get(groupIds[i]);
  338. this._updateGroup(group, groupIds[i]);
  339. }
  340. this._updateGraph();
  341. this.redraw();
  342. };
  343. LineGraph.prototype._onAddGroups = function (groupIds) {this._onUpdateGroups(groupIds);};
  344. /**
  345. * this cleans the group out off the legends and the dataaxis, updates the ungrouped and updates the graph
  346. * @param {Array} groupIds
  347. * @private
  348. */
  349. LineGraph.prototype._onRemoveGroups = function (groupIds) {
  350. for (var i = 0; i < groupIds.length; i++) {
  351. if (this.groups.hasOwnProperty(groupIds[i])) {
  352. if (this.groups[groupIds[i]].options.yAxisOrientation == 'right') {
  353. this.yAxisRight.removeGroup(groupIds[i]);
  354. this.legendRight.removeGroup(groupIds[i]);
  355. this.legendRight.redraw();
  356. }
  357. else {
  358. this.yAxisLeft.removeGroup(groupIds[i]);
  359. this.legendLeft.removeGroup(groupIds[i]);
  360. this.legendLeft.redraw();
  361. }
  362. delete this.groups[groupIds[i]];
  363. }
  364. }
  365. this._updateUngrouped();
  366. this._updateGraph();
  367. this.redraw();
  368. };
  369. /**
  370. * update a group object with the group dataset entree
  371. *
  372. * @param group
  373. * @param groupId
  374. * @private
  375. */
  376. LineGraph.prototype._updateGroup = function (group, groupId) {
  377. if (!this.groups.hasOwnProperty(groupId)) {
  378. this.groups[groupId] = new GraphGroup(group, groupId, this.options, this.groupsUsingDefaultStyles);
  379. if (this.groups[groupId].options.yAxisOrientation == 'right') {
  380. this.yAxisRight.addGroup(groupId, this.groups[groupId]);
  381. this.legendRight.addGroup(groupId, this.groups[groupId]);
  382. }
  383. else {
  384. this.yAxisLeft.addGroup(groupId, this.groups[groupId]);
  385. this.legendLeft.addGroup(groupId, this.groups[groupId]);
  386. }
  387. }
  388. else {
  389. this.groups[groupId].update(group);
  390. if (this.groups[groupId].options.yAxisOrientation == 'right') {
  391. this.yAxisRight.updateGroup(groupId, this.groups[groupId]);
  392. this.legendRight.updateGroup(groupId, this.groups[groupId]);
  393. }
  394. else {
  395. this.yAxisLeft.updateGroup(groupId, this.groups[groupId]);
  396. this.legendLeft.updateGroup(groupId, this.groups[groupId]);
  397. }
  398. }
  399. this.legendLeft.redraw();
  400. this.legendRight.redraw();
  401. };
  402. /**
  403. * this updates all groups, it is used when there is an update the the itemset.
  404. *
  405. * @private
  406. */
  407. LineGraph.prototype._updateAllGroupData = function () {
  408. if (this.itemsData != null) {
  409. var groupsContent = {};
  410. var groupId;
  411. for (groupId in this.groups) {
  412. if (this.groups.hasOwnProperty(groupId)) {
  413. groupsContent[groupId] = [];
  414. }
  415. }
  416. for (var itemId in this.itemsData._data) {
  417. if (this.itemsData._data.hasOwnProperty(itemId)) {
  418. var item = this.itemsData._data[itemId];
  419. if (groupsContent[item.group] === undefined) {
  420. throw new Error('Cannot find referenced group. Possible reason: items added before groups? Groups need to be added before items, as items refer to groups.')
  421. }
  422. item.x = util.convert(item.x,'Date');
  423. groupsContent[item.group].push(item);
  424. }
  425. }
  426. for (groupId in this.groups) {
  427. if (this.groups.hasOwnProperty(groupId)) {
  428. this.groups[groupId].setItems(groupsContent[groupId]);
  429. }
  430. }
  431. }
  432. };
  433. /**
  434. * Create or delete the group holding all ungrouped items. This group is used when
  435. * there are no groups specified. This anonymous group is called 'graph'.
  436. * @protected
  437. */
  438. LineGraph.prototype._updateUngrouped = function() {
  439. if (this.itemsData && this.itemsData != null) {
  440. var ungroupedCounter = 0;
  441. for (var itemId in this.itemsData._data) {
  442. if (this.itemsData._data.hasOwnProperty(itemId)) {
  443. var item = this.itemsData._data[itemId];
  444. if (item != undefined) {
  445. if (item.hasOwnProperty('group')) {
  446. if (item.group === undefined) {
  447. item.group = UNGROUPED;
  448. }
  449. }
  450. else {
  451. item.group = UNGROUPED;
  452. }
  453. ungroupedCounter = item.group == UNGROUPED ? ungroupedCounter + 1 : ungroupedCounter;
  454. }
  455. }
  456. }
  457. if (ungroupedCounter == 0) {
  458. delete this.groups[UNGROUPED];
  459. this.legendLeft.removeGroup(UNGROUPED);
  460. this.legendRight.removeGroup(UNGROUPED);
  461. this.yAxisLeft.removeGroup(UNGROUPED);
  462. this.yAxisRight.removeGroup(UNGROUPED);
  463. }
  464. else {
  465. var group = {id: UNGROUPED, content: this.options.defaultGroup};
  466. this._updateGroup(group, UNGROUPED);
  467. }
  468. }
  469. else {
  470. delete this.groups[UNGROUPED];
  471. this.legendLeft.removeGroup(UNGROUPED);
  472. this.legendRight.removeGroup(UNGROUPED);
  473. this.yAxisLeft.removeGroup(UNGROUPED);
  474. this.yAxisRight.removeGroup(UNGROUPED);
  475. }
  476. this.legendLeft.redraw();
  477. this.legendRight.redraw();
  478. };
  479. /**
  480. * Redraw the component, mandatory function
  481. * @return {boolean} Returns true if the component is resized
  482. */
  483. LineGraph.prototype.redraw = function() {
  484. var resized = false;
  485. this.svg.style.height = ('' + this.options.graphHeight).replace('px','') + 'px';
  486. if (this.lastWidth === undefined && this.width || this.lastWidth != this.width) {
  487. resized = true;
  488. }
  489. // check if this component is resized
  490. resized = this._isResized() || resized;
  491. // check whether zoomed (in that case we need to re-stack everything)
  492. var visibleInterval = this.body.range.end - this.body.range.start;
  493. var zoomed = (visibleInterval != this.lastVisibleInterval) || (this.width != this.lastWidth);
  494. this.lastVisibleInterval = visibleInterval;
  495. this.lastWidth = this.width;
  496. // calculate actual size and position
  497. this.width = this.dom.frame.offsetWidth;
  498. // the svg element is three times as big as the width, this allows for fully dragging left and right
  499. // without reloading the graph. the controls for this are bound to events in the constructor
  500. if (resized == true) {
  501. this.svg.style.width = util.option.asSize(3*this.width);
  502. this.svg.style.left = util.option.asSize(-this.width);
  503. }
  504. if (zoomed == true || this.abortedGraphUpdate == true) {
  505. this._updateGraph();
  506. }
  507. else {
  508. // move the whole svg while dragging
  509. if (this.lastStart != 0) {
  510. var offset = this.body.range.start - this.lastStart;
  511. var range = this.body.range.end - this.body.range.start;
  512. if (this.width != 0) {
  513. var rangePerPixelInv = this.width/range;
  514. var xOffset = offset * rangePerPixelInv;
  515. this.svg.style.left = (-this.width - xOffset) + 'px';
  516. }
  517. }
  518. }
  519. this.legendLeft.redraw();
  520. this.legendRight.redraw();
  521. return resized;
  522. };
  523. /**
  524. * Update and redraw the graph.
  525. *
  526. */
  527. LineGraph.prototype._updateGraph = function () {
  528. // reset the svg elements
  529. DOMutil.prepareElements(this.svgElements);
  530. if (this.width != 0 && this.itemsData != null) {
  531. var group, i;
  532. var preprocessedGroupData = {};
  533. var processedGroupData = {};
  534. var groupRanges = {};
  535. var changeCalled = false;
  536. // update the height of the graph on each redraw of the graph.
  537. if (this.autoSizeSVG == true) {
  538. if (this.options.graphHeight != this.body.domProps.centerContainer.height + 'px') {
  539. this.options.graphHeight = this.body.domProps.centerContainer.height + 'px';
  540. this.svg.style.height = this.body.domProps.centerContainer.height + 'px';
  541. }
  542. this.autoSizeSVG = false;
  543. }
  544. // getting group Ids
  545. var groupIds = [];
  546. for (var groupId in this.groups) {
  547. if (this.groups.hasOwnProperty(groupId)) {
  548. group = this.groups[groupId];
  549. if (group.visible == true && (this.options.groups.visibility[groupId] === undefined || this.options.groups.visibility[groupId] == true)) {
  550. groupIds.push(groupId);
  551. }
  552. }
  553. }
  554. if (groupIds.length > 0) {
  555. // this is the range of the SVG canvas
  556. var minDate = this.body.util.toGlobalTime(- this.body.domProps.root.width);
  557. var maxDate = this.body.util.toGlobalTime(2 * this.body.domProps.root.width);
  558. var groupsData = {};
  559. // fill groups data, this only loads the data we require based on the timewindow
  560. this._getRelevantData(groupIds, groupsData, minDate, maxDate);
  561. // apply sampling, if disabled, it will pass through this function.
  562. this._applySampling(groupIds, groupsData);
  563. // we transform the X coordinates to detect collisions
  564. for (i = 0; i < groupIds.length; i++) {
  565. preprocessedGroupData[groupIds[i]] = this._convertXcoordinates(groupsData[groupIds[i]]);
  566. }
  567. // now all needed data has been collected we start the processing.
  568. this._getYRanges(groupIds, preprocessedGroupData, groupRanges);
  569. // update the Y axis first, we use this data to draw at the correct Y points
  570. // changeCalled is required to clean the SVG on a change emit.
  571. changeCalled = this._updateYAxis(groupIds, groupRanges);
  572. if (changeCalled == true) {
  573. DOMutil.cleanupElements(this.svgElements);
  574. this.abortedGraphUpdate = true;
  575. this.body.emitter.emit('change');
  576. return;
  577. }
  578. this.abortedGraphUpdate = false;
  579. // With the yAxis scaled correctly, use this to get the Y values of the points.
  580. for (i = 0; i < groupIds.length; i++) {
  581. group = this.groups[groupIds[i]];
  582. processedGroupData[groupIds[i]] = this._convertYcoordinates(groupsData[groupIds[i]], group);
  583. }
  584. // draw the groups
  585. for (i = 0; i < groupIds.length; i++) {
  586. group = this.groups[groupIds[i]];
  587. if (group.options.style != 'bar') { // bar needs to be drawn enmasse
  588. group.draw(processedGroupData[groupIds[i]], group, this.framework);
  589. }
  590. }
  591. BarGraphFunctions.draw(groupIds, processedGroupData, this.framework);
  592. }
  593. }
  594. // cleanup unused svg elements
  595. DOMutil.cleanupElements(this.svgElements);
  596. };
  597. /**
  598. * first select and preprocess the data from the datasets.
  599. * the groups have their preselection of data, we now loop over this data to see
  600. * what data we need to draw. Sorted data is much faster.
  601. * more optimization is possible by doing the sampling before and using the binary search
  602. * to find the end date to determine the increment.
  603. *
  604. * @param {array} groupIds
  605. * @param {object} groupsData
  606. * @param {date} minDate
  607. * @param {date} maxDate
  608. * @private
  609. */
  610. LineGraph.prototype._getRelevantData = function (groupIds, groupsData, minDate, maxDate) {
  611. var group, i, j, item;
  612. if (groupIds.length > 0) {
  613. for (i = 0; i < groupIds.length; i++) {
  614. group = this.groups[groupIds[i]];
  615. groupsData[groupIds[i]] = [];
  616. var dataContainer = groupsData[groupIds[i]];
  617. // optimization for sorted data
  618. if (group.options.sort == true) {
  619. var guess = Math.max(0, util.binarySearchValue(group.itemsData, minDate, 'x', 'before'));
  620. for (j = guess; j < group.itemsData.length; j++) {
  621. item = group.itemsData[j];
  622. if (item !== undefined) {
  623. if (item.x > maxDate) {
  624. dataContainer.push(item);
  625. break;
  626. }
  627. else {
  628. dataContainer.push(item);
  629. }
  630. }
  631. }
  632. }
  633. else {
  634. for (j = 0; j < group.itemsData.length; j++) {
  635. item = group.itemsData[j];
  636. if (item !== undefined) {
  637. if (item.x > minDate && item.x < maxDate) {
  638. dataContainer.push(item);
  639. }
  640. }
  641. }
  642. }
  643. }
  644. }
  645. };
  646. /**
  647. *
  648. * @param groupIds
  649. * @param groupsData
  650. * @private
  651. */
  652. LineGraph.prototype._applySampling = function (groupIds, groupsData) {
  653. var group;
  654. if (groupIds.length > 0) {
  655. for (var i = 0; i < groupIds.length; i++) {
  656. group = this.groups[groupIds[i]];
  657. if (group.options.sampling == true) {
  658. var dataContainer = groupsData[groupIds[i]];
  659. if (dataContainer.length > 0) {
  660. var increment = 1;
  661. var amountOfPoints = dataContainer.length;
  662. // the global screen is used because changing the width of the yAxis may affect the increment, resulting in an endless loop
  663. // of width changing of the yAxis.
  664. var xDistance = this.body.util.toGlobalScreen(dataContainer[dataContainer.length - 1].x) - this.body.util.toGlobalScreen(dataContainer[0].x);
  665. var pointsPerPixel = amountOfPoints / xDistance;
  666. increment = Math.min(Math.ceil(0.2 * amountOfPoints), Math.max(1, Math.round(pointsPerPixel)));
  667. var sampledData = [];
  668. for (var j = 0; j < amountOfPoints; j += increment) {
  669. sampledData.push(dataContainer[j]);
  670. }
  671. groupsData[groupIds[i]] = sampledData;
  672. }
  673. }
  674. }
  675. }
  676. };
  677. /**
  678. *
  679. *
  680. * @param {array} groupIds
  681. * @param {object} groupsData
  682. * @param {object} groupRanges | this is being filled here
  683. * @private
  684. */
  685. LineGraph.prototype._getYRanges = function (groupIds, groupsData, groupRanges) {
  686. var groupData, group, i;
  687. var barCombinedDataLeft = [];
  688. var barCombinedDataRight = [];
  689. var options;
  690. if (groupIds.length > 0) {
  691. for (i = 0; i < groupIds.length; i++) {
  692. groupData = groupsData[groupIds[i]];
  693. options = this.groups[groupIds[i]].options;
  694. if (groupData.length > 0) {
  695. group = this.groups[groupIds[i]];
  696. // if bar graphs are stacked, their range need to be handled differently and accumulated over all groups.
  697. if (options.barChart.handleOverlap == 'stack' && options.style == 'bar') {
  698. if (options.yAxisOrientation == 'left') {barCombinedDataLeft = barCombinedDataLeft.concat(group.getYRange(groupData)) ;}
  699. else {barCombinedDataRight = barCombinedDataRight.concat(group.getYRange(groupData));}
  700. }
  701. else {
  702. groupRanges[groupIds[i]] = group.getYRange(groupData,groupIds[i]);
  703. }
  704. }
  705. }
  706. // if bar graphs are stacked, their range need to be handled differently and accumulated over all groups.
  707. BarGraphFunctions.getStackedBarYRange(barCombinedDataLeft , groupRanges, groupIds, '__barchartLeft' , 'left' );
  708. BarGraphFunctions.getStackedBarYRange(barCombinedDataRight, groupRanges, groupIds, '__barchartRight', 'right');
  709. }
  710. };
  711. /**
  712. * this sets the Y ranges for the Y axis. It also determines which of the axis should be shown or hidden.
  713. * @param {Array} groupIds
  714. * @param {Object} groupRanges
  715. * @private
  716. */
  717. LineGraph.prototype._updateYAxis = function (groupIds, groupRanges) {
  718. var changeCalled = false;
  719. var yAxisLeftUsed = false;
  720. var yAxisRightUsed = false;
  721. var minLeft = 1e9, minRight = 1e9, maxLeft = -1e9, maxRight = -1e9, minVal, maxVal;
  722. // if groups are present
  723. if (groupIds.length > 0) {
  724. for (var i = 0; i < groupIds.length; i++) {
  725. if (groupRanges.hasOwnProperty(groupIds[i])) {
  726. if (groupRanges[groupIds[i]].ignore !== true) {
  727. minVal = groupRanges[groupIds[i]].min;
  728. maxVal = groupRanges[groupIds[i]].max;
  729. if (groupRanges[groupIds[i]].yAxisOrientation == 'left') {
  730. yAxisLeftUsed = true;
  731. minLeft = minLeft > minVal ? minVal : minLeft;
  732. maxLeft = maxLeft < maxVal ? maxVal : maxLeft;
  733. }
  734. else {
  735. yAxisRightUsed = true;
  736. minRight = minRight > minVal ? minVal : minRight;
  737. maxRight = maxRight < maxVal ? maxVal : maxRight;
  738. }
  739. }
  740. }
  741. }
  742. if (yAxisLeftUsed == true) {
  743. this.yAxisLeft.setRange(minLeft, maxLeft);
  744. }
  745. if (yAxisRightUsed == true) {
  746. this.yAxisRight.setRange(minRight, maxRight);
  747. }
  748. }
  749. changeCalled = this._toggleAxisVisiblity(yAxisLeftUsed , this.yAxisLeft) || changeCalled;
  750. changeCalled = this._toggleAxisVisiblity(yAxisRightUsed, this.yAxisRight) || changeCalled;
  751. if (yAxisRightUsed == true && yAxisLeftUsed == true) {
  752. this.yAxisLeft.drawIcons = true;
  753. this.yAxisRight.drawIcons = true;
  754. }
  755. else {
  756. this.yAxisLeft.drawIcons = false;
  757. this.yAxisRight.drawIcons = false;
  758. }
  759. this.yAxisRight.master = !yAxisLeftUsed;
  760. if (this.yAxisRight.master == false) {
  761. if (yAxisRightUsed == true) {this.yAxisLeft.lineOffset = this.yAxisRight.width;}
  762. else {this.yAxisLeft.lineOffset = 0;}
  763. changeCalled = this.yAxisLeft.redraw() || changeCalled;
  764. this.yAxisRight.stepPixelsForced = this.yAxisLeft.stepPixels;
  765. this.yAxisRight.zeroCrossing = this.yAxisLeft.zeroCrossing;
  766. changeCalled = this.yAxisRight.redraw() || changeCalled;
  767. }
  768. else {
  769. changeCalled = this.yAxisRight.redraw() || changeCalled;
  770. }
  771. // clean the accumulated lists
  772. if (groupIds.indexOf('__barchartLeft') != -1) {
  773. groupIds.splice(groupIds.indexOf('__barchartLeft'),1);
  774. }
  775. if (groupIds.indexOf('__barchartRight') != -1) {
  776. groupIds.splice(groupIds.indexOf('__barchartRight'),1);
  777. }
  778. return changeCalled;
  779. };
  780. /**
  781. * This shows or hides the Y axis if needed. If there is a change, the changed event is emitted by the updateYAxis function
  782. *
  783. * @param {boolean} axisUsed
  784. * @returns {boolean}
  785. * @private
  786. * @param axis
  787. */
  788. LineGraph.prototype._toggleAxisVisiblity = function (axisUsed, axis) {
  789. var changed = false;
  790. if (axisUsed == false) {
  791. if (axis.dom.frame.parentNode) {
  792. axis.hide();
  793. changed = true;
  794. }
  795. }
  796. else {
  797. if (!axis.dom.frame.parentNode) {
  798. axis.show();
  799. changed = true;
  800. }
  801. }
  802. return changed;
  803. };
  804. /**
  805. * This uses the DataAxis object to generate the correct X coordinate on the SVG window. It uses the
  806. * util function toScreen to get the x coordinate from the timestamp. It also pre-filters the data and get the minMax ranges for
  807. * the yAxis.
  808. *
  809. * @param datapoints
  810. * @returns {Array}
  811. * @private
  812. */
  813. LineGraph.prototype._convertXcoordinates = function (datapoints) {
  814. var extractedData = [];
  815. var xValue, yValue;
  816. var toScreen = this.body.util.toScreen;
  817. for (var i = 0; i < datapoints.length; i++) {
  818. xValue = toScreen(datapoints[i].x) + this.width;
  819. yValue = datapoints[i].y;
  820. extractedData.push({x: xValue, y: yValue});
  821. }
  822. return extractedData;
  823. };
  824. /**
  825. * This uses the DataAxis object to generate the correct X coordinate on the SVG window. It uses the
  826. * util function toScreen to get the x coordinate from the timestamp. It also pre-filters the data and get the minMax ranges for
  827. * the yAxis.
  828. *
  829. * @param datapoints
  830. * @param group
  831. * @returns {Array}
  832. * @private
  833. */
  834. LineGraph.prototype._convertYcoordinates = function (datapoints, group) {
  835. var extractedData = [];
  836. var xValue, yValue;
  837. var toScreen = this.body.util.toScreen;
  838. var axis = this.yAxisLeft;
  839. var svgHeight = Number(this.svg.style.height.replace('px',''));
  840. if (group.options.yAxisOrientation == 'right') {
  841. axis = this.yAxisRight;
  842. }
  843. for (var i = 0; i < datapoints.length; i++) {
  844. xValue = toScreen(datapoints[i].x) + this.width;
  845. yValue = Math.round(axis.convertValue(datapoints[i].y));
  846. extractedData.push({x: xValue, y: yValue});
  847. }
  848. group.setZeroPosition(Math.min(svgHeight, axis.convertValue(0)));
  849. return extractedData;
  850. };
  851. module.exports = LineGraph;