layout.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293
  1. import { ascending, group } from 'd3-array';
  2. import { isParallel, isPolar, isRadar, radiusOf } from '../utils/coordinate';
  3. import { capitalizeFirst } from '../utils/helper';
  4. import { divide } from '../utils/array';
  5. import { camelCase } from '../utils/string';
  6. import { computeComponentSize } from './component';
  7. export function computeLayout(components, options, theme, library) {
  8. const { width, height, x = 0, y = 0, inset = 0, insetLeft = inset, insetTop = inset, insetBottom = inset, insetRight = inset, margin = 0, marginLeft = margin, marginBottom = margin, marginTop = margin, marginRight = margin, padding, paddingBottom = padding, paddingLeft = padding, paddingRight = padding, paddingTop = padding, } = options;
  9. const MAX_PADDING_RATIO = 1 / 3;
  10. const clamp = (origin, computed, max) => origin === 'auto' ? Math.min(max, computed) : computed;
  11. // Compute paddingLeft and paddingRight first to get innerWidth.
  12. const horizontalPadding = computePadding(components, height, [0, 0], ['left', 'right'], options, theme, library);
  13. const { paddingLeft: pl0, paddingRight: pr0 } = horizontalPadding;
  14. const viewWidth = width - marginLeft - marginRight;
  15. const pl = clamp(paddingLeft, pl0, viewWidth * MAX_PADDING_RATIO);
  16. const pr = clamp(paddingRight, pr0, viewWidth * MAX_PADDING_RATIO);
  17. const iw = viewWidth - pl - pr;
  18. // Compute paddingBottom and paddingTop based on innerWidth.
  19. const verticalPadding = computePadding(components, iw, [pl, pr], ['bottom', 'top'], options, theme, library);
  20. const { paddingTop: pt0, paddingBottom: pb0 } = verticalPadding;
  21. const viewHeight = height - marginBottom - marginTop;
  22. const pb = clamp(paddingBottom, pb0, viewHeight * MAX_PADDING_RATIO);
  23. const pt = clamp(paddingTop, pt0, viewHeight * MAX_PADDING_RATIO);
  24. const ih = viewHeight - pb - pt;
  25. return {
  26. width,
  27. height,
  28. insetLeft,
  29. insetTop,
  30. insetBottom,
  31. insetRight,
  32. innerWidth: iw,
  33. innerHeight: ih,
  34. paddingLeft: pl,
  35. paddingRight: pr,
  36. paddingTop: pt,
  37. paddingBottom: pb,
  38. marginLeft,
  39. marginBottom,
  40. marginTop,
  41. marginRight,
  42. x,
  43. y,
  44. };
  45. }
  46. /**
  47. * @todo Support percentage size(e.g. 50%)
  48. */
  49. function computePadding(components, crossSize, crossPadding, positions, options, theme, library) {
  50. const positionComponents = group(components, (d) => d.position);
  51. const { padding, paddingLeft = padding, paddingRight = padding, paddingBottom = padding, paddingTop = padding, } = options;
  52. const layout = {
  53. paddingBottom,
  54. paddingLeft,
  55. paddingTop,
  56. paddingRight,
  57. };
  58. for (const position of positions) {
  59. const key = `padding${capitalizeFirst(camelCase(position))}`;
  60. const value = layout[key];
  61. if (value === undefined || value === 'auto') {
  62. if (!positionComponents.has(position)) {
  63. layout[key] = 30;
  64. }
  65. else {
  66. const components = positionComponents.get(position);
  67. if (value === 'auto') {
  68. components.forEach((component) => computeComponentSize(component, crossSize, crossPadding, position, theme, library));
  69. }
  70. const totalSize = components.reduce((sum, { size }) => sum + size, 0);
  71. layout[key] = totalSize;
  72. }
  73. }
  74. }
  75. return layout;
  76. }
  77. export function placeComponents(components, coordinate, layout) {
  78. const positionComponents = group(components, (d) => d.position);
  79. const { paddingLeft, paddingRight, paddingTop, paddingBottom, marginLeft, marginTop, marginBottom, marginRight, innerHeight, innerWidth, height, width, } = layout;
  80. const pl = paddingLeft + marginLeft;
  81. const pt = paddingTop + marginTop;
  82. const pr = paddingRight + marginRight;
  83. const pb = paddingBottom + marginBottom;
  84. const section = {
  85. top: [pl, 0, innerWidth, pt, 'vertical', true, ascending],
  86. right: [width - pr, pt, pr, innerHeight, 'horizontal', false, ascending],
  87. bottom: [pl, height - pb, innerWidth, pb, 'vertical', false, ascending],
  88. left: [0, pt, pl, innerHeight, 'horizontal', true, ascending],
  89. 'top-left': [pl, 0, innerWidth, pt, 'vertical', true, ascending],
  90. 'top-right': [pl, 0, innerWidth, pt, 'vertical', true, ascending],
  91. 'bottom-left': [
  92. pl,
  93. height - pb,
  94. innerWidth,
  95. pb,
  96. 'vertical',
  97. false,
  98. ascending,
  99. ],
  100. 'bottom-right': [
  101. pl,
  102. height - pb,
  103. innerWidth,
  104. pb,
  105. 'vertical',
  106. false,
  107. ascending,
  108. ],
  109. center: [pl, pt, innerWidth, innerHeight, 'center', null, null],
  110. inner: [pl, pt, innerWidth, innerHeight, 'center', null, null],
  111. outer: [pl, pt, innerWidth, innerHeight, 'center', null, null],
  112. };
  113. for (const [position, components] of positionComponents.entries()) {
  114. const area = section[position];
  115. /**
  116. * @description non-entity components: axis in the center, inner, outer, component in the center
  117. * @description entity components: other components
  118. * @description no volume components take up no extra space
  119. */
  120. const [nonEntityComponents, entityComponents] = divide(components, (component) => {
  121. if (typeof component.type !== 'string')
  122. return false;
  123. if (position === 'center')
  124. return true;
  125. if (component.type.startsWith('axis') &&
  126. ['inner', 'outer'].includes(position)) {
  127. return true;
  128. }
  129. return false;
  130. });
  131. if (nonEntityComponents.length) {
  132. placeNonEntityComponents(nonEntityComponents, coordinate, area, position);
  133. }
  134. if (entityComponents.length) {
  135. placePaddingArea(components, coordinate, area);
  136. }
  137. }
  138. }
  139. function placeNonEntityComponents(components, coordinate, area, position) {
  140. const [axisComponents, nonAxisComponents] = divide(components, (component) => {
  141. if (typeof component.type === 'string' &&
  142. component.type.startsWith('axis')) {
  143. return true;
  144. }
  145. return false;
  146. });
  147. placeNonEntityAxis(axisComponents, coordinate, area, position);
  148. // in current stage, only legend component which located in the center can be placed
  149. placeCenter(nonAxisComponents, coordinate, area);
  150. }
  151. function placeNonEntityAxis(components, coordinate, area, position) {
  152. if (position === 'center') {
  153. if (isRadar(coordinate)) {
  154. placeAxisRadar(components, coordinate, area, position);
  155. }
  156. else if (isPolar(coordinate)) {
  157. placeArcLinear(components, coordinate, area);
  158. }
  159. else if (isParallel(coordinate)) {
  160. placeAxisParallel(components, coordinate, area, components[0].orientation);
  161. }
  162. }
  163. else if (position === 'inner') {
  164. placeAxisArcInner(components, coordinate, area);
  165. }
  166. else if (position === 'outer') {
  167. placeAxisArcOuter(components, coordinate, area);
  168. }
  169. }
  170. function placeAxisArcInner(components, coordinate, area) {
  171. const [x, y, , height] = area;
  172. const [cx, cy] = coordinate.getCenter();
  173. const [innerRadius] = radiusOf(coordinate);
  174. const r = height / 2;
  175. const size = innerRadius * r;
  176. const x0 = cx - size;
  177. const y0 = cy - size;
  178. for (let i = 0; i < components.length; i++) {
  179. const component = components[i];
  180. component.bbox = {
  181. x: x + x0,
  182. y: y + y0,
  183. width: size * 2,
  184. height: size * 2,
  185. };
  186. }
  187. }
  188. function placeAxisArcOuter(components, coordinate, area) {
  189. const [x, y, width, height] = area;
  190. for (const component of components) {
  191. component.bbox = { x, y, width, height };
  192. }
  193. }
  194. /**
  195. * @example arcX, arcY, axisLinear with angle
  196. */
  197. function placeArcLinear(components, coordinate, area) {
  198. const [x, y, width, height] = area;
  199. for (const component of components) {
  200. component.bbox = { x: x, y, width, height };
  201. }
  202. }
  203. function placeAxisParallel(components, coordinate, area, orientation) {
  204. if (orientation === 'horizontal') {
  205. placeAxisParallelHorizontal(components, coordinate, area);
  206. }
  207. else if (orientation === 'vertical') {
  208. placeAxisParallelVertical(components, coordinate, area);
  209. }
  210. }
  211. function placeAxisParallelVertical(components, coordinate, area) {
  212. const [x, y, , height] = area;
  213. // Create a high dimension vector and map to a list of two-dimension points.
  214. // [0, 0, 0] -> [x0, 0, x1, 0, x2, 0]
  215. const vector = new Array(components.length + 1).fill(0);
  216. const points = coordinate.map(vector);
  217. // Extract x of each points.
  218. // [x0, 0, x1, 0, x2, 0] -> [x0, x1, x2]
  219. const X = points.filter((_, i) => i % 2 === 0).map((d) => d + x);
  220. // Place each axis by coordinate in parallel coordinate.
  221. for (let i = 0; i < components.length; i++) {
  222. const component = components[i];
  223. const x = X[i];
  224. const width = X[i + 1] - x;
  225. component.bbox = { x, y, width, height };
  226. }
  227. }
  228. function placeAxisParallelHorizontal(components, coordinate, area) {
  229. const [x, y, width] = area;
  230. // Create a high dimension vector and map to a list of two-dimension points.
  231. // [0, 0, 0] -> [height, y0, height, y1, height, y2]
  232. const vector = new Array(components.length + 1).fill(0);
  233. const points = coordinate.map(vector);
  234. // Extract y of each points.
  235. // [x0, 0, x1, 0, x2, 0] -> [x0, x1, x2]
  236. const Y = points.filter((_, i) => i % 2 === 1).map((d) => d + y);
  237. // Place each axis by coordinate in parallel coordinate.
  238. for (let i = 0; i < components.length; i++) {
  239. const component = components[i];
  240. const y = Y[i];
  241. const height = Y[i + 1] - y;
  242. component.bbox = { x, y, width, height };
  243. }
  244. // override style
  245. }
  246. function placeAxisRadar(components, coordinate, area, position) {
  247. const [x, y, width, height] = area;
  248. for (const component of components) {
  249. component.bbox = { x, y, width, height };
  250. component.radar = {
  251. index: components.indexOf(component),
  252. count: components.length,
  253. };
  254. }
  255. }
  256. function placePaddingArea(components, coordinate, area) {
  257. const [x, y, width, height, direction, reverse, comparator] = area;
  258. const [mainStartKey, mainStartValue, crossStartKey, crossStartValue, mainSizeKey, mainSizeValue, crossSizeKey, crossSizeValue,] = direction === 'vertical'
  259. ? ['y', y, 'x', x, 'height', height, 'width', width]
  260. : ['x', x, 'y', y, 'width', width, 'height', height];
  261. // Sort components by order.
  262. // The smaller the order, the closer to center.
  263. components.sort((a, b) => comparator === null || comparator === void 0 ? void 0 : comparator(a.order, b.order));
  264. const startValue = reverse ? mainStartValue + mainSizeValue : mainStartValue;
  265. for (let i = 0, start = startValue; i < components.length; i++) {
  266. const component = components[i];
  267. const { size } = component;
  268. component.bbox = {
  269. [mainStartKey]: reverse ? start - size : start,
  270. [crossStartKey]: crossStartValue,
  271. [mainSizeKey]: size,
  272. [crossSizeKey]: crossSizeValue,
  273. };
  274. start += size * (reverse ? -1 : 1);
  275. }
  276. }
  277. /**
  278. * @example legend in the center of radial or polar system
  279. */
  280. function placeCenter(components, coordinate, area) {
  281. if (components.length === 0)
  282. return;
  283. const [x, y, width, height] = area;
  284. const [innerRadius] = radiusOf(coordinate);
  285. const r = ((height / 2) * innerRadius) / Math.sqrt(2);
  286. const cx = x + width / 2;
  287. const cy = y + height / 2;
  288. for (let i = 0; i < components.length; i++) {
  289. const component = components[i];
  290. component.bbox = { x: cx - r, y: cy - r, width: r * 2, height: r * 2 };
  291. }
  292. }
  293. //# sourceMappingURL=layout.js.map