"use strict"; 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; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.computeComponentSize = exports.renderComponent = exports.inferComponent = void 0; const util_1 = require("@antv/util"); const d3_array_1 = require("d3-array"); const d3_format_1 = require("d3-format"); const g_1 = require("@antv/g"); const coordinate_1 = require("../coordinate"); const array_1 = require("../utils/array"); const helper_1 = require("../utils/helper"); const constant_1 = require("../component/constant"); const coordinate_2 = require("./coordinate"); const library_1 = require("./library"); const scale_1 = require("./scale"); const scale_2 = require("./types/scale"); function inferComponent(scales, partialOptions, library) { const { component: partialComponents = [], coordinates = [], title, theme, } = partialOptions; const [, createGuideComponent] = (0, library_1.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; } exports.inferComponent = inferComponent; function renderComponent(component, coordinate, theme, library, markState) { const [useGuideComponent] = (0, library_1.useLibrary)('component', library); const { scales: scaleDescriptors = [], bbox } = component, options = __rest(component, ["scales", "bbox"]); const scales = scaleDescriptors.map((descriptor) => (0, scale_1.useRelationScale)(descriptor, library)); const value = { bbox, scales: scaleDescriptors, library }; const render = useGuideComponent(options); return render({ coordinate, library, markState, scales, theme, value }); } exports.renderComponent = renderComponent; 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((0, d3_array_1.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 scale_2.ContinuousScale) return 'continuous'; if (type in scale_2.DiscreteScale) return 'discrete'; if (type in scale_2.DistributionScale) return 'distribution'; if (type in scale_2.ConstantScale) return 'constant'; return null; } const components = Array.from(scalesByField) .map(([channel, scs]) => { const combinations = (0, array_1.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 constant_1.LEGEND_INFER_STRATEGIES) { for (const { option, combination } of options) { if (accords.some((accord) => (0, util_1.isEqual)(sort(accord), sort(option)))) { return [componentType, combination]; } } } return null; }) .filter(helper_1.defined); return components; } function inferAxisComponentType(scales, coordinates) { return scales .map((scale) => { const { name } = scale; // todo wait for gui provide helix axis if ((0, coordinate_2.isHelix)(coordinates) || (0, coordinate_2.isTheta)(coordinates)) return null; if ((0, coordinate_2.isTranspose)(coordinates) && ((0, coordinate_2.isPolar)(coordinates) || (0, coordinate_2.isRadial)(coordinates))) return null; // infer axis if (name.startsWith('x')) { if ((0, coordinate_2.isPolar)(coordinates)) return ['axisArc', [scale]]; if ((0, coordinate_2.isRadial)(coordinates)) return ['axisLinear', [scale]]; return [(0, coordinate_2.isTranspose)(coordinates) ? 'axisY' : 'axisX', [scale]]; } if (name.startsWith('y')) { if ((0, coordinate_2.isPolar)(coordinates)) return ['axisLinear', [scale]]; if ((0, coordinate_2.isRadial)(coordinates)) return ['axisArc', [scale]]; return [(0, coordinate_2.isTranspose)(coordinates) ? 'axisX' : 'axisY', [scale]]; } if (name.startsWith('position')) { if ((0, coordinate_2.isRadar)(coordinates)) return ['axisRadar', [scale]]; if (!(0, coordinate_2.isPolar)(coordinates)) return ['axisY', [scale]]; } return null; }) .filter(helper_1.defined); } function inferComponentsType(scales, coordinates) { const availableScales = scales.filter((scale) => (0, scale_1.isValidScale)(scale)); return [ ...inferLegendComponentType(availableScales, coordinates), ...inferAxisComponentType(availableScales, coordinates), ]; } function angleOf(coordinates) { const polar = (0, coordinate_2.coordOf)(coordinates, 'polar'); if (polar.length) { const lastPolar = polar[polar.length - 1]; const { startAngle, endAngle } = (0, coordinate_1.getPolarOptions)(lastPolar); return [startAngle, endAngle]; } const radial = (0, coordinate_2.coordOf)(coordinates, 'radial'); if (radial.length) { const lastRadial = radial[radial.length - 1]; const { startAngle, endAngle } = (0, coordinate_1.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' && (0, coordinate_2.isParallel)(coordinates)) { // name looks like `position${number}` const index = matchPosition(name); if (index === null) return ordinalPosition; if ((0, coordinate_2.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 ((0, coordinate_2.isPolar)(coordinates)) return ['center', null]; if ((0, coordinate_2.isRadial)(coordinates)) return ['center', null]; if ((type === 'axisX' && (0, coordinate_2.isReflect)(coordinates)) || (type === 'axisX' && (0, coordinate_2.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') && (0, coordinate_2.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 (0, coordinate_2.isTranspose)(coordinates) ? `${type}Y` : `${type}X`; if (name === 'y') return (0, coordinate_2.isTranspose)(coordinates) ? `${type}X` : `${type}Y`; return null; } /** * Infer scrollable components, such as slider and scrollbar. */ function inferScrollableComponents(partialOptions, scales, library) { const [, createGuideComponent] = (0, library_1.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. function computeComponentSize(component, crossSize, crossPadding, position, theme, library) { const [useScale] = (0, library_1.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 = (0, helper_1.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 = (0, d3_array_1.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 = (0, d3_array_1.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 = (0, helper_1.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; } } exports.computeComponentSize = computeComponentSize; function styleOf(axis, position, theme) { const { axis: baseStyle, // @ts-ignore [`axis${(0, helper_1.capitalizeFirst)(position)}`]: positionStyle, } = theme; return (0, util_1.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' ? (0, d3_format_1.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 = (0, d3_array_1.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 g_1.DisplayObject) return d; return new g_1.Text({ style: { text: `${d}` } }); } function prettyNumber(n) { return Math.abs(n) < 1e-15 ? n : parseFloat(n.toFixed(15)); } //# sourceMappingURL=component.js.map