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.

1071 lines
32 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
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. align: 'center' // left, center, right
  34. },
  35. catmullRom: {
  36. enabled: true,
  37. parametrization: 'centripetal', // uniform (alpha = 0.0), chordal (alpha = 1.0), centripetal (alpha = 0.5)
  38. alpha: 0.5
  39. },
  40. drawPoints: {
  41. enabled: true,
  42. size: 6,
  43. style: 'square' // square, circle
  44. },
  45. dataAxis: {
  46. showMinorLabels: true,
  47. showMajorLabels: true,
  48. icons: false,
  49. width: '40px',
  50. visible: true
  51. },
  52. legend: {
  53. enabled: false,
  54. icons: true,
  55. left: {
  56. visible: true,
  57. position: 'top-left' // top/bottom - left,right
  58. },
  59. right: {
  60. visible: true,
  61. position: 'top-right' // top/bottom - left,right
  62. }
  63. }
  64. };
  65. // options is shared by this ItemSet and all its items
  66. this.options = util.extend({}, this.defaultOptions);
  67. this.dom = {};
  68. this.props = {};
  69. this.hammer = null;
  70. this.groups = {};
  71. var me = this;
  72. this.itemsData = null; // DataSet
  73. this.groupsData = null; // DataSet
  74. // listeners for the DataSet of the items
  75. this.itemListeners = {
  76. 'add': function (event, params, senderId) {
  77. me._onAdd(params.items);
  78. },
  79. 'update': function (event, params, senderId) {
  80. me._onUpdate(params.items);
  81. },
  82. 'remove': function (event, params, senderId) {
  83. me._onRemove(params.items);
  84. }
  85. };
  86. // listeners for the DataSet of the groups
  87. this.groupListeners = {
  88. 'add': function (event, params, senderId) {
  89. me._onAddGroups(params.items);
  90. },
  91. 'update': function (event, params, senderId) {
  92. me._onUpdateGroups(params.items);
  93. },
  94. 'remove': function (event, params, senderId) {
  95. me._onRemoveGroups(params.items);
  96. }
  97. };
  98. this.items = {}; // object with an Item for every data item
  99. this.selection = []; // list with the ids of all selected nodes
  100. this.lastStart = this.body.range.start;
  101. this.touchParams = {}; // stores properties while dragging
  102. this.svgElements = {};
  103. this.setOptions(options);
  104. this.groupsUsingDefaultStyles = [0];
  105. this.body.emitter.on("rangechange",function() {
  106. if (me.lastStart != 0) {
  107. var offset = me.body.range.start - me.lastStart;
  108. var range = me.body.range.end - me.body.range.start;
  109. if (me.width != 0) {
  110. var rangePerPixelInv = me.width/range;
  111. var xOffset = offset * rangePerPixelInv;
  112. me.svg.style.left = (-me.width - xOffset) + "px";
  113. }
  114. }
  115. });
  116. this.body.emitter.on("rangechanged", function() {
  117. me.lastStart = me.body.range.start;
  118. me.svg.style.left = util.option.asSize(-me.width);
  119. me._updateGraph.apply(me);
  120. });
  121. // create the HTML DOM
  122. this._create();
  123. this.body.emitter.emit("change");
  124. }
  125. LineGraph.prototype = new Component();
  126. /**
  127. * Create the HTML DOM for the ItemSet
  128. */
  129. LineGraph.prototype._create = function(){
  130. var frame = document.createElement('div');
  131. frame.className = 'LineGraph';
  132. this.dom.frame = frame;
  133. // create svg element for graph drawing.
  134. this.svg = document.createElementNS('http://www.w3.org/2000/svg',"svg");
  135. this.svg.style.position = "relative";
  136. this.svg.style.height = ('' + this.options.graphHeight).replace("px",'') + 'px';
  137. this.svg.style.display = "block";
  138. frame.appendChild(this.svg);
  139. // data axis
  140. this.options.dataAxis.orientation = 'left';
  141. this.yAxisLeft = new DataAxis(this.body, this.options.dataAxis, this.svg);
  142. this.options.dataAxis.orientation = 'right';
  143. this.yAxisRight = new DataAxis(this.body, this.options.dataAxis, this.svg);
  144. delete this.options.dataAxis.orientation;
  145. // legends
  146. this.legendLeft = new Legend(this.body, this.options.legend, 'left');
  147. this.legendRight = new Legend(this.body, this.options.legend, 'right');
  148. this.show();
  149. };
  150. /**
  151. * set the options of the LineGraph. the mergeOptions is used for subObjects that have an enabled element.
  152. * @param options
  153. */
  154. LineGraph.prototype.setOptions = function(options) {
  155. if (options) {
  156. var fields = ['sampling','defaultGroup','graphHeight','yAxisOrientation','style','barChart','dataAxis','sort'];
  157. util.selectiveDeepExtend(fields, this.options, options);
  158. util.mergeOptions(this.options, options,'catmullRom');
  159. util.mergeOptions(this.options, options,'drawPoints');
  160. util.mergeOptions(this.options, options,'shaded');
  161. util.mergeOptions(this.options, options,'legend');
  162. if (options.catmullRom) {
  163. if (typeof options.catmullRom == 'object') {
  164. if (options.catmullRom.parametrization) {
  165. if (options.catmullRom.parametrization == 'uniform') {
  166. this.options.catmullRom.alpha = 0;
  167. }
  168. else if (options.catmullRom.parametrization == 'chordal') {
  169. this.options.catmullRom.alpha = 1.0;
  170. }
  171. else {
  172. this.options.catmullRom.parametrization = 'centripetal';
  173. this.options.catmullRom.alpha = 0.5;
  174. }
  175. }
  176. }
  177. }
  178. if (this.yAxisLeft) {
  179. if (options.dataAxis !== undefined) {
  180. this.yAxisLeft.setOptions(this.options.dataAxis);
  181. this.yAxisRight.setOptions(this.options.dataAxis);
  182. }
  183. }
  184. if (this.legendLeft) {
  185. if (options.legend !== undefined) {
  186. this.legendLeft.setOptions(this.options.legend);
  187. this.legendRight.setOptions(this.options.legend);
  188. }
  189. }
  190. if (this.groups.hasOwnProperty(UNGROUPED)) {
  191. this.groups[UNGROUPED].setOptions(options);
  192. }
  193. }
  194. if (this.dom.frame) {
  195. this._updateGraph();
  196. }
  197. };
  198. /**
  199. * Hide the component from the DOM
  200. */
  201. LineGraph.prototype.hide = function() {
  202. // remove the frame containing the items
  203. if (this.dom.frame.parentNode) {
  204. this.dom.frame.parentNode.removeChild(this.dom.frame);
  205. }
  206. };
  207. /**
  208. * Show the component in the DOM (when not already visible).
  209. * @return {Boolean} changed
  210. */
  211. LineGraph.prototype.show = function() {
  212. // show frame containing the items
  213. if (!this.dom.frame.parentNode) {
  214. this.body.dom.center.appendChild(this.dom.frame);
  215. }
  216. };
  217. /**
  218. * Set items
  219. * @param {vis.DataSet | null} items
  220. */
  221. LineGraph.prototype.setItems = function(items) {
  222. var me = this,
  223. ids,
  224. oldItemsData = this.itemsData;
  225. // replace the dataset
  226. if (!items) {
  227. this.itemsData = null;
  228. }
  229. else if (items instanceof DataSet || items instanceof DataView) {
  230. this.itemsData = items;
  231. }
  232. else {
  233. throw new TypeError('Data must be an instance of DataSet or DataView');
  234. }
  235. if (oldItemsData) {
  236. // unsubscribe from old dataset
  237. util.forEach(this.itemListeners, function (callback, event) {
  238. oldItemsData.off(event, callback);
  239. });
  240. // remove all drawn items
  241. ids = oldItemsData.getIds();
  242. this._onRemove(ids);
  243. }
  244. if (this.itemsData) {
  245. // subscribe to new dataset
  246. var id = this.id;
  247. util.forEach(this.itemListeners, function (callback, event) {
  248. me.itemsData.on(event, callback, id);
  249. });
  250. // add all new items
  251. ids = this.itemsData.getIds();
  252. this._onAdd(ids);
  253. }
  254. this._updateUngrouped();
  255. this._updateGraph();
  256. this.redraw();
  257. };
  258. /**
  259. * Set groups
  260. * @param {vis.DataSet} groups
  261. */
  262. LineGraph.prototype.setGroups = function(groups) {
  263. var me = this,
  264. ids;
  265. // unsubscribe from current dataset
  266. if (this.groupsData) {
  267. util.forEach(this.groupListeners, function (callback, event) {
  268. me.groupsData.unsubscribe(event, callback);
  269. });
  270. // remove all drawn groups
  271. ids = this.groupsData.getIds();
  272. this.groupsData = null;
  273. this._onRemoveGroups(ids); // note: this will cause a redraw
  274. }
  275. // replace the dataset
  276. if (!groups) {
  277. this.groupsData = null;
  278. }
  279. else if (groups instanceof DataSet || groups instanceof DataView) {
  280. this.groupsData = groups;
  281. }
  282. else {
  283. throw new TypeError('Data must be an instance of DataSet or DataView');
  284. }
  285. if (this.groupsData) {
  286. // subscribe to new dataset
  287. var id = this.id;
  288. util.forEach(this.groupListeners, function (callback, event) {
  289. me.groupsData.on(event, callback, id);
  290. });
  291. // draw all ms
  292. ids = this.groupsData.getIds();
  293. this._onAddGroups(ids);
  294. }
  295. this._onUpdate();
  296. };
  297. LineGraph.prototype._onUpdate = function(ids) {
  298. this._updateUngrouped();
  299. this._updateAllGroupData();
  300. this._updateGraph();
  301. this.redraw();
  302. };
  303. LineGraph.prototype._onAdd = function (ids) {this._onUpdate(ids);};
  304. LineGraph.prototype._onRemove = function (ids) {this._onUpdate(ids);};
  305. LineGraph.prototype._onUpdateGroups = function (groupIds) {
  306. for (var i = 0; i < groupIds.length; i++) {
  307. var group = this.groupsData.get(groupIds[i]);
  308. this._updateGroup(group, groupIds[i]);
  309. }
  310. this._updateGraph();
  311. this.redraw();
  312. };
  313. LineGraph.prototype._onAddGroups = function (groupIds) {this._onUpdateGroups(groupIds);};
  314. LineGraph.prototype._onRemoveGroups = function (groupIds) {
  315. for (var i = 0; i < groupIds.length; i++) {
  316. if (!this.groups.hasOwnProperty(groupIds[i])) {
  317. if (this.groups[groupIds[i]].options.yAxisOrientation == 'right') {
  318. this.yAxisRight.removeGroup(groupIds[i]);
  319. this.legendRight.removeGroup(groupIds[i]);
  320. this.legendRight.redraw();
  321. }
  322. else {
  323. this.yAxisLeft.removeGroup(groupIds[i]);
  324. this.legendLeft.removeGroup(groupIds[i]);
  325. this.legendLeft.redraw();
  326. }
  327. delete this.groups[groupIds[i]];
  328. }
  329. }
  330. this._updateUngrouped();
  331. this._updateGraph();
  332. this.redraw();
  333. };
  334. /**
  335. * update a group object
  336. *
  337. * @param group
  338. * @param groupId
  339. * @private
  340. */
  341. LineGraph.prototype._updateGroup = function (group, groupId) {
  342. if (!this.groups.hasOwnProperty(groupId)) {
  343. this.groups[groupId] = new GraphGroup(group, groupId, this.options, this.groupsUsingDefaultStyles);
  344. if (this.groups[groupId].options.yAxisOrientation == 'right') {
  345. this.yAxisRight.addGroup(groupId, this.groups[groupId]);
  346. this.legendRight.addGroup(groupId, this.groups[groupId]);
  347. }
  348. else {
  349. this.yAxisLeft.addGroup(groupId, this.groups[groupId]);
  350. this.legendLeft.addGroup(groupId, this.groups[groupId]);
  351. }
  352. }
  353. else {
  354. this.groups[groupId].update(group);
  355. if (this.groups[groupId].options.yAxisOrientation == 'right') {
  356. this.yAxisRight.updateGroup(groupId, this.groups[groupId]);
  357. this.legendRight.updateGroup(groupId, this.groups[groupId]);
  358. }
  359. else {
  360. this.yAxisLeft.updateGroup(groupId, this.groups[groupId]);
  361. this.legendLeft.updateGroup(groupId, this.groups[groupId]);
  362. }
  363. }
  364. this.legendLeft.redraw();
  365. this.legendRight.redraw();
  366. };
  367. LineGraph.prototype._updateAllGroupData = function () {
  368. if (this.itemsData != null) {
  369. // ~450 ms @ 500k
  370. var groupsContent = {};
  371. for (var groupId in this.groups) {
  372. if (this.groups.hasOwnProperty(groupId)) {
  373. groupsContent[groupId] = [];
  374. }
  375. }
  376. for (var itemId in this.itemsData._data) {
  377. if (this.itemsData._data.hasOwnProperty(itemId)) {
  378. var item = this.itemsData._data[itemId];
  379. item.x = util.convert(item.x,"Date");
  380. groupsContent[item.group].push(item);
  381. }
  382. }
  383. for (var groupId in this.groups) {
  384. if (this.groups.hasOwnProperty(groupId)) {
  385. this.groups[groupId].setItems(groupsContent[groupId]);
  386. }
  387. }
  388. // // ~4500ms @ 500k
  389. // for (var groupId in this.groups) {
  390. // if (this.groups.hasOwnProperty(groupId)) {
  391. // this.groups[groupId].setItems(this.itemsData.get({filter:
  392. // function (item) {
  393. // return (item.group == groupId);
  394. // }, type:{x:"Date"}}
  395. // ));
  396. // }
  397. // }
  398. }
  399. };
  400. /**
  401. * Create or delete the group holding all ungrouped items. This group is used when
  402. * there are no groups specified. This anonymous group is called 'graph'.
  403. * @protected
  404. */
  405. LineGraph.prototype._updateUngrouped = function() {
  406. if (this.itemsData != null) {
  407. // var t0 = new Date();
  408. var group = {id: UNGROUPED, content: this.options.defaultGroup};
  409. this._updateGroup(group, UNGROUPED);
  410. var ungroupedCounter = 0;
  411. if (this.itemsData) {
  412. for (var itemId in this.itemsData._data) {
  413. if (this.itemsData._data.hasOwnProperty(itemId)) {
  414. var item = this.itemsData._data[itemId];
  415. if (item != undefined) {
  416. if (item.hasOwnProperty('group')) {
  417. if (item.group === undefined) {
  418. item.group = UNGROUPED;
  419. }
  420. }
  421. else {
  422. item.group = UNGROUPED;
  423. }
  424. ungroupedCounter = item.group == UNGROUPED ? ungroupedCounter + 1 : ungroupedCounter;
  425. }
  426. }
  427. }
  428. }
  429. // much much slower
  430. // var datapoints = this.itemsData.get({
  431. // filter: function (item) {return item.group === undefined;},
  432. // showInternalIds:true
  433. // });
  434. // if (datapoints.length > 0) {
  435. // var updateQuery = [];
  436. // for (var i = 0; i < datapoints.length; i++) {
  437. // updateQuery.push({id:datapoints[i].id, group: UNGROUPED});
  438. // }
  439. // this.itemsData.update(updateQuery, true);
  440. // }
  441. // var t1 = new Date();
  442. // var pointInUNGROUPED = this.itemsData.get({filter: function (item) {return item.group == UNGROUPED;}});
  443. if (ungroupedCounter == 0) {
  444. delete this.groups[UNGROUPED];
  445. this.legendLeft.removeGroup(UNGROUPED);
  446. this.legendRight.removeGroup(UNGROUPED);
  447. this.yAxisLeft.removeGroup(UNGROUPED);
  448. this.yAxisRight.removeGroup(UNGROUPED);
  449. }
  450. // console.log("getting amount ungrouped",new Date() - t1);
  451. // console.log("putting in ungrouped",new Date() - t0);
  452. }
  453. else {
  454. delete this.groups[UNGROUPED];
  455. this.legendLeft.removeGroup(UNGROUPED);
  456. this.legendRight.removeGroup(UNGROUPED);
  457. this.yAxisLeft.removeGroup(UNGROUPED);
  458. this.yAxisRight.removeGroup(UNGROUPED);
  459. }
  460. this.legendLeft.redraw();
  461. this.legendRight.redraw();
  462. };
  463. /**
  464. * Redraw the component, mandatory function
  465. * @return {boolean} Returns true if the component is resized
  466. */
  467. LineGraph.prototype.redraw = function() {
  468. var resized = false;
  469. this.svg.style.height = ('' + this.options.graphHeight).replace('px','') + 'px';
  470. if (this.lastWidth === undefined && this.width || this.lastWidth != this.width) {
  471. resized = true;
  472. }
  473. // check if this component is resized
  474. resized = this._isResized() || resized;
  475. // check whether zoomed (in that case we need to re-stack everything)
  476. var visibleInterval = this.body.range.end - this.body.range.start;
  477. var zoomed = (visibleInterval != this.lastVisibleInterval) || (this.width != this.lastWidth);
  478. this.lastVisibleInterval = visibleInterval;
  479. this.lastWidth = this.width;
  480. // calculate actual size and position
  481. this.width = this.dom.frame.offsetWidth;
  482. // the svg element is three times as big as the width, this allows for fully dragging left and right
  483. // without reloading the graph. the controls for this are bound to events in the constructor
  484. if (resized == true) {
  485. this.svg.style.width = util.option.asSize(3*this.width);
  486. this.svg.style.left = util.option.asSize(-this.width);
  487. }
  488. if (zoomed == true) {
  489. this._updateGraph();
  490. }
  491. this.legendLeft.redraw();
  492. this.legendRight.redraw();
  493. return resized;
  494. };
  495. /**
  496. * Update and redraw the graph.
  497. *
  498. */
  499. LineGraph.prototype._updateGraph = function () {
  500. // reset the svg elements
  501. DOMutil.prepareElements(this.svgElements);
  502. // // very slow...
  503. // groupData = group.itemsData.get({filter:
  504. // function (item) {
  505. // return (item.x > minDate && item.x < maxDate);
  506. // }}
  507. // );
  508. if (this.width != 0 && this.itemsData != null) {
  509. var group, groupData, preprocessedGroup, i;
  510. var preprocessedGroupData = [];
  511. var processedGroupData = [];
  512. var groupRanges = [];
  513. var changeCalled = false;
  514. // getting group Ids
  515. var groupIds = [];
  516. for (var groupId in this.groups) {
  517. if (this.groups.hasOwnProperty(groupId)) {
  518. groupIds.push(groupId);
  519. }
  520. }
  521. // this is the range of the SVG canvas
  522. var minDate = this.body.util.toGlobalTime(- this.body.domProps.root.width);
  523. var maxDate = this.body.util.toGlobalTime(2 * this.body.domProps.root.width);
  524. // first select and preprocess the data from the datasets.
  525. // the groups have their preselection of data, we now loop over this data to see
  526. // what data we need to draw. Sorted data is much faster.
  527. // more optimization is possible by doing the sampling before and using the binary search
  528. // to find the end date to determine the increment.
  529. if (groupIds.length > 0) {
  530. for (i = 0; i < groupIds.length; i++) {
  531. group = this.groups[groupIds[i]];
  532. groupData = [];
  533. // optimization for sorted data
  534. if (group.options.sort == true) {
  535. var guess = Math.max(0,util.binarySearchGeneric(group.itemsData, minDate, 'x', 'before'));
  536. for (var j = guess; j < group.itemsData.length; j++) {
  537. var item = group.itemsData[j];
  538. if (item !== undefined) {
  539. if (item.x > maxDate) {
  540. groupData.push(item);
  541. break;
  542. }
  543. else {
  544. groupData.push(item);
  545. }
  546. }
  547. }
  548. }
  549. else {
  550. for (var j = 0; j < group.itemsData.length; j++) {
  551. var item = group.itemsData[j];
  552. if (item !== undefined) {
  553. if (item.x > minDate && item.x < maxDate) {
  554. groupData.push(item);
  555. }
  556. }
  557. }
  558. }
  559. // preprocess, split into ranges and data
  560. preprocessedGroup = this._preprocessData(groupData, group);
  561. groupRanges.push({min: preprocessedGroup.min, max: preprocessedGroup.max});
  562. preprocessedGroupData.push(preprocessedGroup.data);
  563. }
  564. // update the Y axis first, we use this data to draw at the correct Y points
  565. // changeCalled is required to clean the SVG on a change emit.
  566. changeCalled = this._updateYAxis(groupIds, groupRanges);
  567. if (changeCalled == true) {
  568. DOMutil.cleanupElements(this.svgElements);
  569. this.body.emitter.emit("change");
  570. return;
  571. }
  572. // with the yAxis scaled correctly, use this to get the Y values of the points.
  573. for (i = 0; i < groupIds.length; i++) {
  574. group = this.groups[groupIds[i]];
  575. processedGroupData.push(this._convertYvalues(preprocessedGroupData[i],group))
  576. }
  577. // draw the groups
  578. for (i = 0; i < groupIds.length; i++) {
  579. group = this.groups[groupIds[i]];
  580. if (group.options.style == 'line') {
  581. this._drawLineGraph(processedGroupData[i], group);
  582. }
  583. else {
  584. this._drawBarGraph (processedGroupData[i], group);
  585. }
  586. }
  587. }
  588. }
  589. // cleanup unused svg elements
  590. DOMutil.cleanupElements(this.svgElements);
  591. };
  592. /**
  593. * this sets the Y ranges for the Y axis. It also determines which of the axis should be shown or hidden.
  594. * @param {array} groupIds
  595. * @private
  596. */
  597. LineGraph.prototype._updateYAxis = function (groupIds, groupRanges) {
  598. var changeCalled = false;
  599. var yAxisLeftUsed = false;
  600. var yAxisRightUsed = false;
  601. var minLeft = 1e9, minRight = 1e9, maxLeft = -1e9, maxRight = -1e9, minVal, maxVal;
  602. var orientation = 'left';
  603. // if groups are present
  604. if (groupIds.length > 0) {
  605. for (var i = 0; i < groupIds.length; i++) {
  606. orientation = 'left';
  607. var group = this.groups[groupIds[i]];
  608. if (group.options.yAxisOrientation == 'right') {
  609. orientation = 'right';
  610. }
  611. minVal = groupRanges[i].min;
  612. maxVal = groupRanges[i].max;
  613. if (orientation == 'left') {
  614. yAxisLeftUsed = true;
  615. minLeft = minLeft > minVal ? minVal : minLeft;
  616. maxLeft = maxLeft < maxVal ? maxVal : maxLeft;
  617. }
  618. else {
  619. yAxisRightUsed = true;
  620. minRight = minRight > minVal ? minVal : minRight;
  621. maxRight = maxRight < maxVal ? maxVal : maxRight;
  622. }
  623. }
  624. if (yAxisLeftUsed == true) {
  625. this.yAxisLeft.setRange(minLeft, maxLeft);
  626. }
  627. if (yAxisRightUsed == true) {
  628. this.yAxisRight.setRange(minRight, maxRight);
  629. }
  630. }
  631. changeCalled = this._toggleAxisVisiblity(yAxisLeftUsed , this.yAxisLeft) || changeCalled;
  632. changeCalled = this._toggleAxisVisiblity(yAxisRightUsed, this.yAxisRight) || changeCalled;
  633. if (yAxisRightUsed == true && yAxisLeftUsed == true) {
  634. this.yAxisLeft.drawIcons = true;
  635. this.yAxisRight.drawIcons = true;
  636. }
  637. else {
  638. this.yAxisLeft.drawIcons = false;
  639. this.yAxisRight.drawIcons = false;
  640. }
  641. this.yAxisRight.master = !yAxisLeftUsed;
  642. if (this.yAxisRight.master == false) {
  643. if (yAxisRightUsed == true) {
  644. this.yAxisLeft.lineOffset = this.yAxisRight.width;
  645. }
  646. changeCalled = this.yAxisLeft.redraw() || changeCalled;
  647. this.yAxisRight.stepPixelsForced = this.yAxisLeft.stepPixels;
  648. changeCalled = this.yAxisRight.redraw() || changeCalled;
  649. }
  650. else {
  651. changeCalled = this.yAxisRight.redraw() || changeCalled;
  652. }
  653. return changeCalled;
  654. };
  655. /**
  656. * This shows or hides the Y axis if needed. If there is a change, the changed event is emitted by the updateYAxis function
  657. *
  658. * @param {boolean} axisUsed
  659. * @returns {boolean}
  660. * @private
  661. * @param axis
  662. */
  663. LineGraph.prototype._toggleAxisVisiblity = function (axisUsed, axis) {
  664. var changed = false;
  665. if (axisUsed == false) {
  666. if (axis.dom.frame.parentNode) {
  667. axis.hide();
  668. changed = true;
  669. }
  670. }
  671. else {
  672. if (!axis.dom.frame.parentNode) {
  673. axis.show();
  674. changed = true;
  675. }
  676. }
  677. return changed;
  678. };
  679. /**
  680. * draw a bar graph
  681. * @param datapoints
  682. * @param group
  683. */
  684. LineGraph.prototype._drawBarGraph = function (dataset, group) {
  685. if (dataset != null) {
  686. if (dataset.length > 0) {
  687. var coreDistance;
  688. var minWidth = 0.1 * group.options.barChart.width;
  689. var offset = 0;
  690. var width = group.options.barChart.width;
  691. if (group.options.barChart.align == 'left') {offset -= 0.5*width;}
  692. else if (group.options.barChart.align == 'right') {offset += 0.5*width;}
  693. for (var i = 0; i < dataset.length; i++) {
  694. // dynammically downscale the width so there is no overlap up to 1/10th the original width
  695. if (i+1 < dataset.length) {coreDistance = Math.abs(dataset[i+1].x - dataset[i].x);}
  696. if (i > 0) {coreDistance = Math.min(coreDistance,Math.abs(dataset[i-1].x - dataset[i].x));}
  697. if (coreDistance < width) {width = coreDistance < minWidth ? minWidth : coreDistance;}
  698. DOMutil.drawBar(dataset[i].x + offset, dataset[i].y, width, group.zeroPosition - dataset[i].y, group.className + ' bar', this.svgElements, this.svg);
  699. }
  700. // draw points
  701. if (group.options.drawPoints.enabled == true) {
  702. this._drawPoints(dataset, group, this.svgElements, this.svg, offset);
  703. }
  704. }
  705. }
  706. };
  707. /**
  708. * draw a line graph
  709. *
  710. * @param datapoints
  711. * @param group
  712. */
  713. LineGraph.prototype._drawLineGraph = function (dataset, group) {
  714. if (dataset != null) {
  715. if (dataset.length > 0) {
  716. var path, d;
  717. var svgHeight = Number(this.svg.style.height.replace("px",""));
  718. path = DOMutil.getSVGElement('path', this.svgElements, this.svg);
  719. path.setAttributeNS(null, "class", group.className);
  720. // construct path from dataset
  721. if (group.options.catmullRom.enabled == true) {
  722. d = this._catmullRom(dataset, group);
  723. }
  724. else {
  725. d = this._linear(dataset);
  726. }
  727. // append with points for fill and finalize the path
  728. if (group.options.shaded.enabled == true) {
  729. var fillPath = DOMutil.getSVGElement('path',this.svgElements, this.svg);
  730. var dFill;
  731. if (group.options.shaded.orientation == 'top') {
  732. dFill = "M" + dataset[0].x + "," + 0 + " " + d + "L" + dataset[dataset.length - 1].x + "," + 0;
  733. }
  734. else {
  735. dFill = "M" + dataset[0].x + "," + svgHeight + " " + d + "L" + dataset[dataset.length - 1].x + "," + svgHeight;
  736. }
  737. fillPath.setAttributeNS(null, "class", group.className + " fill");
  738. fillPath.setAttributeNS(null, "d", dFill);
  739. }
  740. // copy properties to path for drawing.
  741. path.setAttributeNS(null, "d", "M" + d);
  742. // draw points
  743. if (group.options.drawPoints.enabled == true) {
  744. this._drawPoints(dataset, group, this.svgElements, this.svg);
  745. }
  746. }
  747. }
  748. };
  749. /**
  750. * draw the data points
  751. *
  752. * @param dataset
  753. * @param JSONcontainer
  754. * @param svg
  755. * @param group
  756. */
  757. LineGraph.prototype._drawPoints = function (dataset, group, JSONcontainer, svg, offset) {
  758. if (offset === undefined) {offset = 0;}
  759. for (var i = 0; i < dataset.length; i++) {
  760. DOMutil.drawPoint(dataset[i].x + offset, dataset[i].y, group, JSONcontainer, svg);
  761. }
  762. };
  763. /**
  764. * This uses the DataAxis object to generate the correct X coordinate on the SVG window. It uses the
  765. * util function toScreen to get the x coordinate from the timestamp. It also pre-filters the data and get the minMax ranges for
  766. * the yAxis.
  767. *
  768. * @param datapoints
  769. * @returns {Array}
  770. * @private
  771. */
  772. LineGraph.prototype._preprocessData = function (datapoints, group) {
  773. var extractedData = [];
  774. var xValue, yValue;
  775. var toScreen = this.body.util.toScreen;
  776. var increment = 1;
  777. var amountOfPoints = datapoints.length;
  778. var yMin = datapoints[0].y;
  779. var yMax = datapoints[0].y;
  780. // the global screen is used because changing the width of the yAxis may affect the increment, resulting in an endless loop
  781. // of width changing of the yAxis.
  782. if (group.options.sampling == true) {
  783. var xDistance = this.body.util.toGlobalScreen(datapoints[datapoints.length-1].x) - this.body.util.toGlobalScreen(datapoints[0].x);
  784. var pointsPerPixel = amountOfPoints/xDistance;
  785. increment = Math.min(Math.ceil(0.2 * amountOfPoints), Math.max(1,Math.round(pointsPerPixel)));
  786. }
  787. for (var i = 0; i < amountOfPoints; i += increment) {
  788. xValue = toScreen(datapoints[i].x) + this.width - 1;
  789. yValue = datapoints[i].y;
  790. extractedData.push({x: xValue, y: yValue});
  791. yMin = yMin > yValue ? yValue : yMin;
  792. yMax = yMax < yValue ? yValue : yMax;
  793. }
  794. // extractedData.sort(function (a,b) {return a.x - b.x;});
  795. return {min: yMin, max: yMax, data: extractedData};
  796. };
  797. /**
  798. * This uses the DataAxis object to generate the correct Y coordinate on the SVG window. It uses the
  799. * util function toScreen to get the x coordinate from the timestamp.
  800. *
  801. * @param datapoints
  802. * @param options
  803. * @returns {Array}
  804. * @private
  805. */
  806. LineGraph.prototype._convertYvalues = function (datapoints, group) {
  807. var extractedData = [];
  808. var xValue, yValue;
  809. var axis = this.yAxisLeft;
  810. var svgHeight = Number(this.svg.style.height.replace("px",""));
  811. if (group.options.yAxisOrientation == 'right') {
  812. axis = this.yAxisRight;
  813. }
  814. for (var i = 0; i < datapoints.length; i++) {
  815. xValue = datapoints[i].x;
  816. yValue = Math.round(axis.convertValue(datapoints[i].y));
  817. extractedData.push({x: xValue, y: yValue});
  818. }
  819. group.setZeroPosition(Math.min(svgHeight, axis.convertValue(0)));
  820. // extractedData.sort(function (a,b) {return a.x - b.x;});
  821. return extractedData;
  822. };
  823. /**
  824. * This uses an uniform parametrization of the CatmullRom algorithm:
  825. * "On the Parameterization of Catmull-Rom Curves" by Cem Yuksel et al.
  826. * @param data
  827. * @returns {string}
  828. * @private
  829. */
  830. LineGraph.prototype._catmullRomUniform = function(data) {
  831. // catmull rom
  832. var p0, p1, p2, p3, bp1, bp2;
  833. var d = Math.round(data[0].x) + "," + Math.round(data[0].y) + " ";
  834. var normalization = 1/6;
  835. var length = data.length;
  836. for (var i = 0; i < length - 1; i++) {
  837. p0 = (i == 0) ? data[0] : data[i-1];
  838. p1 = data[i];
  839. p2 = data[i+1];
  840. p3 = (i + 2 < length) ? data[i+2] : p2;
  841. // Catmull-Rom to Cubic Bezier conversion matrix
  842. // 0 1 0 0
  843. // -1/6 1 1/6 0
  844. // 0 1/6 1 -1/6
  845. // 0 0 1 0
  846. // bp0 = { x: p1.x, y: p1.y };
  847. bp1 = { x: ((-p0.x + 6*p1.x + p2.x) *normalization), y: ((-p0.y + 6*p1.y + p2.y) *normalization)};
  848. bp2 = { x: (( p1.x + 6*p2.x - p3.x) *normalization), y: (( p1.y + 6*p2.y - p3.y) *normalization)};
  849. // bp0 = { x: p2.x, y: p2.y };
  850. d += "C" +
  851. bp1.x + "," +
  852. bp1.y + " " +
  853. bp2.x + "," +
  854. bp2.y + " " +
  855. p2.x + "," +
  856. p2.y + " ";
  857. }
  858. return d;
  859. };
  860. /**
  861. * This uses either the chordal or centripetal parameterization of the catmull-rom algorithm.
  862. * By default, the centripetal parameterization is used because this gives the nicest results.
  863. * These parameterizations are relatively heavy because the distance between 4 points have to be calculated.
  864. *
  865. * One optimization can be used to reuse distances since this is a sliding window approach.
  866. * @param data
  867. * @returns {string}
  868. * @private
  869. */
  870. LineGraph.prototype._catmullRom = function(data, group) {
  871. var alpha = group.options.catmullRom.alpha;
  872. if (alpha == 0 || alpha === undefined) {
  873. return this._catmullRomUniform(data);
  874. }
  875. else {
  876. var p0, p1, p2, p3, bp1, bp2, d1,d2,d3, A, B, N, M;
  877. var d3powA, d2powA, d3pow2A, d2pow2A, d1pow2A, d1powA;
  878. var d = Math.round(data[0].x) + "," + Math.round(data[0].y) + " ";
  879. var length = data.length;
  880. for (var i = 0; i < length - 1; i++) {
  881. p0 = (i == 0) ? data[0] : data[i-1];
  882. p1 = data[i];
  883. p2 = data[i+1];
  884. p3 = (i + 2 < length) ? data[i+2] : p2;
  885. d1 = Math.sqrt(Math.pow(p0.x - p1.x,2) + Math.pow(p0.y - p1.y,2));
  886. d2 = Math.sqrt(Math.pow(p1.x - p2.x,2) + Math.pow(p1.y - p2.y,2));
  887. d3 = Math.sqrt(Math.pow(p2.x - p3.x,2) + Math.pow(p2.y - p3.y,2));
  888. // Catmull-Rom to Cubic Bezier conversion matrix
  889. //
  890. // A = 2d1^2a + 3d1^a * d2^a + d3^2a
  891. // B = 2d3^2a + 3d3^a * d2^a + d2^2a
  892. //
  893. // [ 0 1 0 0 ]
  894. // [ -d2^2a/N A/N d1^2a/N 0 ]
  895. // [ 0 d3^2a/M B/M -d2^2a/M ]
  896. // [ 0 0 1 0 ]
  897. // [ 0 1 0 0 ]
  898. // [ -d2pow2a/N A/N d1pow2a/N 0 ]
  899. // [ 0 d3pow2a/M B/M -d2pow2a/M ]
  900. // [ 0 0 1 0 ]
  901. d3powA = Math.pow(d3, alpha);
  902. d3pow2A = Math.pow(d3,2*alpha);
  903. d2powA = Math.pow(d2, alpha);
  904. d2pow2A = Math.pow(d2,2*alpha);
  905. d1powA = Math.pow(d1, alpha);
  906. d1pow2A = Math.pow(d1,2*alpha);
  907. A = 2*d1pow2A + 3*d1powA * d2powA + d2pow2A;
  908. B = 2*d3pow2A + 3*d3powA * d2powA + d2pow2A;
  909. N = 3*d1powA * (d1powA + d2powA);
  910. if (N > 0) {N = 1 / N;}
  911. M = 3*d3powA * (d3powA + d2powA);
  912. if (M > 0) {M = 1 / M;}
  913. bp1 = { x: ((-d2pow2A * p0.x + A*p1.x + d1pow2A * p2.x) * N),
  914. y: ((-d2pow2A * p0.y + A*p1.y + d1pow2A * p2.y) * N)};
  915. bp2 = { x: (( d3pow2A * p1.x + B*p2.x - d2pow2A * p3.x) * M),
  916. y: (( d3pow2A * p1.y + B*p2.y - d2pow2A * p3.y) * M)};
  917. if (bp1.x == 0 && bp1.y == 0) {bp1 = p1;}
  918. if (bp2.x == 0 && bp2.y == 0) {bp2 = p2;}
  919. d += "C" +
  920. bp1.x + "," +
  921. bp1.y + " " +
  922. bp2.x + "," +
  923. bp2.y + " " +
  924. p2.x + "," +
  925. p2.y + " ";
  926. }
  927. return d;
  928. }
  929. };
  930. /**
  931. * this generates the SVG path for a linear drawing between datapoints.
  932. * @param data
  933. * @returns {string}
  934. * @private
  935. */
  936. LineGraph.prototype._linear = function(data) {
  937. // linear
  938. var d = "";
  939. for (var i = 0; i < data.length; i++) {
  940. if (i == 0) {
  941. d += data[i].x + "," + data[i].y;
  942. }
  943. else {
  944. d += " " + data[i].x + "," + data[i].y;
  945. }
  946. }
  947. return d;
  948. };
  949. module.exports = LineGraph;