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.

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