layout_manager.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373
  1. /**
  2. * DevExtreme (viz/chart_components/layout_manager.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 extend = require("../../core/utils/extend").extend;
  11. var layoutElementModule = require("../core/layout_element");
  12. var _isNumber = require("../../core/utils/type").isNumeric;
  13. var _min = Math.min;
  14. var _max = Math.max;
  15. var _floor = Math.floor;
  16. var _sqrt = Math.sqrt;
  17. var consts = require("../components/consts");
  18. var RADIAL_LABEL_INDENT = consts.radialLabelIndent;
  19. function getNearestCoord(firstCoord, secondCoord, pointCenterCoord) {
  20. var nearestCoord;
  21. if (pointCenterCoord < firstCoord) {
  22. nearestCoord = firstCoord
  23. } else {
  24. if (secondCoord < pointCenterCoord) {
  25. nearestCoord = secondCoord
  26. } else {
  27. nearestCoord = pointCenterCoord
  28. }
  29. }
  30. return nearestCoord
  31. }
  32. function getLabelLayout(point) {
  33. if (point._label.isVisible() && "inside" !== point._label.getLayoutOptions().position) {
  34. return point._label.getBoundingRect()
  35. }
  36. }
  37. function getPieRadius(series, paneCenterX, paneCenterY, accessibleRadius, minR) {
  38. series.some(function(singleSeries) {
  39. return singleSeries.getVisiblePoints().reduce(function(radiusIsFound, point) {
  40. var labelBBox = getLabelLayout(point);
  41. if (labelBBox) {
  42. var xCoords = getNearestCoord(labelBBox.x, labelBBox.x + labelBBox.width, paneCenterX);
  43. var yCoords = getNearestCoord(labelBBox.y, labelBBox.y + labelBBox.height, paneCenterY);
  44. accessibleRadius = _min(_max(getLengthFromCenter(xCoords, yCoords, paneCenterX, paneCenterY) - RADIAL_LABEL_INDENT, minR), accessibleRadius);
  45. radiusIsFound = true
  46. }
  47. return radiusIsFound
  48. }, false)
  49. });
  50. return accessibleRadius
  51. }
  52. function getSizeLabels(series) {
  53. return series.reduce(function(res, singleSeries) {
  54. var maxWidth = singleSeries.getVisiblePoints().reduce(function(width, point) {
  55. var labelBBox = getLabelLayout(point);
  56. if (labelBBox && labelBBox.width > width) {
  57. width = labelBBox.width
  58. }
  59. return width
  60. }, 0);
  61. var rWidth = maxWidth;
  62. if (maxWidth) {
  63. res.outerLabelsCount++;
  64. if (res.outerLabelsCount > 1) {
  65. maxWidth += consts.pieLabelSpacing
  66. }
  67. rWidth += consts.pieLabelSpacing
  68. }
  69. res.sizes.push(maxWidth);
  70. res.rSizes.push(rWidth);
  71. res.common += maxWidth;
  72. return res
  73. }, {
  74. sizes: [],
  75. rSizes: [],
  76. common: 0,
  77. outerLabelsCount: 0
  78. })
  79. }
  80. function correctLabelRadius(labelSizes, radius, series, canvas, averageWidthLabels, centerX) {
  81. var curRadius;
  82. var i;
  83. var runningWidth = 0;
  84. var sizes = labelSizes.sizes;
  85. var rSizes = labelSizes.rSizes;
  86. for (i = 0; i < series.length; i++) {
  87. if (0 === sizes[i]) {
  88. curRadius && (curRadius += rSizes[i - 1]);
  89. continue
  90. }
  91. curRadius = _floor(curRadius ? curRadius + rSizes[i - 1] : radius);
  92. series[i].correctLabelRadius(curRadius);
  93. runningWidth += averageWidthLabels || sizes[i];
  94. rSizes[i] = averageWidthLabels || rSizes[i];
  95. series[i].setVisibleArea({
  96. left: _floor(centerX - radius - runningWidth),
  97. right: _floor(canvas.width - (centerX + radius + runningWidth)),
  98. top: canvas.top,
  99. bottom: canvas.bottom,
  100. width: canvas.width,
  101. height: canvas.height
  102. })
  103. }
  104. }
  105. function getLengthFromCenter(x, y, paneCenterX, paneCenterY) {
  106. return _sqrt((x - paneCenterX) * (x - paneCenterX) + (y - paneCenterY) * (y - paneCenterY))
  107. }
  108. function getInnerRadius(series) {
  109. var innerRadius;
  110. if ("pie" === series.type) {
  111. innerRadius = 0
  112. } else {
  113. innerRadius = _isNumber(series.innerRadius) ? Number(series.innerRadius) : .5;
  114. innerRadius = innerRadius < .2 ? .2 : innerRadius;
  115. innerRadius = innerRadius > .8 ? .8 : innerRadius
  116. }
  117. return innerRadius
  118. }
  119. var inverseAlign = {
  120. left: "right",
  121. right: "left",
  122. top: "bottom",
  123. bottom: "top",
  124. center: "center"
  125. };
  126. function downSize(canvas, layoutOptions) {
  127. canvas[layoutOptions.cutLayoutSide] += "horizontal" === layoutOptions.cutSide ? layoutOptions.width : layoutOptions.height
  128. }
  129. function getOffset(layoutOptions, offsets) {
  130. var side = layoutOptions.cutLayoutSide;
  131. var offset = {
  132. horizontal: 0,
  133. vertical: 0
  134. };
  135. switch (side) {
  136. case "top":
  137. case "left":
  138. offset[layoutOptions.cutSide] = -offsets[side];
  139. break;
  140. case "bottom":
  141. case "right":
  142. offset[layoutOptions.cutSide] = offsets[side]
  143. }
  144. return offset
  145. }
  146. function LayoutManager() {}
  147. function toLayoutElementCoords(canvas) {
  148. return new layoutElementModule.WrapperLayoutElement(null, {
  149. x: canvas.left,
  150. y: canvas.top,
  151. width: canvas.width - canvas.left - canvas.right,
  152. height: canvas.height - canvas.top - canvas.bottom
  153. })
  154. }
  155. function getAverageLabelWidth(centerX, radius, canvas, sizeLabels) {
  156. return (centerX - radius - RADIAL_LABEL_INDENT - canvas.left) / sizeLabels.outerLabelsCount
  157. }
  158. function getFullRadiusWithLabels(centerX, canvas, sizeLabels) {
  159. return centerX - canvas.left - (sizeLabels.outerLabelsCount > 0 ? sizeLabels.common + RADIAL_LABEL_INDENT : 0)
  160. }
  161. function correctAvailableRadius(availableRadius, canvas, series, minR, paneCenterX, paneCenterY) {
  162. var sizeLabels = getSizeLabels(series);
  163. var averageWidthLabels;
  164. var fullRadiusWithLabels = getFullRadiusWithLabels(paneCenterX, canvas, sizeLabels);
  165. if (fullRadiusWithLabels < minR) {
  166. availableRadius = minR;
  167. averageWidthLabels = getAverageLabelWidth(paneCenterX, availableRadius, canvas, sizeLabels)
  168. } else {
  169. availableRadius = _min(getPieRadius(series, paneCenterX, paneCenterY, availableRadius, minR), fullRadiusWithLabels)
  170. }
  171. correctLabelRadius(sizeLabels, availableRadius + RADIAL_LABEL_INDENT, series, canvas, averageWidthLabels, paneCenterX);
  172. return availableRadius
  173. }
  174. LayoutManager.prototype = {
  175. constructor: LayoutManager,
  176. setOptions: function(options) {
  177. this._options = options
  178. },
  179. applyPieChartSeriesLayout: function(canvas, series, hideLayoutLabels) {
  180. var paneSpaceHeight = canvas.height - canvas.top - canvas.bottom;
  181. var paneSpaceWidth = canvas.width - canvas.left - canvas.right;
  182. var paneCenterX = paneSpaceWidth / 2 + canvas.left;
  183. var paneCenterY = paneSpaceHeight / 2 + canvas.top;
  184. var piePercentage = this._options.piePercentage;
  185. var availableRadius;
  186. var minR;
  187. if (_isNumber(piePercentage)) {
  188. availableRadius = minR = piePercentage * _min(canvas.height, canvas.width) / 2
  189. } else {
  190. availableRadius = _min(paneSpaceWidth, paneSpaceHeight) / 2;
  191. minR = this._options.minPiePercentage * availableRadius
  192. }
  193. if (!hideLayoutLabels) {
  194. availableRadius = correctAvailableRadius(availableRadius, canvas, series, minR, paneCenterX, paneCenterY)
  195. }
  196. return {
  197. centerX: _floor(paneCenterX),
  198. centerY: _floor(paneCenterY),
  199. radiusInner: _floor(availableRadius * getInnerRadius(series[0])),
  200. radiusOuter: _floor(availableRadius)
  201. }
  202. },
  203. applyEqualPieChartLayout: function(series, layout) {
  204. var radius = layout.radius;
  205. return {
  206. centerX: _floor(layout.x),
  207. centerY: _floor(layout.y),
  208. radiusInner: _floor(radius * getInnerRadius(series[0])),
  209. radiusOuter: _floor(radius)
  210. }
  211. },
  212. correctPieLabelRadius: function(series, layout, canvas) {
  213. var sizeLabels = getSizeLabels(series);
  214. var averageWidthLabels;
  215. var radius = layout.radiusOuter + RADIAL_LABEL_INDENT;
  216. var availableLabelWidth = layout.centerX - canvas.left - radius;
  217. if (sizeLabels.common + RADIAL_LABEL_INDENT > availableLabelWidth) {
  218. averageWidthLabels = getAverageLabelWidth(layout.centerX, layout.radiusOuter, canvas, sizeLabels)
  219. }
  220. correctLabelRadius(sizeLabels, radius, series, canvas, averageWidthLabels, layout.centerX)
  221. },
  222. needMoreSpaceForPanesCanvas: function(panes, rotated) {
  223. var options = this._options;
  224. var width = options.width;
  225. var height = options.height;
  226. var piePercentage = options.piePercentage;
  227. var percentageIsValid = _isNumber(piePercentage);
  228. var needHorizontalSpace = 0;
  229. var needVerticalSpace = 0;
  230. panes.forEach(function(pane) {
  231. var paneCanvas = pane.canvas;
  232. var minSize = percentageIsValid ? _min(paneCanvas.width, paneCanvas.height) * piePercentage : void 0;
  233. var needPaneHorizontalSpace = (percentageIsValid ? minSize : width) - (paneCanvas.width - paneCanvas.left - paneCanvas.right);
  234. var needPaneVerticalSpace = (percentageIsValid ? minSize : height) - (paneCanvas.height - paneCanvas.top - paneCanvas.bottom);
  235. if (rotated) {
  236. needHorizontalSpace += needPaneHorizontalSpace > 0 ? needPaneHorizontalSpace : 0;
  237. needVerticalSpace = _max(needPaneVerticalSpace > 0 ? needPaneVerticalSpace : 0, needVerticalSpace)
  238. } else {
  239. needHorizontalSpace = _max(needPaneHorizontalSpace > 0 ? needPaneHorizontalSpace : 0, needHorizontalSpace);
  240. needVerticalSpace += needPaneVerticalSpace > 0 ? needPaneVerticalSpace : 0
  241. }
  242. });
  243. return needHorizontalSpace > 0 || needVerticalSpace > 0 ? {
  244. width: needHorizontalSpace,
  245. height: needVerticalSpace
  246. } : false
  247. },
  248. layoutElements: function(elements, canvas, funcAxisDrawer, panes, rotated) {
  249. this._elements = elements;
  250. this._probeDrawing(canvas);
  251. this._drawElements(canvas);
  252. funcAxisDrawer();
  253. this._processAdaptiveLayout(panes, rotated, canvas, funcAxisDrawer);
  254. this._positionElements(canvas)
  255. },
  256. _processAdaptiveLayout: function(panes, rotated, canvas, funcAxisDrawer) {
  257. var that = this;
  258. var size = that.needMoreSpaceForPanesCanvas(panes, rotated);
  259. var items = this._elements;
  260. if (!size) {
  261. return
  262. }
  263. function processCanvases(item, layoutOptions, side) {
  264. if (!item.getLayoutOptions()[side]) {
  265. canvas[layoutOptions.cutLayoutSide] -= layoutOptions[side];
  266. size[side] = size[side] - layoutOptions[side]
  267. }
  268. }
  269. items.slice().reverse().forEach(function(item) {
  270. var layoutOptions = item.getLayoutOptions();
  271. var needRedraw = false;
  272. if (!layoutOptions) {
  273. return
  274. }
  275. var sizeObject = extend({}, layoutOptions);
  276. needRedraw = "vertical" === layoutOptions.cutSide && size.width < 0 || "horizontal" === layoutOptions.cutSide && size.height < 0 || "vertical" === layoutOptions.cutSide && size.height > 0 || "horizontal" === layoutOptions.cutSide && size.width > 0;
  277. var cutSide = "horizontal" === layoutOptions.cutSide ? "width" : "height";
  278. if (needRedraw) {
  279. var width = sizeObject.width - size.width;
  280. var height = sizeObject.height - size.height;
  281. if ("height" === cutSide && size.width < 0) {
  282. width = canvas.width - canvas.left - canvas.right
  283. }
  284. if ("width" === cutSide && size.height < 0) {
  285. height = canvas.height - canvas.top - canvas.bottom
  286. }
  287. item.draw(width, height)
  288. }
  289. processCanvases(item, layoutOptions, cutSide)
  290. });
  291. funcAxisDrawer(size)
  292. },
  293. _probeDrawing: function(canvas) {
  294. var that = this;
  295. this._elements.forEach(function(item) {
  296. var layoutOptions = item.getLayoutOptions();
  297. if (!layoutOptions) {
  298. return
  299. }
  300. var sizeObject = {
  301. width: canvas.width - canvas.left - canvas.right,
  302. height: canvas.height - canvas.top - canvas.bottom
  303. };
  304. if ("vertical" === layoutOptions.cutSide) {
  305. sizeObject.height -= that._options.height
  306. } else {
  307. sizeObject.width -= that._options.width
  308. }
  309. item.probeDraw(sizeObject.width, sizeObject.height);
  310. downSize(canvas, item.getLayoutOptions())
  311. })
  312. },
  313. _drawElements: function(canvas) {
  314. this._elements.slice().reverse().forEach(function(item) {
  315. var layoutOptions = item.getLayoutOptions();
  316. if (!layoutOptions) {
  317. return
  318. }
  319. var sizeObject = {
  320. width: canvas.width - canvas.left - canvas.right,
  321. height: canvas.height - canvas.top - canvas.bottom
  322. };
  323. var cutSide = layoutOptions.cutSide;
  324. var length = "horizontal" === cutSide ? "width" : "height";
  325. sizeObject[length] = layoutOptions[length];
  326. item.draw(sizeObject.width, sizeObject.height)
  327. })
  328. },
  329. _positionElements: function(canvas) {
  330. var offsets = {
  331. left: 0,
  332. right: 0,
  333. top: 0,
  334. bottom: 0
  335. };
  336. this._elements.slice().reverse().forEach(function(item) {
  337. var layoutOptions = item.getLayoutOptions();
  338. if (!layoutOptions) {
  339. return
  340. }
  341. var position = layoutOptions.position;
  342. var cutSide = layoutOptions.cutSide;
  343. var my = {
  344. horizontal: position.horizontal,
  345. vertical: position.vertical
  346. };
  347. my[cutSide] = inverseAlign[my[cutSide]];
  348. item.position({
  349. of: toLayoutElementCoords(canvas),
  350. my: my,
  351. at: position,
  352. offset: getOffset(layoutOptions, offsets)
  353. });
  354. offsets[layoutOptions.cutLayoutSide] += layoutOptions["horizontal" === layoutOptions.cutSide ? "width" : "height"]
  355. })
  356. }
  357. };
  358. exports.LayoutManager = LayoutManager;