@antv/g2
Version:
the Grammar of Graphics in Javascript
784 lines • 33.8 kB
JavaScript
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.styleOf = exports.computeTitleBBox = exports.computeLabelsBBox = exports.createScale = exports.computeComponentSize = exports.groupComponents = exports.flatComponents = exports.normalizeComponents = 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 number_1 = require("../utils/number");
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 { coordinates = [], title } = partialOptions;
const [, createGuideComponent] = (0, library_1.useLibrary)('component', library);
const displayedScales = scales.filter(({ guide }) => {
if (guide === null)
return false;
return true;
});
const components = [];
// Sliders and scrollbar component.
const sliders = inferScrollableComponents(partialOptions, scales, library);
components.push(...sliders);
// Title components.
if (title) {
const { props } = createGuideComponent('title');
const { defaultPosition, defaultOrientation, defaultOrder, defaultSize, defaultCrossPadding, } = props;
const titleOptions = typeof title === 'string' ? { title } : title;
components.push(Object.assign({ type: 'title', position: defaultPosition, orientation: defaultOrientation, order: defaultOrder, crossPadding: defaultCrossPadding[0], defaultSize }, titleOptions));
}
// Axis and legends.
const inferredComponents = inferComponentsType(displayedScales, coordinates);
inferredComponents.forEach(([type, relativeScales]) => {
const { props } = createGuideComponent(type);
const { defaultPosition, defaultPlane = 'xy', defaultOrientation, defaultSize, defaultOrder, defaultLength, defaultPadding: DP = [0, 0], defaultCrossPadding: DCP = [0, 0], } = props;
// @todo to be confirm if the scale can be merged.
// const scale: G2ScaleOptions = Object.assign({}, ...relativeScales);
const scale = (0, util_1.deepMix)({}, ...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 isVertical = position === 'left' || position === 'right';
const defaultPadding = isVertical ? DP[1] : DP[0];
const defaultCrossPadding = isVertical ? DCP[1] : DCP[0];
const { size, order = defaultOrder, length = defaultLength, padding = defaultPadding, crossPadding = defaultCrossPadding, } = partialGuide;
components.push(Object.assign(Object.assign({ title: field }, partialGuide), { defaultSize,
length,
position, plane: defaultPlane, orientation,
padding,
order,
crossPadding,
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 { scaleInstances: scales, scale, bbox } = component, options = __rest(component, ["scaleInstances", "scale", "bbox"]);
const value = { bbox, library };
const render = useGuideComponent(options);
return render({
coordinate,
library,
markState,
scales,
theme,
value,
scale,
});
}
exports.renderComponent = renderComponent;
function normalizeComponents(components) {
return components.map((d) => {
const component = (0, util_1.deepMix)(d, d.style);
delete component.style;
return component;
});
}
exports.normalizeComponents = normalizeComponents;
function flatComponents(components) {
return components.flatMap((d) => (d.type == 'group' ? d.children : d));
}
exports.flatComponents = flatComponents;
// Wrap legends into a group component.
function groupComponents(components, crossSize) {
// Group components by key.
const P = ['left', 'right', 'bottom', 'top'];
const key = ({ type, position, group }) => {
if (!P.includes(position))
return Symbol('independent');
if (group === undefined) {
if (type.startsWith('legend'))
return `legend-${position}`;
return Symbol('independent');
}
if (group === 'independent')
return Symbol('independent');
return group;
};
const grouped = (0, d3_array_1.groups)(components, key);
// Update attributes of group components,
// and maybe flatten group components without enough room.
return grouped.flatMap(([, components]) => {
if (components.length === 1)
return components[0];
// If crossSize defined, group components only when has
// enough room.
if (crossSize !== undefined) {
// Compute total length.
const DL = components
.filter((d) => d.length !== undefined)
.map((d) => d.length);
const totalLength = (0, d3_array_1.sum)(DL);
// If there is no enough room for components,
// do not group.
if (totalLength > crossSize) {
components.forEach((d) => (d.group = Symbol('independent')));
return components;
}
// Group legends and update legend length.
const emptyLength = crossSize - totalLength;
const emptyCount = components.length - DL.length;
const length = emptyLength / emptyCount;
components.forEach((d) => {
if (d.length !== undefined)
return;
d.length = length;
});
}
// Create a group component.
const size = (0, d3_array_1.max)(components, (d) => d.size);
const order = (0, d3_array_1.max)(components, (d) => d.order);
const crossPadding = (0, d3_array_1.max)(components, (d) => d.crossPadding);
const position = components[0].position;
return {
type: 'group',
size,
order,
position,
children: components,
crossPadding,
};
});
}
exports.groupComponents = groupComponents;
function inferLegendComponentType(scales, coordinates) {
// Filter accepts scales.
const channels = ['shape', 'size', 'color', 'opacity'];
const isConstantSize = (type, name) => type === 'constant' && name === 'size';
const accepts = scales.filter(({ type, name }) => typeof type === 'string' &&
channels.includes(name) &&
!isConstantSize(type, name));
// Group scales by fields.
const constants = accepts.filter(({ type }) => type === 'constant');
const nonConstants = accepts.filter(({ type }) => type !== 'constant');
const groupKey = (d) => (d.field ? d.field : Symbol('independent'));
const fieldScales = (0, d3_array_1.groups)(nonConstants, groupKey)
.map(([key, scales]) => [key, [...scales, ...constants]])
.filter(([, scales]) => scales.some((scale) => scale.type !== 'constant'));
const scalesByField = new Map(fieldScales);
// Skip empty scales.
if (scalesByField.size === 0)
return [];
// Infer components.
const sort = (arr) => arr.sort(([a], [b]) => a.localeCompare(b));
const components = Array.from(scalesByField)
.map(([, 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)]),
}));
// For category legend.
for (const { option, combination } of options) {
// If every scale is constant, do not display legend.
if (option.every((d) => d[1] === 'constant'))
continue;
if (option.every((d) => d[1] === 'discrete' || d[1] === 'constant')) {
return ['legendCategory', combination];
}
}
// For reset legend.
// @todo Remove this.
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 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;
}
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]];
}
// Only support linear axis for z.
if (name.startsWith('z')) {
return ['axisZ', [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];
}
if (type === 'axisY' && (0, coordinate_2.isParallel)(coordinates)) {
return (0, coordinate_2.isTranspose)(coordinates)
? ['center', 'horizontal']
: ['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, defaultCrossPadding: [crossPadding], } = props;
return Object.assign(Object.assign({ position: defaultPosition, defaultSize, order: defaultOrder, type: componentType, crossPadding }, 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.
function computeComponentSize(component, crossSize, crossPadding, position, theme, library) {
// Only compute and update size of components in padding area.
const { type } = component;
const paddingAreas = ['left', 'right', 'bottom', 'top'];
if (!paddingAreas.includes(position))
return;
if (typeof type !== 'string')
return;
const t = type;
const createCompute = () => {
if (t.startsWith('axis'))
return computeAxisSize;
if (t.startsWith('group'))
return computeGroupSize;
if (t.startsWith('legendContinuous'))
return computeContinuousLegendSize;
if (t === 'legendCategory')
return computeCategoryLegendSize;
if (t.startsWith('slider'))
return computeSliderSize;
if (t === 'title')
return computeTitleSize;
if (t.startsWith('scrollbar'))
return computeScrollbarSize;
return () => { };
};
return createCompute()(component, crossSize, crossPadding, position, theme, library);
}
exports.computeComponentSize = computeComponentSize;
function computeGroupSize(component, crossSize, crossPadding, position, theme, library) {
const { children } = component;
const maxCrossPadding = (0, d3_array_1.max)(children, (d) => d.crossPadding);
children.forEach((d) => (d.crossPadding = maxCrossPadding));
children.forEach((child) => computeComponentSize(child, crossSize, crossPadding, position, theme, library));
const maxSize = (0, d3_array_1.max)(children, (d) => d.size);
component.size = maxSize;
children.forEach((d) => (d.size = maxSize));
}
function computeScrollbarSize(component, crossSize, crossPadding, position, theme, library) {
const { trackSize = 6 } = (0, util_1.deepMix)({}, theme.scrollbar, component);
component.size = trackSize;
}
function computeTitleSize(component, crossSize, crossPadding, position, theme, library) {
const _a = (0, util_1.deepMix)({}, theme.title, component), { title, subtitle, spacing = 0 } = _a, style = __rest(_a, ["title", "subtitle", "spacing"]);
if (title) {
const titleStyle = (0, helper_1.subObject)(style, 'title');
const titleBBox = computeLabelSize(title, titleStyle);
component.size = titleBBox.height;
}
if (subtitle) {
const subtitleStyle = (0, helper_1.subObject)(style, 'subtitle');
const subtitleBBox = computeLabelSize(subtitle, subtitleStyle);
component.size += spacing + subtitleBBox.height;
}
}
function computeSliderSize(component, crossSize, crossPadding, position, theme, library) {
const styleOf = () => {
const { slider } = theme;
return (0, util_1.deepMix)({}, slider, component);
};
const { trackSize, handleIconSize } = styleOf();
const size = Math.max(trackSize, handleIconSize * 2.4);
component.size = size;
}
function computeAxisSize(component, crossSize, crossPadding, position, theme, library) {
var _a;
// If padding is auto, use hide as the labelTransform by default
// to avoid overlap between labels.
component.transform = component.transform || [{ type: 'hide' }];
// Vertical or horizontal.
const isVertical = position === 'left' || position === 'right';
// Get styles to be applied.
const style = styleOf(component, position, theme);
const { tickLength = 0, labelSpacing = 0, titleSpacing = 0, labelAutoRotate } = style, rest = __rest(style, ["tickLength", "labelSpacing", "titleSpacing", "labelAutoRotate"]);
// Compute Labels.
const scale = createScale(component, library);
const labelBBoxes = computeLabelsBBox(rest, scale);
const paddingTick = tickLength + labelSpacing;
if (labelBBoxes && labelBBoxes.length) {
const maxLabelWidth = (0, d3_array_1.max)(labelBBoxes, (d) => d.width);
const maxLabelHeight = (0, d3_array_1.max)(labelBBoxes, (d) => d.height);
if (isVertical) {
component.size = maxLabelWidth + paddingTick;
}
else {
const { tickFilter, labelTransform } = component;
// If the labels can't be placed horizontally, and labelTransform is unset,
// rotate 90 deg to display them.
if (overflowX(scale, labelBBoxes, crossSize, crossPadding, tickFilter) &&
!labelTransform &&
labelAutoRotate !== false &&
labelAutoRotate !== null) {
component.labelTransform = 'rotate(90)';
component.size = maxLabelWidth + paddingTick;
}
else {
component.labelTransform = (_a = component.labelTransform) !== null && _a !== void 0 ? _a : 'rotate(0)';
component.size = maxLabelHeight + paddingTick;
}
}
}
else {
component.size = tickLength;
}
// Compute title.
const titleBBox = computeTitleBBox(rest);
if (titleBBox) {
if (isVertical) {
component.size += titleSpacing + titleBBox.width;
}
else {
component.size += titleSpacing + titleBBox.height;
}
}
}
function computeContinuousLegendSize(component, crossSize, crossPadding, position, theme, library) {
// Get styles.
const styleOf = () => {
const { legendContinuous } = theme;
return (0, util_1.deepMix)({}, legendContinuous, component);
};
const _a = styleOf(), { labelSpacing = 0, titleSpacing = 0 } = _a, rest = __rest(_a, ["labelSpacing", "titleSpacing"]);
// Vertical or horizontal.
const isVertical = position === 'left' || position === 'right';
// Ribbon styles.
const ribbonStyles = (0, helper_1.subObject)(rest, 'ribbon');
const { size: ribbonSize } = ribbonStyles;
const handleIconStyles = (0, helper_1.subObject)(rest, 'handleIcon');
const { size: handleIconSize } = handleIconStyles;
const mainSize = Math.max(ribbonSize, handleIconSize * 2.4);
component.size = mainSize;
// Compute labels.
const scale = createScale(component, library);
const labelBBoxes = computeLabelsBBox(rest, scale);
if (labelBBoxes) {
const key = isVertical ? 'width' : 'height';
const size = (0, d3_array_1.max)(labelBBoxes, (d) => d[key]);
component.size += size + labelSpacing;
}
// Compute title.
const titleBBox = computeTitleBBox(rest);
if (titleBBox) {
if (isVertical) {
component.size = Math.max(component.size, titleBBox.width);
}
else {
component.size += titleSpacing + titleBBox.height;
}
}
}
function computeCategoryLegendSize(component, crossSize0, crossPadding, position, theme, library) {
const styleOf = () => {
const { legendCategory } = theme;
const { title } = component;
const [defaultTitle, specifiedTitle] = Array.isArray(title)
? [title, undefined]
: [undefined, title];
return (0, util_1.deepMix)({ title: defaultTitle }, legendCategory, Object.assign(Object.assign({}, component), { title: specifiedTitle }));
};
const _a = styleOf(), { itemSpacing, itemMarkerSize, titleSpacing, rowPadding, colPadding, maxCols = Infinity, maxRows = Infinity } = _a, rest = __rest(_a, ["itemSpacing", "itemMarkerSize", "titleSpacing", "rowPadding", "colPadding", "maxCols", "maxRows"]);
const { cols, length } = component;
const getRows = (rows) => Math.min(rows, maxRows);
const getCols = (cols) => Math.min(cols, maxCols);
// Vertical or horizontal.
const isVertical = position === 'left' || position === 'right';
const crossSize = length === undefined
? crossSize0 + (isVertical ? 0 : crossPadding[0] + crossPadding[1])
: length;
// Compute title.
const titleBBox = computeTitleBBox(rest);
const scale = createScale(component, library);
const labelBBoxes = computeLabelsBBox(rest, scale, 'itemLabel');
const height = Math.max(labelBBoxes[0].height, itemMarkerSize) + rowPadding;
const widthOf = (w, padding = 0) => itemMarkerSize + w + itemSpacing[0] + padding;
// Only support grid layout for vertical area.
const computeVerticalSize = () => {
let maxSize = -Infinity;
let pos = 0;
let cols = 1;
let rows = 0;
let maxRows = -Infinity;
let maxPos = -Infinity;
const titleHeight = titleBBox ? titleBBox.height : 0;
const maxHeight = crossSize - titleHeight;
for (const { width } of labelBBoxes) {
const w = widthOf(width, colPadding);
maxSize = Math.max(maxSize, w);
if (pos + height > maxHeight) {
cols++;
maxRows = Math.max(maxRows, rows);
maxPos = Math.max(maxPos, pos);
rows = 1;
pos = height;
}
else {
pos += height;
rows++;
}
}
if (cols <= 1) {
maxRows = rows;
maxPos = pos;
}
component.size = maxSize * getCols(cols);
component.length = maxPos + titleHeight;
(0, util_1.deepMix)(component, { cols: getCols(cols), gridRow: maxRows });
};
// Horizontal grid layout.
const computeHorizontalGrid = () => {
const rows = Math.ceil(labelBBoxes.length / cols);
const maxWidth = (0, d3_array_1.max)(labelBBoxes, (d) => widthOf(d.width)) * cols;
component.size = height * getRows(rows) - rowPadding;
component.length = Math.min(maxWidth, crossSize);
};
// Horizontal flex layout.
const computeHorizontalFlex = () => {
let rows = 1;
let pos = 0;
let maxPos = -Infinity;
for (const { width } of labelBBoxes) {
const w = widthOf(width, colPadding);
if (pos + w > crossSize) {
maxPos = Math.max(maxPos, pos);
pos = w;
rows++;
}
else {
pos += w;
}
}
if (rows === 1)
maxPos = pos;
component.size = height * getRows(rows) - rowPadding;
component.length = maxPos;
};
if (isVertical)
computeVerticalSize();
else if (typeof cols === 'number')
computeHorizontalGrid();
else
computeHorizontalFlex();
// Compute titles.
if (titleBBox) {
if (isVertical) {
component.size = Math.max(component.size, titleBBox.width);
}
else {
component.size += titleSpacing + titleBBox.height;
}
}
}
function createScale(component, library) {
const [useScale] = (0, library_1.useLibrary)('scale', library);
// Init scale, the tickCount of axis has higher priority than scale.
const { scales, tickCount, tickMethod } = component;
const scaleOptions = scales.find((d) => d.type !== 'constant' && d.type !== 'identity');
if (tickCount !== undefined)
scaleOptions.tickCount = tickCount;
if (tickMethod !== undefined)
scaleOptions.tickMethod = tickMethod;
return useScale(scaleOptions);
}
exports.createScale = createScale;
function computeLabelsBBox(component, scale, key = 'label') {
const { labelFormatter, tickFilter, label = true } = component, style = __rest(component, ["labelFormatter", "tickFilter", "label"]);
if (!label)
return null;
// Get labels to be rendered.
const labels = labelsOf(scale, labelFormatter, tickFilter);
const labelStyle = (0, helper_1.subObject)(style, key);
const labelStyles = labels.map((d, i) => Object.fromEntries(Object.entries(labelStyle).map(([key, value]) => [
key,
typeof value === 'function' ? value(d, i) : value,
])));
const labelBBoxes = labels.map((d, i) => {
const normalizeStyle = labelStyles[i];
return computeLabelSize(d, normalizeStyle);
});
// Cache boxes to avoid computed twice.
// @todo GUI use untransformed bbox, so it can't cache if
// label.style has transform attributes.
const hasTransform = labelStyles.some((d) => d.transform);
if (!hasTransform) {
const I = labels.map((_, i) => i);
component.indexBBox = new Map(I.map((i) => [i, [labels[i], labelBBoxes[i]]]));
}
return labelBBoxes;
}
exports.computeLabelsBBox = computeLabelsBBox;
function computeTitleBBox(component) {
const isFalsy = (x) => x === false || x === null;
const { title } = component, style = __rest(component, ["title"]);
if (isFalsy(title) || title === undefined)
return null;
const titleStyle = (0, helper_1.subObject)(style, 'title');
const { direction, transform } = titleStyle;
const titleText = Array.isArray(title) ? title.join(',') : title;
if (typeof titleText !== 'string')
return null;
const titleBBox = computeLabelSize(titleText, Object.assign(Object.assign({}, titleStyle), { transform: transform || (direction === 'vertical' ? 'rotate(-90)' : '') }));
return titleBBox;
}
exports.computeTitleBBox = computeTitleBBox;
function styleOf(axis, position, theme) {
const { title } = axis;
const [defaultTitle, specifiedTitle] = Array.isArray(title)
? [title, undefined]
: [undefined, title];
const { axis: baseStyle,
// @ts-ignore
[`axis${(0, helper_1.capitalizeFirst)(position)}`]: positionStyle, } = theme;
return (0, util_1.deepMix)({ title: defaultTitle }, baseStyle, positionStyle, Object.assign(Object.assign({}, axis), { title: specifiedTitle }));
}
exports.styleOf = styleOf;
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' ? (0, number_1.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);
const { filter } = style, rest = __rest(style, ["filter"]);
shape.attr(Object.assign(Object.assign({}, rest), { 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}` } });
}
//# sourceMappingURL=component.js.map
;