| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447 |
- var __rest = (this && this.__rest) || function (s, e) {
- var t = {};
- for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
- t[p] = s[p];
- if (s != null && typeof Object.getOwnPropertySymbols === "function")
- for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
- if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
- t[p[i]] = s[p[i]];
- }
- return t;
- };
- import { deepMix, isEqual } from '@antv/util';
- import { group, max, sum } from 'd3-array';
- import { format } from 'd3-format';
- import { DisplayObject, Text } from '@antv/g';
- import { getPolarOptions, getRadialOptions, } from '../coordinate';
- import { combine } from '../utils/array';
- import { capitalizeFirst, defined, subObject } from '../utils/helper';
- import { LEGEND_INFER_STRATEGIES } from '../component/constant';
- import { coordOf, isHelix, isParallel, isPolar, isRadar, isRadial, isReflect, isReflectY, isTheta, isTranspose, } from './coordinate';
- import { useLibrary } from './library';
- import { isValidScale, useRelationScale } from './scale';
- import { ConstantScale, ContinuousScale, DiscreteScale, DistributionScale, } from './types/scale';
- export function inferComponent(scales, partialOptions, library) {
- const { component: partialComponents = [], coordinates = [], title, theme, } = partialOptions;
- const [, createGuideComponent] = useLibrary('component', library);
- const displayedScales = scales.filter(({ guide, name }) => {
- if (guide === null)
- return false;
- return true;
- });
- const sliders = inferScrollableComponents(partialOptions, scales, library);
- const components = [...partialComponents, ...sliders];
- if (title) {
- const { props } = createGuideComponent('title');
- const { defaultPosition, defaultOrientation, defaultOrder, defaultSize } = props;
- const titleOptions = typeof title === 'string' ? { title } : title;
- components.push(Object.assign({ type: 'title', position: defaultPosition, orientation: defaultOrientation, order: defaultOrder, size: defaultSize }, titleOptions));
- }
- const inferredComponents = inferComponentsType(displayedScales, coordinates);
- inferredComponents.forEach(([type, relativeScales]) => {
- const { props } = createGuideComponent(type);
- const { defaultPosition, defaultOrientation, defaultSize, defaultOrder } = props;
- // @todo to be confirm if the scale can be merged.
- const scale = Object.assign({}, ...relativeScales);
- const { guide: guideOptions, field } = scale;
- // A scale may have multiple guides.
- const guides = Array.isArray(guideOptions) ? guideOptions : [guideOptions];
- for (const partialGuide of guides) {
- const [position, orientation] = inferComponentPositionAndOrientation(type, defaultPosition, defaultOrientation, partialGuide, relativeScales, displayedScales, coordinates);
- // Skip if position and orientation are not specified.
- // @example the last axis of radar chart
- if (!position && !orientation)
- continue;
- const { size = defaultSize, order = defaultOrder } = partialGuide;
- components.push(Object.assign(Object.assign({ title: field }, partialGuide), { position,
- orientation,
- order,
- size,
- type, scales: relativeScales }));
- }
- });
- return components;
- }
- export function renderComponent(component, coordinate, theme, library, markState) {
- const [useGuideComponent] = useLibrary('component', library);
- const { scales: scaleDescriptors = [], bbox } = component, options = __rest(component, ["scales", "bbox"]);
- const scales = scaleDescriptors.map((descriptor) => useRelationScale(descriptor, library));
- const value = { bbox, scales: scaleDescriptors, library };
- const render = useGuideComponent(options);
- return render({ coordinate, library, markState, scales, theme, value });
- }
- function inferLegendComponentType(scales, coordinates) {
- const acceptScales = scales
- .filter((scale) => typeof scale.type === 'string'
- ? ['shape', 'size', 'color', 'opacity'].includes(scale.name)
- : true)
- // not support constant size scale
- .filter((scale) => !(scale.type === 'constant' && scale.name === 'size'));
- // scale with undefined field
- const undefinedScales = acceptScales.filter((scale) => !scale.field);
- const definedScales = acceptScales.filter((scale) => !!scale.field);
- // exclude the scales that all type are constant
- const scalesByField = new Map(Array.from(group(definedScales, (d) => d.field))
- .map(([field, scales]) => [
- field,
- [
- ...scales,
- ...undefinedScales.filter((scale) => scale.type === 'constant'),
- ],
- ])
- .concat([[undefined, undefinedScales]])
- .filter(([field, scales]) => scales.some((scale) => scale.type !== 'constant')));
- if (scalesByField.size === 0)
- return [];
- function getScaleType(scale) {
- const { type } = scale;
- if (typeof type !== 'string')
- return null;
- if (type in ContinuousScale)
- return 'continuous';
- if (type in DiscreteScale)
- return 'discrete';
- if (type in DistributionScale)
- return 'distribution';
- if (type in ConstantScale)
- return 'constant';
- return null;
- }
- const components = Array.from(scalesByField)
- .map(([channel, scs]) => {
- const combinations = combine(scs).sort((a, b) => b.length - a.length);
- const options = combinations.map((combination) => ({
- combination,
- option: combination.map((scale) => [scale.name, getScaleType(scale)]),
- }));
- const sort = (arr) => arr.sort((a, b) => a[0].localeCompare(b[0]));
- for (const [componentType, accords] of LEGEND_INFER_STRATEGIES) {
- for (const { option, combination } of options) {
- if (accords.some((accord) => isEqual(sort(accord), sort(option)))) {
- return [componentType, combination];
- }
- }
- }
- return null;
- })
- .filter(defined);
- return components;
- }
- function inferAxisComponentType(scales, coordinates) {
- return scales
- .map((scale) => {
- const { name } = scale;
- // todo wait for gui provide helix axis
- if (isHelix(coordinates) || isTheta(coordinates))
- return null;
- if (isTranspose(coordinates) &&
- (isPolar(coordinates) || isRadial(coordinates)))
- return null;
- // infer axis
- if (name.startsWith('x')) {
- if (isPolar(coordinates))
- return ['axisArc', [scale]];
- if (isRadial(coordinates))
- return ['axisLinear', [scale]];
- return [isTranspose(coordinates) ? 'axisY' : 'axisX', [scale]];
- }
- if (name.startsWith('y')) {
- if (isPolar(coordinates))
- return ['axisLinear', [scale]];
- if (isRadial(coordinates))
- return ['axisArc', [scale]];
- return [isTranspose(coordinates) ? 'axisX' : 'axisY', [scale]];
- }
- if (name.startsWith('position')) {
- if (isRadar(coordinates))
- return ['axisRadar', [scale]];
- if (!isPolar(coordinates))
- return ['axisY', [scale]];
- }
- return null;
- })
- .filter(defined);
- }
- function inferComponentsType(scales, coordinates) {
- const availableScales = scales.filter((scale) => isValidScale(scale));
- return [
- ...inferLegendComponentType(availableScales, coordinates),
- ...inferAxisComponentType(availableScales, coordinates),
- ];
- }
- function angleOf(coordinates) {
- const polar = coordOf(coordinates, 'polar');
- if (polar.length) {
- const lastPolar = polar[polar.length - 1];
- const { startAngle, endAngle } = getPolarOptions(lastPolar);
- return [startAngle, endAngle];
- }
- const radial = coordOf(coordinates, 'radial');
- if (radial.length) {
- const lastRadial = radial[radial.length - 1];
- const { startAngle, endAngle } = getRadialOptions(lastRadial);
- return [startAngle, endAngle];
- }
- return [-Math.PI / 2, (Math.PI / 2) * 3];
- }
- /**
- * match index of position
- */
- function matchPosition(name) {
- const match = /position(\d*)/g.exec(name);
- if (!match)
- return null;
- return +match[1];
- }
- function inferAxisPositionAndOrientation(type, ordinalPosition, relativeScales, scales, coordinates) {
- // a axis only has one scale
- const { name } = relativeScales[0];
- // todo, in current resolution, the radar chart is implement by parallel + polar coordinate.
- // implementation plan to be confirmed.
- // in current implementation, it must to add the first position encode to it's last.
- // so we won't render the last axis repeatably.
- if (type === 'axisRadar') {
- const positions = scales.filter((scale) => scale.name.startsWith('position'));
- const index = matchPosition(name);
- if (name === positions.slice(-1)[0].name || index === null)
- return [null, null];
- // infer radar axis orientation
- const [startAngle, endAngle] = angleOf(coordinates);
- const angle = ((endAngle - startAngle) / (positions.length - 1)) * index + startAngle;
- return ['center', angle];
- }
- // There are multiple axes for parallel coordinate.
- // Place the first one in the border area and put others in the center.
- if (type === 'axisY' && isParallel(coordinates)) {
- // name looks like `position${number}`
- const index = matchPosition(name);
- if (index === null)
- return ordinalPosition;
- if (isTranspose(coordinates)) {
- return index === 0 ? ['top', null] : ['center', 'horizontal'];
- }
- return index === 0 ? ordinalPosition : ['center', 'vertical'];
- }
- // in non-cartesian coordinate systems, infer the arc axis angle
- if (type === 'axisLinear') {
- const [startAngle] = angleOf(coordinates);
- return ['center', startAngle];
- }
- if (type === 'axisArc') {
- if (ordinalPosition[0] === 'inner')
- return ['inner', null];
- return ['outer', null];
- }
- if (isPolar(coordinates))
- return ['center', null];
- if (isRadial(coordinates))
- return ['center', null];
- if ((type === 'axisX' && isReflect(coordinates)) ||
- (type === 'axisX' && isReflectY(coordinates))) {
- return ['top', null];
- }
- // if (type === 'axisX') return ['bottom', null];
- return ordinalPosition;
- }
- // @todo Infer position by coordinates.
- function inferComponentPositionAndOrientation(type, defaultPosition, defaultOrientation, guide, relativeScales, scales, coordinates) {
- const [startAngle] = angleOf(coordinates);
- const ordinalPositionAndOrientation = [
- guide.position || defaultPosition,
- startAngle !== null && startAngle !== void 0 ? startAngle : defaultOrientation,
- ];
- if (typeof type === 'string' && type.startsWith('axis')) {
- return inferAxisPositionAndOrientation(type, ordinalPositionAndOrientation, relativeScales, scales, coordinates);
- }
- if (typeof type === 'string' &&
- type.startsWith('legend') &&
- isPolar(coordinates)) {
- if (guide.position === 'center')
- return ['center', 'vertical'];
- }
- // for general component, use default position
- return ordinalPositionAndOrientation;
- }
- function inferScrollableType(name, type, coordinates = []) {
- if (name === 'x')
- return isTranspose(coordinates) ? `${type}Y` : `${type}X`;
- if (name === 'y')
- return isTranspose(coordinates) ? `${type}X` : `${type}Y`;
- return null;
- }
- /**
- * Infer scrollable components, such as slider and scrollbar.
- */
- function inferScrollableComponents(partialOptions, scales, library) {
- const [, createGuideComponent] = useLibrary('component', library);
- const { coordinates } = partialOptions;
- function normalized(type, channelName, scale, options) {
- const componentType = inferScrollableType(channelName, type, coordinates);
- if (!options || !componentType)
- return;
- const { props } = createGuideComponent(componentType);
- const { defaultPosition, defaultSize, defaultOrder } = props;
- return Object.assign(Object.assign({ position: defaultPosition, size: defaultSize, order: defaultOrder, type: componentType }, options), { scales: [scale] });
- }
- return scales
- .filter((d) => d.slider || d.scrollbar)
- .flatMap((scale) => {
- const { slider, scrollbar, name: channelName } = scale;
- return [
- normalized('slider', channelName, scale, slider),
- normalized('scrollbar', channelName, scale, scrollbar),
- ];
- })
- .filter((d) => !!d);
- }
- // !!! Note Mutate component.size and component.style.
- export function computeComponentSize(component, crossSize, crossPadding, position, theme, library) {
- const [useScale] = useLibrary('scale', library);
- // Only compute and update size of axis component in padding area.
- // @todo Legend, slider.
- const { type } = component;
- const paddingAreas = ['left', 'right', 'bottom', 'top'];
- if (typeof type !== 'string' || !type.startsWith('axis'))
- return;
- if (!paddingAreas.includes(position))
- return;
- // If padding is auto, use hide as the labelTransform by default
- // to avoid overlap between labels.
- component.transform = component.transform || [{ type: 'hide' }];
- const { labelFormatter, scales, title, tickCount, tickMethod, tickFilter } = component;
- const isVertical = position === 'left' || position === 'right';
- // Get styles to be applied.
- const style = styleOf(component, position, theme);
- const { tickLength = 0, labelSpacing = 0, titleSpacing = 0 } = style, rest = __rest(style, ["tickLength", "labelSpacing", "titleSpacing"]);
- // Init scale, the tickCount of axis has higher priority than scale.
- const [scaleOptions] = scales;
- if (tickCount !== undefined)
- scaleOptions.tickCount = tickCount;
- if (tickMethod !== undefined)
- scaleOptions.tickMethod = tickMethod;
- const scale = useScale(scaleOptions);
- // Get labels to be rendered.
- const labels = labelsOf(scale, labelFormatter, tickFilter);
- const labeStyle = subObject(rest, 'label');
- const labelBBoxes = labels.map((d, i) => {
- const normalizeStyle = Object.fromEntries(Object.entries(labeStyle).map(([key, value]) => [
- key,
- typeof value === 'function' ? value(d, i) : value,
- ]));
- // Auto padding should ignore transform for horizontal axis.
- if (!isVertical)
- normalizeStyle.transform = 'none';
- return computeLabelSize(d, normalizeStyle);
- });
- const maxLabelWidth = max(labelBBoxes, (d) => d.width);
- const paddingTick = tickLength + labelSpacing;
- if (isVertical) {
- component.size = maxLabelWidth + paddingTick;
- }
- else {
- // If the labels can't be placed horizontally,
- // rotate 90 deg to display them.
- if (overflowX(scale, labelBBoxes, crossSize, crossPadding, tickFilter)) {
- component.size = maxLabelWidth + paddingTick;
- component.style = Object.assign(Object.assign({}, component.style), { labelTransform: 'rotate(90)' });
- }
- else {
- const maxLabelHeight = max(labelBBoxes, (d) => d.height);
- component.size = maxLabelHeight + paddingTick;
- }
- }
- // Cache boxes to avoid computed twice.
- const I = labels.map((_, i) => i);
- component.indexBBox = new Map(I.map((i) => [i, [labels[i], labelBBoxes[i]]]));
- if (title === false || title === null || title === undefined)
- return;
- // Get title to be rendered.
- const titleStyle = subObject(rest, 'title');
- const titleText = Array.isArray(title) ? title.join(',') : title;
- const titleBBox = computeLabelSize(titleText, titleStyle);
- if (isVertical) {
- component.size += titleSpacing + titleBBox.width;
- }
- else {
- component.size += titleSpacing + titleBBox.height;
- }
- }
- function styleOf(axis, position, theme) {
- const { axis: baseStyle,
- // @ts-ignore
- [`axis${capitalizeFirst(position)}`]: positionStyle, } = theme;
- return deepMix({}, baseStyle, positionStyle, axis.style);
- }
- function ticksOf(scale, tickFilter) {
- const ticks = scale.getTicks ? scale.getTicks() : scale.getOptions().domain;
- if (!tickFilter)
- return ticks;
- return ticks.filter(tickFilter);
- }
- function labelsOf(scale, labelFormatter, tickFilter) {
- const T = ticksOf(scale, tickFilter);
- const ticks = T.map((d) => (typeof d === 'number' ? prettyNumber(d) : d));
- const formatter = labelFormatter
- ? typeof labelFormatter === 'string'
- ? format(labelFormatter)
- : labelFormatter
- : scale.getFormatter
- ? scale.getFormatter()
- : (d) => `${d}`;
- return ticks.map(formatter);
- }
- function offsetOf(scale, d) {
- if (!scale.getBandWidth)
- return 0;
- const offset = scale.getBandWidth(d) / 2;
- return offset;
- }
- function overflowX(scale, labelBBoxes, crossSize, crossPadding, tickFilter) {
- // If actual size bigger than container size, overflow.
- const totalSize = sum(labelBBoxes, (d) => d.width);
- if (totalSize > crossSize)
- return true;
- // Clone scale to get visual position for labels.
- const scaleX = scale.clone();
- scaleX.update({ range: [0, crossSize] });
- const ticks = ticksOf(scale, tickFilter);
- const X = ticks.map((d) => scaleX.map(d) + offsetOf(scaleX, d));
- const I = ticks.map((_, i) => i);
- const startX = -crossPadding[0];
- const endX = crossSize + crossPadding[1];
- const extent = (x, bbox) => {
- const { width } = bbox;
- return [x - width / 2, x + width / 2];
- };
- // Collision detection.
- for (let i = 0; i < I.length; i++) {
- const x = X[i];
- const [x0, x1] = extent(x, labelBBoxes[i]);
- // If a label is out of plot area, overflow.
- if (x0 < startX || x1 > endX)
- return true;
- const y = X[i + 1];
- if (y) {
- // If two labels intersect, overflow.
- const [y0] = extent(y, labelBBoxes[i + 1]);
- if (x1 > y0)
- return true;
- }
- }
- return false;
- }
- function computeLabelSize(d, style) {
- const shape = normalizeLabel(d);
- shape.attr(Object.assign(Object.assign({}, style), { visibility: 'none' }));
- const bbox = shape.getBBox();
- return bbox;
- }
- function normalizeLabel(d) {
- if (d instanceof DisplayObject)
- return d;
- return new Text({ style: { text: `${d}` } });
- }
- function prettyNumber(n) {
- return Math.abs(n) < 1e-15 ? n : parseFloat(n.toFixed(15));
- }
- //# sourceMappingURL=component.js.map
|