sankey.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394
  1. /**
  2. * DevExtreme (viz/sankey/sankey.js)
  3. * Version: 19.1.16
  4. * Build date: Tue Oct 18 2022
  5. *
  6. * Copyright (c) 2012 - 2022 Developer Express Inc. ALL RIGHTS RESERVED
  7. * Read about DevExtreme licensing here: https://js.devexpress.com/Licensing/
  8. */
  9. "use strict";
  10. var _constants = require("./constants");
  11. var noop = require("../../core/utils/common").noop;
  12. var Node = require("./node_item");
  13. var Link = require("./link_item");
  14. var defaultLayoutBuilder = require("./layout");
  15. var typeUtils = require("../../core/utils/type");
  16. var _isString = typeUtils.isString;
  17. var _isNumber = typeUtils.isNumeric;
  18. function moveLabel(node, labelOptions, availableLabelWidth, rect) {
  19. if (node.label.getBBox().width > availableLabelWidth) {
  20. node.labelText.applyEllipsis(availableLabelWidth)
  21. }
  22. var bBox = node.label.getBBox();
  23. var verticalOffset = labelOptions.verticalOffset;
  24. var horizontalOffset = labelOptions.horizontalOffset;
  25. var labelOffsetY = Math.round(node.rect.y + node.rect.height / 2 - bBox.y - bBox.height / 2) + verticalOffset;
  26. var labelOffsetX = node.rect.x + horizontalOffset + node.rect.width - bBox.x;
  27. if (labelOffsetX + bBox.width >= rect[2] - rect[0]) {
  28. labelOffsetX = node.rect.x - horizontalOffset - bBox.x - bBox.width
  29. }
  30. if (labelOffsetY >= rect[3]) {
  31. labelOffsetY = rect[3]
  32. }
  33. if (labelOffsetY - bBox.height < rect[1]) {
  34. labelOffsetY = node.rect.y - bBox.y + verticalOffset
  35. }
  36. node.labelText.attr({
  37. translateX: labelOffsetX,
  38. translateY: labelOffsetY
  39. })
  40. }
  41. function getConnectedLinks(layout, nodeName, linkType) {
  42. var result = [];
  43. var attrName = "in" === linkType ? "_to" : "_from";
  44. var invertedAttrName = "in" === linkType ? "_from" : "_to";
  45. layout.links.map(function(link) {
  46. return link[attrName]._name === nodeName
  47. }).forEach(function(connected, idx) {
  48. connected && result.push({
  49. index: idx,
  50. weight: layout.links[idx]._weight,
  51. node: layout.links[idx][invertedAttrName]._name
  52. })
  53. });
  54. return result
  55. }
  56. var dxSankey = require("../core/base_widget").inherit({
  57. _rootClass: "dxs-sankey",
  58. _rootClassPrefix: "dxs",
  59. _proxyData: [],
  60. _optionChangesMap: {
  61. dataSource: "DATA_SOURCE",
  62. sortData: "DATA_SOURCE",
  63. alignment: "DATA_SOURCE",
  64. node: "BUILD_LAYOUT",
  65. link: "BUILD_LAYOUT",
  66. palette: "BUILD_LAYOUT",
  67. paletteExtensionMode: "BUILD_LAYOUT"
  68. },
  69. _themeDependentChanges: ["BUILD_LAYOUT"],
  70. _getDefaultSize: function() {
  71. return {
  72. width: 400,
  73. height: 400
  74. }
  75. },
  76. _themeSection: "sankey",
  77. _fontFields: ["label.font"],
  78. _optionChangesOrder: ["DATA_SOURCE"],
  79. _initialChanges: ["DATA_SOURCE"],
  80. _initCore: function() {
  81. this._groupLinks = this._renderer.g().append(this._renderer.root);
  82. this._groupNodes = this._renderer.g().append(this._renderer.root);
  83. this._groupLabels = this._renderer.g().attr({
  84. "class": this._rootClassPrefix + "-labels"
  85. }).append(this._renderer.root);
  86. this._drawLabels = true;
  87. this._nodes = [];
  88. this._links = [];
  89. this._gradients = []
  90. },
  91. _disposeCore: noop,
  92. _applySize: function(rect) {
  93. this._rect = rect.slice();
  94. var adaptiveLayout = this._getOption("adaptiveLayout");
  95. if (adaptiveLayout.keepLabels || this._rect[2] - this._rect[0] > adaptiveLayout.width) {
  96. this._drawLabels = true
  97. } else {
  98. this._drawLabels = false
  99. }
  100. this._change(["BUILD_LAYOUT"]);
  101. return this._rect
  102. },
  103. _eventsMap: {
  104. onNodeHoverChanged: {
  105. name: "nodeHoverChanged"
  106. },
  107. onLinkHoverChanged: {
  108. name: "linkHoverChanged"
  109. }
  110. },
  111. _customChangesOrder: ["BUILD_LAYOUT", "NODES_DRAW", "LINKS_DRAW", "LABELS", "DRAWN"],
  112. _dataSourceChangedHandler: function() {
  113. this._requestChange(["BUILD_LAYOUT"])
  114. },
  115. _change_DRAWN: function() {
  116. this._drawn()
  117. },
  118. _change_DATA_SOURCE: function() {
  119. this._change(["DRAWN"]);
  120. this._updateDataSource()
  121. },
  122. _change_LABELS: function() {
  123. this._applyLabelsAppearance()
  124. },
  125. _change_BUILD_LAYOUT: function() {
  126. this._groupNodes.clear();
  127. this._groupLinks.clear();
  128. this._groupLabels.clear();
  129. this._buildLayout()
  130. },
  131. _change_NODES_DRAW: function() {
  132. var that = this;
  133. var nodes = that._nodes;
  134. nodes.forEach(function(node, index) {
  135. var element = that._renderer.rect().attr(node.rect).append(that._groupNodes);
  136. node.element = element
  137. });
  138. this._applyNodesAppearance()
  139. },
  140. _change_LINKS_DRAW: function() {
  141. var that = this;
  142. var links = that._links;
  143. links.forEach(function(link, index) {
  144. var group = that._renderer.g().attr({
  145. "class": "link",
  146. "data-link-idx": index
  147. }).append(that._groupLinks);
  148. link.overlayElement = that._renderer.path([], "area").attr({
  149. d: link.d
  150. }).append(group);
  151. link.element = that._renderer.path([], "area").attr({
  152. d: link.d
  153. }).append(group)
  154. });
  155. this._applyLinksAppearance()
  156. },
  157. _suspend: function() {
  158. if (!this._applyingChanges) {
  159. this._suspendChanges()
  160. }
  161. },
  162. _resume: function() {
  163. if (!this._applyingChanges) {
  164. this._resumeChanges()
  165. }
  166. },
  167. _showTooltip: noop,
  168. hideTooltip: noop,
  169. clearHover: function() {
  170. this._suspend();
  171. this._nodes.forEach(function(node) {
  172. node.isHovered() && node.hover(false)
  173. });
  174. this._links.forEach(function(link) {
  175. link.isHovered() && link.hover(false);
  176. link.isAdjacentNodeHovered() && link.adjacentNodeHover(false)
  177. });
  178. this._resume()
  179. },
  180. _applyNodesAppearance: function() {
  181. this._nodes.forEach(function(node) {
  182. var state = node.getState();
  183. node.element.smartAttr(node.states[state])
  184. })
  185. },
  186. _applyLinksAppearance: function() {
  187. this._links.forEach(function(link) {
  188. var state = link.getState();
  189. link.element.smartAttr(link.states[state]);
  190. link.overlayElement.smartAttr(link.overlayStates[state])
  191. })
  192. },
  193. _hitTestTargets: function(x, y) {
  194. var that = this;
  195. var data;
  196. this._proxyData.some(function(callback) {
  197. data = callback.call(that, x, y);
  198. if (data) {
  199. return true
  200. }
  201. });
  202. return data
  203. },
  204. _getData: function() {
  205. var that = this;
  206. var data = that._dataSourceItems() || [];
  207. var sourceField = that._getOption("sourceField", true);
  208. var targetField = that._getOption("targetField", true);
  209. var weightField = that._getOption("weightField", true);
  210. var processedData = [];
  211. data.forEach(function(item) {
  212. var hasItemOwnProperty = Object.prototype.hasOwnProperty.bind(item);
  213. if (!hasItemOwnProperty(sourceField)) {
  214. that._incidentOccurred("E2007", sourceField)
  215. } else {
  216. if (!hasItemOwnProperty(targetField)) {
  217. that._incidentOccurred("E2007", targetField)
  218. } else {
  219. if (!hasItemOwnProperty(weightField)) {
  220. that._incidentOccurred("E2007", weightField)
  221. } else {
  222. if (!_isString(item[sourceField])) {
  223. that._incidentOccurred("E2008", sourceField)
  224. } else {
  225. if (!_isString(item[targetField])) {
  226. that._incidentOccurred("E2008", targetField)
  227. } else {
  228. if (!_isNumber(item[weightField]) || item[weightField] <= 0) {
  229. that._incidentOccurred("E2009", weightField)
  230. } else {
  231. processedData.push([item[sourceField], item[targetField], item[weightField]])
  232. }
  233. }
  234. }
  235. }
  236. }
  237. }
  238. });
  239. return processedData
  240. },
  241. _buildLayout: function() {
  242. var _this = this;
  243. var that = this;
  244. var data = that._getData();
  245. var availableRect = this._rect;
  246. var nodeOptions = that._getOption("node");
  247. var sortData = that._getOption("sortData");
  248. var layoutBuilder = that._getOption("layoutBuilder", true) || defaultLayoutBuilder;
  249. var rect = {
  250. x: availableRect[0],
  251. y: availableRect[1],
  252. width: availableRect[2] - availableRect[0],
  253. height: availableRect[3] - availableRect[1]
  254. };
  255. var layout = layoutBuilder.computeLayout(data, sortData, {
  256. availableRect: rect,
  257. nodePadding: nodeOptions.padding,
  258. nodeWidth: nodeOptions.width,
  259. nodeAlign: that._getOption("alignment", true)
  260. }, that._incidentOccurred);
  261. that._layoutMap = layout;
  262. if (!Object.prototype.hasOwnProperty.call(layout, "error")) {
  263. var nodeColors = {};
  264. var nodeIdx = 0;
  265. var linkOptions = that._getOption("link");
  266. var totalNodesNum = layout.nodes.map(function(item) {
  267. return item.length
  268. }).reduce(function(previousValue, currentValue) {
  269. return previousValue + currentValue
  270. }, 0);
  271. var palette = that._themeManager.createPalette(that._getOption("palette", true), {
  272. useHighlight: true,
  273. extensionMode: that._getOption("paletteExtensionMode", true),
  274. count: totalNodesNum
  275. });
  276. that._nodes = [];
  277. that._links = [];
  278. that._gradients.forEach(function(gradient) {
  279. gradient.dispose()
  280. });
  281. that._gradients = [];
  282. that._shadowFilter && that._shadowFilter.dispose();
  283. layout.nodes.forEach(function(cascadeNodes) {
  284. cascadeNodes.forEach(function(node) {
  285. var color = nodeOptions.color || palette.getNextColor();
  286. var nodeItem = new Node(that, {
  287. id: nodeIdx,
  288. color: color,
  289. rect: node,
  290. options: nodeOptions,
  291. linksIn: getConnectedLinks(layout, node._name, "in"),
  292. linksOut: getConnectedLinks(layout, node._name, "out")
  293. });
  294. that._nodes.push(nodeItem);
  295. nodeIdx++;
  296. nodeColors[node._name] = color
  297. })
  298. });
  299. layout.links.forEach(function(link) {
  300. var gradient = null;
  301. if (linkOptions.colorMode === _constants.COLOR_MODE_GRADIENT) {
  302. gradient = that._renderer.linearGradient([{
  303. offset: "0%",
  304. "stop-color": nodeColors[link._from._name]
  305. }, {
  306. offset: "100%",
  307. "stop-color": nodeColors[link._to._name]
  308. }]);
  309. _this._gradients.push(gradient)
  310. }
  311. var color = linkOptions.color;
  312. if (linkOptions.colorMode === _constants.COLOR_MODE_SOURCE) {
  313. color = nodeColors[link._from._name]
  314. } else {
  315. if (linkOptions.colorMode === _constants.COLOR_MODE_TARGET) {
  316. color = nodeColors[link._to._name]
  317. }
  318. }
  319. var linkItem = new Link(that, {
  320. d: link.d,
  321. boundingRect: link._boundingRect,
  322. color: color,
  323. options: linkOptions,
  324. connection: {
  325. source: link._from._name,
  326. target: link._to._name,
  327. weight: link._weight
  328. },
  329. gradient: gradient
  330. });
  331. that._links.push(linkItem)
  332. });
  333. that._renderer.initHatching();
  334. that._change(["NODES_DRAW", "LINKS_DRAW", "LABELS"])
  335. }
  336. that._change(["DRAWN"])
  337. },
  338. _applyLabelsAppearance: function() {
  339. var that = this;
  340. var labelOptions = that._getOption("label");
  341. var availableWidth = that._rect[2] - that._rect[0];
  342. var nodeOptions = that._getOption("node");
  343. that._shadowFilter = that._renderer.shadowFilter("-50%", "-50%", "200%", "200%").attr(labelOptions.shadow);
  344. that._groupLabels.clear();
  345. if (that._drawLabels && labelOptions.visible) {
  346. var availableLabelWidth = (availableWidth - (nodeOptions.width + labelOptions.horizontalOffset) - that._layoutMap.cascades.length * nodeOptions.width) / (that._layoutMap.cascades.length - 1) - labelOptions.horizontalOffset;
  347. that._nodes.forEach(function(node) {
  348. that._createLabel(node, labelOptions, that._shadowFilter.id);
  349. moveLabel(node, labelOptions, availableLabelWidth, that._rect)
  350. });
  351. if ("none" !== labelOptions.overlappingBehavior) {
  352. that._nodes.forEach(function(thisNode) {
  353. var thisBox = thisNode.label.getBBox();
  354. that._nodes.forEach(function(otherNode) {
  355. var otherBox = otherNode.label.getBBox();
  356. if (thisNode.id !== otherNode.id && defaultLayoutBuilder.overlap(thisBox, otherBox)) {
  357. if ("ellipsis" === labelOptions.overlappingBehavior) {
  358. thisNode.labelText.applyEllipsis(otherBox.x - thisBox.x)
  359. } else {
  360. if ("hide" === labelOptions.overlappingBehavior) {
  361. thisNode.labelText.remove()
  362. }
  363. }
  364. }
  365. })
  366. })
  367. }
  368. }
  369. },
  370. _createLabel: function(node, labelOptions, filter) {
  371. var textData = labelOptions.customizeText(node);
  372. var settings = node.getLabelAttributes(labelOptions, filter);
  373. if (textData) {
  374. node.label = this._renderer.g().append(this._groupLabels);
  375. node.labelText = this._renderer.text(textData).attr(settings.attr).css(settings.css);
  376. node.labelText.append(node.label)
  377. }
  378. },
  379. _getMinSize: function() {
  380. var adaptiveLayout = this._getOption("adaptiveLayout");
  381. return [adaptiveLayout.width, adaptiveLayout.height]
  382. },
  383. getAllNodes: function() {
  384. return this._nodes.slice()
  385. },
  386. getAllLinks: function() {
  387. return this._links.slice()
  388. }
  389. });
  390. require("../../core/component_registrator")("dxSankey", dxSankey);
  391. module.exports = dxSankey;
  392. dxSankey.addPlugin(require("../core/data_source").plugin);