@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.
Files changed (219) hide show
  1. package/README.md +640 -0
  2. package/dist/ComparisonSummaryTable.test.d.ts +2 -0
  3. package/dist/ComparisonSummaryTable.test.d.ts.map +1 -0
  4. package/dist/ComparisonSummaryTable.test.js +53 -0
  5. package/dist/ComparisonSummaryTable.test.js.map +1 -0
  6. package/dist/Dashboard.d.ts +25 -0
  7. package/dist/Dashboard.d.ts.map +1 -0
  8. package/dist/Dashboard.js +87 -0
  9. package/dist/Dashboard.js.map +1 -0
  10. package/dist/Dashboard.test.d.ts +2 -0
  11. package/dist/Dashboard.test.d.ts.map +1 -0
  12. package/dist/Dashboard.test.js +49 -0
  13. package/dist/Dashboard.test.js.map +1 -0
  14. package/dist/RunDashboard.d.ts +20 -0
  15. package/dist/RunDashboard.d.ts.map +1 -0
  16. package/dist/RunDashboard.js +174 -0
  17. package/dist/RunDashboard.js.map +1 -0
  18. package/dist/RunDashboard.test.d.ts +2 -0
  19. package/dist/RunDashboard.test.d.ts.map +1 -0
  20. package/dist/RunDashboard.test.js +58 -0
  21. package/dist/RunDashboard.test.js.map +1 -0
  22. package/dist/RunDetail.d.ts +26 -0
  23. package/dist/RunDetail.d.ts.map +1 -0
  24. package/dist/RunDetail.js +100 -0
  25. package/dist/RunDetail.js.map +1 -0
  26. package/dist/RunDetail.test.d.ts +2 -0
  27. package/dist/RunDetail.test.d.ts.map +1 -0
  28. package/dist/RunDetail.test.js +168 -0
  29. package/dist/RunDetail.test.js.map +1 -0
  30. package/dist/RunSelector.test.d.ts +2 -0
  31. package/dist/RunSelector.test.d.ts.map +1 -0
  32. package/dist/RunSelector.test.js +45 -0
  33. package/dist/RunSelector.test.js.map +1 -0
  34. package/dist/SampleChart.test.d.ts +2 -0
  35. package/dist/SampleChart.test.d.ts.map +1 -0
  36. package/dist/SampleChart.test.js +48 -0
  37. package/dist/SampleChart.test.js.map +1 -0
  38. package/dist/TagFilter.test.d.ts +2 -0
  39. package/dist/TagFilter.test.d.ts.map +1 -0
  40. package/dist/TagFilter.test.js +106 -0
  41. package/dist/TagFilter.test.js.map +1 -0
  42. package/dist/VerdictBanner.test.d.ts +2 -0
  43. package/dist/VerdictBanner.test.d.ts.map +1 -0
  44. package/dist/VerdictBanner.test.js +56 -0
  45. package/dist/VerdictBanner.test.js.map +1 -0
  46. package/dist/chart-config.d.ts +100 -0
  47. package/dist/chart-config.d.ts.map +1 -0
  48. package/dist/chart-config.js +81 -0
  49. package/dist/chart-config.js.map +1 -0
  50. package/dist/colors.d.ts +11 -0
  51. package/dist/colors.d.ts.map +1 -0
  52. package/dist/colors.js +16 -0
  53. package/dist/colors.js.map +1 -0
  54. package/dist/comparison-transforms.d.ts +22 -0
  55. package/dist/comparison-transforms.d.ts.map +1 -0
  56. package/dist/comparison-transforms.js +20 -0
  57. package/dist/comparison-transforms.js.map +1 -0
  58. package/dist/comparison-transforms.test.d.ts +2 -0
  59. package/dist/comparison-transforms.test.d.ts.map +1 -0
  60. package/dist/comparison-transforms.test.js +75 -0
  61. package/dist/comparison-transforms.test.js.map +1 -0
  62. package/dist/components/ComparisonBar.d.ts +13 -0
  63. package/dist/components/ComparisonBar.d.ts.map +1 -0
  64. package/dist/components/ComparisonBar.js +94 -0
  65. package/dist/components/ComparisonBar.js.map +1 -0
  66. package/dist/components/ComparisonChart.d.ts +31 -0
  67. package/dist/components/ComparisonChart.d.ts.map +1 -0
  68. package/dist/components/ComparisonChart.js +118 -0
  69. package/dist/components/ComparisonChart.js.map +1 -0
  70. package/dist/components/ComparisonSummaryTable.d.ts +29 -0
  71. package/dist/components/ComparisonSummaryTable.d.ts.map +1 -0
  72. package/dist/components/ComparisonSummaryTable.js +41 -0
  73. package/dist/components/ComparisonSummaryTable.js.map +1 -0
  74. package/dist/components/DashboardToolbar.d.ts +21 -0
  75. package/dist/components/DashboardToolbar.d.ts.map +1 -0
  76. package/dist/components/DashboardToolbar.js +14 -0
  77. package/dist/components/DashboardToolbar.js.map +1 -0
  78. package/dist/components/DateRangeFilter.d.ts +19 -0
  79. package/dist/components/DateRangeFilter.d.ts.map +1 -0
  80. package/dist/components/DateRangeFilter.js +44 -0
  81. package/dist/components/DateRangeFilter.js.map +1 -0
  82. package/dist/components/HeroSection.d.ts +12 -0
  83. package/dist/components/HeroSection.d.ts.map +1 -0
  84. package/dist/components/HeroSection.js +6 -0
  85. package/dist/components/HeroSection.js.map +1 -0
  86. package/dist/components/Leaderboard.d.ts +9 -0
  87. package/dist/components/Leaderboard.d.ts.map +1 -0
  88. package/dist/components/Leaderboard.js +41 -0
  89. package/dist/components/Leaderboard.js.map +1 -0
  90. package/dist/components/MetricCard.d.ts +16 -0
  91. package/dist/components/MetricCard.d.ts.map +1 -0
  92. package/dist/components/MetricCard.js +25 -0
  93. package/dist/components/MetricCard.js.map +1 -0
  94. package/dist/components/MetricDetailView.d.ts +15 -0
  95. package/dist/components/MetricDetailView.d.ts.map +1 -0
  96. package/dist/components/MetricDetailView.js +11 -0
  97. package/dist/components/MetricDetailView.js.map +1 -0
  98. package/dist/components/MonitorSection.d.ts +21 -0
  99. package/dist/components/MonitorSection.d.ts.map +1 -0
  100. package/dist/components/MonitorSection.js +38 -0
  101. package/dist/components/MonitorSection.js.map +1 -0
  102. package/dist/components/OverviewGrid.d.ts +17 -0
  103. package/dist/components/OverviewGrid.d.ts.map +1 -0
  104. package/dist/components/OverviewGrid.js +15 -0
  105. package/dist/components/OverviewGrid.js.map +1 -0
  106. package/dist/components/RunSelector.d.ts +20 -0
  107. package/dist/components/RunSelector.d.ts.map +1 -0
  108. package/dist/components/RunSelector.js +53 -0
  109. package/dist/components/RunSelector.js.map +1 -0
  110. package/dist/components/RunTable.d.ts +13 -0
  111. package/dist/components/RunTable.d.ts.map +1 -0
  112. package/dist/components/RunTable.js +24 -0
  113. package/dist/components/RunTable.js.map +1 -0
  114. package/dist/components/SampleChart.d.ts +21 -0
  115. package/dist/components/SampleChart.d.ts.map +1 -0
  116. package/dist/components/SampleChart.js +77 -0
  117. package/dist/components/SampleChart.js.map +1 -0
  118. package/dist/components/TagFilter.d.ts +12 -0
  119. package/dist/components/TagFilter.d.ts.map +1 -0
  120. package/dist/components/TagFilter.js +59 -0
  121. package/dist/components/TagFilter.js.map +1 -0
  122. package/dist/components/TrendChart.d.ts +25 -0
  123. package/dist/components/TrendChart.d.ts.map +1 -0
  124. package/dist/components/TrendChart.js +95 -0
  125. package/dist/components/TrendChart.js.map +1 -0
  126. package/dist/components/VerdictBanner.d.ts +13 -0
  127. package/dist/components/VerdictBanner.d.ts.map +1 -0
  128. package/dist/components/VerdictBanner.js +22 -0
  129. package/dist/components/VerdictBanner.js.map +1 -0
  130. package/dist/dashboard-labels.d.ts +47 -0
  131. package/dist/dashboard-labels.d.ts.map +1 -0
  132. package/dist/dashboard-labels.js +44 -0
  133. package/dist/dashboard-labels.js.map +1 -0
  134. package/dist/dashboard-labels.test.d.ts +2 -0
  135. package/dist/dashboard-labels.test.d.ts.map +1 -0
  136. package/dist/dashboard-labels.test.js +34 -0
  137. package/dist/dashboard-labels.test.js.map +1 -0
  138. package/dist/dataset-transforms.d.ts +41 -0
  139. package/dist/dataset-transforms.d.ts.map +1 -0
  140. package/dist/dataset-transforms.js +132 -0
  141. package/dist/dataset-transforms.js.map +1 -0
  142. package/dist/dataset-transforms.test.d.ts +2 -0
  143. package/dist/dataset-transforms.test.d.ts.map +1 -0
  144. package/dist/dataset-transforms.test.js +208 -0
  145. package/dist/dataset-transforms.test.js.map +1 -0
  146. package/dist/date-range.test.d.ts +2 -0
  147. package/dist/date-range.test.d.ts.map +1 -0
  148. package/dist/date-range.test.js +116 -0
  149. package/dist/date-range.test.js.map +1 -0
  150. package/dist/embed.d.ts +45 -0
  151. package/dist/embed.d.ts.map +1 -0
  152. package/dist/embed.js +170 -0
  153. package/dist/embed.js.map +1 -0
  154. package/dist/embed.test.d.ts +2 -0
  155. package/dist/embed.test.d.ts.map +1 -0
  156. package/dist/embed.test.js +15 -0
  157. package/dist/embed.test.js.map +1 -0
  158. package/dist/fetch.d.ts +27 -0
  159. package/dist/fetch.d.ts.map +1 -0
  160. package/dist/fetch.js +73 -0
  161. package/dist/fetch.js.map +1 -0
  162. package/dist/fetch.test.d.ts +2 -0
  163. package/dist/fetch.test.d.ts.map +1 -0
  164. package/dist/fetch.test.js +75 -0
  165. package/dist/fetch.test.js.map +1 -0
  166. package/dist/format-utils.d.ts +48 -0
  167. package/dist/format-utils.d.ts.map +1 -0
  168. package/dist/format-utils.js +98 -0
  169. package/dist/format-utils.js.map +1 -0
  170. package/dist/format-utils.test.d.ts +2 -0
  171. package/dist/format-utils.test.d.ts.map +1 -0
  172. package/dist/format-utils.test.js +103 -0
  173. package/dist/format-utils.test.js.map +1 -0
  174. package/dist/hooks/useChartLifecycle.d.ts +20 -0
  175. package/dist/hooks/useChartLifecycle.d.ts.map +1 -0
  176. package/dist/hooks/useChartLifecycle.js +46 -0
  177. package/dist/hooks/useChartLifecycle.js.map +1 -0
  178. package/dist/index.d.ts +27 -0
  179. package/dist/index.d.ts.map +1 -0
  180. package/dist/index.js +36 -0
  181. package/dist/index.js.map +1 -0
  182. package/dist/index.test.d.ts +2 -0
  183. package/dist/index.test.d.ts.map +1 -0
  184. package/dist/index.test.js +10 -0
  185. package/dist/index.test.js.map +1 -0
  186. package/dist/labels.d.ts +5 -0
  187. package/dist/labels.d.ts.map +1 -0
  188. package/dist/labels.js +41 -0
  189. package/dist/labels.js.map +1 -0
  190. package/dist/labels.test.d.ts +2 -0
  191. package/dist/labels.test.d.ts.map +1 -0
  192. package/dist/labels.test.js +28 -0
  193. package/dist/labels.test.js.map +1 -0
  194. package/dist/leaderboard.d.ts +29 -0
  195. package/dist/leaderboard.d.ts.map +1 -0
  196. package/dist/leaderboard.js +40 -0
  197. package/dist/leaderboard.js.map +1 -0
  198. package/dist/leaderboard.test.d.ts +2 -0
  199. package/dist/leaderboard.test.d.ts.map +1 -0
  200. package/dist/leaderboard.test.js +105 -0
  201. package/dist/leaderboard.test.js.map +1 -0
  202. package/dist/sample-utils.d.ts +4 -0
  203. package/dist/sample-utils.d.ts.map +1 -0
  204. package/dist/sample-utils.js +12 -0
  205. package/dist/sample-utils.js.map +1 -0
  206. package/dist/style.css +912 -0
  207. package/dist/theme.d.ts +12 -0
  208. package/dist/theme.d.ts.map +1 -0
  209. package/dist/theme.js +17 -0
  210. package/dist/theme.js.map +1 -0
  211. package/dist/utils.d.ts +37 -0
  212. package/dist/utils.d.ts.map +1 -0
  213. package/dist/utils.js +63 -0
  214. package/dist/utils.js.map +1 -0
  215. package/dist/utils.test.d.ts +2 -0
  216. package/dist/utils.test.d.ts.map +1 -0
  217. package/dist/utils.test.js +196 -0
  218. package/dist/utils.test.js.map +1 -0
  219. 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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=ComparisonSummaryTable.test.d.ts.map
@@ -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