|
|
@@ -112,70 +112,64 @@ describe('formatAirVolumeLabel 风量标注', () => {
|
|
|
});
|
|
|
});
|
|
|
|
|
|
-describe('computeLayout 列式布局(自适应铺满)', () => {
|
|
|
+describe('computeLayout 列式布局(固定间距)', () => {
|
|
|
const centerY = 350; // 700 / 2
|
|
|
+ const laneX = (idx: number) => LAYOUT.margin + LAYOUT.colGap * idx;
|
|
|
|
|
|
- /** 与布局实现一致的自适应列距计算(1200 宽、8 列) */
|
|
|
- const colCount = 8;
|
|
|
- const xGap = Math.min(LAYOUT.colGap, Math.max(LAYOUT.colGapMin, (1200 - 2 * LAYOUT.margin) / (colCount - 1)));
|
|
|
- const xStart = LAYOUT.margin + (1200 - 2 * LAYOUT.margin - xGap * (colCount - 1)) / 2;
|
|
|
- const laneX = (idx: number) => xStart + idx * xGap;
|
|
|
-
|
|
|
- test('列序:总进 → lv1..lv5 → 总回,x 依次递增自适应列距并铺满;单节点列垂直居中', () => {
|
|
|
+ test('列序:总进 → lv1..lv5 → 总回,x 依次递增固定 colGap;单节点列垂直居中', () => {
|
|
|
const layout = computeLayout(laneTopo(), 1200, 700);
|
|
|
const pos = layout.positions;
|
|
|
- expect(pos[ROOT_IN_ID].x).toBeCloseTo(laneX(0), 5);
|
|
|
- expect(pos['point:a1'].x).toBeCloseTo(laneX(1), 5);
|
|
|
- expect(pos['point:b1'].x).toBeCloseTo(laneX(2), 5);
|
|
|
- expect(pos['point:c1'].x).toBeCloseTo(laneX(3), 5);
|
|
|
- expect(pos['point:d1'].x).toBeCloseTo(laneX(4), 5);
|
|
|
- expect(pos['point:e1'].x).toBeCloseTo(laneX(5), 5);
|
|
|
- expect(pos[ROOT_OUT_ID].x).toBeCloseTo(laneX(7), 5); // 未分级列之后为总回
|
|
|
+ expect(pos[ROOT_IN_ID].x).toBe(laneX(0));
|
|
|
+ expect(pos['point:a1'].x).toBe(laneX(1));
|
|
|
+ expect(pos['point:b1'].x).toBe(laneX(2));
|
|
|
+ expect(pos['point:c1'].x).toBe(laneX(3));
|
|
|
+ expect(pos['point:d1'].x).toBe(laneX(4));
|
|
|
+ expect(pos['point:e1'].x).toBe(laneX(5));
|
|
|
+ expect(pos[ROOT_OUT_ID].x).toBe(laneX(7)); // 未分级列之后为总回
|
|
|
expect(pos[ROOT_IN_ID].y).toBe(centerY);
|
|
|
expect(pos[ROOT_OUT_ID].y).toBe(centerY);
|
|
|
- // 铺满:总进贴左边距、总回贴右边距
|
|
|
- expect(pos[ROOT_IN_ID].x).toBeCloseTo(LAYOUT.margin, 5);
|
|
|
- expect(pos[ROOT_OUT_ID].x).toBeCloseTo(1200 - LAYOUT.margin, 5);
|
|
|
});
|
|
|
|
|
|
- test('同层级同列,行距按最高列自适应(clamp((H-2vMargin)/(n-1), min, max)),围绕中心对称', () => {
|
|
|
+ test('同层级同列,行距 = 该层级 rowGap(固定),围绕中心对称', () => {
|
|
|
const data = laneTopo();
|
|
|
data.nodes.push({ id: 'point:b2', name: 'b2', category: 1, level: 2, airVolume: 85 });
|
|
|
const pos = computeLayout(data, 1200, 700).positions;
|
|
|
expect(pos['point:b2'].x).toBe(pos['point:b1'].x);
|
|
|
- const expected = Math.min(LAYOUT.rowGap, Math.max(LAYOUT.rowGapMin, (700 - 2 * LAYOUT.vMargin) / (2 - 1)));
|
|
|
- expect(Math.abs(pos['point:b2'].y - pos['point:b1'].y)).toBeCloseTo(expected, 5);
|
|
|
+ const expected = LAYOUT.rowGap[2] ?? LAYOUT.rowGapDefault;
|
|
|
+ expect(Math.abs(pos['point:b2'].y - pos['point:b1'].y)).toBe(expected);
|
|
|
expect((pos['point:b1'].y + pos['point:b2'].y) / 2).toBeCloseTo(centerY, 5);
|
|
|
});
|
|
|
|
|
|
- test('colGap/rowGap 上下限可配置生效', () => {
|
|
|
- const orig = {
|
|
|
- colGap: LAYOUT.colGap,
|
|
|
- colGapMin: LAYOUT.colGapMin,
|
|
|
- rowGap: LAYOUT.rowGap,
|
|
|
- rowGapMin: LAYOUT.rowGapMin,
|
|
|
- };
|
|
|
+ test('colGap 固定生效', () => {
|
|
|
+ const orig = LAYOUT.colGap;
|
|
|
+ try {
|
|
|
+ LAYOUT.colGap = 250;
|
|
|
+ const pos = computeLayout(laneTopo(), 1200, 700).positions;
|
|
|
+ expect(pos['point:a1'].x - pos[ROOT_IN_ID].x).toBe(250);
|
|
|
+ expect(pos['point:b1'].x - pos['point:a1'].x).toBe(250);
|
|
|
+ } finally {
|
|
|
+ LAYOUT.colGap = orig;
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ test('rowGap 按 level 分别配置生效:只影响对应层级列', () => {
|
|
|
+ const orig2 = LAYOUT.rowGap[2];
|
|
|
try {
|
|
|
- // 列距上限调小(低于自适应理想值 154.29)→ 相邻列间距 = colGap,整组在画布内居中
|
|
|
- LAYOUT.colGap = 140;
|
|
|
- const data1 = laneTopo();
|
|
|
- const pos1 = computeLayout(data1, 1200, 700).positions;
|
|
|
- expect(pos1['point:a1'].x - pos1[ROOT_IN_ID].x).toBeCloseTo(140, 5);
|
|
|
- expect(pos1[ROOT_IN_ID].x).toBeGreaterThan(LAYOUT.margin);
|
|
|
- // 行距上限调小(低于理想值 620)→ 行距 = rowGap
|
|
|
- LAYOUT.rowGap = 200;
|
|
|
- const data2 = laneTopo();
|
|
|
- data2.nodes.push({ id: 'point:b2', name: 'b2', category: 1, level: 2, airVolume: 85 });
|
|
|
- const pos2 = computeLayout(data2, 1200, 700).positions;
|
|
|
- expect(Math.abs(pos2['point:b2'].y - pos2['point:b1'].y)).toBe(200);
|
|
|
- // 列距下限调大(高于理想值,带宽超画布)→ 相邻列间距 = colGapMin,保持左对齐不向左侧溢出
|
|
|
- LAYOUT.colGap = 300;
|
|
|
- LAYOUT.colGapMin = 200;
|
|
|
- const pos3 = computeLayout(laneTopo(), 1200, 700).positions;
|
|
|
- expect(pos3['point:a1'].x - pos3[ROOT_IN_ID].x).toBeCloseTo(200, 5);
|
|
|
- expect(pos3[ROOT_IN_ID].x).toBe(LAYOUT.margin);
|
|
|
+ LAYOUT.rowGap[2] = 300;
|
|
|
+ const data = laneTopo();
|
|
|
+ data.nodes.push({ id: 'point:b2', name: 'b2', category: 1, level: 2, airVolume: 85 });
|
|
|
+ data.nodes.push({ id: 'point:c2', name: 'c2', category: 1, level: 3, airVolume: 75 });
|
|
|
+ const pos = computeLayout(data, 1200, 700).positions;
|
|
|
+ // lv2 列使用本次配置值 300
|
|
|
+ expect(Math.abs(pos['point:b2'].y - pos['point:b1'].y)).toBe(300);
|
|
|
+ // lv3 列仍使用其自身配置值(未被本次修改影响)
|
|
|
+ const gap3 = LAYOUT.rowGap[3] ?? LAYOUT.rowGapDefault;
|
|
|
+ expect(Math.abs(pos['point:c2'].y - pos['point:c1'].y)).toBe(gap3);
|
|
|
+ // 两列分别围绕 centerY 对称
|
|
|
+ expect((pos['point:b1'].y + pos['point:b2'].y) / 2).toBeCloseTo(centerY, 5);
|
|
|
+ expect((pos['point:c1'].y + pos['point:c2'].y) / 2).toBeCloseTo(centerY, 5);
|
|
|
} finally {
|
|
|
- Object.assign(LAYOUT, orig);
|
|
|
+ LAYOUT.rowGap[2] = orig2;
|
|
|
}
|
|
|
});
|
|
|
|
|
|
@@ -206,12 +200,17 @@ describe('computeLayout 列式布局(自适应铺满)', () => {
|
|
|
expect(layout.positions['point:a1'].x).toBe(layout.positions['point:a2'].x);
|
|
|
expect(layout.positions['point:c1'].x).toBe(layout.positions['point:c2'].x);
|
|
|
expect(layout.positions['point:e1'].x).toBe(layout.positions['point:e2'].x);
|
|
|
- // 同秩跨列 y 对齐:各列节点数相同(2),首节点 y 一致
|
|
|
- expect(layout.positions['point:a1'].y).toBe(layout.positions['point:c1'].y);
|
|
|
- expect(layout.positions['point:c1'].y).toBe(layout.positions['point:e1'].y);
|
|
|
+ // 各 2 节点列按各自层级行距围绕 centerY 对称(首节点 = centerY − 行距/2)
|
|
|
+ const gap1 = LAYOUT.rowGap[1] ?? LAYOUT.rowGapDefault;
|
|
|
+ const gap3 = LAYOUT.rowGap[3] ?? LAYOUT.rowGapDefault;
|
|
|
+ const gap5 = LAYOUT.rowGap[5] ?? LAYOUT.rowGapDefault;
|
|
|
+ expect(layout.positions['point:a1'].y).toBeCloseTo(centerY - gap1 / 2, 5);
|
|
|
+ expect(layout.positions['point:c1'].y).toBeCloseTo(centerY - gap3 / 2, 5);
|
|
|
+ expect(layout.positions['point:e1'].y).toBeCloseTo(centerY - gap5 / 2, 5);
|
|
|
+ expect((layout.positions['point:a1'].y + layout.positions['point:a2'].y) / 2).toBeCloseTo(centerY, 5);
|
|
|
});
|
|
|
|
|
|
- test('不同节点数列独立垂直居中(菱形轮廓):1/2/4 节点列各自围绕 centerY 对称,单节点列在 centerY', () => {
|
|
|
+ test('不同节点数列独立垂直居中:1/2/4 节点列各自围绕 centerY 对称,单节点列在 centerY', () => {
|
|
|
// 列分布:总进(1) lv1(1) lv2(2) lv3(4) lv4(2) lv5(1) 总回(1)
|
|
|
const areas = [
|
|
|
area('a1', 1, 500),
|
|
|
@@ -233,16 +232,20 @@ describe('computeLayout 列式布局(自适应铺满)', () => {
|
|
|
expect(pos['point:a1'].y).toBe(centerY);
|
|
|
expect(pos['point:e1'].y).toBe(centerY);
|
|
|
expect(pos[ROOT_OUT_ID].y).toBe(centerY);
|
|
|
- // 2 节点列(lv2/lv4)围绕 centerY 对称,中点 = centerY
|
|
|
+ // 2 节点列(lv2/lv4)围绕 centerY 对称,中点 = centerY,行距取各自层级配置
|
|
|
+ const gap2 = LAYOUT.rowGap[2] ?? LAYOUT.rowGapDefault;
|
|
|
+ const gap3 = LAYOUT.rowGap[3] ?? LAYOUT.rowGapDefault;
|
|
|
+ const gap4 = LAYOUT.rowGap[4] ?? LAYOUT.rowGapDefault;
|
|
|
expect((pos['point:b1'].y + pos['point:b2'].y) / 2).toBeCloseTo(centerY, 5);
|
|
|
expect((pos['point:d1'].y + pos['point:d2'].y) / 2).toBeCloseTo(centerY, 5);
|
|
|
- // 4 节点列(lv3)围绕 centerY 对称,中点 = centerY
|
|
|
+ expect(Math.abs(pos['point:b1'].y - pos['point:b2'].y)).toBe(gap2);
|
|
|
+ expect(Math.abs(pos['point:d1'].y - pos['point:d2'].y)).toBe(gap4);
|
|
|
+ // 4 节点列(lv3)围绕 centerY 对称,中点 = centerY,跨度 = 3×该层级行距
|
|
|
expect((pos['point:c1'].y + pos['point:c4'].y) / 2).toBeCloseTo(centerY, 5);
|
|
|
- // 菱形轮廓:4 节点列纵向跨度最大(顶底超出 2 节点列)
|
|
|
- const span4 = Math.abs(pos['point:c1'].y - pos['point:c4'].y);
|
|
|
- const span2 = Math.abs(pos['point:b1'].y - pos['point:b2'].y);
|
|
|
- expect(span4).toBeGreaterThan(span2);
|
|
|
- expect(span2).toBeGreaterThan(0);
|
|
|
+ expect(Math.abs(pos['point:c1'].y - pos['point:c4'].y)).toBe(3 * gap3);
|
|
|
+ // 中间列跨度大于两端列(菱形轮廓)
|
|
|
+ expect(3 * gap3).toBeGreaterThan(gap2);
|
|
|
+ expect(gap2).toBeGreaterThan(0);
|
|
|
});
|
|
|
|
|
|
test('空数据(仅总进/总回地面节点)可布局且不抛错', () => {
|