Home: one sparkline style with a left fade, SI values, a full-width activity table, roomier panels
The trend curve was two components: a rich one in the node panel and on edges,
and a bare line in the flow table. It is one `Sparkline` now, taking the colour
token, the height and whether the live dot and the readout show. The flow table
draws its rollup in the chart ramp with no dot — the rollups are polled, so the
right edge is the last completed slice rather than this instant — and keeps its
"nothing yet" state, as the panel keeps its three distinct silences.
All of them fade out to the left, through an SVG mask over the curve and its
area. The dot sits outside the mask: the newest reading is the one thing that
must stay solid.
`si` replaces `compact` and the ad-hoc "k" the uPlot axis carried. It prefixes
k/M/G and m/µ, but only outside 0.01–1000, where the plain number is already
the shortest thing to read and a written unit ("0.4 ms") stays honest. The
sparkline readout asks for four digits, so two neighbouring readings never
collapse into one string. Exact counts and anything the user acts on — a
payload, a form field, the edge inspector's value — are left unrounded.
`src/lib/utils.check.ts` asserts the rounding cases.
The activity table now spans its card: the flow name anchors the left, the four
numbers read down their own centre, and the trend takes the slack on the right.
Panel rhythm steps up one notch, gap-5 to gap-6 outside and gap-2 to gap-3
within a section.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XC2jX6Hdj7pxGGKzBTrbqB
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* The cases `si` gets wrong when its rounding is written the obvious way.
|
||||
*
|
||||
* Run: `bun src/lib/utils.check.ts` (there is no unit runner; the suite in
|
||||
* `tests/` drives a running stack).
|
||||
*/
|
||||
import assert from "node:assert/strict"
|
||||
|
||||
import { si } from "./utils"
|
||||
|
||||
// Nothing is prefixed in the plain window: these are written with a unit after
|
||||
// them, and "400m ms" is not an improvement on "0.4 ms".
|
||||
assert.equal(si(0), "0")
|
||||
assert.equal(si(0.4), "0.4")
|
||||
assert.equal(si(203.63), "204")
|
||||
|
||||
// Rounding must not leave a value in the step it just rounded out of.
|
||||
assert.equal(si(999.7), "1k")
|
||||
assert.equal(si(15000), "15k")
|
||||
assert.equal(si(-2.5e6), "-2.5M")
|
||||
|
||||
// A small reading has to survive as itself; a series of these is not zeroes.
|
||||
assert.equal(si(0.0031, 4), "3.1m")
|
||||
assert.equal(si(0.000031), "31µ")
|
||||
assert.equal(si(1e-9), "1.0e-9")
|
||||
|
||||
// Two neighbouring readings stay apart at the sparkline readout's four digits.
|
||||
assert.notEqual(si(1234, 4), si(1235, 4))
|
||||
|
||||
assert.equal(si(Number.NaN), "--")
|
||||
|
||||
console.log("si: ok")
|
||||
@@ -5,12 +5,37 @@ export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
|
||||
/** Steps of a thousand, largest first. */
|
||||
const SI_STEPS: [number, string][] = [
|
||||
[1e9, "G"],
|
||||
[1e6, "M"],
|
||||
[1e3, "k"],
|
||||
[1e-3, "m"],
|
||||
[1e-6, "µ"],
|
||||
]
|
||||
|
||||
/**
|
||||
* A reading at about three significant digits: 203.63 → 204, 2.714 → 2.7.
|
||||
* A reading at `digits` significant digits, with an SI prefix where the plain
|
||||
* number would be long: 15000 → "15k", 0.0031 → "3.1m", 203.63 → "204".
|
||||
*
|
||||
* Shared by the health tables and the chart legends, so a value read off the
|
||||
* cursor cannot grow wide enough to push a legend out of its card.
|
||||
* Between 0.01 and 1000 the plain number is the shortest thing to read, so
|
||||
* nothing is prefixed there — "0.4" beats "400m", and a value written with its
|
||||
* unit after it ("0.4 ms") stays honest. Past those bounds the prefix is what
|
||||
* keeps a reading in its column, and beyond giga or micro the exponent is.
|
||||
*
|
||||
* Where it applies: anything read at a glance and bounded by the width of a
|
||||
* tile, a table cell, an axis gutter or a legend. Exact counts (executions,
|
||||
* failures) and anything the user is about to act on — a payload, a form
|
||||
* field, the edge inspector's value — stay unrounded.
|
||||
*/
|
||||
export function compact(value: number): string {
|
||||
return String(Math.abs(value) >= 100 ? Math.round(value) : +value.toFixed(1))
|
||||
export function si(value: number, digits = 3): string {
|
||||
if (!Number.isFinite(value)) return "--"
|
||||
// Rounded before the step is picked, so 999.7 reads as "1k", not "1000".
|
||||
const rounded = Number(value.toPrecision(digits))
|
||||
const size = Math.abs(rounded)
|
||||
if (size === 0) return "0"
|
||||
if (size >= 0.01 && size < 1e3) return String(rounded)
|
||||
if (size >= 1e12 || size < 1e-6) return rounded.toExponential(1)
|
||||
const [factor, suffix] = SI_STEPS.find(([step]) => size >= step) ?? [1, ""]
|
||||
return `${Number((rounded / factor).toPrecision(digits))}${suffix}`
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user