Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 7 additions & 10 deletions apps/web/src/lab/charts/spike-timeseries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { scaleTime } from "d3-scale"
import { Option, Array as Arr, String as Str } from "effect"
import { ChartTooltip } from "@maple/views/charts/chart-spec"
import type { BuiltChart } from "@maple/views/charts/definitions/built-chart"
import { gradientId, gradientUrl } from "@maple/views/charts/definitions/gradient-id"
import {
AREA_FILL_END,
roundCapDasharray,
Expand Down Expand Up @@ -191,34 +192,30 @@ export const buildAreaSpike = (spec: SpikeSpec): BuiltChart<TimeseriesSpikeRow,
const axisContext = timeseriesAxisContext(rows)
const focus = tooltipFocusCapture()
const { solid, dashed } = splitOf(spec, rows)
const idPrefix = `areaSpike-${spec.arm}-`
const fillOf = (id: string, faded: boolean) =>
faded ? gradientId("areaSpike", spec.arm, id, "faded") : gradientId("areaSpike", spec.arm, id)
const opacityFor = (id: string) => (muted.has(id) ? MUTED_OPACITY : 1)
const x = (d: TimeseriesSpikeRow) => d.date
const definition = defineChart({
gradients: Arr.flatMap(series, (entry) => [
verticalGradient(`${idPrefix}${entry.id}`, entry.color),
verticalGradient(
`${idPrefix}${entry.id}Faded`,
entry.color,
FADED_START_OPACITY,
FADED_END_OPACITY,
),
verticalGradient(fillOf(entry.id, false), entry.color),
verticalGradient(fillOf(entry.id, true), entry.color, FADED_START_OPACITY, FADED_END_OPACITY),
]),
marks: [
...Arr.flatMap(series, (entry) => [
areaY(solid, {
id: `${entry.id}Area`,
x,
y: entry.value,
fill: `url(#${idPrefix}${entry.id})`,
fill: gradientUrl(fillOf(entry.id, false)),
stroke: "none",
fillOpacity: opacityFor(entry.id),
}),
areaY(dashed, {
id: `${entry.id}AreaIncomplete`,
x,
y: entry.value,
fill: `url(#${idPrefix}${entry.id}Faded)`,
fill: gradientUrl(fillOf(entry.id, true)),
stroke: "none",
fillOpacity: opacityFor(entry.id),
}),
Expand Down
36 changes: 34 additions & 2 deletions apps/web/src/lab/service-api-fixture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,16 @@ import type {
EndpointDetail,
StatusRow,
} from "../page/services/detail/model-tabs"
import { Record as Rec } from "effect"
import { Array as Arr, Record as Rec } from "effect"

// `/lab/service-api`: the ingest service's API tab over 12 hours of production numbers
// (2026-10-24 03:00-15:00 UTC, 1-in-20 sampled), generated by a one-off script.

/** 2026-10-24T15:00:00Z. */
export const LAB_NOW = 1_792_854_000_000

const UNMATCHED = "GET (unmatched)"

export const LAB_ENDPOINTS: ReadonlyArray<Endpoint> = [
{
spanName: "POST /v1/traces",
Expand Down Expand Up @@ -174,6 +176,27 @@ export const LAB_ENDPOINTS: ReadonlyArray<Endpoint> = [
requestBodyBytes: 14195,
requestBodyCount: 34,
},
// The ingest router names unmatched requests `GET (unmatched)`; the parens once crashed the sparkline.
{
spanName: UNMATCHED,
method: "GET",
route: "(unmatched)",
spanCount: 412,
estimatedSpanCount: 8240,
errorCount: 412,
estimatedErrorCount: 8240,
errorRate: 1,
avgDurationMs: 0.21,
p50DurationMs: 0.12,
p95DurationMs: 0.6,
p99DurationMs: 1.4,
httpClassifiedSpanCount: 412,
httpStatusCount: 412,
http4xxCount: 412,
http5xxCount: 0,
requestBodyBytes: 0,
requestBodyCount: 0,
},
]

export const LAB_STATUS_ROWS: ReadonlyArray<StatusRow> = [
Expand Down Expand Up @@ -312,7 +335,7 @@ export const LAB_STATUS_ROWS: ReadonlyArray<StatusRow> = [
},
]

export const LAB_ENDPOINT_SERIES: ReadonlyArray<ApiBucket> = [
const RECORDED_SERIES: ReadonlyArray<ApiBucket> = [
{
bucket: "2026-10-24T03:00:00.000Z",
spanName: "POST /v1/traces",
Expand Down Expand Up @@ -5723,6 +5746,15 @@ export const LAB_ENDPOINT_SERIES: ReadonlyArray<ApiBucket> = [
},
]

/** The unmatched route over the `/v1/events` buckets, a few probes per bucket. */
export const LAB_ENDPOINT_SERIES: ReadonlyArray<ApiBucket> = [
...RECORDED_SERIES,
...Arr.map(
Arr.filter(RECORDED_SERIES, (row) => row.spanName === "POST /v1/events"),
(row, index) => ({ ...row, spanName: UNMATCHED, estimatedSpanCount: 120 + ((index * 37) % 90) }),
),
]

export const LAB_API_OVERVIEW: ApiOverview = {
totals: {
spanCount: 234370,
Expand Down
28 changes: 26 additions & 2 deletions apps/web/src/page/dashboards/kit/tiles.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, expect, it } from "vitest"
import { EMPTY_VALUE } from "@maple/ui/lib/format"
import { listCellText } from "./tiles"
import { EMPTY_VALUE, formatValueByUnit } from "@maple/ui/lib/format"
import { formatCell, listCellText } from "./tiles"

describe("listCellText", () => {
it("formats object items instead of printing [object Object]", () => {
Expand All @@ -19,3 +19,27 @@ describe("listCellText", () => {
expect(listCellText(null)).toBe(EMPTY_VALUE)
})
})

describe("formatCell", () => {
it("trims float noise from a unitless measure", () => {
expect(formatCell(0.1 + 0.2)).toBe("0.3")
expect(formatCell(1234567.891)).toBe("1,234,567.89")
expect(formatCell(0.004567891)).toBe("0.00457")
expect(formatCell(1234.5)).toBe("1234.5")
})

it("keeps small integers and every string exactly as they came", () => {
expect(formatCell(200)).toBe("200")
expect(formatCell("3.10")).toBe("3.10")
expect(formatCell("18446744073709551615")).toBe("18446744073709551615")
expect(formatCell("GET /users/{id}")).toBe("GET /users/{id}")
})

it("groups a large integer measure", () => {
expect(formatCell(4687400)).toBe("4,687,400")
})

it("still formats a cell that has a unit by its unit", () => {
expect(formatCell(0.25, "percent")).toBe(formatValueByUnit(0.25, "percent"))
})
})
18 changes: 16 additions & 2 deletions apps/web/src/page/dashboards/kit/tiles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,11 +90,25 @@
? String(value)
: Formatter.format(value)

/** `formatCellValue`: units format numbers; unitless cells stay raw. */
/**
* A unitless number, readable but exact: float noise trimmed (`0.1 + 0.2` is `0.3`), 10,000 and up
* grouped. Strings stay raw: ids, versions (`3.10`) and UInt64 counts arrive as strings.
*/
const plainNumber = (value: unknown): string | undefined =>
typeof value === "number" && Number.isFinite(value)
? value.toLocaleString("en-US", {
useGrouping: Math.abs(value) >= 10_000,
...(Math.abs(value) < 1 && value !== 0
? { maximumSignificantDigits: 3 }
: { maximumFractionDigits: 2 }),
})
: undefined

/** `formatCellValue`: units format numbers; unitless measures are trimmed, other cells stay raw. */
export const formatCell = (value: unknown, unit?: string): string => {
if (value === null || value === undefined) return EMPTY_VALUE
const num = Number(value)
if (Number.isNaN(num) || unit === undefined || unit === "") return cellText(value)
if (Number.isNaN(num) || unit === undefined || unit === "") return plainNumber(value) ?? cellText(value)

Check notice on line 111 in apps/web/src/page/dashboards/kit/tiles.ts

View check run for this annotation

Maple Review Bot / Maple / review

correctness: Numeric items inside a list cell still print raw float noise

`listCellText` sends array items through `cellText` (`tiles.ts:299`), so a list column holding `[0.30000000000000004, 1234567.891]` still renders those exactly as they came, while the same value in a scalar cell now reads `0.3` and `1,234,567.89`. The list tile is left half-formatted.
return formatValueByUnit(num, unit)
}

Expand Down
5 changes: 3 additions & 2 deletions packages/views/src/charts/definitions/alert-rule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { ChartTooltip, type ChartSpec } from "../chart-spec"
import type { BuiltChart } from "./built-chart"
import { roundCapDasharray, SERIES_STROKE_WIDTH } from "./fixed-metric-axes"
import { cursorTooltip } from "./plot-constants"
import { gradientId as gradientIdOf, gradientUrl } from "./gradient-id"

// The alert rule chart's definition. The view half is `apps/web/src/page/alerts/shared/rule-chart.ts`.

Expand Down Expand Up @@ -66,7 +67,7 @@ export const buildAlertRule = (spec: AlertRuleSpec): BuiltChart<SignalPoint, Cha
return typeof value === "number" ? value : null
}
const curve = d3Curve(curveMonotoneX)
const gradientId = spec.gradientId
const gradientId = gradientIdOf(spec.gradientId)
const runsOf = (key: string) => seriesRuns(points, valueOf(key), at)
const loneDot = (key: string, fill: string) =>
dot(runsOf(key).lone, { x: at, y: valueOf(key), r: 2.5, fill })
Expand Down Expand Up @@ -188,7 +189,7 @@ export const buildAlertRule = (spec: AlertRuleSpec): BuiltChart<SignalPoint, Cha
x: at,
y: valueOf(SINGLE_KEY),
y1: () => yDomain[0],
fill: `url(#${gradientId})`,
fill: gradientUrl(gradientId),
stroke: "none",
curve,
}),
Expand Down
5 changes: 3 additions & 2 deletions packages/views/src/charts/definitions/analytics-traffic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { ChartTooltip, type ChartSpec } from "../chart-spec"
import type { BuiltChart } from "./built-chart"
import { verticalGradient } from "./fixed-metric-axes"
import { cursorTooltip, tooltipFocusCapture } from "./plot-constants"
import { gradientId, gradientUrl } from "./gradient-id"

// The analytics traffic chart's definition. Colours arrive resolved in the spec; series arrive in paint order.

Expand Down Expand Up @@ -52,7 +53,7 @@ export const buildAnalyticsTraffic = (spec: TrafficSpec): BuiltChart<TrafficPoin
const focus = tooltipFocusCapture()
const definition = defineChart({
gradients: Arr.map(spec.series, (entry) =>
verticalGradient(`${spec.gradientPrefix}-${entry.key}`, colorOf(entry.key)),
verticalGradient(gradientId(spec.gradientPrefix, entry.key), colorOf(entry.key)),
),
marks: [
...Arr.flatMap(spec.series, (entry) => {
Expand All @@ -71,7 +72,7 @@ export const buildAnalyticsTraffic = (spec: TrafficSpec): BuiltChart<TrafficPoin
x: at,
y: valueOf(entry.key),
y1: () => 0,
fill: `url(#${spec.gradientPrefix}-${entry.key})`,
fill: gradientUrl(gradientId(spec.gradientPrefix, entry.key)),
fillOpacity: 1,
stroke: "none",
curve,
Expand Down
5 changes: 3 additions & 2 deletions packages/views/src/charts/definitions/anomaly-signal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { ChartTooltip, type ChartSpec } from "../chart-spec"
import type { BuiltChart } from "./built-chart"
import { SERIES_STROKE_WIDTH, verticalGradient } from "./fixed-metric-axes"
import { cursorTooltip, tooltipFocusCapture } from "./plot-constants"
import { gradientId, gradientUrl } from "./gradient-id"

// The anomaly timeseries chart's definition. `thresholdRules` is copied below because
// threshold-rules.ts reads the theme through a React module; colours arrive resolved.
Expand Down Expand Up @@ -105,7 +106,7 @@ export const buildAnomalySignal = (spec: AnomalySignalSpec): BuiltChart<SignalPo

return {
definition: defineChart({
gradients: [verticalGradient(spec.gradientId, stroke)],
gradients: [verticalGradient(gradientId(spec.gradientId), stroke)],
marks: [
...(window
? [
Expand Down Expand Up @@ -133,7 +134,7 @@ export const buildAnomalySignal = (spec: AnomalySignalSpec): BuiltChart<SignalPo
x: at,
y: value,
y1: () => yDomain[0],
fill: `url(#${spec.gradientId})`,
fill: gradientUrl(gradientId(spec.gradientId)),
fillOpacity: 1,
stroke: "none",
curve: d3Curve(curveMonotoneX),
Expand Down
5 changes: 3 additions & 2 deletions packages/views/src/charts/definitions/area.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
verticalGradient,
} from "./fixed-metric-axes"
import { cursorTooltip, UNBOUNDED_FOCUS_DISTANCE } from "./plot-constants"
import { gradientId, gradientUrl } from "./gradient-id"

// Port of `QueryBuilderAreaChart` and `QueryBuilderLineChart` (packages/ui/src/components/charts/area/,
// line/), definition half: bands or lines, stacked or not, linear or monotone.
Expand Down Expand Up @@ -128,7 +129,7 @@ export const buildQueryBuilderArea = (spec: AreaSpec): BuiltChart<TimeseriesRow,
const curve = spec.curve === "monotone" ? d3Curve(curveMonotoneX) : undefined
const partialDash = roundCapDasharray(4, 4, isStacked ? STACKED_STROKE_WIDTH : STROKE_WIDTH)
const gradientIdFor = (key: string, partial: boolean) =>
`${spec.gradientPrefix}-${key}${partial ? "-partial" : ""}`
partial ? gradientId(spec.gradientPrefix, key, "partial") : gradientId(spec.gradientPrefix, key)
const hasDashed = dashed.length > 0
const fillOpacityFor = (partial: boolean) =>
isStacked ? (partial ? PARTIAL_STACKED_FILL_OPACITY : STACKED_FILL_OPACITY) : 1
Expand All @@ -139,7 +140,7 @@ export const buildQueryBuilderArea = (spec: AreaSpec): BuiltChart<TimeseriesRow,
x: (row: TimeseriesRow) => row.date,
y: (row: TimeseriesRow) => topOf(row, entry.key),
y1: (row: TimeseriesRow) => baseOf(row, entry.key),
fill: isStacked ? entry.color : `url(#${gradientIdFor(entry.key, partial)})`,
fill: isStacked ? entry.color : gradientUrl(gradientIdFor(entry.key, partial)),
fillOpacity: fillOpacityFor(partial),
stroke: "none",
curve,
Expand Down
8 changes: 6 additions & 2 deletions packages/views/src/charts/definitions/bench-overview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import type { BuiltChart } from "./built-chart"
import { verticalGradient } from "./fixed-metric-axes"
import { cursorTooltip } from "./plot-constants"
import { nonZeroOr } from "@maple/ui/lib/number"
import { gradientId, gradientUrl } from "./gradient-id"

// The `/lab/bench/tanstack` chart definitions: throughput area, error rate area and latency line.

Expand Down Expand Up @@ -171,7 +172,7 @@ export const buildBenchOverview = (spec: BenchSpec): BuiltChart<Row, ChartValue,
id: `${lead.key}Area`,
x,
y: y(lead.key),
fill: `url(#${spec.gradientId})`,
fill: gradientUrl(gradientId(spec.gradientId)),
fillOpacity: 1,
stroke: "none",
}),
Expand All @@ -194,7 +195,10 @@ export const buildBenchOverview = (spec: BenchSpec): BuiltChart<Row, ChartValue,
]
return {
definition: defineChart({
gradients: isArea && lead !== undefined ? [verticalGradient(spec.gradientId, lead.color)] : [],
gradients:
isArea && lead !== undefined
? [verticalGradient(gradientId(spec.gradientId), lead.color)]
: [],
marks,
scales: { x: xScale(axisContext), y: yOf(spec, rows, rateLabel) },
margin: spec.marginLeft === undefined ? undefined : { left: spec.marginLeft },
Expand Down
3 changes: 2 additions & 1 deletion packages/views/src/charts/definitions/fixed-metric-axes.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { ChartLinearGradient } from "@tanstack/charts"
import type { GradientId } from "./gradient-id"

// copied: the paint helpers of packages/ui/src/components/plot/plot-paint.ts (its module imports
// React). The axes are in `../timeseries`.
Expand All @@ -15,7 +16,7 @@ export const AREA_FILL_PARTIAL = 0.1

/** `verticalGradient` (plot-paint.ts). */
export const verticalGradient = (
id: string,
id: GradientId,
color: string,
startOpacity = AREA_FILL_START,
endOpacity = AREA_FILL_END,
Expand Down
Loading
Loading