component.js 20 KB

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