UNPKG

@antv/g2

Version:

the Grammar of Graphics in Javascript

248 lines (218 loc) 8.04 kB
import { Coordinate } from '@antv/coord'; import { BBox, IGroup, IShape, IElement } from '@antv/g-base'; import { isObject, each, get, groupBy, isNil, filter } from '@antv/util'; import { polarToCartesian } from '../../../../util/graphics'; import { PolarLabelItem } from '../../interface'; import { antiCollision } from './util'; /** label text和line距离 4px */ const MARGIN = 4; /** * 配置 labelline * @param item PolarLabelItem */ function drawLabelline(item: any /** PolarLabelItem */, coordinate: Coordinate) { /** 坐标圆心 */ const center = coordinate.getCenter(); /** 圆半径 */ const radius = coordinate.getRadius(); if (item && item.labelLine) { const { angle, offset: labelOffset } = item; // 贴近圆周 const startPoint = polarToCartesian(center.x, center.y, radius, angle); const itemX = item.x + get(item, 'offsetX', 0) * (Math.cos(angle) > 0 ? 1 : -1); const itemY = item.y + get(item, 'offsetY', 0) * (Math.sin(angle) > 0 ? 1 : -1); const endPoint = { x: itemX - Math.cos(angle) * MARGIN, y: itemY - Math.sin(angle) * MARGIN, }; const smoothConnector = item.labelLine.smooth; const path = []; const dx = endPoint.x - center.x; const dy = endPoint.y - center.y; let endAngle = Math.atan(dy / dx); // 第三象限 & 第四象限 if (dx < 0) { endAngle += Math.PI; } // 默认 smooth, undefined 也为 smooth if (smoothConnector === false) { if (!isObject(item.labelLine)) { // labelLine: true item.labelLine = {}; } // 表示弧线的方向,0 表示从起点到终点沿逆时针画弧, 1 表示顺时针 let sweepFlag = 0; // 第一象限 if ((angle < 0 && angle > -Math.PI / 2) || angle > Math.PI * 1.5) { if (endPoint.y > startPoint.y) { sweepFlag = 1; } } // 第二象限 if (angle >= 0 && angle < Math.PI / 2) { if (endPoint.y > startPoint.y) { sweepFlag = 1; } } // 第三象限 if (angle >= Math.PI / 2 && angle < Math.PI) { if (startPoint.y > endPoint.y) { sweepFlag = 1; } } // 第四象限 if (angle < -Math.PI / 2 || (angle >= Math.PI && angle < Math.PI * 1.5)) { if (startPoint.y > endPoint.y) { sweepFlag = 1; } } const distance = labelOffset / 2 > 4 ? 4 : Math.max(labelOffset / 2 - 1, 0); const breakPoint = polarToCartesian(center.x, center.y, radius + distance, angle); // 圆弧的结束点 const breakPoint3 = polarToCartesian(center.x, center.y, radius + labelOffset / 2, endAngle); /** * @example * M 100 100 L100 90 A 50 50 0 0 0 150 50 * 移动至 (100, 100), 连接到 (100, 90), 以 (50, 50) 为圆心,绘制圆弧至 (150, 50); * A 命令的第 4 个参数 large-arc-flag, 决定弧线是大于还是小于 180 度: 0 表示小角度弧,1 表示大角 * 第 5 个参数: 是否顺时针绘制 */ // 默认小弧 const largeArcFlag = 0; // step1: 移动至起点 path.push(`M ${startPoint.x} ${startPoint.y}`); // step2: 连接拐点 path.push(`L ${breakPoint.x} ${breakPoint.y}`); // step3: 绘制圆弧 至 结束点 path.push(`A ${center.x} ${center.y} 0 ${largeArcFlag} ${sweepFlag} ${breakPoint3.x} ${breakPoint3.y}`); // step4: 连接结束点 path.push(`L ${endPoint.x} ${endPoint.y}`); } else { const breakPoint = polarToCartesian( center.x, center.y, radius + (labelOffset / 2 > 4 ? 4 : Math.max(labelOffset / 2 - 1, 0)), angle ); // G2 旧的拉线 // path.push('Q', `${breakPoint.x}`, `${breakPoint.y}`, `${endPoint.x}`, `${endPoint.y}`); const xSign = startPoint.x < center.x ? 1 : -1; // step1: 连接结束点 path.push(`M ${endPoint.x} ${endPoint.y}`); const slope1 = (startPoint.y - center.y) / (startPoint.x - center.x); const slope2 = (endPoint.y - center.y) / (endPoint.x - center.x); if (Math.abs(slope1 - slope2) > Math.pow(Math.E, -16)) { // step2: 绘制 curve line (起点 & 结合点与圆心的斜率不等时, 由于存在误差, 使用近似处理) path.push( ...[ 'C', endPoint.x + xSign * 4, endPoint.y, 2 * breakPoint.x - startPoint.x, 2 * breakPoint.y - startPoint.y, startPoint.x, startPoint.y, ] ); } // step3: 连接至起点 path.push(`L ${startPoint.x} ${startPoint.y}`); } item.labelLine.path = path.join(' '); } } /** * 饼图 outer-label 布局, 适用于 type = pie 且 label offset > 0 的标签 */ export function pieOuterLabelLayout( originalItems: PolarLabelItem[], labels: IGroup[], shapes: IShape[] | IGroup[], region: BBox ) { const items = filter(originalItems, (item) => !isNil(item)); /** 坐标系 */ const coordinate = labels[0] && labels[0].get('coordinate'); if (!coordinate) { return; } /** 坐标圆心 */ const center = coordinate.getCenter(); /** 圆半径 */ const radius = coordinate.getRadius(); /** label shapes */ const labelsMap: Record<string /** id */, IGroup> = {}; for (const labelShape of labels) { labelsMap[labelShape.get('id')] = labelShape; } // note labelHeight 可以控制 label 的行高 const labelHeight: number = get(items[0], 'labelHeight', 14); const labelOffset: number = get(items[0], 'offset', 0); if (labelOffset <= 0) { return; } const LEFT_HALF_KEY = 'left'; const RIGHT_HALF_KEY = 'right'; // step 1: separate labels const separateLabels = groupBy(items, (item) => (item.x < center.x ? LEFT_HALF_KEY : RIGHT_HALF_KEY)); const { start, end } = coordinate; // step2: calculate totalHeight const totalHeight = Math.min((radius + labelOffset + labelHeight) * 2, coordinate.getHeight()); const totalR = totalHeight / 2; /** labels 容器的范围(后续根据组件的布局设计进行调整) */ const labelsContainerRange = { minX: start.x, maxX: end.x, minY: center.y - totalR, maxY: center.y + totalR, }; // step 3: antiCollision each(separateLabels, (half, key) => { const maxLabelsCountForOneSide = Math.floor(totalHeight / labelHeight); if (half.length > maxLabelsCountForOneSide) { half.sort((a, b) => { // sort by percentage DESC return b.percent - a.percent; }); each(half, (labelItem: PolarLabelItem, idx) => { if (idx + 1 > maxLabelsCountForOneSide) { labelsMap[labelItem.id].set('visible', false); labelItem.invisible = true; } }); } antiCollision(half, labelHeight, labelsContainerRange); }); each(separateLabels, (half: PolarLabelItem[], key: string) => { each(half, (item: PolarLabelItem) => { const isRight = key === RIGHT_HALF_KEY; const labelShape = labelsMap[item.id]; // because group could not effect content-shape, should set content-shape position manually const content = labelShape.getChildByIndex(0) as IElement; // textShape 发生过调整 if (content) { const r = radius + labelOffset; // (x - cx)^2 + (y - cy)^2 = totalR^2 const dy = item.y - center.y; const rPow2 = Math.pow(r, 2); const dyPow2 = Math.pow(dy, 2); const dxPow2 = rPow2 - dyPow2 > 0 ? rPow2 - dyPow2 : 0; const dx = Math.sqrt(dxPow2); const dx_offset = Math.abs(Math.cos(item.angle) * r); if (!isRight) { // left item.x = center.x - Math.max(dx, dx_offset); } else { // right item.x = center.x + Math.max(dx, dx_offset); } } // adjust labelShape if (content) { content.attr('y', item.y); content.attr('x', item.x); } drawLabelline(item, coordinate); }); }); }