@benchkit/chart 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +640 -0
- package/dist/ComparisonSummaryTable.test.d.ts +2 -0
- package/dist/ComparisonSummaryTable.test.d.ts.map +1 -0
- package/dist/ComparisonSummaryTable.test.js +53 -0
- package/dist/ComparisonSummaryTable.test.js.map +1 -0
- package/dist/Dashboard.d.ts +25 -0
- package/dist/Dashboard.d.ts.map +1 -0
- package/dist/Dashboard.js +87 -0
- package/dist/Dashboard.js.map +1 -0
- package/dist/Dashboard.test.d.ts +2 -0
- package/dist/Dashboard.test.d.ts.map +1 -0
- package/dist/Dashboard.test.js +49 -0
- package/dist/Dashboard.test.js.map +1 -0
- package/dist/RunDashboard.d.ts +20 -0
- package/dist/RunDashboard.d.ts.map +1 -0
- package/dist/RunDashboard.js +174 -0
- package/dist/RunDashboard.js.map +1 -0
- package/dist/RunDashboard.test.d.ts +2 -0
- package/dist/RunDashboard.test.d.ts.map +1 -0
- package/dist/RunDashboard.test.js +58 -0
- package/dist/RunDashboard.test.js.map +1 -0
- package/dist/RunDetail.d.ts +26 -0
- package/dist/RunDetail.d.ts.map +1 -0
- package/dist/RunDetail.js +100 -0
- package/dist/RunDetail.js.map +1 -0
- package/dist/RunDetail.test.d.ts +2 -0
- package/dist/RunDetail.test.d.ts.map +1 -0
- package/dist/RunDetail.test.js +168 -0
- package/dist/RunDetail.test.js.map +1 -0
- package/dist/RunSelector.test.d.ts +2 -0
- package/dist/RunSelector.test.d.ts.map +1 -0
- package/dist/RunSelector.test.js +45 -0
- package/dist/RunSelector.test.js.map +1 -0
- package/dist/SampleChart.test.d.ts +2 -0
- package/dist/SampleChart.test.d.ts.map +1 -0
- package/dist/SampleChart.test.js +48 -0
- package/dist/SampleChart.test.js.map +1 -0
- package/dist/TagFilter.test.d.ts +2 -0
- package/dist/TagFilter.test.d.ts.map +1 -0
- package/dist/TagFilter.test.js +106 -0
- package/dist/TagFilter.test.js.map +1 -0
- package/dist/VerdictBanner.test.d.ts +2 -0
- package/dist/VerdictBanner.test.d.ts.map +1 -0
- package/dist/VerdictBanner.test.js +56 -0
- package/dist/VerdictBanner.test.js.map +1 -0
- package/dist/chart-config.d.ts +100 -0
- package/dist/chart-config.d.ts.map +1 -0
- package/dist/chart-config.js +81 -0
- package/dist/chart-config.js.map +1 -0
- package/dist/colors.d.ts +11 -0
- package/dist/colors.d.ts.map +1 -0
- package/dist/colors.js +16 -0
- package/dist/colors.js.map +1 -0
- package/dist/comparison-transforms.d.ts +22 -0
- package/dist/comparison-transforms.d.ts.map +1 -0
- package/dist/comparison-transforms.js +20 -0
- package/dist/comparison-transforms.js.map +1 -0
- package/dist/comparison-transforms.test.d.ts +2 -0
- package/dist/comparison-transforms.test.d.ts.map +1 -0
- package/dist/comparison-transforms.test.js +75 -0
- package/dist/comparison-transforms.test.js.map +1 -0
- package/dist/components/ComparisonBar.d.ts +13 -0
- package/dist/components/ComparisonBar.d.ts.map +1 -0
- package/dist/components/ComparisonBar.js +94 -0
- package/dist/components/ComparisonBar.js.map +1 -0
- package/dist/components/ComparisonChart.d.ts +31 -0
- package/dist/components/ComparisonChart.d.ts.map +1 -0
- package/dist/components/ComparisonChart.js +118 -0
- package/dist/components/ComparisonChart.js.map +1 -0
- package/dist/components/ComparisonSummaryTable.d.ts +29 -0
- package/dist/components/ComparisonSummaryTable.d.ts.map +1 -0
- package/dist/components/ComparisonSummaryTable.js +41 -0
- package/dist/components/ComparisonSummaryTable.js.map +1 -0
- package/dist/components/DashboardToolbar.d.ts +21 -0
- package/dist/components/DashboardToolbar.d.ts.map +1 -0
- package/dist/components/DashboardToolbar.js +14 -0
- package/dist/components/DashboardToolbar.js.map +1 -0
- package/dist/components/DateRangeFilter.d.ts +19 -0
- package/dist/components/DateRangeFilter.d.ts.map +1 -0
- package/dist/components/DateRangeFilter.js +44 -0
- package/dist/components/DateRangeFilter.js.map +1 -0
- package/dist/components/HeroSection.d.ts +12 -0
- package/dist/components/HeroSection.d.ts.map +1 -0
- package/dist/components/HeroSection.js +6 -0
- package/dist/components/HeroSection.js.map +1 -0
- package/dist/components/Leaderboard.d.ts +9 -0
- package/dist/components/Leaderboard.d.ts.map +1 -0
- package/dist/components/Leaderboard.js +41 -0
- package/dist/components/Leaderboard.js.map +1 -0
- package/dist/components/MetricCard.d.ts +16 -0
- package/dist/components/MetricCard.d.ts.map +1 -0
- package/dist/components/MetricCard.js +25 -0
- package/dist/components/MetricCard.js.map +1 -0
- package/dist/components/MetricDetailView.d.ts +15 -0
- package/dist/components/MetricDetailView.d.ts.map +1 -0
- package/dist/components/MetricDetailView.js +11 -0
- package/dist/components/MetricDetailView.js.map +1 -0
- package/dist/components/MonitorSection.d.ts +21 -0
- package/dist/components/MonitorSection.d.ts.map +1 -0
- package/dist/components/MonitorSection.js +38 -0
- package/dist/components/MonitorSection.js.map +1 -0
- package/dist/components/OverviewGrid.d.ts +17 -0
- package/dist/components/OverviewGrid.d.ts.map +1 -0
- package/dist/components/OverviewGrid.js +15 -0
- package/dist/components/OverviewGrid.js.map +1 -0
- package/dist/components/RunSelector.d.ts +20 -0
- package/dist/components/RunSelector.d.ts.map +1 -0
- package/dist/components/RunSelector.js +53 -0
- package/dist/components/RunSelector.js.map +1 -0
- package/dist/components/RunTable.d.ts +13 -0
- package/dist/components/RunTable.d.ts.map +1 -0
- package/dist/components/RunTable.js +24 -0
- package/dist/components/RunTable.js.map +1 -0
- package/dist/components/SampleChart.d.ts +21 -0
- package/dist/components/SampleChart.d.ts.map +1 -0
- package/dist/components/SampleChart.js +77 -0
- package/dist/components/SampleChart.js.map +1 -0
- package/dist/components/TagFilter.d.ts +12 -0
- package/dist/components/TagFilter.d.ts.map +1 -0
- package/dist/components/TagFilter.js +59 -0
- package/dist/components/TagFilter.js.map +1 -0
- package/dist/components/TrendChart.d.ts +25 -0
- package/dist/components/TrendChart.d.ts.map +1 -0
- package/dist/components/TrendChart.js +95 -0
- package/dist/components/TrendChart.js.map +1 -0
- package/dist/components/VerdictBanner.d.ts +13 -0
- package/dist/components/VerdictBanner.d.ts.map +1 -0
- package/dist/components/VerdictBanner.js +22 -0
- package/dist/components/VerdictBanner.js.map +1 -0
- package/dist/dashboard-labels.d.ts +47 -0
- package/dist/dashboard-labels.d.ts.map +1 -0
- package/dist/dashboard-labels.js +44 -0
- package/dist/dashboard-labels.js.map +1 -0
- package/dist/dashboard-labels.test.d.ts +2 -0
- package/dist/dashboard-labels.test.d.ts.map +1 -0
- package/dist/dashboard-labels.test.js +34 -0
- package/dist/dashboard-labels.test.js.map +1 -0
- package/dist/dataset-transforms.d.ts +41 -0
- package/dist/dataset-transforms.d.ts.map +1 -0
- package/dist/dataset-transforms.js +132 -0
- package/dist/dataset-transforms.js.map +1 -0
- package/dist/dataset-transforms.test.d.ts +2 -0
- package/dist/dataset-transforms.test.d.ts.map +1 -0
- package/dist/dataset-transforms.test.js +208 -0
- package/dist/dataset-transforms.test.js.map +1 -0
- package/dist/date-range.test.d.ts +2 -0
- package/dist/date-range.test.d.ts.map +1 -0
- package/dist/date-range.test.js +116 -0
- package/dist/date-range.test.js.map +1 -0
- package/dist/embed.d.ts +45 -0
- package/dist/embed.d.ts.map +1 -0
- package/dist/embed.js +170 -0
- package/dist/embed.js.map +1 -0
- package/dist/embed.test.d.ts +2 -0
- package/dist/embed.test.d.ts.map +1 -0
- package/dist/embed.test.js +15 -0
- package/dist/embed.test.js.map +1 -0
- package/dist/fetch.d.ts +27 -0
- package/dist/fetch.d.ts.map +1 -0
- package/dist/fetch.js +73 -0
- package/dist/fetch.js.map +1 -0
- package/dist/fetch.test.d.ts +2 -0
- package/dist/fetch.test.d.ts.map +1 -0
- package/dist/fetch.test.js +75 -0
- package/dist/fetch.test.js.map +1 -0
- package/dist/format-utils.d.ts +48 -0
- package/dist/format-utils.d.ts.map +1 -0
- package/dist/format-utils.js +98 -0
- package/dist/format-utils.js.map +1 -0
- package/dist/format-utils.test.d.ts +2 -0
- package/dist/format-utils.test.d.ts.map +1 -0
- package/dist/format-utils.test.js +103 -0
- package/dist/format-utils.test.js.map +1 -0
- package/dist/hooks/useChartLifecycle.d.ts +20 -0
- package/dist/hooks/useChartLifecycle.d.ts.map +1 -0
- package/dist/hooks/useChartLifecycle.js +46 -0
- package/dist/hooks/useChartLifecycle.js.map +1 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +36 -0
- package/dist/index.js.map +1 -0
- package/dist/index.test.d.ts +2 -0
- package/dist/index.test.d.ts.map +1 -0
- package/dist/index.test.js +10 -0
- package/dist/index.test.js.map +1 -0
- package/dist/labels.d.ts +5 -0
- package/dist/labels.d.ts.map +1 -0
- package/dist/labels.js +41 -0
- package/dist/labels.js.map +1 -0
- package/dist/labels.test.d.ts +2 -0
- package/dist/labels.test.d.ts.map +1 -0
- package/dist/labels.test.js +28 -0
- package/dist/labels.test.js.map +1 -0
- package/dist/leaderboard.d.ts +29 -0
- package/dist/leaderboard.d.ts.map +1 -0
- package/dist/leaderboard.js +40 -0
- package/dist/leaderboard.js.map +1 -0
- package/dist/leaderboard.test.d.ts +2 -0
- package/dist/leaderboard.test.d.ts.map +1 -0
- package/dist/leaderboard.test.js +105 -0
- package/dist/leaderboard.test.js.map +1 -0
- package/dist/sample-utils.d.ts +4 -0
- package/dist/sample-utils.d.ts.map +1 -0
- package/dist/sample-utils.js +12 -0
- package/dist/sample-utils.js.map +1 -0
- package/dist/style.css +912 -0
- package/dist/theme.d.ts +12 -0
- package/dist/theme.d.ts.map +1 -0
- package/dist/theme.js +17 -0
- package/dist/theme.js.map +1 -0
- package/dist/utils.d.ts +37 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +63 -0
- package/dist/utils.js.map +1 -0
- package/dist/utils.test.d.ts +2 -0
- package/dist/utils.test.d.ts.map +1 -0
- package/dist/utils.test.js +196 -0
- package/dist/utils.test.js.map +1 -0
- package/package.json +46 -0
package/README.md
ADDED
|
@@ -0,0 +1,640 @@
|
|
|
1
|
+
# @benchkit/chart
|
|
2
|
+
|
|
3
|
+
Preact components for rendering [benchkit](../../README.md) benchmark dashboards. Fetches pre-aggregated JSON from a `bench-data` branch and renders interactive trend charts, comparison bars, leaderboards, tag filters, and runner-metrics panels — all client-side with no backend.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
> **Note:** `@benchkit/chart` is not yet published to the npm registry.
|
|
8
|
+
> Until the first release, install from source as described below.
|
|
9
|
+
|
|
10
|
+
Clone the benchkit repository, install dependencies, and build the packages:
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
git clone https://github.com/strawgate/benchkit.git
|
|
14
|
+
cd benchkit
|
|
15
|
+
npm ci
|
|
16
|
+
npm run build
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Then, from your project directory, link the local packages (adjust the path
|
|
20
|
+
to where you cloned benchkit):
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm link <path-to-benchkit>/packages/chart <path-to-benchkit>/packages/format
|
|
24
|
+
npm install preact
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Or use `file:` references in your project's `package.json`:
|
|
28
|
+
|
|
29
|
+
```jsonc
|
|
30
|
+
{
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"@benchkit/chart": "file:<path-to-benchkit>/packages/chart",
|
|
33
|
+
"@benchkit/format": "file:<path-to-benchkit>/packages/format",
|
|
34
|
+
"preact": "^10.0.0"
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Once published, you will be able to install directly:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
npm install @benchkit/chart preact
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Quick start
|
|
46
|
+
|
|
47
|
+
```tsx
|
|
48
|
+
import "@benchkit/chart/css";
|
|
49
|
+
import { Dashboard } from "@benchkit/chart";
|
|
50
|
+
|
|
51
|
+
export function App() {
|
|
52
|
+
return (
|
|
53
|
+
<Dashboard
|
|
54
|
+
source={{
|
|
55
|
+
owner: "your-org",
|
|
56
|
+
repo: "your-repo",
|
|
57
|
+
branch: "bench-data", // optional, this is the default
|
|
58
|
+
}}
|
|
59
|
+
/>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
The `Dashboard` component fetches `data/index.json` and `data/series/*.json` from `https://raw.githubusercontent.com/{owner}/{repo}/{branch}/…` and renders all charts automatically.
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Components
|
|
69
|
+
|
|
70
|
+
### `Dashboard`
|
|
71
|
+
|
|
72
|
+
The top-level ready-made dashboard. Automatically fetches data, partitions metrics into user benchmarks and `_monitor/` system metrics, detects regressions, and renders all sub-components.
|
|
73
|
+
|
|
74
|
+
```tsx
|
|
75
|
+
import "@benchkit/chart/css";
|
|
76
|
+
import { Dashboard } from "@benchkit/chart";
|
|
77
|
+
|
|
78
|
+
<Dashboard
|
|
79
|
+
source={{ owner: "your-org", repo: "your-repo" }}
|
|
80
|
+
metricLabelFormatter={(m) => m.replace(/_/g, " ")}
|
|
81
|
+
seriesNameFormatter={(name) => name.replace(/^Benchmark/, "")}
|
|
82
|
+
commitHref={(sha, run) => `https://github.com/your-org/your-repo/commit/${sha}`}
|
|
83
|
+
regressionThreshold={10}
|
|
84
|
+
regressionWindow={5}
|
|
85
|
+
/>
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
#### `DashboardProps`
|
|
89
|
+
|
|
90
|
+
| Prop | Type | Default | Description |
|
|
91
|
+
|------|------|---------|-------------|
|
|
92
|
+
| `source` | `DataSource` | — | **Required.** Where to fetch data from. |
|
|
93
|
+
| `class` | `string` | — | CSS class applied to the root element. |
|
|
94
|
+
| `maxPoints` | `number` | `20` | Max data points per sparkline. |
|
|
95
|
+
| `maxRuns` | `number` | `20` | Max rows in the recent-runs table. |
|
|
96
|
+
| `metricLabelFormatter` | `(metric: string) => string` | — | Custom metric name renderer. |
|
|
97
|
+
| `seriesNameFormatter` | `(name: string, entry: SeriesEntry) => string` | — | Custom series name renderer. |
|
|
98
|
+
| `commitHref` | `(commit: string, run: RunEntry) => string \| undefined` | — | Builds a URL for each commit SHA in the run table. |
|
|
99
|
+
| `regressionThreshold` | `number` | `10` | Percentage change that triggers a regression warning. |
|
|
100
|
+
| `regressionWindow` | `number` | `5` | Number of preceding data points averaged for regression detection. |
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
### `TrendChart`
|
|
105
|
+
|
|
106
|
+
Renders a time-series line chart for a single metric. Optionally highlights regressed series with a red dot on their latest point.
|
|
107
|
+
|
|
108
|
+
```tsx
|
|
109
|
+
import "@benchkit/chart/css";
|
|
110
|
+
import { TrendChart } from "@benchkit/chart";
|
|
111
|
+
import type { SeriesFile } from "@benchkit/format";
|
|
112
|
+
|
|
113
|
+
<TrendChart
|
|
114
|
+
series={seriesFile}
|
|
115
|
+
title="ns/op"
|
|
116
|
+
height={300}
|
|
117
|
+
lineWidth={1.5}
|
|
118
|
+
maxPoints={20}
|
|
119
|
+
seriesNameFormatter={(name) => name.replace(/^Benchmark/, "")}
|
|
120
|
+
regressions={regressionResults}
|
|
121
|
+
/>
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
#### `TrendChartProps`
|
|
125
|
+
|
|
126
|
+
| Prop | Type | Default | Description |
|
|
127
|
+
|------|------|---------|-------------|
|
|
128
|
+
| `series` | `SeriesFile` | — | **Required.** Pre-aggregated series data. |
|
|
129
|
+
| `title` | `string` | — | Chart heading. |
|
|
130
|
+
| `height` | `number` | `300` | Canvas height in pixels. |
|
|
131
|
+
| `lineWidth` | `number` | `1.75` (`1.5` in compact mode) | Stroke width for trend lines. |
|
|
132
|
+
| `maxPoints` | `number` | — | Truncate each series to the most recent N points. |
|
|
133
|
+
| `seriesNameFormatter` | `(name: string, entry: SeriesEntry) => string` | — | Custom legend label renderer. |
|
|
134
|
+
| `class` | `string` | — | CSS class applied to the wrapper `<div>`. |
|
|
135
|
+
| `regressions` | `RegressionResult[]` | — | Regression results; affected series get a red dot on their last point. |
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
### `ComparisonBar`
|
|
140
|
+
|
|
141
|
+
Renders a horizontal (or vertical) bar chart comparing the **latest value** of each series within a metric, with optional error bars.
|
|
142
|
+
|
|
143
|
+
```tsx
|
|
144
|
+
import "@benchkit/chart/css";
|
|
145
|
+
import { ComparisonBar } from "@benchkit/chart";
|
|
146
|
+
|
|
147
|
+
<ComparisonBar
|
|
148
|
+
series={seriesFile}
|
|
149
|
+
title="Latest throughput"
|
|
150
|
+
height={250}
|
|
151
|
+
seriesNameFormatter={(name) => name.replace(/^Benchmark/, "")}
|
|
152
|
+
/>
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
#### `ComparisonBarProps`
|
|
156
|
+
|
|
157
|
+
| Prop | Type | Default | Description |
|
|
158
|
+
|------|------|---------|-------------|
|
|
159
|
+
| `series` | `SeriesFile` | — | **Required.** Pre-aggregated series data. |
|
|
160
|
+
| `title` | `string` | — | Chart heading. |
|
|
161
|
+
| `height` | `number` | `250` | Canvas height in pixels. |
|
|
162
|
+
| `seriesNameFormatter` | `(name: string, entry: SeriesEntry) => string` | — | Custom bar label renderer. |
|
|
163
|
+
| `class` | `string` | — | CSS class applied to the wrapper `<div>`. |
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
### `Leaderboard`
|
|
168
|
+
|
|
169
|
+
Renders a ranked table of series sorted by their latest value, direction-aware. Highlights the winner with a ★ badge and colors delta arrows green/red.
|
|
170
|
+
|
|
171
|
+
```tsx
|
|
172
|
+
import "@benchkit/chart/css";
|
|
173
|
+
import { Leaderboard } from "@benchkit/chart";
|
|
174
|
+
|
|
175
|
+
<Leaderboard
|
|
176
|
+
series={seriesFile}
|
|
177
|
+
seriesNameFormatter={(name) => name.replace(/^Benchmark/, "")}
|
|
178
|
+
/>
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
#### `LeaderboardProps`
|
|
182
|
+
|
|
183
|
+
| Prop | Type | Default | Description |
|
|
184
|
+
|------|------|---------|-------------|
|
|
185
|
+
| `series` | `SeriesFile` | — | **Required.** Pre-aggregated series data. |
|
|
186
|
+
| `seriesNameFormatter` | `(name: string, entry: SeriesEntry) => string` | — | Custom name renderer for each row. |
|
|
187
|
+
| `class` | `string` | — | CSS class applied to the wrapper `<div>`. |
|
|
188
|
+
|
|
189
|
+
The component renders `null` when there are no series with data, and a plain text label when only one series is present (no table needed).
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
### `TagFilter`
|
|
194
|
+
|
|
195
|
+
Renders a row of pill buttons for filtering series by their `tags`. Only rendered when at least one series carries tags; returns `null` otherwise.
|
|
196
|
+
|
|
197
|
+
```tsx
|
|
198
|
+
import "@benchkit/chart/css";
|
|
199
|
+
import { TagFilter, filterSeriesFile } from "@benchkit/chart";
|
|
200
|
+
import { useState } from "preact/hooks";
|
|
201
|
+
|
|
202
|
+
function MyDashboard({ seriesMap }: { seriesMap: Map<string, SeriesFile> }) {
|
|
203
|
+
const [activeFilters, setActiveFilters] = useState<Record<string, string>>({});
|
|
204
|
+
|
|
205
|
+
return (
|
|
206
|
+
<>
|
|
207
|
+
<TagFilter
|
|
208
|
+
seriesMap={seriesMap}
|
|
209
|
+
activeFilters={activeFilters}
|
|
210
|
+
onFilterChange={setActiveFilters}
|
|
211
|
+
/>
|
|
212
|
+
{/* pass filtered series to charts */}
|
|
213
|
+
{[...seriesMap.entries()].map(([metric, sf]) => (
|
|
214
|
+
<TrendChart key={metric} series={filterSeriesFile(sf, activeFilters)} title={metric} />
|
|
215
|
+
))}
|
|
216
|
+
</>
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
#### `TagFilterProps`
|
|
222
|
+
|
|
223
|
+
| Prop | Type | Default | Description |
|
|
224
|
+
|------|------|---------|-------------|
|
|
225
|
+
| `seriesMap` | `Map<string, SeriesFile>` | — | **Required.** All series for the current view; tags are extracted from this map. |
|
|
226
|
+
| `activeFilters` | `Record<string, string>` | — | **Required.** Currently active `{ tagKey: tagValue }` pairs. |
|
|
227
|
+
| `onFilterChange` | `(filters: Record<string, string>) => void` | — | **Required.** Called with a new filter map whenever the user toggles a pill. |
|
|
228
|
+
|
|
229
|
+
Each tag key is rendered as a group of pill buttons. Clicking an active pill deactivates it; clicking an inactive pill activates it (one active value per key at a time). A **Clear filters** button appears when any filter is active.
|
|
230
|
+
|
|
231
|
+
---
|
|
232
|
+
|
|
233
|
+
### `MonitorSection`
|
|
234
|
+
|
|
235
|
+
Renders the **Runner Metrics** section for `_monitor/` prefixed metrics produced by the [Benchkit Monitor action](../../actions/monitor). Displays a runner-context card (OS, CPU, memory, poll interval) and a grid of sparklines — one per monitor metric.
|
|
236
|
+
|
|
237
|
+
```tsx
|
|
238
|
+
import "@benchkit/chart/css";
|
|
239
|
+
import { MonitorSection } from "@benchkit/chart";
|
|
240
|
+
|
|
241
|
+
<MonitorSection
|
|
242
|
+
monitorSeriesMap={monitorSeriesMap}
|
|
243
|
+
index={indexFile}
|
|
244
|
+
maxPoints={20}
|
|
245
|
+
metricLabelFormatter={(m) => m.replace(/^_monitor\//, "")}
|
|
246
|
+
seriesNameFormatter={(name) => name}
|
|
247
|
+
onMetricClick={(metric) => setSelected(metric)}
|
|
248
|
+
selectedMetric={selectedMetric}
|
|
249
|
+
/>
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
#### `MonitorSectionProps`
|
|
253
|
+
|
|
254
|
+
| Prop | Type | Default | Description |
|
|
255
|
+
|------|------|---------|-------------|
|
|
256
|
+
| `monitorSeriesMap` | `Map<string, SeriesFile>` | — | **Required.** Map of `_monitor/…` metric names to their series files. |
|
|
257
|
+
| `index` | `IndexFile` | — | **Required.** Full index; used to surface the latest runner context card. |
|
|
258
|
+
| `maxPoints` | `number` | `20` | Max data points per sparkline. |
|
|
259
|
+
| `metricLabelFormatter` | `(metric: string) => string` | — | Custom label renderer; defaults to stripping the `_monitor/` prefix. |
|
|
260
|
+
| `seriesNameFormatter` | `(name: string, entry: SeriesEntry) => string` | — | Custom series name renderer. |
|
|
261
|
+
| `onMetricClick` | `(metric: string) => void` | — | Called when the user clicks a monitor metric card. |
|
|
262
|
+
| `selectedMetric` | `string \| null` | — | Highlights the card with a matching metric name. |
|
|
263
|
+
|
|
264
|
+
The component renders `null` when `monitorSeriesMap` is empty.
|
|
265
|
+
|
|
266
|
+
---
|
|
267
|
+
|
|
268
|
+
### `RunTable`
|
|
269
|
+
|
|
270
|
+
Renders a paginated table of recent benchmark runs with columns for run ID, timestamp, commit SHA, Git ref, benchmark count, and metrics list.
|
|
271
|
+
|
|
272
|
+
```tsx
|
|
273
|
+
import "@benchkit/chart/css";
|
|
274
|
+
import { RunTable } from "@benchkit/chart";
|
|
275
|
+
|
|
276
|
+
<RunTable
|
|
277
|
+
index={indexFile}
|
|
278
|
+
maxRows={20}
|
|
279
|
+
commitHref={(sha, run) => `https://github.com/your-org/your-repo/commit/${sha}`}
|
|
280
|
+
/>
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
#### `RunTableProps`
|
|
284
|
+
|
|
285
|
+
| Prop | Type | Default | Description |
|
|
286
|
+
|------|------|---------|-------------|
|
|
287
|
+
| `index` | `IndexFile` | — | **Required.** Full index file. |
|
|
288
|
+
| `maxRows` | `number` | — | Limit the number of rows shown. |
|
|
289
|
+
| `onSelectRun` | `(runId: string) => void` | — | Called when a row is clicked. |
|
|
290
|
+
| `commitHref` | `(commit: string, run: RunEntry) => string \| undefined` | — | Builds a URL for each commit SHA. |
|
|
291
|
+
| `class` | `string` | — | CSS class applied to the `<table>` element. |
|
|
292
|
+
|
|
293
|
+
---
|
|
294
|
+
|
|
295
|
+
### `RunDetail`
|
|
296
|
+
|
|
297
|
+
Renders a deep-dive view of a single benchmark run, including metadata, metric snapshots partitioned into user and monitor metrics, and optional comparison results. Can fetch data on demand or accept a preloaded detail object.
|
|
298
|
+
|
|
299
|
+
```tsx
|
|
300
|
+
import "@benchkit/chart/css";
|
|
301
|
+
import { RunDetail } from "@benchkit/chart";
|
|
302
|
+
|
|
303
|
+
<RunDetail
|
|
304
|
+
source={{ owner: "your-org", repo: "your-repo" }}
|
|
305
|
+
runId="123456789-1"
|
|
306
|
+
commitHref={(sha) => `https://github.com/your-org/your-repo/commit/${sha}`}
|
|
307
|
+
metricLabelFormatter={(m) => m.replace(/_/g, " ")}
|
|
308
|
+
/>
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
#### `RunDetailProps`
|
|
312
|
+
|
|
313
|
+
| Prop | Type | Default | Description |
|
|
314
|
+
|------|------|---------|-------------|
|
|
315
|
+
| `detail` | `RunDetailView` | — | Preloaded run detail. When provided, `source` and `runId` are ignored. |
|
|
316
|
+
| `source` | `DataSource` | — | **Required when `detail` is not set.** Data source for on-demand fetching. |
|
|
317
|
+
| `runId` | `string` | — | **Required when `detail` is not set.** Run ID to fetch. |
|
|
318
|
+
| `comparison` | `ComparisonResult \| null` | — | Optional comparison result to show a verdict banner + comparison table. |
|
|
319
|
+
| `currentLabel` | `string` | — | Label for the current run in comparison context. |
|
|
320
|
+
| `baselineLabel` | `string` | — | Label for the baseline run in comparison context. |
|
|
321
|
+
| `commitHref` | `(commit: string) => string \| undefined` | — | Builds a URL for a commit hash. |
|
|
322
|
+
| `metricLabelFormatter` | `(metric: string) => string` | `defaultMetricLabel` | Custom metric label renderer. |
|
|
323
|
+
| `class` | `string` | — | CSS class applied to the root element. |
|
|
324
|
+
|
|
325
|
+
---
|
|
326
|
+
|
|
327
|
+
### `RunDashboard`
|
|
328
|
+
|
|
329
|
+
A PR-oriented dashboard that auto-selects the latest run, resolves a baseline from the default branch, and renders run selectors, comparison verdict, and a summary table.
|
|
330
|
+
|
|
331
|
+
```tsx
|
|
332
|
+
import "@benchkit/chart/css";
|
|
333
|
+
import { RunDashboard } from "@benchkit/chart";
|
|
334
|
+
|
|
335
|
+
<RunDashboard
|
|
336
|
+
source={{ owner: "your-org", repo: "your-repo" }}
|
|
337
|
+
defaultBranch="main"
|
|
338
|
+
regressionThreshold={5}
|
|
339
|
+
commitHref={(sha) => `https://github.com/your-org/your-repo/commit/${sha}`}
|
|
340
|
+
/>
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
#### `RunDashboardProps`
|
|
344
|
+
|
|
345
|
+
| Prop | Type | Default | Description |
|
|
346
|
+
|------|------|---------|-------------|
|
|
347
|
+
| `source` | `DataSource` | — | **Required.** Where to fetch data from. |
|
|
348
|
+
| `defaultBranch` | `string` | `"main"` | Branch used for baseline resolution. |
|
|
349
|
+
| `regressionThreshold` | `number` | `5` | Percentage change threshold for regressions. |
|
|
350
|
+
| `commitHref` | `(commit: string) => string \| undefined` | — | Link builder for commit hashes. |
|
|
351
|
+
| `metricLabelFormatter` | `(metric: string) => string` | — | Custom metric label renderer. |
|
|
352
|
+
| `class` | `string` | — | CSS class applied to the root element. |
|
|
353
|
+
|
|
354
|
+
---
|
|
355
|
+
|
|
356
|
+
## Formatting and label helpers
|
|
357
|
+
|
|
358
|
+
The package root intentionally exports a small set of reusable formatting and
|
|
359
|
+
label helpers for custom dashboards:
|
|
360
|
+
|
|
361
|
+
- `formatValue()`
|
|
362
|
+
- `formatFixedValue()`
|
|
363
|
+
- `formatRef()`
|
|
364
|
+
- `formatPct()`
|
|
365
|
+
- `formatTimestamp()`
|
|
366
|
+
- `shortCommit()`
|
|
367
|
+
- `formatDirection()`
|
|
368
|
+
- `defaultMetricLabel()`
|
|
369
|
+
- `defaultMonitorMetricLabel()`
|
|
370
|
+
- `isMonitorMetric()`
|
|
371
|
+
|
|
372
|
+
These are the stable helpers to build on when you need benchkit-flavored
|
|
373
|
+
display logic in your own UI. Component-local implementation helpers such as
|
|
374
|
+
comparison-table sorting or icon selection are not part of the package-root API.
|
|
375
|
+
|
|
376
|
+
---
|
|
377
|
+
|
|
378
|
+
## Data fetching
|
|
379
|
+
|
|
380
|
+
### `DataSource`
|
|
381
|
+
|
|
382
|
+
Describes where to fetch benchmark data from.
|
|
383
|
+
|
|
384
|
+
```ts
|
|
385
|
+
interface DataSource {
|
|
386
|
+
owner?: string; // GitHub repository owner
|
|
387
|
+
repo?: string; // GitHub repository name
|
|
388
|
+
branch?: string; // Data branch (default: "bench-data")
|
|
389
|
+
baseUrl?: string; // Absolute URL override — owner/repo/branch are ignored when set
|
|
390
|
+
}
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
When `baseUrl` is provided, files are resolved relative to that URL. Otherwise data is fetched from `https://raw.githubusercontent.com/{owner}/{repo}/{branch}/`.
|
|
394
|
+
|
|
395
|
+
### `fetchIndex(source, signal?)`
|
|
396
|
+
|
|
397
|
+
Fetches `data/index.json` and returns an `IndexFile`.
|
|
398
|
+
|
|
399
|
+
```ts
|
|
400
|
+
import { fetchIndex } from "@benchkit/chart";
|
|
401
|
+
|
|
402
|
+
const index = await fetchIndex({ owner: "your-org", repo: "your-repo" });
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
### `fetchSeries(source, metric, signal?)`
|
|
406
|
+
|
|
407
|
+
Fetches `data/series/{metric}.json` and returns a `SeriesFile`.
|
|
408
|
+
|
|
409
|
+
```ts
|
|
410
|
+
import { fetchSeries } from "@benchkit/chart";
|
|
411
|
+
|
|
412
|
+
const series = await fetchSeries(
|
|
413
|
+
{ owner: "your-org", repo: "your-repo" },
|
|
414
|
+
"ns_per_op",
|
|
415
|
+
);
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
### `fetchRun(source, runId, signal?)`
|
|
419
|
+
|
|
420
|
+
Fetches `data/runs/{runId}.json` and returns an `OtlpMetricsDocument`.
|
|
421
|
+
|
|
422
|
+
```ts
|
|
423
|
+
import { fetchRun } from "@benchkit/chart";
|
|
424
|
+
|
|
425
|
+
const run = await fetchRun(
|
|
426
|
+
{ owner: "your-org", repo: "your-repo" },
|
|
427
|
+
"123456789-1",
|
|
428
|
+
);
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
---
|
|
432
|
+
|
|
433
|
+
## Ranking utilities
|
|
434
|
+
|
|
435
|
+
### `rankSeries(sf)`
|
|
436
|
+
|
|
437
|
+
Ranks all series in a `SeriesFile` by latest value, direction-aware. Returns a `RankedEntry[]` sorted by rank ascending (rank 1 = best).
|
|
438
|
+
|
|
439
|
+
```ts
|
|
440
|
+
import { rankSeries } from "@benchkit/chart";
|
|
441
|
+
|
|
442
|
+
const ranked = rankSeries(seriesFile);
|
|
443
|
+
ranked.forEach((r) => {
|
|
444
|
+
console.log(`${r.rank}. ${r.name}: ${r.latestValue} (winner: ${r.isWinner})`);
|
|
445
|
+
});
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
#### `RankedEntry`
|
|
449
|
+
|
|
450
|
+
| Field | Type | Description |
|
|
451
|
+
|-------|------|-------------|
|
|
452
|
+
| `name` | `string` | Series name (key in `SeriesFile.series`). |
|
|
453
|
+
| `entry` | `SeriesEntry` | The original series entry. |
|
|
454
|
+
| `latestValue` | `number` | Most recent data-point value. |
|
|
455
|
+
| `previousValue` | `number \| undefined` | Second-most-recent value, if available. |
|
|
456
|
+
| `delta` | `number \| undefined` | `latestValue − previousValue`. |
|
|
457
|
+
| `rank` | `number` | 1-based rank. |
|
|
458
|
+
| `isWinner` | `boolean` | `true` for the first-ranked entry. |
|
|
459
|
+
|
|
460
|
+
Ranking direction:
|
|
461
|
+
|
|
462
|
+
| `SeriesFile.direction` | Rank 1 |
|
|
463
|
+
|------------------------|--------|
|
|
464
|
+
| `smaller_is_better` | Lowest value |
|
|
465
|
+
| `bigger_is_better` | Highest value |
|
|
466
|
+
| *(unset)* | Lowest value |
|
|
467
|
+
|
|
468
|
+
### `getWinner(sf)`
|
|
469
|
+
|
|
470
|
+
Returns the `name` of the rank-1 series, or `undefined` when there are no series with data points.
|
|
471
|
+
|
|
472
|
+
```ts
|
|
473
|
+
import { getWinner } from "@benchkit/chart";
|
|
474
|
+
|
|
475
|
+
const winner = getWinner(seriesFile);
|
|
476
|
+
if (winner) console.log(`Winner: ${winner}`);
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
---
|
|
480
|
+
|
|
481
|
+
## Regression detection
|
|
482
|
+
|
|
483
|
+
### `detectRegressions(series, threshold?, window?)`
|
|
484
|
+
|
|
485
|
+
Scans each series in a `SeriesFile` for a regression on the most recent data point relative to the rolling mean of the previous `window` points. Returns a `RegressionResult[]` (empty array when there are insufficient data points or no regressions are found).
|
|
486
|
+
|
|
487
|
+
```ts
|
|
488
|
+
import { detectRegressions } from "@benchkit/chart";
|
|
489
|
+
|
|
490
|
+
const regressions = detectRegressions(
|
|
491
|
+
seriesFile,
|
|
492
|
+
10, // threshold: flag when change exceeds 10 %
|
|
493
|
+
5, // window: average the previous 5 data points
|
|
494
|
+
);
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
A regression is detected when:
|
|
498
|
+
|
|
499
|
+
| `SeriesFile.direction` | Condition |
|
|
500
|
+
|------------------------|-----------|
|
|
501
|
+
| `smaller_is_better` | Latest value **increased** by more than `threshold`% vs the rolling mean |
|
|
502
|
+
| `bigger_is_better` | Latest value **decreased** by more than `threshold`% vs the rolling mean |
|
|
503
|
+
|
|
504
|
+
Returns `[]` when any series has fewer than `window + 1` data points (not enough history).
|
|
505
|
+
|
|
506
|
+
#### `RegressionResult`
|
|
507
|
+
|
|
508
|
+
| Field | Type | Description |
|
|
509
|
+
|-------|------|-------------|
|
|
510
|
+
| `seriesName` | `string` | Series name (key in `SeriesFile.series`). |
|
|
511
|
+
| `latestValue` | `number` | The most recent data-point value. |
|
|
512
|
+
| `previousMean` | `number` | Mean of the previous `window` data points. |
|
|
513
|
+
| `percentChange` | `number` | Percentage change from `previousMean` to `latestValue` (positive = increase). |
|
|
514
|
+
| `window` | `number` | Actual number of preceding points that were averaged. |
|
|
515
|
+
|
|
516
|
+
### `regressionTooltip(metric, result, metricLabelFormatter?)`
|
|
517
|
+
|
|
518
|
+
Builds a human-readable tooltip string for a single `RegressionResult`.
|
|
519
|
+
|
|
520
|
+
```ts
|
|
521
|
+
import { regressionTooltip } from "@benchkit/chart";
|
|
522
|
+
|
|
523
|
+
const tip = regressionTooltip("ns_per_op", regressionResult);
|
|
524
|
+
// e.g. "ns_per_op increased 15.3% vs 5-run average (320 → 368)"
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
---
|
|
528
|
+
|
|
529
|
+
## Tag filtering utilities
|
|
530
|
+
|
|
531
|
+
### `extractTags(seriesMap)`
|
|
532
|
+
|
|
533
|
+
Extracts all unique tag keys and their possible values from a collection of `SeriesFile`s. Returns `Record<string, string[]>` with values sorted alphabetically.
|
|
534
|
+
|
|
535
|
+
```ts
|
|
536
|
+
import { extractTags } from "@benchkit/chart";
|
|
537
|
+
|
|
538
|
+
const tags = extractTags(seriesMap);
|
|
539
|
+
// e.g. { arch: ["arm64", "x86_64"], runtime: ["go1.22", "go1.23"] }
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
### `filterSeriesFile(sf, activeFilters)`
|
|
543
|
+
|
|
544
|
+
Returns a copy of a `SeriesFile` with only the series entries that match **all** active filters. When `activeFilters` is empty the original object is returned unchanged.
|
|
545
|
+
|
|
546
|
+
```ts
|
|
547
|
+
import { filterSeriesFile } from "@benchkit/chart";
|
|
548
|
+
|
|
549
|
+
const filtered = filterSeriesFile(seriesFile, { arch: "arm64" });
|
|
550
|
+
```
|
|
551
|
+
|
|
552
|
+
---
|
|
553
|
+
|
|
554
|
+
## Usage patterns
|
|
555
|
+
|
|
556
|
+
> **Note:** The functions below (`CompetitiveDashboard`, `EvolutionDashboard`) are **illustrative usage patterns**, not exported components. They show how to compose the exported primitives for common scenarios. See [issue #83](https://github.com/strawgate/benchkit/issues/83) for the status of a real `CompetitiveDashboard` component.
|
|
557
|
+
|
|
558
|
+
### Competitive benchmarking
|
|
559
|
+
|
|
560
|
+
Use this pattern when you want to compare multiple implementations (series) for the same metric. `Leaderboard` and `ComparisonBar` are the primary components here.
|
|
561
|
+
|
|
562
|
+
```tsx
|
|
563
|
+
import { TrendChart, ComparisonBar, Leaderboard, TagFilter, filterSeriesFile } from "@benchkit/chart";
|
|
564
|
+
import { useState } from "preact/hooks";
|
|
565
|
+
|
|
566
|
+
// Example pattern — not an exported component
|
|
567
|
+
function CompetitiveDashboard({ seriesMap }: { seriesMap: Map<string, SeriesFile> }) {
|
|
568
|
+
const [activeFilters, setActiveFilters] = useState<Record<string, string>>({});
|
|
569
|
+
|
|
570
|
+
return (
|
|
571
|
+
<>
|
|
572
|
+
{/* Filter pills — only shown when series carry tags */}
|
|
573
|
+
<TagFilter
|
|
574
|
+
seriesMap={seriesMap}
|
|
575
|
+
activeFilters={activeFilters}
|
|
576
|
+
onFilterChange={setActiveFilters}
|
|
577
|
+
/>
|
|
578
|
+
|
|
579
|
+
{[...seriesMap.entries()].map(([metric, sf]) => {
|
|
580
|
+
const filtered = filterSeriesFile(sf, activeFilters);
|
|
581
|
+
return (
|
|
582
|
+
<div key={metric} style={{ marginBottom: "32px" }}>
|
|
583
|
+
<h2>{metric}</h2>
|
|
584
|
+
|
|
585
|
+
{/* Trend lines for every implementation */}
|
|
586
|
+
<TrendChart series={filtered} title="Over time" />
|
|
587
|
+
|
|
588
|
+
{/* Side-by-side latest-value comparison */}
|
|
589
|
+
<ComparisonBar series={filtered} title="Latest comparison" />
|
|
590
|
+
|
|
591
|
+
{/* Ranked table with winner badge */}
|
|
592
|
+
<Leaderboard series={filtered} />
|
|
593
|
+
</div>
|
|
594
|
+
);
|
|
595
|
+
})}
|
|
596
|
+
</>
|
|
597
|
+
);
|
|
598
|
+
}
|
|
599
|
+
```
|
|
600
|
+
|
|
601
|
+
### Evolution tracking
|
|
602
|
+
|
|
603
|
+
Use this pattern when you have a single implementation and want to track how it changes over time across commits. `TrendChart` with `regressions` highlighting is the primary component here.
|
|
604
|
+
|
|
605
|
+
```tsx
|
|
606
|
+
import { TrendChart, detectRegressions, regressionTooltip } from "@benchkit/chart";
|
|
607
|
+
|
|
608
|
+
// Example pattern — not an exported component
|
|
609
|
+
function EvolutionDashboard({ seriesMap }: { seriesMap: Map<string, SeriesFile> }) {
|
|
610
|
+
return (
|
|
611
|
+
<>
|
|
612
|
+
{[...seriesMap.entries()].map(([metric, sf]) => {
|
|
613
|
+
const regressions = detectRegressions(sf, 10, 5);
|
|
614
|
+
const hasRegression = regressions.length > 0;
|
|
615
|
+
|
|
616
|
+
return (
|
|
617
|
+
<div
|
|
618
|
+
key={metric}
|
|
619
|
+
style={{ border: hasRegression ? "1px solid #fca5a5" : "1px solid #e5e7eb" }}
|
|
620
|
+
title={regressions.map((r) => regressionTooltip(metric, r)).join("\n")}
|
|
621
|
+
>
|
|
622
|
+
{hasRegression && <span>⚠ regression detected</span>}
|
|
623
|
+
<TrendChart
|
|
624
|
+
series={sf}
|
|
625
|
+
title={metric}
|
|
626
|
+
regressions={regressions}
|
|
627
|
+
/>
|
|
628
|
+
</div>
|
|
629
|
+
);
|
|
630
|
+
})}
|
|
631
|
+
</>
|
|
632
|
+
);
|
|
633
|
+
}
|
|
634
|
+
```
|
|
635
|
+
|
|
636
|
+
---
|
|
637
|
+
|
|
638
|
+
## License
|
|
639
|
+
|
|
640
|
+
MIT
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ComparisonSummaryTable.test.d.ts","sourceRoot":"","sources":["../src/ComparisonSummaryTable.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { describe, it } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { sortBySeverity } from "./components/ComparisonSummaryTable.js";
|
|
4
|
+
import { formatPct, formatFixedValue } from "./format-utils.js";
|
|
5
|
+
describe("ComparisonSummaryTable helpers", () => {
|
|
6
|
+
describe("formatPct", () => {
|
|
7
|
+
it("adds + sign for positive values", () => {
|
|
8
|
+
assert.equal(formatPct(12.345), "+12.35%");
|
|
9
|
+
});
|
|
10
|
+
it("preserves - sign for negative values", () => {
|
|
11
|
+
assert.equal(formatPct(-7.891), "-7.89%");
|
|
12
|
+
});
|
|
13
|
+
it("handles zero", () => {
|
|
14
|
+
assert.equal(formatPct(0), "0.00%");
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
describe("formatFixedValue", () => {
|
|
18
|
+
it("shows integers without decimals", () => {
|
|
19
|
+
assert.equal(formatFixedValue(320), "320");
|
|
20
|
+
});
|
|
21
|
+
it("shows 1 decimal for large floats", () => {
|
|
22
|
+
assert.equal(formatFixedValue(1234.567), "1234.6");
|
|
23
|
+
});
|
|
24
|
+
it("shows 2 decimals for small floats", () => {
|
|
25
|
+
assert.equal(formatFixedValue(3.14159), "3.14");
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
describe("sortBySeverity", () => {
|
|
29
|
+
it("sorts regressions first, then improvements, then stable", () => {
|
|
30
|
+
const entries = [
|
|
31
|
+
{ benchmark: "A", metric: "m", direction: "smaller_is_better", baseline: 0, current: 0, status: "stable", percentChange: 1 },
|
|
32
|
+
{ benchmark: "B", metric: "m", direction: "smaller_is_better", baseline: 0, current: 0, status: "improved", percentChange: -10 },
|
|
33
|
+
{ benchmark: "C", metric: "m", direction: "smaller_is_better", baseline: 0, current: 0, status: "regressed", percentChange: 15 },
|
|
34
|
+
{ benchmark: "D", metric: "m", direction: "smaller_is_better", baseline: 0, current: 0, status: "regressed", percentChange: 5 },
|
|
35
|
+
];
|
|
36
|
+
const sorted = sortBySeverity(entries);
|
|
37
|
+
assert.deepEqual(sorted.map((e) => e.status), ["regressed", "regressed", "improved", "stable"]);
|
|
38
|
+
});
|
|
39
|
+
it("sorts within group by |percentChange| descending", () => {
|
|
40
|
+
const entries = [
|
|
41
|
+
{ benchmark: "A", metric: "m", direction: "smaller_is_better", baseline: 0, current: 0, status: "regressed", percentChange: 5.2 },
|
|
42
|
+
{ benchmark: "B", metric: "m", direction: "smaller_is_better", baseline: 0, current: 0, status: "regressed", percentChange: 22.1 },
|
|
43
|
+
{ benchmark: "C", metric: "m", direction: "smaller_is_better", baseline: 0, current: 0, status: "regressed", percentChange: 8.7 },
|
|
44
|
+
];
|
|
45
|
+
const sorted = sortBySeverity(entries);
|
|
46
|
+
assert.deepEqual(sorted.map((e) => e.percentChange), [22.1, 8.7, 5.2]);
|
|
47
|
+
});
|
|
48
|
+
it("returns empty array for empty input", () => {
|
|
49
|
+
assert.deepEqual(sortBySeverity([]), []);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
//# sourceMappingURL=ComparisonSummaryTable.test.js.map
|