component.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447
  1. var __rest = (this && this.__rest) || function (s, e) {
  2. var t = {};
  3. for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
  4. t[p] = s[p];
  5. if (s != null && typeof Object.getOwnPropertySymbols === "function")
  6. for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
  7. if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
  8. t[p[i]] = s[p[i]];
  9. }
  10. return t;
  11. };
  12. import { deepMix, isEqual } from '@antv/util';
  13. import { group, max, sum } from 'd3-array';
  14. import { format } from 'd3-format';
  15. import { DisplayObject, Text } from '@antv/g';
  16. import { getPolarOptions, getRadialOptions, } from '../coordinate';
  17. import { combine } from '../utils/array';
  18. import { capitalizeFirst, defined, subObject } from '../utils/helper';
  19. import { LEGEND_INFER_STRATEGIES } from '../component/constant';
  20. import { coordOf, isHelix, isParallel, isPolar, isRadar, isRadial, isReflect, isReflectY, isTheta, isTranspose, } from './coordinate';
  21. import { useLibrary } from './library';
  22. import { isValidScale, useRelationScale } from './scale';
  23. import { ConstantScale, ContinuousScale, DiscreteScale, DistributionScale, } from './types/scale';
  24. export function inferComponent(scales, partialOptions, library) {
  25. const { component: partialComponents = [], coordinates = [], title, theme, } = partialOptions;
  26. const [, createGuideComponent] = useLibrary('component', library);
  27. const displayedScales = scales.filter(({ guide, name }) => {
  28. if (guide === null)
  29. return false;
  30. return true;
  31. });
  32. const sliders = inferScrollableComponents(partialOptions, scales, library);
  33. const components = [...partialComponents, ...sliders];
  34. if (title) {
  35. const { props } = createGuideComponent('title');
  36. const { defaultPosition, defaultOrientation, defaultOrder, defaultSize } = props;
  37. const titleOptions = typeof title === 'string' ? { title } : title;
  38. components.push(Object.assign({ type: 'title', position: defaultPosition, orientation: defaultOrientation, order: defaultOrder, size: defaultSize }, titleOptions));
  39. }
  40. const inferredComponents = inferComponentsType(displayedScales, coordinates);
  41. inferredComponents.forEach(([type, relativeScales]) => {
  42. const { props } = createGuideComponent(type);
  43. const { defaultPosition, defaultOrientation, defaultSize, defaultOrder } = props;
  44. // @todo to be confirm if the scale can be merged.
  45. const scale = Object.assign({}, ...relativeScales);
  46. const { guide: guideOptions, field } = scale;
  47. // A scale may have multiple guides.
  48. const guides = Array.isArray(guideOptions) ? guideOptions : [guideOptions];
  49. for (const partialGuide of guides) {
  50. const [position, orientation] = inferComponentPositionAndOrientation(type, defaultPosition, defaultOrientation, partialGuide, relativeScales, displayedScales, coordinates);
  51. // Skip if position and orientation are not specified.
  52. // @example the last axis of radar chart
  53. if (!position && !orientation)
  54. continue;
  55. const { size = defaultSize, order = defaultOrder } = partialGuide;
  56. components.push(Object.assign(Object.assign({ title: field }, partialGuide), { position,
  57. orientation,
  58. order,
  59. size,
  60. type, scales: relativeScales }));
  61. }
  62. });
  63. return components;
  64. }
  65. export function renderComponent(component, coordinate, theme, library, markState) {
  66. const [useGuideComponent] = useLibrary('component', library);
  67. const { scales: scaleDescriptors = [], bbox } = component, options = __rest(component, ["scales", "bbox"]);
  68. const scales = scaleDescriptors.map((descriptor) => useRelationScale(descriptor, library));
  69. const value = { bbox, scales: scaleDescriptors, library };
  70. const render = useGuideComponent(options);
  71. return render({ coordinate, library, markState, scales, theme, value });
  72. }
  73. function inferLegendComponentType(scales, coordinates) {
  74. const acceptScales = scales
  75. .filter((scale) => typeof scale.type === 'string'
  76. ? ['shape', 'size', 'color', 'opacity'].includes(scale.name)
  77. : true)
  78. // not support constant size scale
  79. .filter((scale) => !(scale.type === 'constant' && scale.name === 'size'));
  80. // scale with undefined field
  81. const undefinedScales = acceptScales.filter((scale) => !scale.field);
  82. const definedScales = acceptScales.filter((scale) => !!scale.field);
  83. // exclude the scales that all type are constant
  84. const scalesByField = new Map(Array.from(group(definedScales, (d) => d.field))
  85. .map(([field, scales]) => [
  86. field,
  87. [
  88. ...scales,
  89. ...undefinedScales.filter((scale) => scale.type === 'constant'),
  90. ],
  91. ])
  92. .concat([[undefined, undefinedScales]])
  93. .filter(([field, scales]) => scales.some((scale) => scale.type !== 'constant')));
  94. if (scalesByField.size === 0)
  95. return [];
  96. function getScaleType(scale) {
  97. const { type } = scale;
  98. if (typeof type !== 'string')
  99. return null;
  100. if (type in ContinuousScale)
  101. return 'continuous';
  102. if (type in DiscreteScale)
  103. return 'discrete';
  104. if (type in DistributionScale)
  105. return 'distribution';
  106. if (type in ConstantScale)
  107. return 'constant';
  108. return null;
  109. }
  110. const components = Array.from(scalesByField)
  111. .map(([channel, scs]) => {
  112. const combinations = combine(scs).sort((a, b) => b.length - a.length);
  113. const options = combinations.map((combination) => ({
  114. combination,
  115. option: combination.map((scale) => [scale.name, getScaleType(scale)]),
  116. }));
  117. const sort = (arr) => arr.sort((a, b) => a[0].localeCompare(b[0]));
  118. for (const [componentType, accords] of LEGEND_INFER_STRATEGIES) {
  119. for (const { option, combination } of options) {
  120. if (accords.some((accord) => isEqual(sort(accord), sort(option)))) {
  121. return [componentType, combination];
  122. }
  123. }
  124. }
  125. return null;
  126. })
  127. .filter(defined);
  128. return components;
  129. }
  130. function inferAxisComponentType(scales, coordinates) {
  131. return scales
  132. .map((scale) => {
  133. const { name } = scale;
  134. // todo wait for gui provide helix axis
  135. if (isHelix(coordinates) || isTheta(coordinates))
  136. return null;
  137. if (isTranspose(coordinates) &&
  138. (isPolar(coordinates) || isRadial(coordinates)))
  139. return null;
  140. // infer axis
  141. if (name.startsWith('x')) {
  142. if (isPolar(coordinates))
  143. return ['axisArc', [scale]];
  144. if (isRadial(coordinates))
  145. return ['axisLinear', [scale]];
  146. return [isTranspose(coordinates) ? 'axisY' : 'axisX', [scale]];
  147. }
  148. if (name.startsWith('y')) {
  149. if (isPolar(coordinates))
  150. return ['axisLinear', [scale]];
  151. if (isRadial(coordinates))
  152. return ['axisArc', [scale]];
  153. return [isTranspose(coordinates) ? 'axisX' : 'axisY', [scale]];
  154. }
  155. if (name.startsWith('position')) {
  156. if (isRadar(coordinates))
  157. return ['axisRadar', [scale]];
  158. if (!isPolar(coordinates))
  159. return ['axisY', [scale]];
  160. }
  161. return null;
  162. })
  163. .filter(defined);
  164. }
  165. function inferComponentsType(scales, coordinates) {
  166. const availableScales = scales.filter((scale) => isValidScale(scale));
  167. return [
  168. ...inferLegendComponentType(availableScales, coordinates),
  169. ...inferAxisComponentType(availableScales, coordinates),
  170. ];
  171. }
  172. function angleOf(coordinates) {
  173. const polar = coordOf(coordinates, 'polar');
  174. if (polar.length) {
  175. const lastPolar = polar[polar.length - 1];
  176. const { startAngle, endAngle } = getPolarOptions(lastPolar);
  177. return [startAngle, endAngle];
  178. }
  179. const radial = coordOf(coordinates, 'radial');
  180. if (radial.length) {
  181. const lastRadial = radial[radial.length - 1];
  182. const { startAngle, endAngle } = getRadialOptions(lastRadial);
  183. return [startAngle, endAngle];
  184. }
  185. return [-Math.PI / 2, (Math.PI / 2) * 3];
  186. }
  187. /**
  188. * match index of position
  189. */
  190. function matchPosition(name) {
  191. const match = /position(\d*)/g.exec(name);
  192. if (!match)
  193. return null;
  194. return +match[1];
  195. }
  196. function inferAxisPositionAndOrientation(type, ordinalPosition, relativeScales, scales, coordinates) {
  197. // a axis only has one scale
  198. const { name } = relativeScales[0];
  199. // todo, in current resolution, the radar chart is implement by parallel + polar coordinate.
  200. // implementation plan to be confirmed.
  201. // in current implementation, it must to add the first position encode to it's last.
  202. // so we won't render the last axis repeatably.
  203. if (type === 'axisRadar') {
  204. const positions = scales.filter((scale) => scale.name.startsWith('position'));
  205. const index = matchPosition(name);
  206. if (name === positions.slice(-1)[0].name || index === null)
  207. return [null, null];
  208. // infer radar axis orientation
  209. const [startAngle, endAngle] = angleOf(coordinates);
  210. const angle = ((endAngle - startAngle) / (positions.length - 1)) * index + startAngle;
  211. return ['center', angle];
  212. }
  213. // There are multiple axes for parallel coordinate.
  214. // Place the first one in the border area and put others in the center.
  215. if (type === 'axisY' && isParallel(coordinates)) {
  216. // name looks like `position${number}`
  217. const index = matchPosition(name);
  218. if (index === null)
  219. return ordinalPosition;
  220. if (isTranspose(coordinates)) {
  221. return index === 0 ? ['top', null] : ['center', 'horizontal'];
  222. }
  223. return index === 0 ? ordinalPosition : ['center', 'vertical'];
  224. }
  225. // in non-cartesian coordinate systems, infer the arc axis angle
  226. if (type === 'axisLinear') {
  227. const [startAngle] = angleOf(coordinates);
  228. return ['center', startAngle];
  229. }
  230. if (type === 'axisArc') {
  231. if (ordinalPosition[0] === 'inner')
  232. return ['inner', null];
  233. return ['outer', null];
  234. }
  235. if (isPolar(coordinates))
  236. return ['center', null];
  237. if (isRadial(coordinates))
  238. return ['center', null];
  239. if ((type === 'axisX' && isReflect(coordinates)) ||
  240. (type === 'axisX' && isReflectY(coordinates))) {
  241. return ['top', null];
  242. }
  243. // if (type === 'axisX') return ['bottom', null];
  244. return ordinalPosition;
  245. }
  246. // @todo Infer position by coordinates.
  247. function inferComponentPositionAndOrientation(type, defaultPosition, defaultOrientation, guide, relativeScales, scales, coordinates) {
  248. const [startAngle] = angleOf(coordinates);
  249. const ordinalPositionAndOrientation = [
  250. guide.position || defaultPosition,
  251. startAngle !== null && startAngle !== void 0 ? startAngle : defaultOrientation,
  252. ];
  253. if (typeof type === 'string' && type.startsWith('axis')) {
  254. return inferAxisPositionAndOrientation(type, ordinalPositionAndOrientation, relativeScales, scales, coordinates);
  255. }
  256. if (typeof type === 'string' &&
  257. type.startsWith('legend') &&
  258. isPolar(coordinates)) {
  259. if (guide.position === 'center')
  260. return ['center', 'vertical'];
  261. }
  262. // for general component, use default position
  263. return ordinalPositionAndOrientation;
  264. }
  265. function inferScrollableType(name, type, coordinates = []) {
  266. if (name === 'x')
  267. return isTranspose(coordinates) ? `${type}Y` : `${type}X`;
  268. if (name === 'y')
  269. return isTranspose(coordinates) ? `${type}X` : `${type}Y`;
  270. return null;
  271. }
  272. /**
  273. * Infer scrollable components, such as slider and scrollbar.
  274. */
  275. function inferScrollableComponents(partialOptions, scales, library) {
  276. const [, createGuideComponent] = useLibrary('component', library);
  277. const { coordinates } = partialOptions;
  278. function normalized(type, channelName, scale, options) {
  279. const componentType = inferScrollableType(channelName, type, coordinates);
  280. if (!options || !componentType)
  281. return;
  282. const { props } = createGuideComponent(componentType);
  283. const { defaultPosition, defaultSize, defaultOrder } = props;
  284. return Object.assign(Object.assign({ position: defaultPosition, size: defaultSize, order: defaultOrder, type: componentType }, options), { scales: [scale] });
  285. }
  286. return scales
  287. .filter((d) => d.slider || d.scrollbar)
  288. .flatMap((scale) => {
  289. const { slider, scrollbar, name: channelName } = scale;
  290. return [
  291. normalized('slider', channelName, scale, slider),
  292. normalized('scrollbar', channelName, scale, scrollbar),
  293. ];
  294. })
  295. .filter((d) => !!d);
  296. }
  297. // !!! Note Mutate component.size and component.style.
  298. export function computeComponentSize(component, crossSize, crossPadding, position, theme, library) {
  299. const [useScale] = useLibrary('scale', library);
  300. // Only compute and update size of axis component in padding area.
  301. // @todo Legend, slider.
  302. const { type } = component;
  303. const paddingAreas = ['left', 'right', 'bottom', 'top'];
  304. if (typeof type !== 'string' || !type.startsWith('axis'))
  305. return;
  306. if (!paddingAreas.includes(position))
  307. return;
  308. // If padding is auto, use hide as the labelTransform by default
  309. // to avoid overlap between labels.
  310. component.transform = component.transform || [{ type: 'hide' }];
  311. const { labelFormatter, scales, title, tickCount, tickMethod, tickFilter } = component;
  312. const isVertical = position === 'left' || position === 'right';
  313. // Get styles to be applied.
  314. const style = styleOf(component, position, theme);
  315. const { tickLength = 0, labelSpacing = 0, titleSpacing = 0 } = style, rest = __rest(style, ["tickLength", "labelSpacing", "titleSpacing"]);
  316. // Init scale, the tickCount of axis has higher priority than scale.
  317. const [scaleOptions] = scales;
  318. if (tickCount !== undefined)
  319. scaleOptions.tickCount = tickCount;
  320. if (tickMethod !== undefined)
  321. scaleOptions.tickMethod = tickMethod;
  322. const scale = useScale(scaleOptions);
  323. // Get labels to be rendered.
  324. const labels = labelsOf(scale, labelFormatter, tickFilter);
  325. const labeStyle = subObject(rest, 'label');
  326. const labelBBoxes = labels.map((d, i) => {
  327. const normalizeStyle = Object.fromEntries(Object.entries(labeStyle).map(([key, value]) => [
  328. key,
  329. typeof value === 'function' ? value(d, i) : value,
  330. ]));
  331. // Auto padding should ignore transform for horizontal axis.
  332. if (!isVertical)
  333. normalizeStyle.transform = 'none';
  334. return computeLabelSize(d, normalizeStyle);
  335. });
  336. const maxLabelWidth = max(labelBBoxes, (d) => d.width);
  337. const paddingTick = tickLength + labelSpacing;
  338. if (isVertical) {
  339. component.size = maxLabelWidth + paddingTick;
  340. }
  341. else {
  342. // If the labels can't be placed horizontally,
  343. // rotate 90 deg to display them.
  344. if (overflowX(scale, labelBBoxes, crossSize, crossPadding, tickFilter)) {
  345. component.size = maxLabelWidth + paddingTick;
  346. component.style = Object.assign(Object.assign({}, component.style), { labelTransform: 'rotate(90)' });
  347. }
  348. else {
  349. const maxLabelHeight = max(labelBBoxes, (d) => d.height);
  350. component.size = maxLabelHeight + paddingTick;
  351. }
  352. }
  353. // Cache boxes to avoid computed twice.
  354. const I = labels.map((_, i) => i);
  355. component.indexBBox = new Map(I.map((i) => [i, [labels[i], labelBBoxes[i]]]));
  356. if (title === false || title === null || title === undefined)
  357. return;
  358. // Get title to be rendered.
  359. const titleStyle = subObject(rest, 'title');
  360. const titleText = Array.isArray(title) ? title.join(',') : title;
  361. const titleBBox = computeLabelSize(titleText, titleStyle);
  362. if (isVertical) {
  363. component.size += titleSpacing + titleBBox.width;
  364. }
  365. else {
  366. component.size += titleSpacing + titleBBox.height;
  367. }
  368. }
  369. function styleOf(axis, position, theme) {
  370. const { axis: baseStyle,
  371. // @ts-ignore
  372. [`axis${capitalizeFirst(position)}`]: positionStyle, } = theme;
  373. return deepMix({}, baseStyle, positionStyle, axis.style);
  374. }
  375. function ticksOf(scale, tickFilter) {
  376. const ticks = scale.getTicks ? scale.getTicks() : scale.getOptions().domain;
  377. if (!tickFilter)
  378. return ticks;
  379. return ticks.filter(tickFilter);
  380. }
  381. function labelsOf(scale, labelFormatter, tickFilter) {
  382. const T = ticksOf(scale, tickFilter);
  383. const ticks = T.map((d) => (typeof d === 'number' ? prettyNumber(d) : d));
  384. const formatter = labelFormatter
  385. ? typeof labelFormatter === 'string'
  386. ? format(labelFormatter)
  387. : labelFormatter
  388. : scale.getFormatter
  389. ? scale.getFormatter()
  390. : (d) => `${d}`;
  391. return ticks.map(formatter);
  392. }
  393. function offsetOf(scale, d) {
  394. if (!scale.getBandWidth)
  395. return 0;
  396. const offset = scale.getBandWidth(d) / 2;
  397. return offset;
  398. }
  399. function overflowX(scale, labelBBoxes, crossSize, crossPadding, tickFilter) {
  400. // If actual size bigger than container size, overflow.
  401. const totalSize = sum(labelBBoxes, (d) => d.width);
  402. if (totalSize > crossSize)
  403. return true;
  404. // Clone scale to get visual position for labels.
  405. const scaleX = scale.clone();
  406. scaleX.update({ range: [0, crossSize] });
  407. const ticks = ticksOf(scale, tickFilter);
  408. const X = ticks.map((d) => scaleX.map(d) + offsetOf(scaleX, d));
  409. const I = ticks.map((_, i) => i);
  410. const startX = -crossPadding[0];
  411. const endX = crossSize + crossPadding[1];
  412. const extent = (x, bbox) => {
  413. const { width } = bbox;
  414. return [x - width / 2, x + width / 2];
  415. };
  416. // Collision detection.
  417. for (let i = 0; i < I.length; i++) {
  418. const x = X[i];
  419. const [x0, x1] = extent(x, labelBBoxes[i]);
  420. // If a label is out of plot area, overflow.
  421. if (x0 < startX || x1 > endX)
  422. return true;
  423. const y = X[i + 1];
  424. if (y) {
  425. // If two labels intersect, overflow.
  426. const [y0] = extent(y, labelBBoxes[i + 1]);
  427. if (x1 > y0)
  428. return true;
  429. }
  430. }
  431. return false;
  432. }
  433. function computeLabelSize(d, style) {
  434. const shape = normalizeLabel(d);
  435. shape.attr(Object.assign(Object.assign({}, style), { visibility: 'none' }));
  436. const bbox = shape.getBBox();
  437. return bbox;
  438. }
  439. function normalizeLabel(d) {
  440. if (d instanceof DisplayObject)
  441. return d;
  442. return new Text({ style: { text: `${d}` } });
  443. }
  444. function prettyNumber(n) {
  445. return Math.abs(n) < 1e-15 ? n : parseFloat(n.toFixed(15));
  446. }
  447. //# sourceMappingURL=component.js.map