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.

1342 lines
42 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
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 UNGROUPED = '__ungrouped__'; // reserved group id for ungrouped items
  10. /**
  11. * This is the constructor of the LineGraph. It requires a Timeline body and options.
  12. *
  13. * @param body
  14. * @param options
  15. * @constructor
  16. */
  17. function LineGraph(body, options) {
  18. this.id = util.randomUUID();
  19. this.body = body;
  20. this.defaultOptions = {
  21. yAxisOrientation: 'left',
  22. defaultGroup: 'default',
  23. sort: true,
  24. sampling: true,
  25. graphHeight: '400px',
  26. shaded: {
  27. enabled: false,
  28. orientation: 'bottom' // top, bottom
  29. },
  30. style: 'line', // line, bar
  31. barChart: {
  32. width: 50,
  33. handleOverlap: 'overlap',
  34. align: 'center' // left, center, right
  35. },
  36. catmullRom: {
  37. enabled: true,
  38. parametrization: 'centripetal', // uniform (alpha = 0.0), chordal (alpha = 1.0), centripetal (alpha = 0.5)
  39. alpha: 0.5
  40. },
  41. drawPoints: {
  42. enabled: true,
  43. size: 6,
  44. style: 'square' // square, circle
  45. },
  46. dataAxis: {
  47. showMinorLabels: true,
  48. showMajorLabels: true,
  49. icons: false,
  50. width: '40px',
  51. visible: true,
  52. customRange: {
  53. left: {min:undefined, max:undefined},
  54. right: {min:undefined, max:undefined}
  55. }
  56. //,
  57. //format: {
  58. // left: {decimals: 2},
  59. // right: {decimals: 2}
  60. //},
  61. //title: {
  62. // left: {
  63. // text: 'left',
  64. // style: 'color:black;'
  65. // },
  66. // right: {
  67. // text: 'right',
  68. // style: 'color:black;'
  69. // }
  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.autoSizeSVG = false;
  96. var me = this;
  97. this.itemsData = null; // DataSet
  98. this.groupsData = null; // DataSet
  99. // listeners for the DataSet of the items
  100. this.itemListeners = {
  101. 'add': function (event, params, senderId) {
  102. me._onAdd(params.items);
  103. },
  104. 'update': function (event, params, senderId) {
  105. me._onUpdate(params.items);
  106. },
  107. 'remove': function (event, params, senderId) {
  108. me._onRemove(params.items);
  109. }
  110. };
  111. // listeners for the DataSet of the groups
  112. this.groupListeners = {
  113. 'add': function (event, params, senderId) {
  114. me._onAddGroups(params.items);
  115. },
  116. 'update': function (event, params, senderId) {
  117. me._onUpdateGroups(params.items);
  118. },
  119. 'remove': function (event, params, senderId) {
  120. me._onRemoveGroups(params.items);
  121. }
  122. };
  123. this.items = {}; // object with an Item for every data item
  124. this.selection = []; // list with the ids of all selected nodes
  125. this.lastStart = this.body.range.start;
  126. this.touchParams = {}; // stores properties while dragging
  127. this.svgElements = {};
  128. this.setOptions(options);
  129. this.groupsUsingDefaultStyles = [0];
  130. this.body.emitter.on('rangechanged', function() {
  131. me.lastStart = me.body.range.start;
  132. me.svg.style.left = util.option.asSize(-me.width);
  133. me._updateGraph.apply(me);
  134. });
  135. // create the HTML DOM
  136. this._create();
  137. this.body.emitter.emit('change');
  138. }
  139. LineGraph.prototype = new Component();
  140. /**
  141. * Create the HTML DOM for the ItemSet
  142. */
  143. LineGraph.prototype._create = function(){
  144. var frame = document.createElement('div');
  145. frame.className = 'LineGraph';
  146. this.dom.frame = frame;
  147. // create svg element for graph drawing.
  148. this.svg = document.createElementNS('http://www.w3.org/2000/svg','svg');
  149. this.svg.style.position = 'relative';
  150. this.svg.style.height = ('' + this.options.graphHeight).replace('px','') + 'px';
  151. this.svg.style.display = 'block';
  152. frame.appendChild(this.svg);
  153. // data axis
  154. this.options.dataAxis.orientation = 'left';
  155. this.yAxisLeft = new DataAxis(this.body, this.options.dataAxis, this.svg, this.options.groups);
  156. this.options.dataAxis.orientation = 'right';
  157. this.yAxisRight = new DataAxis(this.body, this.options.dataAxis, this.svg, this.options.groups);
  158. delete this.options.dataAxis.orientation;
  159. // legends
  160. this.legendLeft = new Legend(this.body, this.options.legend, 'left', this.options.groups);
  161. this.legendRight = new Legend(this.body, this.options.legend, 'right', this.options.groups);
  162. this.show();
  163. };
  164. /**
  165. * set the options of the LineGraph. the mergeOptions is used for subObjects that have an enabled element.
  166. * @param options
  167. */
  168. LineGraph.prototype.setOptions = function(options) {
  169. if (options) {
  170. var fields = ['sampling','defaultGroup','graphHeight','yAxisOrientation','style','barChart','dataAxis','sort','groups'];
  171. if (options.graphHeight === undefined && options.height !== undefined && this.body.domProps.centerContainer.height !== undefined) {
  172. this.autoSizeSVG = true;
  173. }
  174. else if (this.body.domProps.centerContainer.height !== undefined && options.graphHeight !== undefined) {
  175. if (parseInt((options.graphHeight + '').replace("px",'')) < this.body.domProps.centerContainer.height) {
  176. this.autoSizeSVG = true;
  177. }
  178. }
  179. util.selectiveDeepExtend(fields, this.options, options);
  180. util.mergeOptions(this.options, options,'catmullRom');
  181. util.mergeOptions(this.options, options,'drawPoints');
  182. util.mergeOptions(this.options, options,'shaded');
  183. util.mergeOptions(this.options, options,'legend');
  184. if (options.catmullRom) {
  185. if (typeof options.catmullRom == 'object') {
  186. if (options.catmullRom.parametrization) {
  187. if (options.catmullRom.parametrization == 'uniform') {
  188. this.options.catmullRom.alpha = 0;
  189. }
  190. else if (options.catmullRom.parametrization == 'chordal') {
  191. this.options.catmullRom.alpha = 1.0;
  192. }
  193. else {
  194. this.options.catmullRom.parametrization = 'centripetal';
  195. this.options.catmullRom.alpha = 0.5;
  196. }
  197. }
  198. }
  199. }
  200. if (this.yAxisLeft) {
  201. if (options.dataAxis !== undefined) {
  202. this.yAxisLeft.setOptions(this.options.dataAxis);
  203. this.yAxisRight.setOptions(this.options.dataAxis);
  204. }
  205. }
  206. if (this.legendLeft) {
  207. if (options.legend !== undefined) {
  208. this.legendLeft.setOptions(this.options.legend);
  209. this.legendRight.setOptions(this.options.legend);
  210. }
  211. }
  212. if (this.groups.hasOwnProperty(UNGROUPED)) {
  213. this.groups[UNGROUPED].setOptions(options);
  214. }
  215. }
  216. if (this.dom.frame) {
  217. this._updateGraph();
  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();
  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.unsubscribe(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 datapoints
  321. * @param [ids]
  322. * @private
  323. */
  324. LineGraph.prototype._onUpdate = function(ids) {
  325. this._updateUngrouped();
  326. this._updateAllGroupData();
  327. this._updateGraph();
  328. this.redraw();
  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();
  339. };
  340. LineGraph.prototype._onAddGroups = function (groupIds) {this._onUpdateGroups(groupIds);};
  341. LineGraph.prototype._onRemoveGroups = function (groupIds) {
  342. for (var i = 0; i < groupIds.length; i++) {
  343. if (this.groups.hasOwnProperty(groupIds[i])) {
  344. if (this.groups[groupIds[i]].options.yAxisOrientation == 'right') {
  345. this.yAxisRight.removeGroup(groupIds[i]);
  346. this.legendRight.removeGroup(groupIds[i]);
  347. this.legendRight.redraw();
  348. }
  349. else {
  350. this.yAxisLeft.removeGroup(groupIds[i]);
  351. this.legendLeft.removeGroup(groupIds[i]);
  352. this.legendLeft.redraw();
  353. }
  354. delete this.groups[groupIds[i]];
  355. }
  356. }
  357. this._updateUngrouped();
  358. this._updateGraph();
  359. this.redraw();
  360. };
  361. /**
  362. * update a group object
  363. *
  364. * @param group
  365. * @param groupId
  366. * @private
  367. */
  368. LineGraph.prototype._updateGroup = function (group, groupId) {
  369. if (!this.groups.hasOwnProperty(groupId)) {
  370. this.groups[groupId] = new GraphGroup(group, groupId, this.options, this.groupsUsingDefaultStyles);
  371. if (this.groups[groupId].options.yAxisOrientation == 'right') {
  372. this.yAxisRight.addGroup(groupId, this.groups[groupId]);
  373. this.legendRight.addGroup(groupId, this.groups[groupId]);
  374. }
  375. else {
  376. this.yAxisLeft.addGroup(groupId, this.groups[groupId]);
  377. this.legendLeft.addGroup(groupId, this.groups[groupId]);
  378. }
  379. }
  380. else {
  381. this.groups[groupId].update(group);
  382. if (this.groups[groupId].options.yAxisOrientation == 'right') {
  383. this.yAxisRight.updateGroup(groupId, this.groups[groupId]);
  384. this.legendRight.updateGroup(groupId, this.groups[groupId]);
  385. }
  386. else {
  387. this.yAxisLeft.updateGroup(groupId, this.groups[groupId]);
  388. this.legendLeft.updateGroup(groupId, this.groups[groupId]);
  389. }
  390. }
  391. this.legendLeft.redraw();
  392. this.legendRight.redraw();
  393. };
  394. LineGraph.prototype._updateAllGroupData = function () {
  395. if (this.itemsData != null) {
  396. var groupsContent = {};
  397. var groupId;
  398. for (groupId in this.groups) {
  399. if (this.groups.hasOwnProperty(groupId)) {
  400. groupsContent[groupId] = [];
  401. }
  402. }
  403. for (var itemId in this.itemsData._data) {
  404. if (this.itemsData._data.hasOwnProperty(itemId)) {
  405. var item = this.itemsData._data[itemId];
  406. if (groupsContent[item.group] === undefined) {
  407. 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.')
  408. }
  409. item.x = util.convert(item.x,'Date');
  410. groupsContent[item.group].push(item);
  411. }
  412. }
  413. for (groupId in this.groups) {
  414. if (this.groups.hasOwnProperty(groupId)) {
  415. this.groups[groupId].setItems(groupsContent[groupId]);
  416. }
  417. }
  418. }
  419. };
  420. /**
  421. * Create or delete the group holding all ungrouped items. This group is used when
  422. * there are no groups specified. This anonymous group is called 'graph'.
  423. * @protected
  424. */
  425. LineGraph.prototype._updateUngrouped = function() {
  426. if (this.itemsData && this.itemsData != null) {
  427. var ungroupedCounter = 0;
  428. for (var itemId in this.itemsData._data) {
  429. if (this.itemsData._data.hasOwnProperty(itemId)) {
  430. var item = this.itemsData._data[itemId];
  431. if (item != undefined) {
  432. if (item.hasOwnProperty('group')) {
  433. if (item.group === undefined) {
  434. item.group = UNGROUPED;
  435. }
  436. }
  437. else {
  438. item.group = UNGROUPED;
  439. }
  440. ungroupedCounter = item.group == UNGROUPED ? ungroupedCounter + 1 : ungroupedCounter;
  441. }
  442. }
  443. }
  444. if (ungroupedCounter == 0) {
  445. delete this.groups[UNGROUPED];
  446. this.legendLeft.removeGroup(UNGROUPED);
  447. this.legendRight.removeGroup(UNGROUPED);
  448. this.yAxisLeft.removeGroup(UNGROUPED);
  449. this.yAxisRight.removeGroup(UNGROUPED);
  450. }
  451. else {
  452. var group = {id: UNGROUPED, content: this.options.defaultGroup};
  453. this._updateGroup(group, UNGROUPED);
  454. }
  455. }
  456. else {
  457. delete this.groups[UNGROUPED];
  458. this.legendLeft.removeGroup(UNGROUPED);
  459. this.legendRight.removeGroup(UNGROUPED);
  460. this.yAxisLeft.removeGroup(UNGROUPED);
  461. this.yAxisRight.removeGroup(UNGROUPED);
  462. }
  463. this.legendLeft.redraw();
  464. this.legendRight.redraw();
  465. };
  466. /**
  467. * Redraw the component, mandatory function
  468. * @return {boolean} Returns true if the component is resized
  469. */
  470. LineGraph.prototype.redraw = function() {
  471. var resized = false;
  472. this.svg.style.height = ('' + this.options.graphHeight).replace('px','') + 'px';
  473. if (this.lastWidth === undefined && this.width || this.lastWidth != this.width) {
  474. resized = true;
  475. }
  476. // check if this component is resized
  477. resized = this._isResized() || resized;
  478. // check whether zoomed (in that case we need to re-stack everything)
  479. var visibleInterval = this.body.range.end - this.body.range.start;
  480. var zoomed = (visibleInterval != this.lastVisibleInterval) || (this.width != this.lastWidth);
  481. this.lastVisibleInterval = visibleInterval;
  482. this.lastWidth = this.width;
  483. // calculate actual size and position
  484. this.width = this.dom.frame.offsetWidth;
  485. // the svg element is three times as big as the width, this allows for fully dragging left and right
  486. // without reloading the graph. the controls for this are bound to events in the constructor
  487. if (resized == true) {
  488. this.svg.style.width = util.option.asSize(3*this.width);
  489. this.svg.style.left = util.option.asSize(-this.width);
  490. }
  491. if (zoomed == true || this.abortedGraphUpdate == true) {
  492. this._updateGraph();
  493. }
  494. else {
  495. // move the whole svg while dragging
  496. if (this.lastStart != 0) {
  497. var offset = this.body.range.start - this.lastStart;
  498. var range = this.body.range.end - this.body.range.start;
  499. if (this.width != 0) {
  500. var rangePerPixelInv = this.width/range;
  501. var xOffset = offset * rangePerPixelInv;
  502. this.svg.style.left = (-this.width - xOffset) + 'px';
  503. }
  504. }
  505. }
  506. this.legendLeft.redraw();
  507. this.legendRight.redraw();
  508. return resized;
  509. };
  510. /**
  511. * Update and redraw the graph.
  512. *
  513. */
  514. LineGraph.prototype._updateGraph = function () {
  515. // reset the svg elements
  516. DOMutil.prepareElements(this.svgElements);
  517. if (this.width != 0 && this.itemsData != null) {
  518. var group, i;
  519. var preprocessedGroupData = {};
  520. var processedGroupData = {};
  521. var groupRanges = {};
  522. var changeCalled = false;
  523. // update the height of the graph on each redraw of the graph.
  524. if (this.autoSizeSVG == true) {
  525. if (this.options.graphHeight != this.body.domProps.centerContainer.height + 'px') {
  526. this.options.graphHeight = this.body.domProps.centerContainer.height + 'px';
  527. this.svg.style.height = this.body.domProps.centerContainer.height + 'px';
  528. }
  529. }
  530. // getting group Ids
  531. var groupIds = [];
  532. for (var groupId in this.groups) {
  533. if (this.groups.hasOwnProperty(groupId)) {
  534. group = this.groups[groupId];
  535. if (group.visible == true && (this.options.groups.visibility[groupId] === undefined || this.options.groups.visibility[groupId] == true)) {
  536. groupIds.push(groupId);
  537. }
  538. }
  539. }
  540. if (groupIds.length > 0) {
  541. // this is the range of the SVG canvas
  542. var minDate = this.body.util.toGlobalTime(- this.body.domProps.root.width);
  543. var maxDate = this.body.util.toGlobalTime(2 * this.body.domProps.root.width);
  544. var groupsData = {};
  545. // fill groups data
  546. this._getRelevantData(groupIds, groupsData, minDate, maxDate);
  547. // we transform the X coordinates to detect collisions
  548. for (i = 0; i < groupIds.length; i++) {
  549. preprocessedGroupData[groupIds[i]] = this._convertXcoordinates(groupsData[groupIds[i]]);
  550. }
  551. // now all needed data has been collected we start the processing.
  552. this._getYRanges(groupIds, preprocessedGroupData, groupRanges);
  553. // update the Y axis first, we use this data to draw at the correct Y points
  554. // changeCalled is required to clean the SVG on a change emit.
  555. changeCalled = this._updateYAxis(groupIds, groupRanges);
  556. if (changeCalled == true) {
  557. DOMutil.cleanupElements(this.svgElements);
  558. this.abortedGraphUpdate = true;
  559. this.body.emitter.emit('change');
  560. return;
  561. }
  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. processedGroupData[groupIds[i]] = this._convertYcoordinates(groupsData[groupIds[i]], group);
  567. }
  568. // draw the groups
  569. for (i = 0; i < groupIds.length; i++) {
  570. group = this.groups[groupIds[i]];
  571. if (group.options.style == 'line') {
  572. this._drawLineGraph(processedGroupData[groupIds[i]], group);
  573. }
  574. }
  575. this._drawBarGraphs(groupIds, processedGroupData);
  576. }
  577. }
  578. // cleanup unused svg elements
  579. DOMutil.cleanupElements(this.svgElements);
  580. };
  581. LineGraph.prototype._getRelevantData = function (groupIds, groupsData, minDate, maxDate) {
  582. // first select and preprocess the data from the datasets.
  583. // the groups have their preselection of data, we now loop over this data to see
  584. // what data we need to draw. Sorted data is much faster.
  585. // more optimization is possible by doing the sampling before and using the binary search
  586. // to find the end date to determine the increment.
  587. var group, i, j, item;
  588. if (groupIds.length > 0) {
  589. for (i = 0; i < groupIds.length; i++) {
  590. group = this.groups[groupIds[i]];
  591. groupsData[groupIds[i]] = [];
  592. var dataContainer = groupsData[groupIds[i]];
  593. // optimization for sorted data
  594. if (group.options.sort == true) {
  595. var guess = Math.max(0, util.binarySearchGeneric(group.itemsData, minDate, 'x', 'before'));
  596. for (j = guess; j < group.itemsData.length; j++) {
  597. item = group.itemsData[j];
  598. if (item !== undefined) {
  599. if (item.x > maxDate) {
  600. dataContainer.push(item);
  601. break;
  602. }
  603. else {
  604. dataContainer.push(item);
  605. }
  606. }
  607. }
  608. }
  609. else {
  610. for (j = 0; j < group.itemsData.length; j++) {
  611. item = group.itemsData[j];
  612. if (item !== undefined) {
  613. if (item.x > minDate && item.x < maxDate) {
  614. dataContainer.push(item);
  615. }
  616. }
  617. }
  618. }
  619. }
  620. }
  621. this._applySampling(groupIds, groupsData);
  622. };
  623. LineGraph.prototype._applySampling = function (groupIds, groupsData) {
  624. var group;
  625. if (groupIds.length > 0) {
  626. for (var i = 0; i < groupIds.length; i++) {
  627. group = this.groups[groupIds[i]];
  628. if (group.options.sampling == true) {
  629. var dataContainer = groupsData[groupIds[i]];
  630. if (dataContainer.length > 0) {
  631. var increment = 1;
  632. var amountOfPoints = dataContainer.length;
  633. // the global screen is used because changing the width of the yAxis may affect the increment, resulting in an endless loop
  634. // of width changing of the yAxis.
  635. var xDistance = this.body.util.toGlobalScreen(dataContainer[dataContainer.length - 1].x) - this.body.util.toGlobalScreen(dataContainer[0].x);
  636. var pointsPerPixel = amountOfPoints / xDistance;
  637. increment = Math.min(Math.ceil(0.2 * amountOfPoints), Math.max(1, Math.round(pointsPerPixel)));
  638. var sampledData = [];
  639. for (var j = 0; j < amountOfPoints; j += increment) {
  640. sampledData.push(dataContainer[j]);
  641. }
  642. groupsData[groupIds[i]] = sampledData;
  643. }
  644. }
  645. }
  646. }
  647. };
  648. LineGraph.prototype._getYRanges = function (groupIds, groupsData, groupRanges) {
  649. var groupData, group, i,j;
  650. var barCombinedDataLeft = [];
  651. var barCombinedDataRight = [];
  652. var barCombinedData;
  653. if (groupIds.length > 0) {
  654. for (i = 0; i < groupIds.length; i++) {
  655. groupData = groupsData[groupIds[i]];
  656. if (groupData.length > 0) {
  657. group = this.groups[groupIds[i]];
  658. if (group.options.style == 'line' || group.options.barChart.handleOverlap != 'stack') {
  659. var yMin = groupData[0].y;
  660. var yMax = groupData[0].y;
  661. for (j = 0; j < groupData.length; j++) {
  662. yMin = yMin > groupData[j].y ? groupData[j].y : yMin;
  663. yMax = yMax < groupData[j].y ? groupData[j].y : yMax;
  664. }
  665. groupRanges[groupIds[i]] = {min: yMin, max: yMax, yAxisOrientation: group.options.yAxisOrientation};
  666. }
  667. else if (group.options.style == 'bar') {
  668. if (group.options.yAxisOrientation == 'left') {
  669. barCombinedData = barCombinedDataLeft;
  670. }
  671. else {
  672. barCombinedData = barCombinedDataRight;
  673. }
  674. groupRanges[groupIds[i]] = {min: 0, max: 0, yAxisOrientation: group.options.yAxisOrientation, ignore: true};
  675. // combine data
  676. for (j = 0; j < groupData.length; j++) {
  677. barCombinedData.push({
  678. x: groupData[j].x,
  679. y: groupData[j].y,
  680. groupId: groupIds[i]
  681. });
  682. }
  683. }
  684. }
  685. }
  686. var intersections;
  687. if (barCombinedDataLeft.length > 0) {
  688. // sort by time and by group
  689. barCombinedDataLeft.sort(function (a, b) {
  690. if (a.x == b.x) {
  691. return a.groupId - b.groupId;
  692. } else {
  693. return a.x - b.x;
  694. }
  695. });
  696. intersections = {};
  697. this._getDataIntersections(intersections, barCombinedDataLeft);
  698. groupRanges['__barchartLeft'] = this._getStackedBarYRange(intersections, barCombinedDataLeft);
  699. groupRanges['__barchartLeft'].yAxisOrientation = 'left';
  700. groupIds.push('__barchartLeft');
  701. }
  702. if (barCombinedDataRight.length > 0) {
  703. // sort by time and by group
  704. barCombinedDataRight.sort(function (a, b) {
  705. if (a.x == b.x) {
  706. return a.groupId - b.groupId;
  707. } else {
  708. return a.x - b.x;
  709. }
  710. });
  711. intersections = {};
  712. this._getDataIntersections(intersections, barCombinedDataRight);
  713. groupRanges['__barchartRight'] = this._getStackedBarYRange(intersections, barCombinedDataRight);
  714. groupRanges['__barchartRight'].yAxisOrientation = 'right';
  715. groupIds.push('__barchartRight');
  716. }
  717. }
  718. };
  719. LineGraph.prototype._getStackedBarYRange = function (intersections, combinedData) {
  720. var key;
  721. var yMin = combinedData[0].y;
  722. var yMax = combinedData[0].y;
  723. for (var i = 0; i < combinedData.length; i++) {
  724. key = combinedData[i].x;
  725. if (intersections[key] === undefined) {
  726. yMin = yMin > combinedData[i].y ? combinedData[i].y : yMin;
  727. yMax = yMax < combinedData[i].y ? combinedData[i].y : yMax;
  728. }
  729. else {
  730. intersections[key].accumulated += combinedData[i].y;
  731. }
  732. }
  733. for (var xpos in intersections) {
  734. if (intersections.hasOwnProperty(xpos)) {
  735. yMin = yMin > intersections[xpos].accumulated ? intersections[xpos].accumulated : yMin;
  736. yMax = yMax < intersections[xpos].accumulated ? intersections[xpos].accumulated : yMax;
  737. }
  738. }
  739. return {min: yMin, max: yMax};
  740. };
  741. /**
  742. * this sets the Y ranges for the Y axis. It also determines which of the axis should be shown or hidden.
  743. * @param {Array} groupIds
  744. * @param {Object} groupRanges
  745. * @private
  746. */
  747. LineGraph.prototype._updateYAxis = function (groupIds, groupRanges) {
  748. var changeCalled = false;
  749. var yAxisLeftUsed = false;
  750. var yAxisRightUsed = false;
  751. var minLeft = 1e9, minRight = 1e9, maxLeft = -1e9, maxRight = -1e9, minVal, maxVal;
  752. // if groups are present
  753. if (groupIds.length > 0) {
  754. for (var i = 0; i < groupIds.length; i++) {
  755. if (groupRanges.hasOwnProperty(groupIds[i])) {
  756. if (groupRanges[groupIds[i]].ignore !== true) {
  757. minVal = groupRanges[groupIds[i]].min;
  758. maxVal = groupRanges[groupIds[i]].max;
  759. if (groupRanges[groupIds[i]].yAxisOrientation == 'left') {
  760. yAxisLeftUsed = true;
  761. minLeft = minLeft > minVal ? minVal : minLeft;
  762. maxLeft = maxLeft < maxVal ? maxVal : maxLeft;
  763. }
  764. else {
  765. yAxisRightUsed = true;
  766. minRight = minRight > minVal ? minVal : minRight;
  767. maxRight = maxRight < maxVal ? maxVal : maxRight;
  768. }
  769. }
  770. }
  771. }
  772. if (yAxisLeftUsed == true) {
  773. this.yAxisLeft.setRange(minLeft, maxLeft);
  774. }
  775. if (yAxisRightUsed == true) {
  776. this.yAxisRight.setRange(minRight, maxRight);
  777. }
  778. }
  779. changeCalled = this._toggleAxisVisiblity(yAxisLeftUsed , this.yAxisLeft) || changeCalled;
  780. changeCalled = this._toggleAxisVisiblity(yAxisRightUsed, this.yAxisRight) || changeCalled;
  781. if (yAxisRightUsed == true && yAxisLeftUsed == true) {
  782. this.yAxisLeft.drawIcons = true;
  783. this.yAxisRight.drawIcons = true;
  784. }
  785. else {
  786. this.yAxisLeft.drawIcons = false;
  787. this.yAxisRight.drawIcons = false;
  788. }
  789. this.yAxisRight.master = !yAxisLeftUsed;
  790. if (this.yAxisRight.master == false) {
  791. if (yAxisRightUsed == true) {this.yAxisLeft.lineOffset = this.yAxisRight.width;}
  792. else {this.yAxisLeft.lineOffset = 0;}
  793. changeCalled = this.yAxisLeft.redraw() || changeCalled;
  794. this.yAxisRight.stepPixelsForced = this.yAxisLeft.stepPixels;
  795. changeCalled = this.yAxisRight.redraw() || changeCalled;
  796. }
  797. else {
  798. changeCalled = this.yAxisRight.redraw() || changeCalled;
  799. }
  800. // clean the accumulated lists
  801. if (groupIds.indexOf('__barchartLeft') != -1) {
  802. groupIds.splice(groupIds.indexOf('__barchartLeft'),1);
  803. }
  804. if (groupIds.indexOf('__barchartRight') != -1) {
  805. groupIds.splice(groupIds.indexOf('__barchartRight'),1);
  806. }
  807. return changeCalled;
  808. };
  809. /**
  810. * This shows or hides the Y axis if needed. If there is a change, the changed event is emitted by the updateYAxis function
  811. *
  812. * @param {boolean} axisUsed
  813. * @returns {boolean}
  814. * @private
  815. * @param axis
  816. */
  817. LineGraph.prototype._toggleAxisVisiblity = function (axisUsed, axis) {
  818. var changed = false;
  819. if (axisUsed == false) {
  820. if (axis.dom.frame.parentNode) {
  821. axis.hide();
  822. changed = true;
  823. }
  824. }
  825. else {
  826. if (!axis.dom.frame.parentNode) {
  827. axis.show();
  828. changed = true;
  829. }
  830. }
  831. return changed;
  832. };
  833. /**
  834. * draw a bar graph
  835. *
  836. * @param groupIds
  837. * @param processedGroupData
  838. */
  839. LineGraph.prototype._drawBarGraphs = function (groupIds, processedGroupData) {
  840. var combinedData = [];
  841. var intersections = {};
  842. var coreDistance;
  843. var key, drawData;
  844. var group;
  845. var i,j;
  846. var barPoints = 0;
  847. // combine all barchart data
  848. for (i = 0; i < groupIds.length; i++) {
  849. group = this.groups[groupIds[i]];
  850. if (group.options.style == 'bar') {
  851. if (group.visible == true && (this.options.groups.visibility[groupIds[i]] === undefined || this.options.groups.visibility[groupIds[i]] == true)) {
  852. for (j = 0; j < processedGroupData[groupIds[i]].length; j++) {
  853. combinedData.push({
  854. x: processedGroupData[groupIds[i]][j].x,
  855. y: processedGroupData[groupIds[i]][j].y,
  856. groupId: groupIds[i]
  857. });
  858. barPoints += 1;
  859. }
  860. }
  861. }
  862. }
  863. if (barPoints == 0) {return;}
  864. // sort by time and by group
  865. combinedData.sort(function (a, b) {
  866. if (a.x == b.x) {
  867. return a.groupId - b.groupId;
  868. } else {
  869. return a.x - b.x;
  870. }
  871. });
  872. // get intersections
  873. this._getDataIntersections(intersections, combinedData);
  874. // plot barchart
  875. for (i = 0; i < combinedData.length; i++) {
  876. group = this.groups[combinedData[i].groupId];
  877. var minWidth = 0.1 * group.options.barChart.width;
  878. key = combinedData[i].x;
  879. var heightOffset = 0;
  880. if (intersections[key] === undefined) {
  881. if (i+1 < combinedData.length) {coreDistance = Math.abs(combinedData[i+1].x - key);}
  882. if (i > 0) {coreDistance = Math.min(coreDistance,Math.abs(combinedData[i-1].x - key));}
  883. drawData = this._getSafeDrawData(coreDistance, group, minWidth);
  884. }
  885. else {
  886. var nextKey = i + (intersections[key].amount - intersections[key].resolved);
  887. var prevKey = i - (intersections[key].resolved + 1);
  888. if (nextKey < combinedData.length) {coreDistance = Math.abs(combinedData[nextKey].x - key);}
  889. if (prevKey > 0) {coreDistance = Math.min(coreDistance,Math.abs(combinedData[prevKey].x - key));}
  890. drawData = this._getSafeDrawData(coreDistance, group, minWidth);
  891. intersections[key].resolved += 1;
  892. if (group.options.barChart.handleOverlap == 'stack') {
  893. heightOffset = intersections[key].accumulated;
  894. intersections[key].accumulated += group.zeroPosition - combinedData[i].y;
  895. }
  896. else if (group.options.barChart.handleOverlap == 'sideBySide') {
  897. drawData.width = drawData.width / intersections[key].amount;
  898. drawData.offset += (intersections[key].resolved) * drawData.width - (0.5*drawData.width * (intersections[key].amount+1));
  899. if (group.options.barChart.align == 'left') {drawData.offset -= 0.5*drawData.width;}
  900. else if (group.options.barChart.align == 'right') {drawData.offset += 0.5*drawData.width;}
  901. }
  902. }
  903. DOMutil.drawBar(combinedData[i].x + drawData.offset, combinedData[i].y - heightOffset, drawData.width, group.zeroPosition - combinedData[i].y, group.className + ' bar', this.svgElements, this.svg);
  904. // draw points
  905. if (group.options.drawPoints.enabled == true) {
  906. DOMutil.drawPoint(combinedData[i].x + drawData.offset, combinedData[i].y - heightOffset, group, this.svgElements, this.svg);
  907. }
  908. }
  909. };
  910. /**
  911. * Fill the intersections object with counters of how many datapoints share the same x coordinates
  912. * @param intersections
  913. * @param combinedData
  914. * @private
  915. */
  916. LineGraph.prototype._getDataIntersections = function (intersections, combinedData) {
  917. // get intersections
  918. var coreDistance;
  919. for (var i = 0; i < combinedData.length; i++) {
  920. if (i + 1 < combinedData.length) {
  921. coreDistance = Math.abs(combinedData[i + 1].x - combinedData[i].x);
  922. }
  923. if (i > 0) {
  924. coreDistance = Math.min(coreDistance, Math.abs(combinedData[i - 1].x - combinedData[i].x));
  925. }
  926. if (coreDistance == 0) {
  927. if (intersections[combinedData[i].x] === undefined) {
  928. intersections[combinedData[i].x] = {amount: 0, resolved: 0, accumulated: 0};
  929. }
  930. intersections[combinedData[i].x].amount += 1;
  931. }
  932. }
  933. };
  934. /**
  935. * Get the width and offset for bargraphs based on the coredistance between datapoints
  936. *
  937. * @param coreDistance
  938. * @param group
  939. * @param minWidth
  940. * @returns {{width: Number, offset: Number}}
  941. * @private
  942. */
  943. LineGraph.prototype._getSafeDrawData = function (coreDistance, group, minWidth) {
  944. var width, offset;
  945. if (coreDistance < group.options.barChart.width && coreDistance > 0) {
  946. width = coreDistance < minWidth ? minWidth : coreDistance;
  947. offset = 0; // recalculate offset with the new width;
  948. if (group.options.barChart.align == 'left') {
  949. offset -= 0.5 * coreDistance;
  950. }
  951. else if (group.options.barChart.align == 'right') {
  952. offset += 0.5 * coreDistance;
  953. }
  954. }
  955. else {
  956. // default settings
  957. width = group.options.barChart.width;
  958. offset = 0;
  959. if (group.options.barChart.align == 'left') {
  960. offset -= 0.5 * group.options.barChart.width;
  961. }
  962. else if (group.options.barChart.align == 'right') {
  963. offset += 0.5 * group.options.barChart.width;
  964. }
  965. }
  966. return {width: width, offset: offset};
  967. };
  968. /**
  969. * draw a line graph
  970. *
  971. * @param dataset
  972. * @param group
  973. */
  974. LineGraph.prototype._drawLineGraph = function (dataset, group) {
  975. if (dataset != null) {
  976. if (dataset.length > 0) {
  977. var path, d;
  978. var svgHeight = Number(this.svg.style.height.replace('px',''));
  979. path = DOMutil.getSVGElement('path', this.svgElements, this.svg);
  980. path.setAttributeNS(null, "class", group.className);
  981. if(group.style !== undefined) {
  982. path.setAttributeNS(null, "style", group.style);
  983. }
  984. // construct path from dataset
  985. if (group.options.catmullRom.enabled == true) {
  986. d = this._catmullRom(dataset, group);
  987. }
  988. else {
  989. d = this._linear(dataset);
  990. }
  991. // append with points for fill and finalize the path
  992. if (group.options.shaded.enabled == true) {
  993. var fillPath = DOMutil.getSVGElement('path',this.svgElements, this.svg);
  994. var dFill;
  995. if (group.options.shaded.orientation == 'top') {
  996. dFill = 'M' + dataset[0].x + ',' + 0 + ' ' + d + 'L' + dataset[dataset.length - 1].x + ',' + 0;
  997. }
  998. else {
  999. dFill = 'M' + dataset[0].x + ',' + svgHeight + ' ' + d + 'L' + dataset[dataset.length - 1].x + ',' + svgHeight;
  1000. }
  1001. fillPath.setAttributeNS(null, "class", group.className + " fill");
  1002. if(group.options.shaded.style !== undefined) {
  1003. fillPath.setAttributeNS(null, "style", group.options.shaded.style);
  1004. }
  1005. fillPath.setAttributeNS(null, "d", dFill);
  1006. }
  1007. // copy properties to path for drawing.
  1008. path.setAttributeNS(null, 'd', 'M' + d);
  1009. // draw points
  1010. if (group.options.drawPoints.enabled == true) {
  1011. this._drawPoints(dataset, group, this.svgElements, this.svg);
  1012. }
  1013. }
  1014. }
  1015. };
  1016. /**
  1017. * draw the data points
  1018. *
  1019. * @param {Array} dataset
  1020. * @param {Object} JSONcontainer
  1021. * @param {Object} svg | SVG DOM element
  1022. * @param {GraphGroup} group
  1023. * @param {Number} [offset]
  1024. */
  1025. LineGraph.prototype._drawPoints = function (dataset, group, JSONcontainer, svg, offset) {
  1026. if (offset === undefined) {offset = 0;}
  1027. for (var i = 0; i < dataset.length; i++) {
  1028. DOMutil.drawPoint(dataset[i].x + offset, dataset[i].y, group, JSONcontainer, svg);
  1029. }
  1030. };
  1031. /**
  1032. * This uses the DataAxis object to generate the correct X coordinate on the SVG window. It uses the
  1033. * util function toScreen to get the x coordinate from the timestamp. It also pre-filters the data and get the minMax ranges for
  1034. * the yAxis.
  1035. *
  1036. * @param datapoints
  1037. * @returns {Array}
  1038. * @private
  1039. */
  1040. LineGraph.prototype._convertXcoordinates = function (datapoints) {
  1041. var extractedData = [];
  1042. var xValue, yValue;
  1043. var toScreen = this.body.util.toScreen;
  1044. for (var i = 0; i < datapoints.length; i++) {
  1045. xValue = toScreen(datapoints[i].x) + this.width;
  1046. yValue = datapoints[i].y;
  1047. extractedData.push({x: xValue, y: yValue});
  1048. }
  1049. return extractedData;
  1050. };
  1051. /**
  1052. * This uses the DataAxis object to generate the correct X coordinate on the SVG window. It uses the
  1053. * util function toScreen to get the x coordinate from the timestamp. It also pre-filters the data and get the minMax ranges for
  1054. * the yAxis.
  1055. *
  1056. * @param datapoints
  1057. * @returns {Array}
  1058. * @private
  1059. */
  1060. LineGraph.prototype._convertYcoordinates = function (datapoints, group) {
  1061. var extractedData = [];
  1062. var xValue, yValue;
  1063. var toScreen = this.body.util.toScreen;
  1064. var axis = this.yAxisLeft;
  1065. var svgHeight = Number(this.svg.style.height.replace('px',''));
  1066. if (group.options.yAxisOrientation == 'right') {
  1067. axis = this.yAxisRight;
  1068. }
  1069. for (var i = 0; i < datapoints.length; i++) {
  1070. xValue = toScreen(datapoints[i].x) + this.width;
  1071. yValue = Math.round(axis.convertValue(datapoints[i].y));
  1072. extractedData.push({x: xValue, y: yValue});
  1073. }
  1074. group.setZeroPosition(Math.min(svgHeight, axis.convertValue(0)));
  1075. return extractedData;
  1076. };
  1077. /**
  1078. * This uses an uniform parametrization of the CatmullRom algorithm:
  1079. * 'On the Parameterization of Catmull-Rom Curves' by Cem Yuksel et al.
  1080. * @param data
  1081. * @returns {string}
  1082. * @private
  1083. */
  1084. LineGraph.prototype._catmullRomUniform = function(data) {
  1085. // catmull rom
  1086. var p0, p1, p2, p3, bp1, bp2;
  1087. var d = Math.round(data[0].x) + ',' + Math.round(data[0].y) + ' ';
  1088. var normalization = 1/6;
  1089. var length = data.length;
  1090. for (var i = 0; i < length - 1; i++) {
  1091. p0 = (i == 0) ? data[0] : data[i-1];
  1092. p1 = data[i];
  1093. p2 = data[i+1];
  1094. p3 = (i + 2 < length) ? data[i+2] : p2;
  1095. // Catmull-Rom to Cubic Bezier conversion matrix
  1096. // 0 1 0 0
  1097. // -1/6 1 1/6 0
  1098. // 0 1/6 1 -1/6
  1099. // 0 0 1 0
  1100. // bp0 = { x: p1.x, y: p1.y };
  1101. bp1 = { x: ((-p0.x + 6*p1.x + p2.x) *normalization), y: ((-p0.y + 6*p1.y + p2.y) *normalization)};
  1102. bp2 = { x: (( p1.x + 6*p2.x - p3.x) *normalization), y: (( p1.y + 6*p2.y - p3.y) *normalization)};
  1103. // bp0 = { x: p2.x, y: p2.y };
  1104. d += 'C' +
  1105. bp1.x + ',' +
  1106. bp1.y + ' ' +
  1107. bp2.x + ',' +
  1108. bp2.y + ' ' +
  1109. p2.x + ',' +
  1110. p2.y + ' ';
  1111. }
  1112. return d;
  1113. };
  1114. /**
  1115. * This uses either the chordal or centripetal parameterization of the catmull-rom algorithm.
  1116. * By default, the centripetal parameterization is used because this gives the nicest results.
  1117. * These parameterizations are relatively heavy because the distance between 4 points have to be calculated.
  1118. *
  1119. * One optimization can be used to reuse distances since this is a sliding window approach.
  1120. * @param data
  1121. * @returns {string}
  1122. * @private
  1123. */
  1124. LineGraph.prototype._catmullRom = function(data, group) {
  1125. var alpha = group.options.catmullRom.alpha;
  1126. if (alpha == 0 || alpha === undefined) {
  1127. return this._catmullRomUniform(data);
  1128. }
  1129. else {
  1130. var p0, p1, p2, p3, bp1, bp2, d1,d2,d3, A, B, N, M;
  1131. var d3powA, d2powA, d3pow2A, d2pow2A, d1pow2A, d1powA;
  1132. var d = Math.round(data[0].x) + ',' + Math.round(data[0].y) + ' ';
  1133. var length = data.length;
  1134. for (var i = 0; i < length - 1; i++) {
  1135. p0 = (i == 0) ? data[0] : data[i-1];
  1136. p1 = data[i];
  1137. p2 = data[i+1];
  1138. p3 = (i + 2 < length) ? data[i+2] : p2;
  1139. d1 = Math.sqrt(Math.pow(p0.x - p1.x,2) + Math.pow(p0.y - p1.y,2));
  1140. d2 = Math.sqrt(Math.pow(p1.x - p2.x,2) + Math.pow(p1.y - p2.y,2));
  1141. d3 = Math.sqrt(Math.pow(p2.x - p3.x,2) + Math.pow(p2.y - p3.y,2));
  1142. // Catmull-Rom to Cubic Bezier conversion matrix
  1143. //
  1144. // A = 2d1^2a + 3d1^a * d2^a + d3^2a
  1145. // B = 2d3^2a + 3d3^a * d2^a + d2^2a
  1146. //
  1147. // [ 0 1 0 0 ]
  1148. // [ -d2^2a/N A/N d1^2a/N 0 ]
  1149. // [ 0 d3^2a/M B/M -d2^2a/M ]
  1150. // [ 0 0 1 0 ]
  1151. // [ 0 1 0 0 ]
  1152. // [ -d2pow2a/N A/N d1pow2a/N 0 ]
  1153. // [ 0 d3pow2a/M B/M -d2pow2a/M ]
  1154. // [ 0 0 1 0 ]
  1155. d3powA = Math.pow(d3, alpha);
  1156. d3pow2A = Math.pow(d3,2*alpha);
  1157. d2powA = Math.pow(d2, alpha);
  1158. d2pow2A = Math.pow(d2,2*alpha);
  1159. d1powA = Math.pow(d1, alpha);
  1160. d1pow2A = Math.pow(d1,2*alpha);
  1161. A = 2*d1pow2A + 3*d1powA * d2powA + d2pow2A;
  1162. B = 2*d3pow2A + 3*d3powA * d2powA + d2pow2A;
  1163. N = 3*d1powA * (d1powA + d2powA);
  1164. if (N > 0) {N = 1 / N;}
  1165. M = 3*d3powA * (d3powA + d2powA);
  1166. if (M > 0) {M = 1 / M;}
  1167. bp1 = { x: ((-d2pow2A * p0.x + A*p1.x + d1pow2A * p2.x) * N),
  1168. y: ((-d2pow2A * p0.y + A*p1.y + d1pow2A * p2.y) * N)};
  1169. bp2 = { x: (( d3pow2A * p1.x + B*p2.x - d2pow2A * p3.x) * M),
  1170. y: (( d3pow2A * p1.y + B*p2.y - d2pow2A * p3.y) * M)};
  1171. if (bp1.x == 0 && bp1.y == 0) {bp1 = p1;}
  1172. if (bp2.x == 0 && bp2.y == 0) {bp2 = p2;}
  1173. d += 'C' +
  1174. bp1.x + ',' +
  1175. bp1.y + ' ' +
  1176. bp2.x + ',' +
  1177. bp2.y + ' ' +
  1178. p2.x + ',' +
  1179. p2.y + ' ';
  1180. }
  1181. return d;
  1182. }
  1183. };
  1184. /**
  1185. * this generates the SVG path for a linear drawing between datapoints.
  1186. * @param data
  1187. * @returns {string}
  1188. * @private
  1189. */
  1190. LineGraph.prototype._linear = function(data) {
  1191. // linear
  1192. var d = '';
  1193. for (var i = 0; i < data.length; i++) {
  1194. if (i == 0) {
  1195. d += data[i].x + ',' + data[i].y;
  1196. }
  1197. else {
  1198. d += ' ' + data[i].x + ',' + data[i].y;
  1199. }
  1200. }
  1201. return d;
  1202. };
  1203. module.exports = LineGraph;