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.

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