UNPKG

@antv/g2

Version:

the Grammar of Graphics in Javascript

589 lines 23.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.placeComponents = exports.computeRoughPlotSize = exports.computeLayout = exports.processAxisZ = void 0; const d3_array_1 = require("d3-array"); const util_1 = require("@antv/util"); const coordinate_1 = require("../utils/coordinate"); const helper_1 = require("../utils/helper"); const array_1 = require("../utils/array"); const string_1 = require("../utils/string"); const component_1 = require("./component"); const coordinate_2 = require("./coordinate"); function processAxisZ(components) { const axisX = components.find(({ type }) => type === 'axisX'); const axisY = components.find(({ type }) => type === 'axisY'); const axisZ = components.find(({ type }) => type === 'axisZ'); if (axisX && axisY && axisZ) { axisX.plane = 'xy'; axisY.plane = 'xy'; axisZ.plane = 'yz'; axisZ.origin = [axisX.bbox.x, axisX.bbox.y, 0]; axisZ.eulerAngles = [0, -90, 0]; axisZ.bbox.x = axisX.bbox.x; axisZ.bbox.y = axisX.bbox.y; components.push(Object.assign(Object.assign({}, axisX), { plane: 'xz', showLabel: false, showTitle: false, origin: [axisX.bbox.x, axisX.bbox.y, 0], eulerAngles: [-90, 0, 0] })); components.push(Object.assign(Object.assign({}, axisY), { plane: 'yz', showLabel: false, showTitle: false, origin: [axisY.bbox.x + axisY.bbox.width, axisY.bbox.y, 0], eulerAngles: [0, -90, 0] })); components.push(Object.assign(Object.assign({}, axisZ), { plane: 'xz', actualPosition: 'left', showLabel: false, showTitle: false, eulerAngles: [90, -90, 0] })); } } exports.processAxisZ = processAxisZ; function computeLayout(components, options, theme, library) { var _a, _b; const { width, height, depth, x = 0, y = 0, z = 0, inset = (_a = theme.inset) !== null && _a !== void 0 ? _a : 0, insetLeft = inset, insetTop = inset, insetBottom = inset, insetRight = inset, margin = (_b = theme.margin) !== null && _b !== void 0 ? _b : 0, marginLeft = margin, marginBottom = margin, marginTop = margin, marginRight = margin, padding = theme.padding, paddingBottom = padding, paddingLeft = padding, paddingRight = padding, paddingTop = padding, } = computeInset(components, options, theme, library); const MIN_CONTENT_RATIO = 1 / 4; const maybeClamp = (viewWidth, paddingLeft, paddingRight, pl0, pr0) => { // Only clamp when has marks. const { marks } = options; if (marks.length === 0) return [pl0, pr0]; // If size of content is enough, skip. const contentSize = viewWidth - pl0 - pr0; const diff = contentSize - viewWidth * MIN_CONTENT_RATIO; if (diff > 0) return [pl0, pr0]; // Shrink start and end size equally. const shrinkSize = viewWidth * (1 - MIN_CONTENT_RATIO); return [ paddingLeft === 'auto' ? (shrinkSize * pl0) / (pl0 + pr0) : pl0, paddingRight === 'auto' ? (shrinkSize * pr0) / (pl0 + pr0) : pr0, ]; }; const roughPadding = (padding) => (padding === 'auto' ? 20 : padding !== null && padding !== void 0 ? padding : 20); const rpt = roughPadding(paddingTop); const rpb = roughPadding(paddingBottom); // Compute paddingLeft and paddingRight first to get innerWidth. const horizontalPadding = computePadding(components, height - rpt - rpb, [rpt + marginTop, rpb + marginBottom], ['left', 'right'], options, theme, library); const { paddingLeft: pl0, paddingRight: pr0 } = horizontalPadding; const viewWidth = width - marginLeft - marginRight; const [pl, pr] = maybeClamp(viewWidth, paddingLeft, paddingRight, pl0, pr0); const iw = viewWidth - pl - pr; // Compute paddingBottom and paddingTop based on innerWidth. const verticalPadding = computePadding(components, iw, [pl + marginLeft, pr + marginRight], ['bottom', 'top'], options, theme, library); const { paddingTop: pt0, paddingBottom: pb0 } = verticalPadding; const viewHeight = height - marginBottom - marginTop; const [pb, pt] = maybeClamp(viewHeight, paddingBottom, paddingTop, pb0, pt0); const ih = viewHeight - pb - pt; return { width, height, depth, insetLeft, insetTop, insetBottom, insetRight, innerWidth: iw, innerHeight: ih, paddingLeft: pl, paddingRight: pr, paddingTop: pt, paddingBottom: pb, marginLeft, marginBottom, marginTop, marginRight, x, y, z, }; } exports.computeLayout = computeLayout; // For composite mark with a layout algorithm and without axis, // such as worldcloud, circlepack. function computeRoughPlotSize(options) { const { height, width, padding = 0, paddingLeft = padding, paddingRight = padding, paddingTop = padding, paddingBottom = padding, margin = 16, marginLeft = margin, marginRight = margin, marginTop = margin, marginBottom = margin, inset = 0, insetLeft = inset, insetRight = inset, insetTop = inset, insetBottom = inset, } = options; // @todo Add this padding to theme. // 30 is default size for padding, which defined in runtime. const maybeAuto = (padding) => (padding === 'auto' ? 20 : padding); const finalWidth = width - maybeAuto(paddingLeft) - maybeAuto(paddingRight) - marginLeft - marginRight - insetLeft - insetRight; const finalHeight = height - maybeAuto(paddingTop) - maybeAuto(paddingBottom) - marginTop - marginBottom - insetTop - insetBottom; return { width: finalWidth, height: finalHeight }; } exports.computeRoughPlotSize = computeRoughPlotSize; function computeInset(components, options, theme, library) { const { coordinates } = options; if (!(0, coordinate_2.isPolar)(coordinates) && !(0, coordinate_2.isRadial)(coordinates)) { return options; } // Filter axis. const axes = components.filter((d) => typeof d.type === 'string' && d.type.startsWith('axis')); if (axes.length === 0) return options; const styles = axes.map((component) => { const key = component.type === 'axisArc' ? 'arc' : 'linear'; return (0, component_1.styleOf)(component, key, theme); }); // Compute max labelSpacing. const maxLabelSpacing = (0, d3_array_1.max)(styles, (d) => { var _a; return (_a = d.labelSpacing) !== null && _a !== void 0 ? _a : 0; }); // Compute labelBBoxes. const labelBBoxes = axes .flatMap((component, i) => { const style = styles[i]; const scale = (0, component_1.createScale)(component, library); const labels = (0, component_1.computeLabelsBBox)(style, scale); return labels; }) .filter(helper_1.defined); const size = (0, d3_array_1.max)(labelBBoxes, (d) => d.height) + maxLabelSpacing; // Compute titles. const titleBBoxes = axes .flatMap((_, i) => { const style = styles[i]; return (0, component_1.computeTitleBBox)(style); }) .filter((d) => d !== null); const titleSize = titleBBoxes.length === 0 ? 0 : (0, d3_array_1.max)(titleBBoxes, (d) => d.height); // Update inset. const { inset = size, insetLeft = inset, insetBottom = inset, insetTop = inset + titleSize, insetRight = inset, } = options; return Object.assign(Object.assign({}, options), { insetLeft, insetBottom, insetTop, insetRight }); } /** * @todo Support percentage size(e.g. 50%) */ function computePadding(components, crossSize, crossPadding, positions, options, theme, library) { const positionComponents = (0, d3_array_1.group)(components, (d) => d.position); const { padding = theme.padding, paddingLeft = padding, paddingRight = padding, paddingBottom = padding, paddingTop = padding, } = options; const layout = { paddingBottom, paddingLeft, paddingTop, paddingRight, }; for (const position of positions) { const key = `padding${(0, helper_1.capitalizeFirst)((0, string_1.camelCase)(position))}`; const components = positionComponents.get(position) || []; const value = layout[key]; const defaultSizeOf = (d) => { if (d.size === undefined) d.size = d.defaultSize; }; const sizeOf = (d) => { if (d.type === 'group') { d.children.forEach(defaultSizeOf); d.size = (0, d3_array_1.max)(d.children, (d) => d.size); } else { d.size = d.defaultSize; } }; const autoSizeOf = (d) => { if (d.size) return; if (value !== 'auto') sizeOf(d); else { // Compute component size dynamically. (0, component_1.computeComponentSize)(d, crossSize, crossPadding, position, theme, library); defaultSizeOf(d); } }; const maybeHide = (d) => { if (!d.type.startsWith('axis')) return; if (d.labelAutoHide === undefined) d.labelAutoHide = true; }; const isHorizontal = position === 'bottom' || position === 'top'; // !!!Note // Mute axis component padding. // The first axis do not has padding. const minOrder = (0, d3_array_1.min)(components, (d) => d.order); const axes = components.filter((d) => d.type.startsWith('axis') && d.order == minOrder); if (axes.length) axes[0].crossPadding = 0; // Specified padding. if (typeof value === 'number') { components.forEach(defaultSizeOf); components.forEach(maybeHide); } else { // Compute padding dynamically. if (components.length === 0) { layout[key] = 0; } else { const size = isHorizontal ? crossSize + crossPadding[0] + crossPadding[1] : crossSize; const grouped = (0, component_1.groupComponents)(components, size); grouped.forEach(autoSizeOf); const totalSize = grouped.reduce((sum, { size, crossPadding = 12 }) => sum + size + crossPadding, 0); layout[key] = totalSize; } } } return layout; } function placeComponents(components, coordinate, layout) { // Group components by plane & position. const positionComponents = (0, d3_array_1.group)(components, (d) => `${d.plane || 'xy'}-${d.position}`); const { paddingLeft, paddingRight, paddingTop, paddingBottom, marginLeft, marginTop, marginBottom, marginRight, innerHeight, innerWidth, insetBottom, insetLeft, insetRight, insetTop, height, width, depth, } = layout; const planes = { xy: createSection({ width, height, paddingLeft, paddingRight, paddingTop, paddingBottom, marginLeft, marginTop, marginBottom, marginRight, innerHeight, innerWidth, insetBottom, insetLeft, insetRight, insetTop, }), yz: createSection({ width: depth, height: height, paddingLeft: 0, paddingRight: 0, paddingTop: 0, paddingBottom: 0, marginLeft: 0, marginTop: 0, marginBottom: 0, marginRight: 0, innerWidth: depth, innerHeight: height, insetBottom: 0, insetLeft: 0, insetRight: 0, insetTop: 0, }), xz: createSection({ width, height: depth, paddingLeft: 0, paddingRight: 0, paddingTop: 0, paddingBottom: 0, marginLeft: 0, marginTop: 0, marginBottom: 0, marginRight: 0, innerWidth: width, innerHeight: depth, insetBottom: 0, insetLeft: 0, insetRight: 0, insetTop: 0, }), }; for (const [key, components] of positionComponents.entries()) { const [plane, position] = key.split('-'); const area = planes[plane][position]; /** * @description non-entity components: axis in the center, inner, outer, component in the center * @description entity components: other components * @description no volume components take up no extra space */ const [nonEntityComponents, entityComponents] = (0, array_1.divide)(components, (component) => { if (typeof component.type !== 'string') return false; if (position === 'center') return true; if (component.type.startsWith('axis') && ['inner', 'outer'].includes(position)) { return true; } return false; }); if (nonEntityComponents.length) { placeNonEntityComponents(nonEntityComponents, coordinate, area, position); } if (entityComponents.length) { placePaddingArea(components, coordinate, area); } } } exports.placeComponents = placeComponents; function createSection({ width, height, paddingLeft, paddingRight, paddingTop, paddingBottom, marginLeft, marginTop, marginBottom, marginRight, innerHeight, innerWidth, insetBottom, insetLeft, insetRight, insetTop, }) { const pl = paddingLeft + marginLeft; const pt = paddingTop + marginTop; const pr = paddingRight + marginRight; const pb = paddingBottom + marginBottom; const plotWidth = width - marginLeft - marginRight; const centerSection = [ pl + insetLeft, pt + insetTop, innerWidth - insetLeft - insetRight, innerHeight - insetTop - insetBottom, 'center', null, null, ]; const xySection = { top: [ pl, 0, innerWidth, pt, 'vertical', true, d3_array_1.ascending, marginLeft, plotWidth, ], right: [width - pr, pt, pr, innerHeight, 'horizontal', false, d3_array_1.ascending], bottom: [ pl, height - pb, innerWidth, pb, 'vertical', false, d3_array_1.ascending, marginLeft, plotWidth, ], left: [0, pt, pl, innerHeight, 'horizontal', true, d3_array_1.ascending], 'top-left': [pl, 0, innerWidth, pt, 'vertical', true, d3_array_1.ascending], 'top-right': [pl, 0, innerWidth, pt, 'vertical', true, d3_array_1.ascending], 'bottom-left': [ pl, height - pb, innerWidth, pb, 'vertical', false, d3_array_1.ascending, ], 'bottom-right': [ pl, height - pb, innerWidth, pb, 'vertical', false, d3_array_1.ascending, ], center: centerSection, inner: centerSection, outer: centerSection, }; return xySection; } function placeNonEntityComponents(components, coordinate, area, position) { const [axisComponents, nonAxisComponents] = (0, array_1.divide)(components, (component) => { if (typeof component.type === 'string' && component.type.startsWith('axis')) { return true; } return false; }); placeNonEntityAxis(axisComponents, coordinate, area, position); // in current stage, only legend component which located in the center can be placed placeCenter(nonAxisComponents, coordinate, area); } function placeNonEntityAxis(components, coordinate, area, position) { if (position === 'center') { if ((0, coordinate_1.isRadar)(coordinate)) { placeAxisRadar(components, coordinate, area, position); } else if ((0, coordinate_1.isPolar)(coordinate)) { placeArcLinear(components, coordinate, area); } else if ((0, coordinate_1.isParallel)(coordinate)) { placeAxisParallel(components, coordinate, area, components[0].orientation); } } else if (position === 'inner') { placeAxisArcInner(components, coordinate, area); } else if (position === 'outer') { placeAxisArcOuter(components, coordinate, area); } } function placeAxisArcInner(components, coordinate, area) { const [x, y, , height] = area; const [cx, cy] = coordinate.getCenter(); const [innerRadius] = (0, coordinate_1.radiusOf)(coordinate); const r = height / 2; const size = innerRadius * r; const x0 = cx - size; const y0 = cy - size; for (let i = 0; i < components.length; i++) { const component = components[i]; component.bbox = { x: x + x0, y: y + y0, width: size * 2, height: size * 2, }; } } function placeAxisArcOuter(components, coordinate, area) { const [x, y, width, height] = area; for (const component of components) { component.bbox = { x, y, width, height }; } } /** * @example arcX, arcY, axisLinear with angle */ function placeArcLinear(components, coordinate, area) { const [x, y, width, height] = area; for (const component of components) { component.bbox = { x: x, y, width, height }; } } function placeAxisParallel(components, coordinate, area, orientation) { if (orientation === 'horizontal') { placeAxisParallelHorizontal(components, coordinate, area); } else if (orientation === 'vertical') { placeAxisParallelVertical(components, coordinate, area); } } function placeAxisParallelVertical(components, coordinate, area) { const [x, y, , height] = area; // Create a high dimension vector and map to a list of two-dimension points. // [0, 0, 0] -> [x0, 0, x1, 0, x2, 0] const vector = new Array(components.length).fill(0); const points = coordinate.map(vector); // Extract x of each points. // [x0, 0, x1, 0, x2, 0] -> [x0, x1, x2] const X = points.filter((_, i) => i % 2 === 0).map((d) => d + x); // Place each axis by coordinate in parallel coordinate. for (let i = 0; i < components.length; i++) { const component = components[i]; const x = X[i]; const width = X[i + 1] - x; component.bbox = { x, y, width, height }; } } function placeAxisParallelHorizontal(components, coordinate, area) { const [x, y, width] = area; // Create a high dimension vector and map to a list of two-dimension points. // [0, 0, 0] -> [height, y0, height, y1, height, y2] const vector = new Array(components.length).fill(0); const points = coordinate.map(vector); // Extract y of each points. // [x0, 0, x1, 0, x2, 0] -> [x0, x1, x2] const Y = points.filter((_, i) => i % 2 === 1).map((d) => d + y); // Place each axis by coordinate in parallel coordinate. for (let i = 0; i < components.length; i++) { const component = components[i]; const y = Y[i]; const height = Y[i + 1] - y; component.bbox = { x, y, width, height }; } } function placeAxisRadar(components, coordinate, area, position) { const [x, y, width, height] = area; for (const component of components) { component.bbox = { x, y, width, height }; component.radar = { index: components.indexOf(component), count: components.length, }; } } function placePaddingArea(components, coordinate, area) { const [x, y, width, height, direction, reverse, comparator, minX, totalSize] = area; const [mainStartKey, mainStartValue, crossStartKey, crossStartValue, mainSizeKey, mainSizeValue, crossSizeKey, crossSizeValue,] = direction === 'vertical' ? ['y', y, 'x', x, 'height', height, 'width', width] : ['x', x, 'y', y, 'width', width, 'height', height]; // Sort components by order. // The smaller the order, the closer to center. components.sort((a, b) => comparator === null || comparator === void 0 ? void 0 : comparator(a.order, b.order)); const isLarge = (type) => type === 'title' || type === 'group' || type.startsWith('legend'); const crossSizeOf = (type, small, bigger) => { if (bigger === undefined) return small; if (isLarge(type)) return bigger; return small; }; const crossStartOf = (type, x, minX) => { if (minX === undefined) return x; if (isLarge(type)) return minX; return x; }; const startValue = reverse ? mainStartValue + mainSizeValue : mainStartValue; for (let i = 0, start = startValue; i < components.length; i++) { const component = components[i]; const { crossPadding = 0, type } = component; const { size } = component; component.bbox = { [mainStartKey]: reverse ? start - size - crossPadding : start + crossPadding, [crossStartKey]: crossStartOf(type, crossStartValue, minX), [mainSizeKey]: size, [crossSizeKey]: crossSizeOf(type, crossSizeValue, totalSize), }; start += (size + crossPadding) * (reverse ? -1 : 1); } // Place group components. const groupComponents = components.filter((d) => d.type === 'group'); for (const group of groupComponents) { const { bbox, children } = group; const size = bbox[crossSizeKey]; const step = size / children.length; const justifyContent = children.reduce((j, child) => { var _a; const j0 = (_a = child.layout) === null || _a === void 0 ? void 0 : _a.justifyContent; return j0 ? j0 : j; }, 'flex-start'); const L = children.map((d, i) => { const { length = step, padding = 0 } = d; return length + (i === children.length - 1 ? 0 : padding); }); const totalLength = (0, d3_array_1.sum)(L); const diff = size - totalLength; const offset = justifyContent === 'flex-start' ? 0 : justifyContent === 'center' ? diff / 2 : diff; for (let i = 0, start = bbox[crossStartKey] + offset; i < children.length; i++) { const component = children[i]; const { padding = 0 } = component; const interval = i === children.length - 1 ? 0 : padding; component.bbox = { [mainSizeKey]: bbox[mainSizeKey], [mainStartKey]: bbox[mainStartKey], [crossStartKey]: start, [crossSizeKey]: L[i] - interval, }; (0, util_1.deepMix)(component, { layout: { justifyContent } }); start += L[i]; } } } /** * @example legend in the center of radial or polar system */ function placeCenter(components, coordinate, area) { if (components.length === 0) return; const [x, y, width, height] = area; const [innerRadius] = (0, coordinate_1.radiusOf)(coordinate); const r = ((height / 2) * innerRadius) / Math.sqrt(2); const cx = x + width / 2; const cy = y + height / 2; for (let i = 0; i < components.length; i++) { const component = components[i]; component.bbox = { x: cx - r, y: cy - r, width: r * 2, height: r * 2 }; } } //# sourceMappingURL=layout.js.map