fix(ui): 轮动走势图线端名称 ≤10 全显, 高密模式封顶 Top10

旧防叠规则 >8 条只显最新值前 6 名, 默认「前10」下有 4 条线常年无名。改为
≤10 条全部常显 (默认行数所见即所名); 15/20 行高密模式按最新值封顶 Top10,
其余靠悬停聚焦与图例 (endLabel 无引导线, 汇聚区推离会与线端脱钩, 不宜全显)。

验证: SectorRotationCard.test 9 条全显/前20 封顶 Top10 断言; 前端 321 测试全绿。

Signed-off-by: shy3130 <shy3130@qq.com>
This commit is contained in:
shy3130
2026-10-28 12:47:12 +08:00
parent 313be43de4
commit 9edaac28b8
2 changed files with 69 additions and 5 deletions
@@ -87,10 +87,40 @@ function payload() {
}
}
/** N 板块 payload: 板块 i 最新值 = 0.004*(N-i) 严格降序, 用于线端名称封顶断言 */
function bigPayload(n: number) {
const sectors = Array.from({ length: n }, (_, i) => `板块${i}`)
const matrix = sectors.map((_, i) => BUCKETS.map((_, c) => 0.004 * (n - i) + c * 0.0001))
return {
status: 'ok' as const,
date: '2026-10-20',
as_of: '15:00',
member_count: n,
bucket_minutes: 5,
basis: 'prev_close',
flow_available: false,
flow_field: null,
default_exclude_sectors: [],
max_auto_members: 300,
timeline: BUCKETS.map((time, i) => ({
time, leader: sectors[0], leader_pct: matrix[0][i],
market_pct: 0.002, rotation: 0.3, cross_up: 0, cross_down: 0,
})),
series: { sectors, buckets: BUCKETS, matrix },
sectors: sectors.map((name, i) => ({
name, pct_now: matrix[i][5], pct_prev: 0.004, rank_now: i + 1,
rank_change: 0, score: 60 - i, n_members: 30, n_members_with_bars: 30,
activity: 1e8, flow: null,
})),
universe: sectors.map(name => ({ name, activity: 1e8, pct_now: 0.01 })),
cross_events: [],
}
}
let root: Root | null = null
let host: HTMLElement
async function renderCard(kind: 'concept' | 'industry' = 'industry') {
apiMock.sectorRotation.mockResolvedValue(payload())
async function renderCard(kind: 'concept' | 'industry' = 'industry', data = payload()) {
apiMock.sectorRotation.mockResolvedValue(data)
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } })
host = document.createElement('div')
document.body.appendChild(host)
@@ -184,6 +214,35 @@ it('trend 模式: 线末端常显板块名, 接管线上挂易主圆点标注',
expect(host.textContent).toContain('领涨易主')
})
it('线端名称: 9 条 (默认前10 内) 全部常显', async () => {
Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true })
await renderCard('industry', bigPayload(9))
const options = chart.setOption.mock.calls.map(call => call[0] as any)
const trendOption = options.find(o => o?.series?.[0]?.endLabel)
expect(trendOption).toBeTruthy()
// 9 ≤ END_LABEL_MAX: 每条线都有线端名称 — 回归 (2026-10-27): 旧规则 >8 只显 Top6,
// 默认「前10」下出现下缘线无名; 新规则与行数选项对齐
for (const s of trendOption.series) {
expect(s.endLabel?.show).toBe(true)
}
})
it('线端名称: 前20 高密模式按最新值封顶 Top 10', async () => {
Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true })
localStorage.setItem('sector_rotation_auto_rows_industry', '20')
await renderCard('industry', bigPayload(12))
const options = chart.setOption.mock.calls.map(call => call[0] as any)
const trendOption = options.find(o => o?.series?.[0]?.endLabel)
expect(trendOption).toBeTruthy()
expect(trendOption.series).toHaveLength(12)
const labeled = trendOption.series.filter((s: any) => s.endLabel?.show).map((s: any) => s.name)
// 恰好 10 个, 且是最新值最高的前 10 (构造值严格降序 → 板块0..板块9)
expect(labeled).toHaveLength(10)
expect(labeled.sort()).toEqual(Array.from({ length: 10 }, (_, i) => `板块${i}`).sort())
// 最低的两条无常显名 (悬停聚焦/图例辨认)
expect(trendOption.series.filter((s: any) => !s.endLabel)).toHaveLength(2)
})
// ===== 渲染: 热力图模式 =====
it('heatmap 模式: 领涨格子亮琥珀描边+辉光, 格间细缝, 中性中间色', async () => {
@@ -34,6 +34,10 @@ const AUTO_ROWS_PREFIX = 'sector_rotation_auto_rows_'
const EXCLUDE_PREFIX = 'sector_rotation_exclude_'
// 自动活跃榜展示行数可选项; excludeNames === null 表示未自定义 (用后端内置名单)
const AUTO_ROW_OPTIONS = [5, 10, 15, 20]
// 走势线末端常显名称上限: ≤此数全显; 15/20 行高密模式按最新值封顶 Top N。
// 与 AUTO_ROW_OPTIONS 的默认「前10」对齐 — 常规视图所见即所名; 更多标签会在
// 汇聚区被防重叠推离各自线端 (endLabel 无引导线), 高密模式牺牲常显名换可读性
const END_LABEL_MAX = 10
const MAX_EXCLUDE_SECTORS = 100
// 板块来源: 自动榜排序维度 + 自定义监控
type SectorSource = 'activity' | 'score' | 'pct' | 'rank_change' | 'momentum' | 'flow' | 'custom'
@@ -483,16 +487,17 @@ export function SectorRotationCard({ kind }: { kind: 'concept' | 'industry' }) {
const totalCols = heatSeries.buckets.length
const reigns = computeLeaderReigns(heatSeries.matrix, startCol)
const labelCols = selectReignLabels(reigns, totalCols)
// 线末端常显板块名: ≤8 条全显, >8 条按最新值只显 Top 6 (其余靠悬停聚焦/图例)
// 线末端常显板块名: ≤END_LABEL_MAX 条全显 (默认「前10」下所见即所名);
// 15/20 行高密模式按最新值封顶 Top END_LABEL_MAX (其余靠悬停聚焦/图例)
const latestVals = heatSeries.sectors.map((_, i) => {
const row = heatSeries.matrix[i] ?? []
for (let c = row.length - 1; c >= 0; c--) { const v = row[c]; if (v != null) return v }
return -Infinity
})
const endLabelRows = new Set(
heatSeries.sectors.length <= 8
heatSeries.sectors.length <= END_LABEL_MAX
? heatSeries.sectors.map((_, i) => i)
: heatSeries.sectors.map((_, i) => i).sort((a, b) => latestVals[b] - latestVals[a]).slice(0, 6),
: heatSeries.sectors.map((_, i) => i).sort((a, b) => latestVals[b] - latestVals[a]).slice(0, END_LABEL_MAX),
)
return {
// bottom 预留两行 plain 图例的高度 (scroll 图例会翻页, 一眼看不全所有板块)