16 KiB
16 KiB
罗盘组件详细设计文档
1. 概述
本组件实现了一个可配置的圆盘罗盘可视化系统,支持将圆盘分为多个同心圆环和扇区,并在每个区域精确放置文字或图形。
2. 数学模型
2.1 坐标系统
- 角度约定:0° 位于正北(上方),顺时针方向为正
- SVG 坐标系:
- x 轴:向右为正
- y 轴:向下为正
- 原点:画布中心
2.2 参数定义
角度参数
n:总扇区数(pie 数量)angle[i]:第 i 个分度线的角度,i ∈ [0...n]wide(i) = angle[i] - angle[i-1]:第 i 块扇区的角度宽度
半径参数
m:总圆环数radius(j):第 j 个圆环的半径,j ∈ [0...m]height(j) = radius(j) - radius(j-1):第 j 层圆环的高度
区域定义
layer(j):第 j 层,由圆环 j 和圆环 j-1 之间形成pie(i):第 i 个扇区,由分度线 i-1 和分度线 i 之间形成sector(j,i):第 j 层第 i 个扇区的区域
2.3 形心计算公式
对于圆环扇形区域 sector(j,i),其形心位置计算如下:
输入参数
rInner = radius(j-1):内半径rOuter = radius(j):外半径aStart = angle[i-1]:起始角度(度)aEnd = angle[i]:结束角度(度)
计算步骤
- 角度归一化
a₁ = normalize(aStart) // 转换到 [0, 360)
a₂ = normalize(aEnd)
Δ = a₂ - a₁ // 若 < 0,则加 360
- 中心角度
aMid = a₁ + Δ/2 // 扇区中心角度
- 形心半径(面积加权质心)
radialFactor = (2/3) × (r₂³ - r₁³) / (r₂² - r₁²)
sinc(x) = sin(x) / x // x ≠ 0 时
ρ = radialFactor × sinc(Δ/2)
其中:
- r₁ = rInner
- r₂ = rOuter
- Δ 为弧度制
- 极坐标转笛卡尔坐标
cx = ρ × sin(aMid)
cy = -ρ × cos(aMid) // 注意:y 坐标取负以适应 SVG 坐标系
输出结果
(cx, cy):形心的笛卡尔坐标ρ:形心的极坐标半径aMid:形心的极坐标角度
2.4 特殊情况处理
-
纯扇形(rInner = 0)
- 公式仍然适用,内圆退化为点
-
跨越 0° 的扇区
- 例如:aStart = 315°, aEnd = 45°
- 通过归一化自动处理:Δ = 45 - 315 + 360 = 90°
-
退化情况
- rInner ≥ rOuter:返回 (0, 0)
- Δ = 0:返回 (0, 0)
3. 核心算法实现
3.1 极坐标转换
function polarToXY(aDeg: number, r: number): {x: number, y: number} {
const a = (aDeg * Math.PI) / 180;
return {
x: r * Math.sin(a),
y: -r * Math.cos(a) // y 坐标取负
};
}
3.2 角度归一化
function normalizeDeg(deg: number): number {
const d = deg % 360;
return d < 0 ? d + 360 : d;
}
3.3 形心计算
function annularSectorCentroid(params: {
rInner: number,
rOuter: number,
aStartDeg: number,
aEndDeg: number
}): CentroidResult {
const { rInner, rOuter, aStartDeg, aEndDeg } = params;
// 1. 角度归一化
const a1 = normalizeDeg(aStartDeg);
const a2 = normalizeDeg(aEndDeg);
let deltaDeg = a2 - a1;
if (deltaDeg < 0) deltaDeg += 360;
// 2. 中心角度
const aMidDeg = normalizeDeg(a1 + deltaDeg / 2);
const aMidRad = (aMidDeg * Math.PI) / 180;
// 3. 边界检查
if (rOuter <= rInner || deltaDeg === 0) {
return { cx: 0, cy: 0, rho: 0, aMidDeg, aMidRad, deltaDeg };
}
// 4. 形心半径计算
const delta = (deltaDeg * Math.PI) / 180;
const radialFactor = (2 / 3) *
((rOuter ** 3 - rInner ** 3) / (rOuter ** 2 - rInner ** 2));
const half = delta / 2;
const sinc = half === 0 ? 1 : Math.sin(half) / half;
const rho = radialFactor * sinc;
// 5. 坐标转换
const p = polarToXY(aMidDeg, rho);
return { cx: p.x, cy: p.y, rho, aMidDeg, aMidRad, deltaDeg };
}
4. 扇区路径生成
4.1 SVG 路径绘制
圆环扇形使用 SVG 的 <path> 元素绘制,包含以下步骤:
- 移动到外弧起点
- 绘制外弧(arc to)
- 连线到内弧终点
- 绘制内弧(arc to)
- 闭合路径
function annularSectorPath(
rInner: number,
rOuter: number,
aStartDeg: number,
aEndDeg: number
): string {
// 计算关键点
const p1 = polarToXY(aStart, rOuter); // 外弧起点
const p2 = polarToXY(aEnd, rOuter); // 外弧终点
const p3 = polarToXY(aEnd, rInner); // 内弧终点
const p4 = polarToXY(aStart, rInner); // 内弧起点
// 确定 large-arc-flag
const largeArc = delta > 180 ? 1 : 0;
// 生成路径
return [
`M ${p1.x} ${p1.y}`, // 起点
`A ${rOuter} ${rOuter} 0 ${largeArc} 1 ${p2.x} ${p2.y}`, // 外弧
`L ${p3.x} ${p3.y}`, // 连线
`A ${rInner} ${rInner} 0 ${largeArc} 0 ${p4.x} ${p4.y}`, // 内弧
"Z" // 闭合
].join(" ");
}
4.2 特殊情况:纯扇形
当 rInner ≈ 0 时,内弧退化为圆心点:
if (rInner <= 0.000001) {
return [
`M ${p1.x} ${p1.y}`,
`A ${rOuter} ${rOuter} 0 ${largeArc} 1 ${p2.x} ${p2.y}`,
`L 0 0`, // 连线到圆心
"Z"
].join(" ");
}
5. 文字排列算法
5.1 文字路径生成
文字沿圆弧排列,使用 SVG 的 <textPath> 功能。路径使用形心半径:
function generateTextPath(
rInner: number,
rOuter: number,
aStartDeg: number,
aEndDeg: number,
reverse: boolean
): string {
// 使用形心半径作为文字路径
const centroid = annularSectorCentroid({
rInner, rOuter, aStartDeg, aEndDeg
});
const rMid = centroid.rho; // 形心半径
const p1 = polarToXY(aStart, rMid);
const p2 = polarToXY(aEnd, rMid);
const largeArc = delta > 180 ? 1 : 0;
if (reverse) {
// 反向路径(左半圆)
return `M ${p2.x} ${p2.y} A ${rMid} ${rMid} 0 ${largeArc} 0 ${p1.x} ${p1.y}`;
} else {
// 正向路径(右半圆)
return `M ${p1.x} ${p1.y} A ${rMid} ${rMid} 0 ${largeArc} 1 ${p2.x} ${p2.y}`;
}
}
5.2 文字方向判断
为保持文字"头朝外",需要判断是否使用反向路径:
// 左半圆(90° ~ 270°)需要反向路径
const needReverse = aMidDeg > 90 && aMidDeg < 270;
原理:
- 右半圆(0°~90°, 270°~360°):正向路径,文字自然朝外
- 左半圆(90°~270°):反向路径,避免文字倒置
5.3 文字长度适配
根据层数自动调整文字长度:
// 第 j 层显示 (j+1) × 2 个字
const textLength = (layerIndex + 1) * 2;
const label = '测'.repeat(textLength);
示例:
- 第1层:2个字
- 第2层:4个字
- 第3层:6个字
6. 颜色生成策略
使用 HSL 颜色空间实现层次分明的配色:
function generateSectorColor(
layerIndex: number,
pieIndex: number,
totalPies: number = 24
): string {
const hue = (pieIndex * 360) / totalPies; // 色相均匀分布
const light = 78 - layerIndex * 10; // 亮度由内向外递减
return `hsl(${hue} 70% ${light}%)`;
}
效果:
- 相邻扇区色相不同,易于区分
- 内层较亮,外层较暗,突出层次
7. 组件架构
7.1 模块划分
src/
├── types.ts # TypeScript 类型定义
├── utils.ts # 纯函数工具集
├── constants.ts # 配置常量
├── composables/
│ └── useLuopan.ts # 组合函数(业务逻辑)
├── Luopan.vue # 主组件
├── utils.test.ts # 单元测试
└── index.ts # 入口文件
7.2 数据流
配置数据 (Example)
↓
useLuopan (计算扇区数据)
↓
Sector[] (包含形心、路径、颜色等)
↓
Luopan.vue (渲染 SVG)
7.3 关键类型定义
// 配置
interface Example {
name: string;
angles: number[]; // 角度分割点 [0, 30, 60, ..., 360]
radii: number[]; // 圆环半径 [60, 120, 180]
}
// 扇区数据
interface Sector {
key: string;
layerIndex: number;
pieIndex: number;
rInner: number;
rOuter: number;
aStart: number;
aEnd: number;
aMidDeg: number;
aMidRad: number;
cx: number; // 形心 x
cy: number; // 形心 y
fill: string;
label: string;
path: string; // 扇区路径
textPath: string; // 文字路径
textPathId: string;
needReverse: boolean;
}
8. 使用示例
8.1 基本使用
<template>
<Luopan :size="520" />
</template>
<script setup>
import { Luopan } from './src';
</script>
8.2 自定义配置
import { useLuopan } from './src';
import { ref } from 'vue';
const customExample = ref({
name: '自定义罗盘',
angles: [0, 45, 90, 135, 180, 225, 270, 315, 360],
radii: [80, 160, 240]
});
const { sectors } = useLuopan(customExample);
8.3 访问工具函数
import {
polarToXY,
annularSectorCentroid,
generateSectorColor
} from './src';
// 计算特定区域的形心
const centroid = annularSectorCentroid({
rInner: 50,
rOuter: 100,
aStartDeg: 0,
aEndDeg: 30
});
console.log(`形心位置: (${centroid.cx}, ${centroid.cy})`);
9. 测试
9.1 单元测试覆盖
- ✅ 极坐标转换(4个方向)
- ✅ 角度归一化(负数、大于360)
- ✅ 形心计算(纯扇形、圆环、跨0°)
- ✅ 路径生成(正常、退化情况)
- ✅ 颜色生成(色相、亮度)
9.2 运行测试
npm run test # 运行测试
npm run test:ui # 测试 UI
npm run test:coverage # 覆盖率报告
10. 性能优化
10.1 计算优化
- 使用 Vue 的
computed缓存计算结果 - 避免重复计算:形心、路径等一次生成
- 三角函数结果复用
10.2 渲染优化
- 使用
key属性优化列表渲染 - SVG 路径字符串预生成,避免模板内计算
- 辅助线可选显示,减少 DOM 节点
11. 扩展性
11.1 支持的扩展
- ✅ 自定义角度分割
- ✅ 自定义圆环半径
- ✅ 自定义颜色方案
- ✅ 自定义文字内容
- 🔲 图标/图片放置(预留
<foreignObject>) - 🔲 交互事件(点击、悬停)
- 🔲 动画效果
11.2 API 设计原则
- 纯函数优先,便于测试
- 类型安全,完整的 TypeScript 支持
- 组合式 API,灵活复用
- 渐进式增强,保持简单使用
12. 文字布局与自适应
12.1 文字布局配置常量
所有文字布局相关的魔法数字已集中在 TEXT_LAYOUT_CONFIG 中管理:
export const TEXT_LAYOUT_CONFIG = {
/** 字符间距系数:1.2 表示字符实际占用高度 = fontSize × 1.2 */
CHAR_SPACING_RATIO: 1.2,
/** 径向留白比例:0.8 表示文字占用 80%,上下各留 10% */
RADIAL_PADDING_RATIO: 0.8,
/** 切向留白比例:0.85 表示文字占用 85%,左右各留 7.5% */
TANGENT_PADDING_RATIO: 0.85,
/** 小角度扇区字体缩放:防止弧线弯曲导致视觉溢出 */
SMALL_ANGLE_SCALE: {
TINY_THRESHOLD: 15, // < 15° 应用 70% 缩放
TINY_SCALE: 0.7,
SMALL_THRESHOLD: 30, // 15°-30° 应用 85% 缩放
SMALL_SCALE: 0.85,
},
/** 字体大小限制 */
FONT_SIZE: {
MIN: 3, // 最小 3px
MAX: 24, // 最大 24px
},
/** 竖排文字字符数范围 */
VERTICAL_TEXT: {
MIN_CHARS: 1,
MAX_CHARS: 4,
},
}
12.2 字体大小自适应算法
横排文字(沿弧线)
字体大小同时受径向高度和弧长双重约束:
// 1. 径向高度约束
maxByHeight = radialHeight × RADIAL_PADDING_RATIO
// 2. 弧长宽度约束
arcLength = rMid × deltaDeg × π/180
availableArcLength = arcLength × TANGENT_PADDING_RATIO
maxByWidth = availableArcLength / (textLength × 1.1)
// 3. 取较小值
calculatedSize = min(maxByHeight, maxByWidth)
// 4. 小角度额外缩放
if (deltaDeg < 15°) calculatedSize × 0.7
else if (deltaDeg < 30°) calculatedSize × 0.85
关键点:
1.1系数包含字符宽度(1.0) + 字符间距(0.1)- 小角度缩放是因为弧线弯曲使文字在视觉上"更胖"
竖排文字(沿径向)
竖排文字沿径向排列,受不同约束:
// 1. 径向高度约束(主要)
availableHeight = radialHeight × RADIAL_PADDING_RATIO
maxByHeight = availableHeight / (textLength × CHAR_SPACING_RATIO)
// 2. 最内侧字符的弧长约束(防止最里面的字溢出)
estimatedFontSize = radialHeight × 0.5
innerMostRadius = rInner + estimatedFontSize / 2
innerArcLength = innerMostRadius × deltaDeg × π/180
availableArcLength = innerArcLength × TANGENT_PADDING_RATIO
maxByWidth = availableArcLength // 单个字符宽度
// 3. 取较小值
calculatedSize = min(maxByHeight, maxByWidth)
关键点:
- 最内侧字符位置的半径最小,弧长最短,是最严格的宽度限制
- 不需要小角度缩放(竖排不受弧线弯曲影响)
12.3 竖排文字路径生成
竖排文字路径是沿径向的直线,需要特殊处理不同场景:
普通圆环扇区(rInner > 0)
// 以形心或中点为中心,向两边延伸
rMid = centroid.rho // 或 (rInner + rOuter) / 2
requiredPathLength = textLength × CHAR_SPACING_RATIO × fontSize
halfPath = requiredPathLength / 2
// 确保不超出边界
safeHalfPath = min(
halfPath,
radialHeight × RADIAL_PADDING_RATIO / 2,
rMid - rInner,
rOuter - rMid
)
finalStartR = rMid + safeHalfPath // 外端
finalEndR = rMid - safeHalfPath // 内端
从圆心开始的扇区(rInner = 0)
对于最内层扇区,形心会偏向外侧,需要特殊处理:
if (rInner === 0) {
// 检查以形心居中是否会溢出外圆
if (rMid + halfPath > rOuter) {
// 会溢出:改为从外圆向内延伸
finalStartR = rOuter
finalEndR = rOuter - actualPathLength
} else {
// 不会溢出:正常使用形心居中
finalStartR = rMid + halfPath
finalEndR = rMid - halfPath
}
}
原理:
- 最内层的形心通常在 r = 2/3 × rOuter 附近(对于纯扇形)
- 如果以形心为中心,向外空间只有 1/3 × rOuter
- 当文字较多时,会向外溢出
- 因此优先尝试形心居中,溢出时回退到从外圆向内
12.4 文字字符数计算
横排文字
基于扇区尺寸和层数综合决定:
estimatedFontSize = radialHeight × 0.6
charWidth = estimatedFontSize × 1.15
availableWidth = arcWidth × 0.8
maxChars = floor(availableWidth / charWidth)
layerBasedLength = max(2, layerIndex + 1) // 外层字多一些
textLength = max(1, min(6, min(maxChars, layerBasedLength)))
竖排文字
两步计算法,确保字符数与字体大小匹配:
// 步骤1:用中等字符数估算字体大小
tempFontSize = calculateSectorFontSize(rInner, rOuter, aStart, aEnd, 2, isVertical=true)
// 步骤2:根据字体大小计算实际能容纳的字符数(1-4个)
textLength = calculateVerticalTextLength(rInner, rOuter, tempFontSize, 1, 4)
// 步骤3:用实际字符数重新精确计算字体大小
finalFontSize = calculateSectorFontSize(rInner, rOuter, aStart, aEnd, textLength, isVertical=true)
12.5 横排文字路径生成
横排文字沿弧线排列,路径保持完整(不缩短),padding 通过字体大小计算实现:
// 路径从 aStart 到 aEnd 的完整弧线
rMid = centroid.rho // 或 (rInner + rOuter) / 2
p1 = polarToXY(aStart, rMid)
p2 = polarToXY(aEnd, rMid)
// 弧线路径
if (reverse) {
path = `M ${p2.x} ${p2.y} A ${rMid} ${rMid} 0 ${largeArc} 0 ${p1.x} ${p1.y}`
} else {
path = `M ${p1.x} ${p1.y} A ${rMid} ${rMid} 0 ${largeArc} 1 ${p2.x} ${p2.y}`
}
文字通过 <textPath> 的 startOffset="50%" 和 text-anchor="middle" 在路径上居中。
12.6 调试与验证
如需调试特定扇区的计算结果,可临时添加日志:
// 在 useLuopan.ts 中
if (j === 0 && aStart >= 225 && aStart <= 260) {
console.log(`扇区 [${aStart}-${aEnd}°, 层${j}]:`, {
radialHeight, arcWidth, textLength, fontSize
});
}
13. 参考资料
12.1 数学原理
- 圆环扇形形心公式:基于面积加权的质心计算
- SVG 坐标系统:W3C SVG 规范
- 极坐标系统:标准数学定义
12.2 技术栈
- Vue 3.4+
- TypeScript 5.3+
- Vite 5.0+
- Vitest 1.2+
版本: 1.1.0
最后更新: 2026年1月20日
主要变更:
- 新增文字布局配置常量集中管理
- 优化字体大小自适应算法
- 完善竖排文字特殊场景处理
- 修复最内层扇区文字溢出问题