benchforge 0.1.11 → 0.2.4

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 (253) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +99 -294
  3. package/bin/benchforge +1 -2
  4. package/dist/AnalyzeArchive-8NCJhmhS.mjs +145 -0
  5. package/dist/AnalyzeArchive-8NCJhmhS.mjs.map +1 -0
  6. package/dist/BenchMatrix-BZVrBB_h.mjs +1050 -0
  7. package/dist/BenchMatrix-BZVrBB_h.mjs.map +1 -0
  8. package/dist/{BenchRunner-BzyUfiyB.d.mts → BenchRunner-DglX1NOn.d.mts} +119 -66
  9. package/dist/CoverageSampler-D5T9DRqe.mjs +27 -0
  10. package/dist/CoverageSampler-D5T9DRqe.mjs.map +1 -0
  11. package/dist/Formatters-BWj3d4sv.mjs +95 -0
  12. package/dist/Formatters-BWj3d4sv.mjs.map +1 -0
  13. package/dist/{HeapSampler-B8dtKHn1.mjs → HeapSampler-Dq-hpXem.mjs} +4 -4
  14. package/dist/HeapSampler-Dq-hpXem.mjs.map +1 -0
  15. package/dist/RunBenchCLI-C17DrJz8.mjs +3075 -0
  16. package/dist/RunBenchCLI-C17DrJz8.mjs.map +1 -0
  17. package/dist/StatisticalUtils-BD92crgM.mjs +255 -0
  18. package/dist/StatisticalUtils-BD92crgM.mjs.map +1 -0
  19. package/dist/TimeSampler-Ds8n7l2B.mjs +29 -0
  20. package/dist/TimeSampler-Ds8n7l2B.mjs.map +1 -0
  21. package/dist/ViewerServer-BJhdnxlN.mjs +639 -0
  22. package/dist/ViewerServer-BJhdnxlN.mjs.map +1 -0
  23. package/dist/ViewerServer-CuMNdNBz.mjs +2 -0
  24. package/dist/bin/benchforge.mjs +4 -5
  25. package/dist/bin/benchforge.mjs.map +1 -1
  26. package/dist/index.d.mts +711 -558
  27. package/dist/index.mjs +98 -3
  28. package/dist/index.mjs.map +1 -0
  29. package/dist/runners/WorkerScript.d.mts +12 -4
  30. package/dist/runners/WorkerScript.mjs +77 -105
  31. package/dist/runners/WorkerScript.mjs.map +1 -1
  32. package/dist/viewer/assets/CIPlot-BkOvMoMa.js +1 -0
  33. package/dist/viewer/assets/HistogramKde-CmSyUFY0.js +1 -0
  34. package/dist/viewer/assets/LegendUtils-BJpbn_jr.js +55 -0
  35. package/dist/viewer/assets/SampleTimeSeries-C4VBhXr3.js +1 -0
  36. package/dist/viewer/assets/index-Br9bp_cX.js +153 -0
  37. package/dist/viewer/assets/index-NzXXe_CC.css +1 -0
  38. package/dist/viewer/index.html +19 -0
  39. package/dist/viewer/speedscope/LICENSE +21 -0
  40. package/dist/viewer/speedscope/SourceCodePro-Regular.ttf-ILST5JV6.woff2 +0 -0
  41. package/dist/viewer/speedscope/favicon-16x16-V2DMIAZS.js +2 -0
  42. package/dist/viewer/speedscope/favicon-16x16-V2DMIAZS.js.map +7 -0
  43. package/dist/viewer/speedscope/favicon-16x16-VSI62OPJ.png +0 -0
  44. package/dist/viewer/speedscope/favicon-32x32-3EB2YCUY.png +0 -0
  45. package/dist/viewer/speedscope/favicon-32x32-THY3JDJL.js +2 -0
  46. package/dist/viewer/speedscope/favicon-32x32-THY3JDJL.js.map +7 -0
  47. package/dist/viewer/speedscope/favicon-FOKUP5Y5.ico +0 -0
  48. package/dist/viewer/speedscope/favicon-M34RF7BI.js +2 -0
  49. package/dist/viewer/speedscope/favicon-M34RF7BI.js.map +7 -0
  50. package/dist/viewer/speedscope/file-format-schema.json +274 -0
  51. package/dist/viewer/speedscope/index.html +19 -0
  52. package/dist/viewer/speedscope/jfrview_bg-BLJXNNQB.wasm +0 -0
  53. package/dist/viewer/speedscope/perf-vertx-stacks-01-collapsed-all-ZNUIGAJL.txt +199 -0
  54. package/dist/viewer/speedscope/release.txt +3 -0
  55. package/dist/viewer/speedscope/source-code-pro.LICENSE.md +93 -0
  56. package/dist/viewer/speedscope/speedscope-GHPHNKXC.css +2 -0
  57. package/dist/viewer/speedscope/speedscope-GHPHNKXC.css.map +7 -0
  58. package/dist/viewer/speedscope/speedscope-QZFMJ7VP.js +212 -0
  59. package/dist/viewer/speedscope/speedscope-QZFMJ7VP.js.map +7 -0
  60. package/package.json +52 -27
  61. package/src/bin/benchforge.ts +2 -2
  62. package/src/cli/AnalyzeArchive.ts +232 -0
  63. package/src/cli/BrowserBench.ts +322 -0
  64. package/src/cli/CliArgs.ts +164 -51
  65. package/src/cli/CliExport.ts +179 -0
  66. package/src/cli/CliOptions.ts +147 -0
  67. package/src/cli/CliReport.ts +197 -0
  68. package/src/cli/FilterBenchmarks.ts +18 -30
  69. package/src/cli/RunBenchCLI.ts +132 -866
  70. package/src/cli/SuiteRunner.ts +160 -0
  71. package/src/cli/ViewerServer.ts +282 -0
  72. package/src/export/AllocExport.ts +121 -0
  73. package/src/export/ArchiveExport.ts +146 -0
  74. package/src/export/ArchiveFormat.ts +50 -0
  75. package/src/export/CoverageExport.ts +148 -0
  76. package/src/export/EditorUri.ts +10 -0
  77. package/src/export/PerfettoExport.ts +64 -99
  78. package/src/export/SpeedscopeTypes.ts +98 -0
  79. package/src/export/TimeExport.ts +115 -0
  80. package/src/index.ts +86 -67
  81. package/src/matrix/BenchMatrix.ts +230 -0
  82. package/src/matrix/CaseLoader.ts +8 -6
  83. package/src/matrix/MatrixDirRunner.ts +153 -0
  84. package/src/matrix/MatrixFilter.ts +49 -47
  85. package/src/matrix/MatrixInlineRunner.ts +50 -0
  86. package/src/matrix/MatrixReport.ts +90 -250
  87. package/src/matrix/VariantLoader.ts +5 -5
  88. package/src/profiling/browser/BenchLoop.ts +51 -0
  89. package/src/profiling/browser/BrowserCDP.ts +133 -0
  90. package/src/profiling/browser/BrowserGcStats.ts +33 -0
  91. package/src/profiling/browser/BrowserProfiler.ts +160 -0
  92. package/src/profiling/browser/CdpClient.ts +82 -0
  93. package/src/profiling/browser/CdpPage.ts +138 -0
  94. package/src/profiling/browser/ChromeLauncher.ts +158 -0
  95. package/src/profiling/browser/ChromeTraceEvent.ts +28 -0
  96. package/src/profiling/browser/PageLoadMode.ts +61 -0
  97. package/src/profiling/node/CoverageSampler.ts +27 -0
  98. package/src/profiling/node/CoverageTypes.ts +23 -0
  99. package/src/profiling/node/HeapSampleReport.ts +261 -0
  100. package/src/{heap-sample → profiling/node}/HeapSampler.ts +1 -2
  101. package/src/{heap-sample → profiling/node}/ResolvedProfile.ts +18 -9
  102. package/src/profiling/node/TimeSampler.ts +57 -0
  103. package/src/report/BenchmarkReport.ts +146 -0
  104. package/src/report/Colors.ts +9 -0
  105. package/src/report/Formatters.ts +110 -0
  106. package/src/report/GcSections.ts +151 -0
  107. package/src/{GitUtils.ts → report/GitUtils.ts} +18 -19
  108. package/src/report/HtmlReport.ts +223 -0
  109. package/src/report/ParseStats.ts +73 -0
  110. package/src/report/StandardSections.ts +147 -0
  111. package/src/report/ViewerSections.ts +286 -0
  112. package/src/report/text/TableReport.ts +253 -0
  113. package/src/report/text/TextReport.ts +123 -0
  114. package/src/runners/AdaptiveWrapper.ts +116 -236
  115. package/src/runners/BenchRunner.ts +20 -15
  116. package/src/{Benchmark.ts → runners/BenchmarkSpec.ts} +5 -6
  117. package/src/runners/CreateRunner.ts +5 -7
  118. package/src/runners/GcStats.ts +47 -50
  119. package/src/{MeasuredResults.ts → runners/MeasuredResults.ts} +43 -37
  120. package/src/runners/MergeBatches.ts +123 -0
  121. package/src/{NodeGC.ts → runners/NodeGC.ts} +2 -3
  122. package/src/runners/RunnerOrchestrator.ts +127 -243
  123. package/src/runners/RunnerUtils.ts +75 -1
  124. package/src/runners/SampleStats.ts +100 -0
  125. package/src/runners/TimingRunner.ts +244 -0
  126. package/src/runners/TimingUtils.ts +3 -2
  127. package/src/runners/WorkerScript.ts +135 -151
  128. package/src/stats/BootstrapDifference.ts +282 -0
  129. package/src/{PermutationTest.ts → stats/PermutationTest.ts} +8 -17
  130. package/src/stats/StatisticalUtils.ts +445 -0
  131. package/src/{tests → test}/AdaptiveConvergence.test.ts +10 -10
  132. package/src/test/AdaptiveRunner.test.ts +39 -41
  133. package/src/{tests → test}/AdaptiveSampling.test.ts +9 -9
  134. package/src/test/AdaptiveStatistics.integration.ts +2 -2
  135. package/src/{tests → test}/BenchMatrix.test.ts +19 -16
  136. package/src/test/BenchmarkReport.test.ts +63 -13
  137. package/src/test/BrowserBench.e2e.test.ts +186 -17
  138. package/src/test/BrowserBench.test.ts +10 -5
  139. package/src/test/BuildTimeSection.test.ts +130 -0
  140. package/src/test/CapSamples.test.ts +82 -0
  141. package/src/test/CoverageExport.test.ts +115 -0
  142. package/src/test/CoverageSampler.test.ts +33 -0
  143. package/src/test/HeapAttribution.test.ts +14 -14
  144. package/src/{tests → test}/MatrixFilter.test.ts +1 -1
  145. package/src/{tests → test}/MatrixReport.test.ts +1 -1
  146. package/src/test/PermutationTest.test.ts +1 -1
  147. package/src/{tests → test}/RealDataValidation.test.ts +6 -6
  148. package/src/test/RunBenchCLI.test.ts +39 -38
  149. package/src/test/RunnerOrchestrator.test.ts +12 -12
  150. package/src/test/StatisticalUtils.test.ts +48 -12
  151. package/src/{table-util/test → test}/TableReport.test.ts +2 -2
  152. package/src/test/TestUtils.ts +12 -7
  153. package/src/test/TimeExport.test.ts +139 -0
  154. package/src/test/TimeSampler.test.ts +37 -0
  155. package/src/test/ViewerLive.e2e.test.ts +159 -0
  156. package/src/test/ViewerStatic.static.e2e.test.ts +137 -0
  157. package/src/{tests → test}/fixtures/baseline/impl.ts +1 -1
  158. package/src/{tests → test}/fixtures/bevy30-samples.ts +3 -1
  159. package/src/test/fixtures/cases/asyncCases.ts +9 -0
  160. package/src/{tests → test}/fixtures/cases/cases.ts +5 -2
  161. package/src/test/fixtures/cases/variants/product.ts +2 -0
  162. package/src/test/fixtures/cases/variants/sum.ts +2 -0
  163. package/src/test/fixtures/discover/fast.ts +1 -0
  164. package/src/{tests → test}/fixtures/discover/slow.ts +1 -1
  165. package/src/test/fixtures/invalid/bad.ts +1 -0
  166. package/src/test/fixtures/loader/fast.ts +1 -0
  167. package/src/{tests → test}/fixtures/loader/slow.ts +1 -1
  168. package/src/test/fixtures/loader/stateful.ts +2 -0
  169. package/src/test/fixtures/stateful/stateful.ts +2 -0
  170. package/src/test/fixtures/variants/extra.ts +1 -0
  171. package/src/test/fixtures/variants/impl.ts +1 -0
  172. package/src/test/fixtures/worker/fast.ts +1 -0
  173. package/src/{tests → test}/fixtures/worker/slow.ts +1 -1
  174. package/src/viewer/DateFormat.ts +30 -0
  175. package/src/viewer/Helpers.ts +23 -0
  176. package/src/viewer/LineData.ts +120 -0
  177. package/src/viewer/Providers.ts +191 -0
  178. package/src/viewer/ReportData.ts +123 -0
  179. package/src/viewer/State.ts +49 -0
  180. package/src/viewer/Theme.ts +15 -0
  181. package/src/viewer/components/App.tsx +73 -0
  182. package/src/viewer/components/DropZone.tsx +71 -0
  183. package/src/viewer/components/LazyPlot.ts +33 -0
  184. package/src/viewer/components/SamplesPanel.tsx +214 -0
  185. package/src/viewer/components/Shell.tsx +26 -0
  186. package/src/viewer/components/SourcePanel.tsx +216 -0
  187. package/src/viewer/components/SummaryPanel.tsx +332 -0
  188. package/src/viewer/components/TabBar.tsx +131 -0
  189. package/src/viewer/components/TabContent.tsx +46 -0
  190. package/src/viewer/components/ThemeToggle.tsx +50 -0
  191. package/src/viewer/index.html +20 -0
  192. package/src/viewer/main.tsx +4 -0
  193. package/src/viewer/plots/CIPlot.ts +313 -0
  194. package/src/{html/browser → viewer/plots}/HistogramKde.ts +33 -38
  195. package/src/viewer/plots/LegendUtils.ts +134 -0
  196. package/src/viewer/plots/PlotTypes.ts +85 -0
  197. package/src/viewer/plots/RenderPlots.ts +230 -0
  198. package/src/viewer/plots/SampleTimeSeries.ts +306 -0
  199. package/src/viewer/plots/SvgHelpers.ts +136 -0
  200. package/src/viewer/plots/TimeSeriesMarks.ts +319 -0
  201. package/src/viewer/report.css +427 -0
  202. package/src/viewer/shell.css +357 -0
  203. package/src/viewer/tsconfig.json +11 -0
  204. package/dist/BrowserHeapSampler-B6asLKWQ.mjs +0 -202
  205. package/dist/BrowserHeapSampler-B6asLKWQ.mjs.map +0 -1
  206. package/dist/GcStats-wX7Xyblu.mjs +0 -77
  207. package/dist/GcStats-wX7Xyblu.mjs.map +0 -1
  208. package/dist/HeapSampler-B8dtKHn1.mjs.map +0 -1
  209. package/dist/TimingUtils-DwOwkc8G.mjs +0 -597
  210. package/dist/TimingUtils-DwOwkc8G.mjs.map +0 -1
  211. package/dist/browser/index.js +0 -914
  212. package/dist/src-B-DDaCa9.mjs +0 -3108
  213. package/dist/src-B-DDaCa9.mjs.map +0 -1
  214. package/src/BenchMatrix.ts +0 -380
  215. package/src/BenchmarkReport.ts +0 -161
  216. package/src/HtmlDataPrep.ts +0 -148
  217. package/src/StandardSections.ts +0 -261
  218. package/src/StatisticalUtils.ts +0 -175
  219. package/src/TypeUtil.ts +0 -8
  220. package/src/browser/BrowserGcStats.ts +0 -44
  221. package/src/browser/BrowserHeapSampler.ts +0 -271
  222. package/src/export/JsonExport.ts +0 -103
  223. package/src/export/JsonFormat.ts +0 -91
  224. package/src/export/SpeedscopeExport.ts +0 -202
  225. package/src/heap-sample/HeapSampleReport.ts +0 -269
  226. package/src/html/HtmlReport.ts +0 -131
  227. package/src/html/HtmlTemplate.ts +0 -284
  228. package/src/html/Types.ts +0 -88
  229. package/src/html/browser/CIPlot.ts +0 -287
  230. package/src/html/browser/LegendUtils.ts +0 -163
  231. package/src/html/browser/RenderPlots.ts +0 -263
  232. package/src/html/browser/SampleTimeSeries.ts +0 -389
  233. package/src/html/browser/Types.ts +0 -96
  234. package/src/html/browser/index.ts +0 -1
  235. package/src/html/index.ts +0 -17
  236. package/src/runners/BasicRunner.ts +0 -364
  237. package/src/table-util/ConvergenceFormatters.ts +0 -19
  238. package/src/table-util/Formatters.ts +0 -157
  239. package/src/table-util/README.md +0 -70
  240. package/src/table-util/TableReport.ts +0 -293
  241. package/src/tests/fixtures/cases/asyncCases.ts +0 -7
  242. package/src/tests/fixtures/cases/variants/product.ts +0 -2
  243. package/src/tests/fixtures/cases/variants/sum.ts +0 -2
  244. package/src/tests/fixtures/discover/fast.ts +0 -1
  245. package/src/tests/fixtures/invalid/bad.ts +0 -1
  246. package/src/tests/fixtures/loader/fast.ts +0 -1
  247. package/src/tests/fixtures/loader/stateful.ts +0 -2
  248. package/src/tests/fixtures/stateful/stateful.ts +0 -2
  249. package/src/tests/fixtures/variants/extra.ts +0 -1
  250. package/src/tests/fixtures/variants/impl.ts +0 -1
  251. package/src/tests/fixtures/worker/fast.ts +0 -1
  252. /package/src/{table-util/test → test}/TableValueExtractor.test.ts +0 -0
  253. /package/src/{table-util/test → test}/TableValueExtractor.ts +0 -0
package/src/html/Types.ts DELETED
@@ -1,88 +0,0 @@
1
- /** Data passed to the HTML report generator */
2
- export interface ReportData {
3
- groups: GroupData[];
4
- metadata: {
5
- timestamp: string;
6
- bencherVersion: string;
7
- cliArgs?: Record<string, unknown>;
8
- gcTrackingEnabled?: boolean;
9
- currentVersion?: GitVersion;
10
- baselineVersion?: GitVersion;
11
- };
12
- }
13
-
14
- export interface GroupData {
15
- name: string;
16
- baseline?: BenchmarkData;
17
- benchmarks: BenchmarkData[];
18
- }
19
-
20
- export interface BenchmarkData {
21
- name: string;
22
- samples: number[];
23
- warmupSamples?: number[];
24
- allocationSamples?: number[];
25
- heapSamples?: number[];
26
- gcEvents?: GcEvent[];
27
- optSamples?: number[];
28
- pausePoints?: PausePoint[];
29
- stats: {
30
- min: number;
31
- max: number;
32
- avg: number;
33
- p50: number;
34
- p75: number;
35
- p99: number;
36
- p999: number;
37
- };
38
- heapSize?: { min: number; max: number; avg: number };
39
- sectionStats?: FormattedStat[];
40
- comparisonCI?: DifferenceCI;
41
- }
42
-
43
- export interface FormattedStat {
44
- label: string;
45
- value: string;
46
- groupTitle?: string;
47
- }
48
-
49
- export interface GcEvent {
50
- offset: number;
51
- duration: number;
52
- }
53
-
54
- export interface PausePoint {
55
- sampleIndex: number;
56
- durationMs: number;
57
- }
58
-
59
- export interface GitVersion {
60
- hash: string;
61
- date: string;
62
- dirty?: boolean;
63
- }
64
-
65
- export type CIDirection = "faster" | "slower" | "uncertain";
66
-
67
- export interface HistogramBin {
68
- x: number;
69
- count: number;
70
- }
71
-
72
- export interface DifferenceCI {
73
- percent: number;
74
- ci: [number, number];
75
- direction: CIDirection;
76
- histogram?: HistogramBin[];
77
- }
78
-
79
- export interface HtmlReportOptions {
80
- openBrowser: boolean;
81
- outputPath?: string;
82
- }
83
-
84
- export interface HtmlReportResult {
85
- reportDir: string;
86
- server?: import("node:http").Server;
87
- closeServer?: () => void;
88
- }
@@ -1,287 +0,0 @@
1
- import type { ComparisonCI, HistogramBin } from "./Types.ts";
2
-
3
- export interface DistributionPlotOptions {
4
- width?: number;
5
- height?: number;
6
- title?: string;
7
- smooth?: boolean;
8
- direction?: "faster" | "slower" | "uncertain";
9
- }
10
-
11
- type Scales = { x: (v: number) => number; y: (v: number) => number };
12
- type Layout = {
13
- width: number;
14
- height: number;
15
- margin: typeof defaultMargin;
16
- plot: { w: number; h: number };
17
- };
18
- const defaultMargin = { top: 22, right: 12, bottom: 22, left: 12 };
19
-
20
- const defaultOpts: Required<DistributionPlotOptions> = {
21
- width: 260,
22
- height: 85,
23
- title: "p50 Δ%",
24
- smooth: false,
25
- direction: "uncertain",
26
- };
27
-
28
- const colors = {
29
- faster: { fill: "#dcfce7", stroke: "#22c55e" },
30
- slower: { fill: "#fee2e2", stroke: "#ef4444" },
31
- uncertain: { fill: "#dbeafe", stroke: "#3b82f6" },
32
- };
33
-
34
- const formatPct = (v: number) => (v >= 0 ? "+" : "") + v.toFixed(0) + "%";
35
-
36
- /** Create a small distribution plot showing histogram with CI shading */
37
- export function createDistributionPlot(
38
- histogram: HistogramBin[],
39
- ci: [number, number],
40
- pointEstimate: number,
41
- options: DistributionPlotOptions = {},
42
- ): SVGSVGElement {
43
- const opts = { ...defaultOpts, ...options };
44
- const layout = buildLayout(opts.width, opts.height);
45
- const svg = createSvg(layout.width, layout.height);
46
- if (!histogram?.length) return svg;
47
-
48
- const { fill, stroke } = colors[opts.direction];
49
- const scales = buildScales(histogram, ci, layout);
50
-
51
- drawTitle(svg, opts.title, layout.margin.left);
52
- drawCIRegion(svg, ci, scales, layout, fill);
53
- opts.smooth
54
- ? drawSmoothedDist(svg, histogram, scales, stroke)
55
- : drawHistogramBars(svg, histogram, scales, layout, stroke);
56
- drawZeroLine(svg, scales, layout);
57
- drawPointEstimate(svg, pointEstimate, scales, layout, stroke);
58
- drawCILabels(svg, ci, scales, layout.height);
59
- return svg;
60
- }
61
-
62
- /** Convenience wrapper for ComparisonCI data */
63
- export function createCIPlot(
64
- ci: ComparisonCI,
65
- options: Partial<DistributionPlotOptions> = {},
66
- ): SVGSVGElement {
67
- if (!ci.histogram) return createSvg(0, 0);
68
- return createDistributionPlot(ci.histogram, ci.ci, ci.percent, {
69
- direction: ci.direction,
70
- ...options,
71
- });
72
- }
73
-
74
- function buildLayout(width: number, height: number): Layout {
75
- const margin = defaultMargin;
76
- const w = width - margin.left - margin.right;
77
- const h = height - margin.top - margin.bottom;
78
- return { width, height, margin, plot: { w, h } };
79
- }
80
-
81
- function createSvg(w: number, h: number): SVGSVGElement {
82
- const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
83
- svg.setAttribute("width", String(w));
84
- svg.setAttribute("height", String(h));
85
- if (w && h) svg.setAttribute("viewBox", `0 0 ${w} ${h}`);
86
- return svg;
87
- }
88
-
89
- /** Compute x/y scale functions mapping data values to SVG coordinates */
90
- function buildScales(
91
- histogram: HistogramBin[],
92
- ci: [number, number],
93
- layout: Layout,
94
- ): Scales {
95
- const { margin, plot } = layout;
96
- const xMin = Math.min(...histogram.map(b => b.x), ci[0], 0);
97
- const xMax = Math.max(...histogram.map(b => b.x), ci[1], 0);
98
- const yMax = Math.max(...histogram.map(b => b.count));
99
- return {
100
- x: (v: number) => margin.left + ((v - xMin) / (xMax - xMin)) * plot.w,
101
- y: (v: number) => margin.top + plot.h - (v / yMax) * plot.h,
102
- };
103
- }
104
-
105
- function drawTitle(svg: SVGSVGElement, title: string, x: number): void {
106
- svg.appendChild(text(x, 14, title, "start", "13", "#333", "600"));
107
- }
108
-
109
- function drawCIRegion(
110
- svg: SVGSVGElement,
111
- ci: [number, number],
112
- scales: Scales,
113
- layout: Layout,
114
- fill: string,
115
- ) {
116
- const x = scales.x(ci[0]);
117
- const w = scales.x(ci[1]) - x;
118
- svg.appendChild(
119
- rect(x, layout.margin.top, w, layout.plot.h, { fill, opacity: "0.5" }),
120
- );
121
- }
122
-
123
- function drawSmoothedDist(
124
- svg: SVGSVGElement,
125
- histogram: HistogramBin[],
126
- scales: Scales,
127
- stroke: string,
128
- ): void {
129
- const sorted = [...histogram].sort((a, b) => a.x - b.x);
130
- const smoothed = gaussianSmooth(sorted, 2);
131
- const pts = smoothed.map(b => `${scales.x(b.x)},${scales.y(b.count)}`);
132
- const baseline = scales.y(0);
133
- svg.appendChild(
134
- path(
135
- `M${scales.x(smoothed[0].x)},${baseline}L${pts.join("L")}L${scales.x(smoothed.at(-1)!.x)},${baseline}Z`,
136
- { fill: stroke, opacity: "0.3" },
137
- ),
138
- );
139
- svg.appendChild(
140
- path(`M${pts.join("L")}`, { stroke, fill: "none", strokeWidth: "1.5" }),
141
- );
142
- }
143
-
144
- function drawHistogramBars(
145
- svg: SVGSVGElement,
146
- histogram: HistogramBin[],
147
- scales: Scales,
148
- layout: Layout,
149
- stroke: string,
150
- ): void {
151
- const sorted = [...histogram].sort((a, b) => a.x - b.x);
152
- const binW = sorted.length > 1 ? sorted[1].x - sorted[0].x : 1;
153
- const yMax = Math.max(...histogram.map(b => b.count));
154
- const xRange = scales.x(sorted.at(-1)!.x) - scales.x(sorted[0].x) + binW;
155
- for (const bin of sorted) {
156
- const barW = (binW / xRange) * layout.plot.w * 0.9;
157
- const barH = (bin.count / yMax) * layout.plot.h;
158
- svg.appendChild(
159
- rect(
160
- scales.x(bin.x) - barW / 2,
161
- layout.margin.top + layout.plot.h - barH,
162
- barW,
163
- barH,
164
- { fill: stroke, opacity: "0.6" },
165
- ),
166
- );
167
- }
168
- }
169
-
170
- function drawZeroLine(svg: SVGSVGElement, scales: Scales, layout: Layout) {
171
- const zeroX = scales.x(0);
172
- if (zeroX < layout.margin.left || zeroX > layout.width - layout.margin.right)
173
- return;
174
- const top = layout.margin.top;
175
- const attrs = { stroke: "#666", strokeWidth: "1", strokeDasharray: "3,2" };
176
- svg.appendChild(line(zeroX, top, zeroX, top + layout.plot.h, attrs));
177
- }
178
-
179
- function drawPointEstimate(
180
- svg: SVGSVGElement,
181
- pt: number,
182
- scales: Scales,
183
- layout: Layout,
184
- stroke: string,
185
- ) {
186
- const x = scales.x(pt);
187
- const top = layout.margin.top;
188
- svg.appendChild(
189
- line(x, top, x, top + layout.plot.h, { stroke, strokeWidth: "2" }),
190
- );
191
- }
192
-
193
- function drawCILabels(
194
- svg: SVGSVGElement,
195
- ci: [number, number],
196
- scales: Scales,
197
- height: number,
198
- ): void {
199
- svg.appendChild(
200
- text(scales.x(ci[0]), height - 4, formatPct(ci[0]), "middle", "12"),
201
- );
202
- svg.appendChild(
203
- text(scales.x(ci[1]), height - 4, formatPct(ci[1]), "middle", "12"),
204
- );
205
- }
206
-
207
- function text(
208
- x: number,
209
- y: number,
210
- content: string,
211
- anchor = "start",
212
- size = "9",
213
- fill = "#666",
214
- weight = "400",
215
- ): SVGTextElement {
216
- const el = document.createElementNS("http://www.w3.org/2000/svg", "text");
217
- el.setAttribute("x", String(x));
218
- el.setAttribute("y", String(y));
219
- el.setAttribute("text-anchor", anchor);
220
- el.setAttribute("font-size", size);
221
- el.setAttribute("font-weight", weight);
222
- el.setAttribute("fill", fill);
223
- el.textContent = content;
224
- return el;
225
- }
226
-
227
- function rect(
228
- x: number,
229
- y: number,
230
- w: number,
231
- h: number,
232
- attrs: Record<string, string>,
233
- ): SVGRectElement {
234
- const el = document.createElementNS("http://www.w3.org/2000/svg", "rect");
235
- el.setAttribute("x", String(x));
236
- el.setAttribute("y", String(y));
237
- el.setAttribute("width", String(w));
238
- el.setAttribute("height", String(h));
239
- setAttrs(el, attrs);
240
- return el;
241
- }
242
-
243
- /** Apply gaussian kernel smoothing to histogram bins */
244
- function gaussianSmooth(bins: HistogramBin[], sigma: number): HistogramBin[] {
245
- return bins.map((bin, i) => {
246
- let sum = 0;
247
- let wt = 0;
248
- for (let j = 0; j < bins.length; j++) {
249
- const w = Math.exp(-((i - j) ** 2) / (2 * sigma ** 2));
250
- sum += bins[j].count * w;
251
- wt += w;
252
- }
253
- return { x: bin.x, count: sum / wt };
254
- });
255
- }
256
-
257
- function path(d: string, attrs: Record<string, string>): SVGPathElement {
258
- const el = document.createElementNS("http://www.w3.org/2000/svg", "path");
259
- el.setAttribute("d", d);
260
- setAttrs(el, attrs);
261
- return el;
262
- }
263
-
264
- function line(
265
- x1: number,
266
- y1: number,
267
- x2: number,
268
- y2: number,
269
- attrs: Record<string, string>,
270
- ): SVGLineElement {
271
- const el = document.createElementNS("http://www.w3.org/2000/svg", "line");
272
- el.setAttribute("x1", String(x1));
273
- el.setAttribute("y1", String(y1));
274
- el.setAttribute("x2", String(x2));
275
- el.setAttribute("y2", String(y2));
276
- setAttrs(el, attrs);
277
- return el;
278
- }
279
-
280
- /** Set SVG attributes, converting camelCase keys to kebab-case */
281
- function setAttrs(el: SVGElement, attrs: Record<string, string>): void {
282
- for (const [k, v] of Object.entries(attrs))
283
- el.setAttribute(
284
- k.replace(/[A-Z]/g, c => "-" + c.toLowerCase()),
285
- v,
286
- );
287
- }
@@ -1,163 +0,0 @@
1
- import * as Plot from "@observablehq/plot";
2
-
3
- export interface LegendBounds {
4
- xMin: number;
5
- xMax: number;
6
- yMax: number;
7
- }
8
-
9
- export interface LegendItem {
10
- color: string;
11
- label: string;
12
- style:
13
- | "filled-dot"
14
- | "hollow-dot"
15
- | "vertical-bar"
16
- | "vertical-line"
17
- | "rect";
18
- strokeDash?: string;
19
- }
20
-
21
- interface LegendPos {
22
- legendX: number;
23
- y: number;
24
- textX: number;
25
- xRange: number;
26
- yMax: number;
27
- }
28
-
29
- /** Build complete legend marks array */
30
- export function buildLegend(bounds: LegendBounds, items: LegendItem[]): any[] {
31
- const xRange = bounds.xMax - bounds.xMin;
32
- const legendX = bounds.xMin + xRange * 0.68;
33
- const textX = legendX + xRange * 0.04;
34
- const getY = (i: number) => bounds.yMax * 0.98 - i * (bounds.yMax * 0.08);
35
-
36
- const marks: any[] = [legendBackground(bounds)];
37
- for (let i = 0; i < items.length; i++) {
38
- const pos: LegendPos = {
39
- legendX,
40
- y: getY(i),
41
- textX,
42
- xRange,
43
- yMax: bounds.yMax,
44
- };
45
- marks.push(symbolMark(pos, items[i]), textMark(pos, items[i].label));
46
- }
47
- return marks;
48
- }
49
-
50
- /** Draw a semi-transparent white background behind the legend area */
51
- function legendBackground(bounds: LegendBounds): any {
52
- const xRange = bounds.xMax - bounds.xMin;
53
- const data = [
54
- {
55
- x1: bounds.xMin + xRange * 0.65,
56
- x2: bounds.xMin + xRange * 1.05,
57
- y1: bounds.yMax * 0.65,
58
- y2: bounds.yMax * 1.05,
59
- },
60
- ];
61
- const opts = {
62
- x1: "x1",
63
- x2: "x2",
64
- y1: "y1",
65
- y2: "y2",
66
- fill: "white",
67
- fillOpacity: 0.9,
68
- stroke: "#ddd",
69
- strokeWidth: 1,
70
- };
71
- return Plot.rect(data, opts);
72
- }
73
-
74
- function symbolMark(pos: LegendPos, item: LegendItem): any {
75
- switch (item.style) {
76
- case "filled-dot":
77
- return dotMark(pos.legendX, pos.y, item.color, true);
78
- case "hollow-dot":
79
- return dotMark(pos.legendX, pos.y, item.color, false);
80
- case "vertical-bar":
81
- return verticalBarMark(pos, item.color);
82
- case "vertical-line":
83
- return verticalLineMark(pos, item.color, item.strokeDash);
84
- case "rect":
85
- return rectMark(pos, item.color);
86
- }
87
- }
88
-
89
- function textMark(pos: LegendPos, label: string): any {
90
- const data = [{ x: pos.textX, y: pos.y, text: label }];
91
- return Plot.text(data, {
92
- x: "x",
93
- y: "y",
94
- text: "text",
95
- fontSize: 11,
96
- textAnchor: "start",
97
- fill: "#333",
98
- });
99
- }
100
-
101
- function dotMark(x: number, y: number, color: string, filled: boolean): any {
102
- return Plot.dot(
103
- [{ x, y }],
104
- filled
105
- ? { x: "x", y: "y", fill: color, r: 4 }
106
- : { x: "x", y: "y", stroke: color, fill: "none", strokeWidth: 1.5, r: 4 },
107
- );
108
- }
109
-
110
- function verticalBarMark(pos: LegendPos, color: string): any {
111
- const { legendX, y, xRange, yMax } = pos;
112
- const w = xRange * 0.012;
113
- const h = yMax * 0.05;
114
- const data = [
115
- { x1: legendX - w / 2, x2: legendX + w / 2, y1: y - h / 2, y2: y + h / 2 },
116
- ];
117
- return Plot.rect(data, {
118
- x1: "x1",
119
- x2: "x2",
120
- y1: "y1",
121
- y2: "y2",
122
- fill: color,
123
- fillOpacity: 0.6,
124
- });
125
- }
126
-
127
- function verticalLineMark(
128
- pos: LegendPos,
129
- color: string,
130
- strokeDash?: string,
131
- ): any {
132
- const { legendX, y, yMax } = pos;
133
- return Plot.ruleX([legendX], {
134
- y1: y - yMax * 0.025,
135
- y2: y + yMax * 0.025,
136
- stroke: color,
137
- strokeWidth: 2,
138
- strokeDasharray: strokeDash,
139
- });
140
- }
141
-
142
- function rectMark(pos: LegendPos, color: string): any {
143
- const { legendX, y, xRange, yMax } = pos;
144
- const data = [
145
- {
146
- x1: legendX - xRange * 0.01,
147
- x2: legendX + xRange * 0.03,
148
- y1: y - yMax * 0.02,
149
- y2: y + yMax * 0.02,
150
- },
151
- ];
152
- const opts = {
153
- x1: "x1",
154
- x2: "x2",
155
- y1: "y1",
156
- y2: "y2",
157
- fill: color,
158
- fillOpacity: 0.3,
159
- stroke: color,
160
- strokeWidth: 1,
161
- };
162
- return Plot.rect(data, opts);
163
- }