web: make loss and retransmission charts obvious, draw sparse series as points and keep percent axes within 100% [CI]
This commit is contained in:
1 parent
4a9608ead4
commit
557b169dae
3 files changed
+55
-10
No files matched your search
@@ -26,6 +26,8 @@ interface Props {
|
||||
gaps?: { from: number; to: number }[]
|
||||
/** 为真时 `t` 是「从起点算起的毫秒数」而不是 Unix 毫秒:横轴显示 0:00、0:15…,用来把两段时间叠在一起比。 */
|
||||
offset?: boolean
|
||||
/** 丢包、重传这类「没有才是好」的图:整段都是 0(或没有值)时,在图上盖一句说明,而不是留一条贴着底边的平线。 */
|
||||
quiet?: string
|
||||
}
|
||||
|
||||
/** 偏移量(毫秒)→ h:mm:ss / m:ss。 */
|
||||
@@ -38,6 +40,24 @@ export function fmtOffset(ms: number): string {
|
||||
return h > 0 ? `${h}:${two(m)}${sec ? ':' + two(sec) : ''}` : `${m}:${two(sec)}`
|
||||
}
|
||||
|
||||
/** 有值的格子不到三分之一(而且至少有一个)算稀疏。 */
|
||||
function isSparse(values: (number | null)[]): boolean {
|
||||
const filled = values.filter((v) => v !== null).length
|
||||
return filled > 0 && filled < values.length / 3
|
||||
}
|
||||
|
||||
/**
|
||||
* Y 轴范围:默认从 0 开始,上面留 8%。百分比的轴不会超过 100%(成功率贴着 100% 时不再出现 100.75% 这种刻度),
|
||||
* 一条平线(最大等于最小)也给出可读的范围。
|
||||
*/
|
||||
function yRange(min: number, max: number, zero: boolean, unit: string): [number, number] {
|
||||
const percent = unit === '%'
|
||||
const lo = zero ? Math.min(0, min) : percent ? Math.max(0, min - Math.max(1, (max - min) * 0.2)) : min
|
||||
let hi = max <= min ? min + (percent ? Math.max(1, Math.abs(min) * 0.01) : 1) : max + (max - min) * 0.08
|
||||
if (percent && max <= 100) hi = Math.min(100, hi)
|
||||
return [lo, hi > lo ? hi : lo + 1]
|
||||
}
|
||||
|
||||
function cssVar(name: string): string {
|
||||
return getComputedStyle(document.documentElement).getPropertyValue(name).trim()
|
||||
}
|
||||
@@ -61,7 +81,7 @@ function formatValue(v: number | null | undefined, unit: string): string {
|
||||
* - 在图上拖动选一段放大预览,双击或点「还原」恢复;数据刷新时保持放大的区间;
|
||||
* - 鼠标悬停显示这个时刻每条线的值。
|
||||
*/
|
||||
export function TimeChart({ t, series, height = 220, unit = '', zero = true, gaps = [], offset = false }: Props) {
|
||||
export function TimeChart({ t, series, height = 220, unit = '', zero = true, gaps = [], offset = false, quiet }: Props) {
|
||||
const host = useRef<HTMLDivElement>(null)
|
||||
const tip = useRef<HTMLDivElement>(null)
|
||||
const plot = useRef<uPlot | null>(null)
|
||||
@@ -70,7 +90,10 @@ export function TimeChart({ t, series, height = 220, unit = '', zero = true, gap
|
||||
// 数据变了(轮询、实时)只更新数据,不重建图:重建会闪一下,还会丢掉鼠标位置
|
||||
const latest = useRef({ t, series, gaps })
|
||||
latest.current = { t, series, gaps }
|
||||
const structure = JSON.stringify([series.map((x) => [x.label, x.color, x.dash, x.fill, x.faint]), unit, zero, offset, height])
|
||||
// 稀疏的序列(例如每分钟只有几次的建连):大部分格子是空的,单独的一个点画成线就看不见,所以画出点并把点连起来
|
||||
const sparse = series.map((x) => isSparse(x.values))
|
||||
const allQuiet = quiet !== undefined && series.filter((x) => !x.faint).every((x) => x.values.every((v) => v === null || v <= 0))
|
||||
const structure = JSON.stringify([series.map((x, i) => [x.label, x.color, x.dash, x.fill, x.faint, sparse[i]]), unit, zero, offset, height])
|
||||
const dataOf = (): AlignedData => [latest.current.t.map((v) => v / 1000), ...latest.current.series.map((x) => x.values.map((v) => (v === null ? null : v)))]
|
||||
|
||||
const reset = useCallback(() => {
|
||||
@@ -97,7 +120,7 @@ export function TimeChart({ t, series, height = 220, unit = '', zero = true, gap
|
||||
select: { show: true, left: 0, top: 0, width: 0, height: 0 },
|
||||
scales: {
|
||||
x: { time: !offset, range: (_u, min, max) => (zoom.current ? [zoom.current.min, zoom.current.max] : [min, max]) },
|
||||
y: { range: (_u, min, max) => [zero ? Math.min(0, min) : min, max <= min ? min + 1 : max + (max - min) * 0.08] },
|
||||
y: { range: (_u, min, max) => yRange(min, max, zero, unit) },
|
||||
},
|
||||
axes: [
|
||||
{ stroke: axis, grid: { stroke: grid, width: 1 }, ticks: { stroke: grid }, font, ...(offset ? { values: (_u: uPlot, ticks: number[]) => ticks.map((v) => fmtOffset(v * 1000)) } : {}) },
|
||||
@@ -113,8 +136,8 @@ export function TimeChart({ t, series, height = 220, unit = '', zero = true, gap
|
||||
width: s.faint ? 1 : 1.8,
|
||||
dash: s.dash,
|
||||
fill: s.fill ? color + '22' : undefined,
|
||||
spanGaps: false,
|
||||
points: { show: false },
|
||||
spanGaps: sparse[series.indexOf(s)],
|
||||
points: { show: sparse[series.indexOf(s)], size: 5 },
|
||||
}
|
||||
}),
|
||||
],
|
||||
@@ -218,7 +241,10 @@ export function TimeChart({ t, series, height = 220, unit = '', zero = true, gap
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<div ref={host} className="chart" />
|
||||
<div className="chart-body">
|
||||
<div ref={host} className="chart" />
|
||||
{allQuiet ? <div className="chart-quiet">{quiet}</div> : null}
|
||||
</div>
|
||||
<div ref={tip} className="chart-tip" />
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1088,3 +1088,20 @@ pre.code {
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
/* 「没有才是好」的图整段为 0 时:盖一句说明,图的坐标轴留着,说明在绘图区中间 */
|
||||
.chart-body {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.chart-quiet {
|
||||
position: absolute;
|
||||
inset: 0 0 28px 56px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--app-ok);
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
pointer-events: none;
|
||||
}
|
||||
Reference in new issue
Block a user