@mstone6969/graph 0.1.0 → 0.2.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 (238) hide show
  1. package/README.md +111 -14
  2. package/dist/ExcelWriter-3ZSYUH4T.cjs +154 -0
  3. package/dist/GraphType.js +24 -38
  4. package/dist/PdfWriter-6HJFYMCO.cjs +205 -0
  5. package/dist/catalog-YWX6FHRM.cjs +8 -0
  6. package/dist/catalog.js +5 -1
  7. package/dist/charts/bars/index.js +2 -12
  8. package/dist/charts/comparison/EffectForestPlot.js +1 -1
  9. package/dist/charts/comparison/index.js +1 -8
  10. package/dist/charts/demography/Nomogram.js +3 -3
  11. package/dist/charts/density/HexbinChart.js +6 -4
  12. package/dist/charts/diagrams/ArchitectureCanvas.js +68 -0
  13. package/dist/charts/diagrams/ArchitectureDiagram.js +331 -0
  14. package/dist/charts/diagrams/ArchitectureLayout.js +106 -0
  15. package/dist/charts/diagrams/ArchitectureLinkLabels.js +78 -0
  16. package/dist/charts/diagrams/ArchitectureModel.js +108 -0
  17. package/dist/charts/diagrams/ArchitectureRouter.js +348 -0
  18. package/dist/charts/diagrams/ArchitectureSamples.js +312 -0
  19. package/dist/charts/diagrams/ArchitectureShapes.js +130 -0
  20. package/dist/charts/diagrams/ArchitectureText.js +25 -0
  21. package/dist/charts/diagrams/DiagramText.js +38 -0
  22. package/dist/charts/diagrams/EntityRelationshipDiagram.js +465 -0
  23. package/dist/charts/diagrams/ErCrowsFoot.js +33 -0
  24. package/dist/charts/diagrams/ErSamples.js +205 -0
  25. package/dist/charts/diagrams/ErSchema.js +122 -0
  26. package/dist/charts/diagrams/LogicCircuit.js +154 -0
  27. package/dist/charts/diagrams/LogicCircuitLayout.js +600 -0
  28. package/dist/charts/diagrams/LogicCircuitSamples.js +66 -0
  29. package/dist/charts/diagrams/LogicGateDiagram.js +216 -0
  30. package/dist/charts/diagrams/LogicGateSymbol.js +113 -0
  31. package/dist/charts/diagrams/NetworkTopologyDiagram.js +247 -0
  32. package/dist/charts/diagrams/SequenceDiagram.js +351 -0
  33. package/dist/charts/diagrams/SequenceLayout.js +422 -0
  34. package/dist/charts/diagrams/SequenceModel.js +221 -0
  35. package/dist/charts/diagrams/TopologyLayout.js +143 -0
  36. package/dist/charts/diagrams/architectureEnums.js +50 -0
  37. package/dist/charts/diagrams/index.js +26 -0
  38. package/dist/charts/diagrams/logicEnums.js +42 -0
  39. package/dist/charts/diagrams/sequenceEnums.js +40 -0
  40. package/dist/charts/distribution/HistogramChart.js +25 -3
  41. package/dist/charts/distribution/index.js +1 -3
  42. package/dist/charts/earthscience/HovmollerDiagram.js +477 -0
  43. package/dist/charts/earthscience/HovmollerEnums.js +42 -0
  44. package/dist/charts/earthscience/HovmollerField.js +180 -0
  45. package/dist/charts/earthscience/HovmollerSample.js +40 -0
  46. package/dist/charts/earthscience/HovmollerSpeed.js +71 -0
  47. package/dist/charts/earthscience/index.js +10 -2
  48. package/dist/charts/engineering/ConstellationDiagram.js +26 -6
  49. package/dist/charts/engineering/EyeDiagram.js +2 -1
  50. package/dist/charts/engineering/PoleZeroPlot.js +32 -8
  51. package/dist/charts/financial/RelativeStrengthRotation.js +434 -0
  52. package/dist/charts/financial/RelativeStrengthRotationMath.js +71 -0
  53. package/dist/charts/financial/RelativeStrengthRotationSamples.js +27 -0
  54. package/dist/charts/financial/index.js +5 -3
  55. package/dist/charts/flow/index.js +1 -5
  56. package/dist/charts/frameworks/AnsoffMatrix.js +523 -0
  57. package/dist/charts/frameworks/FrameworkText.js +98 -0
  58. package/dist/charts/frameworks/NpsGauge.js +279 -0
  59. package/dist/charts/frameworks/NpsSurvey.js +58 -0
  60. package/dist/charts/frameworks/PerceptualMap.js +419 -0
  61. package/dist/charts/frameworks/PerceptualProjection.js +142 -0
  62. package/dist/charts/frameworks/PositioningSamples.js +191 -0
  63. package/dist/charts/frameworks/RaciMatrix.js +458 -0
  64. package/dist/charts/frameworks/RaciPlan.js +81 -0
  65. package/dist/charts/frameworks/RaciSamples.js +76 -0
  66. package/dist/charts/frameworks/SwotMatrix.js +358 -0
  67. package/dist/charts/frameworks/SwotSamples.js +85 -0
  68. package/dist/charts/frameworks/VanWestendorpChart.js +187 -0
  69. package/dist/charts/frameworks/VanWestendorpSurvey.js +197 -0
  70. package/dist/charts/frameworks/index.js +26 -0
  71. package/dist/charts/frameworks/raciEnums.js +30 -0
  72. package/dist/charts/frameworks/swotEnums.js +34 -0
  73. package/dist/charts/genomics/CircosPlot.js +1 -1
  74. package/dist/charts/geoflows/GeoFlowChart.js +2 -2
  75. package/dist/charts/geoflows/GeoFlowSample.js +2 -1
  76. package/dist/charts/geomaps/FloorPlanMap.js +324 -0
  77. package/dist/charts/geomaps/FloorPlanModel.js +179 -0
  78. package/dist/charts/geomaps/FloorPlanSamples.js +416 -0
  79. package/dist/charts/geomaps/GeoLand.js +2 -2
  80. package/dist/charts/geomaps/GeoSamples.js +2 -1
  81. package/dist/charts/geomaps/GraticuleMap.js +2 -2
  82. package/dist/charts/geomaps/RoomLabelFit.js +200 -0
  83. package/dist/charts/geomaps/TissotMap.js +2 -2
  84. package/dist/charts/geomaps/index.js +8 -4
  85. package/dist/charts/hierarchy/SunburstChart.js +17 -10
  86. package/dist/charts/hierarchy/index.js +2 -11
  87. package/dist/charts/indicators/BulletChart.js +8 -2
  88. package/dist/charts/indicators/SpeedometerChart.js +1 -1
  89. package/dist/charts/indicators/ThermometerChart.js +1 -1
  90. package/dist/charts/lifescience/LoessFit.js +113 -0
  91. package/dist/charts/lifescience/MaPlot.js +337 -0
  92. package/dist/charts/lifescience/MaPlotSamples.js +35 -0
  93. package/dist/charts/lifescience/OncoGlyph.js +137 -0
  94. package/dist/charts/lifescience/OncoPrint.js +373 -0
  95. package/dist/charts/lifescience/OncoPrintMatrix.js +107 -0
  96. package/dist/charts/lifescience/OncoPrintSamples.js +142 -0
  97. package/dist/charts/lifescience/PedigreeChart.js +306 -0
  98. package/dist/charts/lifescience/PedigreeLayout.js +261 -0
  99. package/dist/charts/lifescience/PedigreeRows.js +70 -0
  100. package/dist/charts/lifescience/PedigreeSamples.js +161 -0
  101. package/dist/charts/lifescience/PedigreeSymbol.js +47 -0
  102. package/dist/charts/lifescience/RamachandranPlot.js +364 -0
  103. package/dist/charts/lifescience/RamachandranRegions.js +2712 -0
  104. package/dist/charts/lifescience/RamachandranSamples.js +235 -0
  105. package/dist/charts/lifescience/index.js +25 -0
  106. package/dist/charts/lifescience/pedigreeEnums.js +21 -0
  107. package/dist/charts/lifescience/ramachandranEnums.js +34 -0
  108. package/dist/charts/linesareas/DualAxisChart.js +1 -1
  109. package/dist/charts/linesareas/LineEnums.js +1 -15
  110. package/dist/charts/linesareas/MultiLineChart.js +1 -1
  111. package/dist/charts/linesareas/TrendChart.js +15 -5
  112. package/dist/charts/linesareas/index.js +2 -6
  113. package/dist/charts/massive/MassiveChart.js +3 -2
  114. package/dist/charts/mathematical/FourierSeriesChart.js +7 -1
  115. package/dist/charts/mathematical/index.js +2 -6
  116. package/dist/charts/matrix/GridPlot.js +27 -2
  117. package/dist/charts/matrix/TilegramChart.js +2 -2
  118. package/dist/charts/mlcharts/AttentionGrid.js +79 -0
  119. package/dist/charts/mlcharts/AttentionHeatmap.js +329 -0
  120. package/dist/charts/mlcharts/AttentionModel.js +132 -0
  121. package/dist/charts/mlcharts/AttentionSamples.js +82 -0
  122. package/dist/charts/mlcharts/CellNumber.js +8 -0
  123. package/dist/charts/mlcharts/CurveDiagnosticSamples.js +95 -0
  124. package/dist/charts/mlcharts/CurvePickMark.js +31 -0
  125. package/dist/charts/mlcharts/DecisionBoundaryChart.js +316 -0
  126. package/dist/charts/mlcharts/DecisionSamples.js +60 -0
  127. package/dist/charts/mlcharts/DecisionSurface.js +130 -0
  128. package/dist/charts/mlcharts/DistinctTicks.js +13 -0
  129. package/dist/charts/mlcharts/ElbowMath.js +153 -0
  130. package/dist/charts/mlcharts/ElbowPlot.js +273 -0
  131. package/dist/charts/mlcharts/ExplainabilitySamples.js +70 -0
  132. package/dist/charts/mlcharts/FeatureImportanceChart.js +150 -0
  133. package/dist/charts/mlcharts/ImportanceRanking.js +98 -0
  134. package/dist/charts/mlcharts/LearningCurveChart.js +276 -0
  135. package/dist/charts/mlcharts/LearningCurveMath.js +92 -0
  136. package/dist/charts/mlcharts/PartialDependence.js +92 -0
  137. package/dist/charts/mlcharts/PartialDependencePlot.js +225 -0
  138. package/dist/charts/mlcharts/WideLabel.js +28 -0
  139. package/dist/charts/mlcharts/curveDiagnosticEnums.js +37 -0
  140. package/dist/charts/mlcharts/index.js +32 -0
  141. package/dist/charts/proportion/index.js +5 -9
  142. package/dist/charts/scatter/CategoryScatterBase.js +1 -4
  143. package/dist/charts/scatter/ConnectedScatter.js +7 -4
  144. package/dist/charts/scatter/MarginalScatter.js +13 -8
  145. package/dist/charts/scatter/index.js +4 -17
  146. package/dist/charts/scientific/SpectrogramChart.js +9 -3
  147. package/dist/charts/scientific/index.js +2 -2
  148. package/dist/charts/signals/index.js +3 -13
  149. package/dist/charts/statistics/LorenzCurve.js +7 -4
  150. package/dist/charts/threebars/Candlestick3DChart.js +18 -2
  151. package/dist/charts/threeglobe/GlobeChart.js +2 -2
  152. package/dist/charts/threeglobe/GlobeSamples.js +2 -1
  153. package/dist/charts/threeglobe/RingGlobe.js +1 -1
  154. package/dist/charts/threenetwork/ConeTreeChart.js +2 -1
  155. package/dist/charts/threenetwork/Gantt3DChart.js +3 -2
  156. package/dist/charts/threesurfaces/Ridgeline3DChart.js +22 -4
  157. package/dist/charts/threevolume/LorenzAttractor.js +19 -0
  158. package/dist/charts/threevolume/VolumeRaycastChart.js +4 -3
  159. package/dist/charts/threevolume/volumeEnums.js +1 -0
  160. package/dist/charts/timeline/index.js +2 -10
  161. package/dist/chunk-753UBLV2.cjs +33 -0
  162. package/dist/chunk-VX54A7KA.cjs +17 -0
  163. package/dist/{chunk-W5GAAAZY.cjs → chunk-WAODDPYX.cjs} +24874 -17620
  164. package/dist/chunk-Z3JPIJSC.cjs +338 -0
  165. package/dist/core/CartesianChart.js +87 -11
  166. package/dist/core/Chart.js +63 -15
  167. package/dist/core/Chart2D.js +54 -17
  168. package/dist/core/Chart3D.js +56 -20
  169. package/dist/core/ChartExportError.js +2 -0
  170. package/dist/core/ChartExporter.js +50 -8
  171. package/dist/core/ColumnCheck.js +53 -0
  172. package/dist/core/Context2D.js +29 -15
  173. package/dist/core/Context3D.js +7 -2
  174. package/dist/core/ExcelWriter.js +152 -0
  175. package/dist/core/ExportText.js +28 -0
  176. package/dist/core/MarkOrigin.js +88 -0
  177. package/dist/core/MarkStyle.js +294 -0
  178. package/dist/core/Palette.js +27 -7
  179. package/dist/core/PdfPage.js +13 -0
  180. package/dist/core/PdfWriter.js +245 -0
  181. package/dist/core/Scales.js +52 -2
  182. package/dist/core/SceneOverlay.js +4 -4
  183. package/dist/core/TextFit.js +37 -0
  184. package/dist/core/Theme.js +28 -1
  185. package/dist/core/enums.js +33 -1
  186. package/dist/core/pathMorph.js +15 -0
  187. package/dist/data/SampleData.js +1 -17
  188. package/dist/data/WorldSample.js +23 -0
  189. package/dist/index.cjs +810 -691
  190. package/dist/index.d.cts +8390 -2925
  191. package/dist/index.d.ts +8392 -2927
  192. package/dist/index.js +106 -56
  193. package/dist/react/ChartView.js +18 -8
  194. package/dist/react/GraphHandle.js +2 -0
  195. package/dist/react/components.js +74 -116
  196. package/package.json +13 -3
  197. package/dist/catalog-4UP2YFNR.cjs +0 -7
  198. package/dist/charts/bars/BulletBar.js +0 -184
  199. package/dist/charts/bars/ErrorBarColumn.js +0 -147
  200. package/dist/charts/bars/HistogramBar.js +0 -217
  201. package/dist/charts/bars/MarimekkoBar.js +0 -191
  202. package/dist/charts/bars/ParetoBar.js +0 -143
  203. package/dist/charts/comparison/PanelChart.js +0 -70
  204. package/dist/charts/comparison/SmallMultipleBars.js +0 -134
  205. package/dist/charts/comparison/SpanChart.js +0 -118
  206. package/dist/charts/comparison/TrellisChart.js +0 -158
  207. package/dist/charts/distribution/RidgeSlabs3DChart.js +0 -169
  208. package/dist/charts/financial/Candlestick3D.js +0 -157
  209. package/dist/charts/flow/BumpFlow.js +0 -212
  210. package/dist/charts/flow/StreamFlow.js +0 -174
  211. package/dist/charts/geomaps/ThiessenMap.js +0 -106
  212. package/dist/charts/hierarchy/ConeTree3D.js +0 -135
  213. package/dist/charts/hierarchy/Hierarchy3DChart.js +0 -27
  214. package/dist/charts/hierarchy/SpherePack3D.js +0 -81
  215. package/dist/charts/hierarchy/SunburstTower3D.js +0 -93
  216. package/dist/charts/hierarchy/TreemapSkyline3D.js +0 -98
  217. package/dist/charts/linesareas/ConnectedScatterTime.js +0 -206
  218. package/dist/charts/linesareas/SmallMultipleLines.js +0 -153
  219. package/dist/charts/mathematical/ParametricSurface3D.js +0 -234
  220. package/dist/charts/mathematical/StrangeAttractor3D.js +0 -251
  221. package/dist/charts/proportion/ConeFunnel.js +0 -126
  222. package/dist/charts/proportion/ExtrudedPieChart.js +0 -123
  223. package/dist/charts/scatter/BeeswarmScatter.js +0 -151
  224. package/dist/charts/scatter/BinnedScatterBase.js +0 -51
  225. package/dist/charts/scatter/ContourScatter.js +0 -116
  226. package/dist/charts/scatter/GridBinScatter.js +0 -98
  227. package/dist/charts/scatter/HexbinScatter.js +0 -74
  228. package/dist/charts/scatter/StripScatter.js +0 -107
  229. package/dist/charts/scatter/SunflowerScatter.js +0 -109
  230. package/dist/charts/signals/ConstellationChart.js +0 -153
  231. package/dist/charts/signals/CorrelogramChart.js +0 -171
  232. package/dist/charts/signals/EyeDiagramChart.js +0 -155
  233. package/dist/charts/signals/PoleZeroChart.js +0 -164
  234. package/dist/charts/signals/SpectrogramChart.js +0 -79
  235. package/dist/charts/timeline/CalendarSkylineChart.js +0 -128
  236. package/dist/charts/timeline/ScheduleBlocksChart.js +0 -119
  237. package/dist/charts/timeline/SpiralTimeline.js +0 -154
  238. package/dist/charts/timeline/StreamTimeline.js +0 -141
@@ -0,0 +1,276 @@
1
+ import { CartesianChart } from "../../core/CartesianChart.js";
2
+ import { ChartFamily, DataShape, Dimension, ScaleKind } from "../../core/enums.js";
3
+ import { Scales } from "../../core/Scales.js";
4
+ import { TrendHover } from "../linesareas/TrendHover.js";
5
+ import { cellNumber } from "./CellNumber.js";
6
+ import { CurveDiagnosticSamples } from "./CurveDiagnosticSamples.js";
7
+ import { MetricGoal, OverfitShading, RunSpread } from "./curveDiagnosticEnums.js";
8
+ import { CurvePickMark } from "./CurvePickMark.js";
9
+ import { DistinctTicks } from "./DistinctTicks.js";
10
+ import { LearningCurveMath } from "./LearningCurveMath.js";
11
+ import { WideLabel } from "./WideLabel.js";
12
+ import { area, extent, line, min, scaleLinear, ticks } from "d3";
13
+ // src/charts/mlcharts/LearningCurveChart.ts
14
+ var TRAINING = "Training";
15
+ var VALIDATION = "Validation";
16
+ var LINE = 16;
17
+ var GAP_NAME = {
18
+ [OverfitShading.None]: "",
19
+ [OverfitShading.Generalization]: "Generalization gap",
20
+ [OverfitShading.AfterBest]: "Overfit gap"
21
+ };
22
+ /**
23
+ * Diagnoses training over epochs: mean training and validation metric with a spread band across runs, the best validation
24
+ * epoch, and the generalization gap. Follows the usual early-stopping reading (Prechelt 1998): the best epoch is the
25
+ * earliest with the best mean validation metric, computed after any smoothing, and overfitting is validation worsening
26
+ * while training still improves. Smoothing is TensorBoard's debiased EMA. Runs that stop early shrink the mean's sample.
27
+ */
28
+ var LearningCurveChart = class extends CartesianChart {
29
+ static meta = {
30
+ id: "learning-curve",
31
+ name: "Learning curve",
32
+ family: ChartFamily.Trend,
33
+ dimension: Dimension.Two,
34
+ shape: DataShape.Table,
35
+ description: "Shows a training and validation metric by epoch across runs, with a spread band, the best epoch and the overfitting gap.",
36
+ tags: [
37
+ "machine learning",
38
+ "training",
39
+ "validation",
40
+ "overfitting",
41
+ "early stopping",
42
+ "loss curve"
43
+ ],
44
+ sample: (seed) => CurveDiagnosticSamples.trainingRuns(seed),
45
+ variants: {
46
+ spread: [
47
+ RunSpread.StandardDeviation,
48
+ RunSpread.Confidence95,
49
+ RunSpread.Range
50
+ ],
51
+ gap: [
52
+ OverfitShading.AfterBest,
53
+ OverfitShading.Generalization,
54
+ OverfitShading.None
55
+ ],
56
+ best: [true, false],
57
+ runs: [false, true],
58
+ scale: [ScaleKind.Linear, ScaleKind.Log],
59
+ smoothing: [0, .6]
60
+ }
61
+ };
62
+ #view = null;
63
+ #valueAxis = null;
64
+ defaults() {
65
+ return {
66
+ x: "epoch",
67
+ y: "value",
68
+ run: "run",
69
+ split: "split",
70
+ trainSplit: "train",
71
+ validationSplit: "validation",
72
+ xLabel: "Epoch",
73
+ yLabel: "Loss",
74
+ goal: MetricGoal.Minimize,
75
+ spread: RunSpread.StandardDeviation,
76
+ gap: OverfitShading.AfterBest,
77
+ best: true,
78
+ runs: false,
79
+ scale: ScaleKind.Linear,
80
+ smoothing: 0
81
+ };
82
+ }
83
+ get logarithmic() {
84
+ return this.options.scale === ScaleKind.Log;
85
+ }
86
+ xTicks() {
87
+ const stats = [...this.#view?.train.stats ?? [], ...this.#view?.validation.stats ?? []];
88
+ const [lo = 0, hi = 1] = extent(stats, (stat) => stat.epoch);
89
+ return lo === hi ? 2 : Math.min(7, Math.ceil(hi - lo));
90
+ }
91
+ curvesOf(split) {
92
+ const { data, x, y, run, split: column } = this.options, wanted = String(split);
93
+ const smoothing = Math.min(.999, Math.max(0, this.options.smoothing || 0));
94
+ const runs = /* @__PURE__ */ new Map();
95
+ for (const row of Array.isArray(data) ? data : []) {
96
+ if (!row || String(row[column]) !== wanted) continue;
97
+ const epoch = cellNumber(row[x]), value = cellNumber(row[y]);
98
+ if (epoch === null || value === null || this.logarithmic && value <= 0) continue;
99
+ const name = String(row[run] ?? ""), rows = runs.get(name) ?? runs.set(name, /* @__PURE__ */ new Map()).get(name);
100
+ (rows.get(epoch) ?? rows.set(epoch, []).get(epoch)).push(value);
101
+ }
102
+ return [...runs].map(([name, rows]) => {
103
+ const epochs = [...rows.keys()].sort((a, b) => a - b);
104
+ const values = LearningCurveMath.smooth(epochs.map((epoch) => rows.get(epoch).reduce((a, b) => a + b, 0) / rows.get(epoch).length), smoothing);
105
+ return {
106
+ run: name,
107
+ points: epochs.map((epoch, i) => ({
108
+ epoch,
109
+ value: values[i]
110
+ }))
111
+ };
112
+ });
113
+ }
114
+ /** Rows the split names failed to match, so a curve is not lost without a word. Another split, like test, is fine beside both. */
115
+ get absentSplits() {
116
+ const { data, split, trainSplit, validationSplit } = this.options;
117
+ const named = [String(trainSplit), String(validationSplit)];
118
+ const seen = new Set((Array.isArray(data) ? data : []).flatMap((row) => row?.[split] === null || row?.[split] === void 0 ? [] : [String(row[split])]));
119
+ const absent = named.filter((name) => !seen.has(name)), others = [...seen].filter((name) => !named.includes(name));
120
+ return absent.length && others.length ? `No '${absent.join("' or '")}' rows. Found: ${others.slice(0, 4).join(", ")}` : "";
121
+ }
122
+ read() {
123
+ const { trainSplit, validationSplit, spread, goal, gap } = this.options;
124
+ const parts = [{
125
+ key: TRAINING,
126
+ curves: this.curvesOf(trainSplit)
127
+ }, {
128
+ key: VALIDATION,
129
+ curves: this.curvesOf(validationSplit)
130
+ }];
131
+ const floor = LearningCurveMath.floor(parts.flatMap(({ curves }) => curves.flatMap((curve) => curve.points.map((point) => point.value))), this.logarithmic);
132
+ const [train, validation] = parts.map(({ key, curves }) => ({
133
+ key,
134
+ curves,
135
+ stats: LearningCurveMath.summarize(curves, spread).map((stat) => floor === void 0 ? stat : {
136
+ ...stat,
137
+ low: Math.max(stat.low, floor)
138
+ })
139
+ }));
140
+ const best = LearningCurveMath.best(validation.stats, goal);
141
+ return {
142
+ train,
143
+ validation,
144
+ best,
145
+ gaps: LearningCurveMath.gaps(train.stats, validation.stats, goal, gap, best?.epoch ?? null)
146
+ };
147
+ }
148
+ scales(ctx) {
149
+ const { width, height } = ctx.plot;
150
+ const { train, validation } = this.#view = this.read();
151
+ const splits = [train, validation], log = this.logarithmic;
152
+ const epochs = splits.flatMap(({ stats }) => stats.map((stat) => stat.epoch));
153
+ const values = splits.flatMap(({ stats, curves }) => [...stats.flatMap((stat) => [stat.low, stat.high]), ...this.options.runs ? curves.flatMap((curve) => curve.points.map((point) => point.value)) : []]);
154
+ let [left, right] = extent(epochs), [lo, hi] = extent(values);
155
+ if (left === void 0 || right === void 0) [left, right] = [0, 1];
156
+ if (left === right) [left, right] = [left - 1, right + 1];
157
+ if (lo === void 0 || hi === void 0) [lo, hi] = log ? [1, 10] : [0, 1];
158
+ if (lo === hi) {
159
+ const pad = Math.abs(lo) * .1 || 1;
160
+ [lo, hi] = [log ? lo / 2 : lo - pad, hi + pad];
161
+ }
162
+ const scale = Scales.continuous(this.options.scale).domain([lo, hi]).range([height, 0]);
163
+ const y = !log ? scale.nice() : hi / lo < 10 ? Object.assign(scale, { ticks: () => ticks(lo, hi, this.yTicks()) }) : scale;
164
+ this.#valueAxis = y;
165
+ return {
166
+ x: scaleLinear([left, right], [0, width]),
167
+ y
168
+ };
169
+ }
170
+ /** On a wide log axis only 1, 2 and 5 keep a label; the rest would collide. */
171
+ tickFormat(scale) {
172
+ const format = super.tickFormat(scale), [lo, hi] = scale.domain?.() ?? [];
173
+ if (!this.logarithmic || scale !== this.#valueAxis) return DistinctTicks.format(scale, format);
174
+ if (!format || !(hi / lo > 20)) return format;
175
+ return (value) => [
176
+ 1,
177
+ 2,
178
+ 5
179
+ ].includes(Math.round(+value / 10 ** Math.floor(Math.log10(+value) + 1e-9))) ? format(value) : "";
180
+ }
181
+ legendItems() {
182
+ const gap = this.#view?.gaps.length ? [{
183
+ key: GAP_NAME[this.options.gap],
184
+ color: this.palette.diverging(.7)
185
+ }] : [];
186
+ return [...super.legendItems(), ...gap];
187
+ }
188
+ plotMarks(ctx, axes) {
189
+ const { goal } = this.options;
190
+ const view = this.#view, { train, validation, best, gaps } = view;
191
+ const px = (epoch) => axes.x(epoch) ?? 0, py = (value) => axes.y(value) ?? 0;
192
+ const splits = [train, validation].filter((split) => split.stats.length);
193
+ const color = (split) => ctx.palette.color(split.key);
194
+ for (const split of splits) color(split);
195
+ const gapShape = area().x((d) => px(d.epoch)).y0((d) => py(d.train)).y1((d) => py(d.validation));
196
+ const gapFlat = area().x((d) => px(d.epoch)).y0((d) => py(d.train)).y1((d) => py(d.train));
197
+ const gapFills = ctx.marks("gap", "path", gaps, (_, i) => i).attr("fill", ctx.palette.diverging(.7)).attr("fill-opacity", .3);
198
+ ctx.set(gapFills, { d: (d) => gapShape(d) ?? "" }, { d: (d) => gapFlat(d) ?? "" });
199
+ const band = area().x((d) => px(d.epoch)).y0((d) => py(d.low)).y1((d) => py(d.high));
200
+ const flat = area().x((d) => px(d.epoch)).y0((d) => py(d.center)).y1((d) => py(d.center));
201
+ const bands = ctx.marks("bands", "path", splits.filter((split) => split.curves.length > 1), (d) => d.key).attr("fill", color).attr("fill-opacity", .2);
202
+ ctx.set(bands, { d: (d) => band(d.stats) ?? "" }, { d: (d) => flat(d.stats) ?? "" });
203
+ const trace = line().x((d) => px(d.epoch)).y((d) => py(d.value));
204
+ const every = this.options.runs ? splits.flatMap((split) => split.curves.map((curve) => ({
205
+ ...curve,
206
+ split
207
+ }))) : [];
208
+ const runs = ctx.marks("runs", "path", every, (d) => `${d.split.key}/${d.run}`).attr("fill", "none").attr("stroke", (d) => color(d.split)).attr("stroke-width", 1).attr("stroke-opacity", .4).attr("stroke-linejoin", "round");
209
+ ctx.set(runs, { d: (d) => trace(d.points) ?? "" });
210
+ const center = line().x((d) => px(d.epoch)).y((d) => py(d.center));
211
+ const means = ctx.marks("means", "path", splits, (d) => d.key).attr("fill", "none").attr("stroke", color).attr("stroke-width", 2).attr("stroke-linejoin", "round").attr("stroke-linecap", "round");
212
+ ctx.set(means, { d: (d) => center(d.stats) ?? "" });
213
+ ctx.drawOn(means);
214
+ this.drawLoneEpochs(ctx, splits.filter((split) => split.stats.length === 1), px, py);
215
+ const pick = this.options.best && best ? {
216
+ x: px(best.epoch),
217
+ y: py(best.center),
218
+ text: `Best epoch ${TrendHover.title(best.epoch)} · ${TrendHover.value(best.center)}`
219
+ } : null;
220
+ CurvePickMark.draw(ctx, "best", pick, goal === MetricGoal.Maximize ? ctx.plot.height - 8 : 12);
221
+ this.drawSplitNote(ctx);
222
+ TrendHover.attach(ctx, this.slices(ctx, view, px, py), ctx.plot.width, ctx.plot.height);
223
+ }
224
+ /** A line through one epoch draws nothing, so that mean is a dot and its band a stem. */
225
+ drawLoneEpochs(ctx, splits, px, py) {
226
+ const color = (split) => ctx.palette.color(split.key), only = (split) => split.stats[0];
227
+ const stems = ctx.marks("lone-bands", "line", splits.filter((split) => only(split).high > only(split).low), (d) => d.key).attr("stroke", color).attr("stroke-width", 2).attr("stroke-opacity", .4);
228
+ ctx.set(stems, {
229
+ x1: (d) => px(only(d).epoch),
230
+ x2: (d) => px(only(d).epoch),
231
+ y1: (d) => py(only(d).low),
232
+ y2: (d) => py(only(d).high)
233
+ });
234
+ const dots = ctx.marks("lone-means", "circle", splits, (d) => d.key).attr("fill", color).attr("stroke", ctx.theme.surface).attr("stroke-width", 2);
235
+ ctx.set(dots, {
236
+ cx: (d) => px(only(d).epoch),
237
+ cy: (d) => py(only(d).center),
238
+ r: 5
239
+ }, { r: 0 });
240
+ }
241
+ /** Sits where a curve with this goal ends up empty, and clear of the best-epoch label. */
242
+ drawSplitNote(ctx) {
243
+ const { width, height } = ctx.plot, note = this.absentSplits;
244
+ const { goal, best } = this.options, clear = best && this.#view?.best ? LINE : 0;
245
+ const y = goal === MetricGoal.Maximize ? height - 8 - clear : 12 + clear;
246
+ ctx.text(ctx.marks("missing-split", "text", note ? [WideLabel.fit(note, width - 4)] : []), 11, "muted").attr("text-anchor", "end").attr("x", width - 2).attr("y", y).attr("paint-order", "stroke").attr("stroke", ctx.theme.surface).attr("stroke-width", 3).text((d) => d);
247
+ }
248
+ slices(ctx, { train, validation }, px, py) {
249
+ const trained = new Map(train.stats.map((stat) => [stat.epoch, stat])), held = new Map(validation.stats.map((stat) => [stat.epoch, stat]));
250
+ const spreadRow = (name, stat) => [`${name} band`, `${TrendHover.value(stat.low)} – ${TrendHover.value(stat.high)}`];
251
+ return [.../* @__PURE__ */ new Set([...trained.keys(), ...held.keys()])].sort((a, b) => a - b).map((epoch) => {
252
+ const a = trained.get(epoch), b = held.get(epoch);
253
+ const rows = [];
254
+ if (a) rows.push([TRAINING, TrendHover.value(a.center)], spreadRow(TRAINING, a));
255
+ if (b) rows.push([VALIDATION, TrendHover.value(b.center)], spreadRow(VALIDATION, b));
256
+ if (a && b) rows.push(["Gap", TrendHover.value(Math.abs(b.center - a.center))]);
257
+ rows.push(["Runs", min([a?.runs ?? Infinity, b?.runs ?? Infinity])]);
258
+ const marks = [...a ? [{
259
+ x: px(epoch),
260
+ y: py(a.center),
261
+ color: ctx.palette.color(TRAINING)
262
+ }] : [], ...b ? [{
263
+ x: px(epoch),
264
+ y: py(b.center),
265
+ color: ctx.palette.color(VALIDATION)
266
+ }] : []];
267
+ return {
268
+ x: px(epoch),
269
+ title: `Epoch ${TrendHover.title(epoch)}`,
270
+ rows,
271
+ marks
272
+ };
273
+ });
274
+ }
275
+ };
276
+ export { LearningCurveChart };
@@ -0,0 +1,92 @@
1
+ import { StatMath } from "../statistics/StatMath.js";
2
+ import { MetricGoal, OverfitShading, RunSpread } from "./curveDiagnosticEnums.js";
3
+ import { mean, min, sum } from "d3";
4
+ // src/charts/mlcharts/LearningCurveMath.ts
5
+ /** The calculations behind a learning curve, free of drawing. */
6
+ var LearningCurveMath = class {
7
+ /** TensorBoard's debiased exponential moving average. `weight` 0 returns the input; the first value is never pulled towards zero. */
8
+ static smooth(values, weight) {
9
+ if (!(weight > 0)) return [...values];
10
+ let last = 0;
11
+ return values.map((value, i) => {
12
+ last = last * weight + (1 - weight) * value;
13
+ return last / (1 - weight ** (i + 1));
14
+ });
15
+ }
16
+ /** Band around `center`: mean ± one sample SD, a Student-t 95% CI of the mean, or the observed min and max. One run has no width. */
17
+ static interval(values, center, spread) {
18
+ const n = values.length;
19
+ if (n < 2) return [center, center];
20
+ if (spread === RunSpread.Range) return [Math.min(...values), Math.max(...values)];
21
+ const sd = Math.sqrt(sum(values, (value) => (value - center) ** 2) / (n - 1));
22
+ const reach = spread === RunSpread.Confidence95 ? StatMath.tQuantile(.975, n - 1) * sd / Math.sqrt(n) : sd;
23
+ return [center - reach, center + reach];
24
+ }
25
+ /** Mean and band per epoch. Runs that stopped early leave later epochs with a smaller `runs`. */
26
+ static summarize(curves, spread) {
27
+ const epochs = /* @__PURE__ */ new Map();
28
+ for (const { points } of curves) for (const { epoch, value } of points) (epochs.get(epoch) ?? epochs.set(epoch, []).get(epoch)).push(value);
29
+ return [...epochs].sort(([a], [b]) => a - b).map(([epoch, values]) => {
30
+ const center = mean(values);
31
+ const [low, high] = this.interval(values, center, spread);
32
+ return {
33
+ epoch,
34
+ center,
35
+ low,
36
+ high,
37
+ runs: values.length
38
+ };
39
+ });
40
+ }
41
+ /** The earliest epoch with the best mean, the checkpoint early stopping would restore. */
42
+ static best(stats, goal) {
43
+ const sign = goal === MetricGoal.Minimize ? 1 : -1;
44
+ return stats.reduce((top, stat) => !top || sign * stat.center < sign * top.center ? stat : top, null);
45
+ }
46
+ /**
47
+ * Stretches where validation is worse than training, ending on the exact crossings of the two straight mean lines.
48
+ * `AfterBest` keeps only epochs from the best one on, where further training no longer helps validation.
49
+ */
50
+ static gaps(train, validation, goal, shading, best) {
51
+ if (shading === OverfitShading.None) return [];
52
+ const sign = goal === MetricGoal.Minimize ? 1 : -1;
53
+ const trained = new Map(train.map((stat) => [stat.epoch, stat.center]));
54
+ const shared = validation.flatMap(({ epoch, center }) => {
55
+ const value = trained.get(epoch);
56
+ return value === void 0 || shading === OverfitShading.AfterBest && (best === null || epoch < best) ? [] : [{
57
+ epoch,
58
+ train: value,
59
+ validation: center
60
+ }];
61
+ });
62
+ const excess = (point) => sign * (point.validation - point.train);
63
+ const stretches = [];
64
+ let open = [];
65
+ shared.forEach((point, i) => {
66
+ const before = shared[i - 1];
67
+ if (before && excess(before) > 0 !== excess(point) > 0) {
68
+ const share = excess(before) / (excess(before) - excess(point));
69
+ const level = before.train + share * (point.train - before.train);
70
+ const crossing = {
71
+ epoch: before.epoch + share * (point.epoch - before.epoch),
72
+ train: level,
73
+ validation: level
74
+ };
75
+ if (excess(point) > 0) open = [crossing];
76
+ else {
77
+ stretches.push([...open, crossing]);
78
+ open = [];
79
+ }
80
+ }
81
+ if (excess(point) > 0) open.push(point);
82
+ });
83
+ if (open.length) stretches.push(open);
84
+ return stretches.filter((stretch) => stretch.length > 1);
85
+ }
86
+ /** Lowest value a log axis or a non-negative metric lets a band reach: `undefined` when nothing bounds it. */
87
+ static floor(values, logarithmic) {
88
+ if (logarithmic) return min(values.filter((value) => value > 0));
89
+ return min(values) >= 0 ? 0 : void 0;
90
+ }
91
+ };
92
+ export { LearningCurveMath };
@@ -0,0 +1,92 @@
1
+ import { BarRows } from "../bars/BarRows.js";
2
+ import { ascending, mean } from "d3";
3
+ // src/charts/mlcharts/PartialDependence.ts
4
+ var PLOTTING_POSITION = .4;
5
+ /** Where a model is evaluated: evenly across the value range, or at evenly spaced quantiles. */
6
+ var PartialDependenceGrid = /* @__PURE__ */ function(PartialDependenceGrid) {
7
+ PartialDependenceGrid["Uniform"] = "uniform";
8
+ PartialDependenceGrid["Quantile"] = "quantile";
9
+ return PartialDependenceGrid;
10
+ }({});
11
+ /** The partial dependence calculation of Friedman (2001) with ICE curves (Goldstein et al. 2015). */
12
+ var PartialDependence = class PartialDependence {
13
+ grid;
14
+ curves;
15
+ observed;
16
+ /** The average at each grid value, over the instances predicted there. */
17
+ average;
18
+ constructor(grid, curves, observed) {
19
+ this.grid = grid;
20
+ this.curves = curves;
21
+ this.observed = observed;
22
+ this.average = grid.flatMap((x, i) => {
23
+ const values = curves.flatMap((curve) => curve.values[i] ?? []);
24
+ return values.length ? [{
25
+ x,
26
+ y: mean(values),
27
+ instances: values.length
28
+ }] : [];
29
+ });
30
+ }
31
+ /** The Cunnane quantile of `scipy.stats.mstats.mquantiles` with its default plotting positions, which `partial_dependence` uses. */
32
+ static quantile(sorted, p) {
33
+ const n = sorted.length, position = n * p + PLOTTING_POSITION + p * (1 - 2 * PLOTTING_POSITION);
34
+ const k = Math.floor(Math.min(n - 1, Math.max(1, position))), share = Math.min(1, Math.max(0, position - k));
35
+ return (1 - share) * sorted[k - 1] + share * sorted[k];
36
+ }
37
+ /**
38
+ * Grid values as scikit-learn's `_grid_from_X`: the distinct values when there are fewer than `points`,
39
+ * otherwise `points` steps between the `range` quantiles of the data. The Quantile method is this library's.
40
+ */
41
+ static gridOf(values, { method, points, range }) {
42
+ const sorted = [...values].sort(ascending), distinct = [...new Set(sorted)];
43
+ const count = Math.max(2, Math.floor(points) || 2);
44
+ if (distinct.length < count || sorted.length < 2) return distinct;
45
+ const at = (p) => this.quantile(sorted, p);
46
+ const [from, to] = [Math.min(...range), Math.max(...range)];
47
+ const steps = Array.from({ length: count }, (_, i) => i / (count - 1));
48
+ const grid = method === "quantile" ? steps.map((t) => at(from + (to - from) * t)) : steps.map((t) => at(from) + (at(to) - at(from)) * t);
49
+ return [...new Set(grid)];
50
+ }
51
+ /** From a `predict` callback: every row is swept across the grid with the feature overwritten and the rest held. */
52
+ static fromModel(rows, x, predict, grid) {
53
+ const observed = rows.flatMap((row) => BarRows.number(row[x]) ?? []);
54
+ const axis = this.gridOf(observed, grid);
55
+ const curves = rows.map((row, i) => ({
56
+ instance: String(i + 1),
57
+ values: axis.map((value) => BarRows.number(predict({
58
+ ...row,
59
+ [x]: value
60
+ })))
61
+ }));
62
+ return new PartialDependence(axis, curves, observed.sort(ascending));
63
+ }
64
+ /** From rows that already hold predictions, one per instance and grid value. Repeats of a pair are averaged. */
65
+ static fromPredictions(rows, { x, y, instance, observed }) {
66
+ const sweeps = /* @__PURE__ */ new Map(), seen = /* @__PURE__ */ new Map();
67
+ for (const row of rows) {
68
+ const at = BarRows.number(row[x]), value = BarRows.number(row[y]), id = BarRows.label(row[instance]);
69
+ if (at === null || value === null) continue;
70
+ const sweep = sweeps.get(id) ?? sweeps.set(id, /* @__PURE__ */ new Map()).get(id);
71
+ sweep.set(at, [...sweep.get(at) ?? [], value]);
72
+ const feature = BarRows.number(row[observed]);
73
+ if (feature !== null && !seen.has(id)) seen.set(id, feature);
74
+ }
75
+ const grid = [...new Set([...sweeps.values()].flatMap((sweep) => [...sweep.keys()]))].sort(ascending);
76
+ const curves = [...sweeps].map(([id, sweep]) => ({
77
+ instance: id,
78
+ values: grid.map((at) => mean(sweep.get(at) ?? []) ?? null)
79
+ }));
80
+ return new PartialDependence(grid, curves, [...seen.values()].sort(ascending));
81
+ }
82
+ /** Centred ICE: each curve minus its value at the lowest grid value every instance reaches. */
83
+ centered() {
84
+ const index = Math.max(0, this.grid.findIndex((_, i) => this.curves.every((curve) => curve.values[i] !== null)));
85
+ const curves = this.curves.filter((curve) => curve.values[index] !== null).map(({ instance, values }) => ({
86
+ instance,
87
+ values: values.map((value) => value === null ? null : value - values[index])
88
+ }));
89
+ return new PartialDependence(this.grid, curves, this.observed);
90
+ }
91
+ };
92
+ export { PartialDependence, PartialDependenceGrid };
@@ -0,0 +1,225 @@
1
+ import { CartesianChart } from "../../core/CartesianChart.js";
2
+ import { ChartFamily, DataShape, Dimension } from "../../core/enums.js";
3
+ import { TrendHover } from "../linesareas/TrendHover.js";
4
+ import { DistinctTicks } from "./DistinctTicks.js";
5
+ import { ExplainabilitySamples } from "./ExplainabilitySamples.js";
6
+ import { PartialDependence, PartialDependenceGrid } from "./PartialDependence.js";
7
+ import { extent, format, line, max, quantileSorted, scaleLinear } from "d3";
8
+ // src/charts/mlcharts/PartialDependencePlot.ts
9
+ /** Which curves are drawn: the names follow scikit-learn's `kind`. */
10
+ var PartialDependenceKind = /* @__PURE__ */ function(PartialDependenceKind) {
11
+ PartialDependenceKind["Both"] = "both";
12
+ PartialDependenceKind["Average"] = "average";
13
+ PartialDependenceKind["Individual"] = "individual";
14
+ return PartialDependenceKind;
15
+ }({});
16
+ /** Ticks along the x axis showing where the feature has data. */
17
+ var PartialDependenceRug = /* @__PURE__ */ function(PartialDependenceRug) {
18
+ PartialDependenceRug["Observations"] = "observations";
19
+ PartialDependenceRug["Deciles"] = "deciles";
20
+ PartialDependenceRug["None"] = "none";
21
+ return PartialDependenceRug;
22
+ }({});
23
+ var AVERAGE = "Average (PDP)";
24
+ var INDIVIDUAL = "Individual (ICE)";
25
+ var RUG_HEIGHT = 8;
26
+ var RUG_ROOM = 12;
27
+ var RUG_LIMIT = 400;
28
+ var DECILES = Array.from({ length: 9 }, (_, i) => (i + 1) / 10);
29
+ var figure = format(".4~g");
30
+ /**
31
+ * Shows how a model's prediction moves with one numeric feature, others held at their observed
32
+ * values: partial dependence (Friedman 2001) with ICE curves (Goldstein et al. 2015). Pass rows of
33
+ * precomputed predictions, or background rows and `predict`. Curves that fan or cross mean the
34
+ * average hides an interaction, and any PDP is unreliable where the rug is empty or the feature
35
+ * is correlated with others (Apley & Zhu 2020). The axis spans the grid, so `gridRange` keeps outliers off it.
36
+ */
37
+ var PartialDependencePlot = class extends CartesianChart {
38
+ static meta = {
39
+ id: "partial-dependence",
40
+ name: "Partial dependence plot",
41
+ family: ChartFamily.Statistical,
42
+ dimension: Dimension.Two,
43
+ shape: DataShape.Table,
44
+ description: "Shows a model's average prediction across one feature, with individual conditional expectation curves and a rug of the data.",
45
+ tags: [
46
+ "machine learning",
47
+ "explainability",
48
+ "pdp",
49
+ "ice",
50
+ "marginal effect"
51
+ ],
52
+ sample: (seed) => ExplainabilitySamples.iceCurves(seed),
53
+ variants: {
54
+ kind: Object.values(PartialDependenceKind),
55
+ centered: [false, true],
56
+ rug: Object.values(PartialDependenceRug)
57
+ }
58
+ };
59
+ #cache = null;
60
+ #titled = "";
61
+ defaults() {
62
+ return {
63
+ x: "x",
64
+ y: "prediction",
65
+ instance: "instance",
66
+ observed: "observed",
67
+ gridMethod: PartialDependenceGrid.Uniform,
68
+ gridPoints: 50,
69
+ gridRange: [.05, .95],
70
+ kind: "both",
71
+ centered: false,
72
+ rug: "observations",
73
+ maxCurves: 80
74
+ };
75
+ }
76
+ render() {
77
+ const { yLabel, centered } = this.options;
78
+ if (yLabel === void 0 || yLabel === this.#titled) this.options = {
79
+ ...this.options,
80
+ yLabel: this.#titled = centered ? "Change in prediction" : "Prediction"
81
+ };
82
+ return super.render();
83
+ }
84
+ /** Sweeping rows through a model is costly, and zoom and resize repaint without changing it. */
85
+ get model() {
86
+ const { data, x, y, instance, observed, predict, gridMethod, gridPoints, gridRange, centered } = this.options;
87
+ const rows = Array.isArray(data) ? data.filter(Boolean) : [];
88
+ const key = [
89
+ data,
90
+ rows.length,
91
+ x,
92
+ y,
93
+ instance,
94
+ observed,
95
+ predict,
96
+ gridMethod,
97
+ gridPoints,
98
+ gridRange?.[0],
99
+ gridRange?.[1],
100
+ centered
101
+ ];
102
+ if (this.#cache?.key.every((value, i) => value === key[i])) return this.#cache.model;
103
+ const base = predict ? PartialDependence.fromModel(rows, x, predict, {
104
+ method: gridMethod,
105
+ points: gridPoints,
106
+ range: gridRange
107
+ }) : PartialDependence.fromPredictions(rows, {
108
+ x,
109
+ y,
110
+ instance,
111
+ observed
112
+ });
113
+ const model = centered ? base.centered() : base;
114
+ this.#cache = {
115
+ key,
116
+ model
117
+ };
118
+ return model;
119
+ }
120
+ get drawnCurves() {
121
+ const { curves } = this.model;
122
+ if (this.options.kind === "average") return [];
123
+ const limit = Math.max(1, Math.floor(this.options.maxCurves) || 1);
124
+ if (curves.length <= limit) return curves;
125
+ return Array.from({ length: limit }, (_, i) => curves[Math.floor(i * curves.length / limit)]);
126
+ }
127
+ get rugValues() {
128
+ const { observed, grid } = this.model, { rug } = this.options;
129
+ if (rug === "none" || !observed.length || !grid.length) return [];
130
+ const within = (value) => value >= grid[0] && value <= grid.at(-1);
131
+ if (rug === "deciles") return DECILES.map((p) => quantileSorted(observed, p)).filter(within);
132
+ const kept = observed.filter(within), stride = Math.max(1, kept.length / RUG_LIMIT);
133
+ return Array.from({ length: Math.min(kept.length, RUG_LIMIT) }, (_, i) => kept[Math.floor(i * stride)]);
134
+ }
135
+ table() {
136
+ return {
137
+ columns: [
138
+ this.options.x,
139
+ "partial_dependence",
140
+ "instances"
141
+ ],
142
+ rows: this.model.average.map((point) => [
143
+ point.x,
144
+ point.y,
145
+ point.instances
146
+ ])
147
+ };
148
+ }
149
+ tickFormat(scale) {
150
+ const base = super.tickFormat(scale);
151
+ if (!base) return base;
152
+ const fine = scale;
153
+ return (max(fine.domain(), Math.abs) ?? 0) < 10 ? (value) => fine.tickFormat(this.xTicks())(+value) : DistinctTicks.format(scale, base);
154
+ }
155
+ scales(ctx) {
156
+ const { width, height } = ctx.plot;
157
+ const { grid, average } = this.model;
158
+ const showAverage = this.options.kind !== "individual";
159
+ const heights = [...this.drawnCurves.flatMap((curve) => curve.values.flatMap((value) => value ?? [])), ...showAverage ? average.map((point) => point.y) : []];
160
+ const [left = 0, right = 1] = extent(grid);
161
+ const [low = 0, high = 1] = extent(heights);
162
+ const room = this.options.rug === "none" ? 0 : RUG_ROOM;
163
+ return {
164
+ x: scaleLinear().domain(left === right ? [left - 1, right + 1] : [left, right]).range([0, width]),
165
+ y: scaleLinear().domain(low === high ? [low - 1, high + 1] : [low, high]).nice().range([height - room, 0])
166
+ };
167
+ }
168
+ plotMarks(ctx, axes) {
169
+ const { kind, centered } = this.options, { grid, average } = this.model;
170
+ const showAverage = kind !== "individual", showCurves = kind !== "average";
171
+ const averageInk = showAverage && average.length ? ctx.palette.color(AVERAGE) : "";
172
+ const curveInk = showCurves && average.length ? ctx.palette.color(INDIVIDUAL) : "";
173
+ const trace = line().defined((value) => value !== null).x((_, i) => axes.x(grid[i])).y((value) => axes.y(value));
174
+ const path = line().x((point) => axes.x(point.x)).y((point) => axes.y(point.y));
175
+ this.drawAnchor(ctx, axes, centered);
176
+ this.drawRug(ctx, axes);
177
+ const curves = ctx.marks("ice", "path", this.drawnCurves, (curve) => curve.instance).attr("fill", "none").attr("stroke", curveInk).attr("stroke-width", 1).attr("stroke-opacity", showAverage ? .32 : .5).attr("pointer-events", "none");
178
+ ctx.set(curves, { d: (curve) => trace(curve.values) }, { d: (curve) => trace(curve.values) });
179
+ const lines = showAverage && average.length > 1 ? [average] : [];
180
+ const halo = ctx.marks("average-halo", "path", lines).attr("fill", "none").attr("stroke", ctx.theme.surface).attr("stroke-width", 5).attr("stroke-linejoin", "round").attr("pointer-events", "none");
181
+ const stroke = ctx.marks("average", "path", lines).attr("fill", "none").attr("stroke", averageInk).attr("stroke-width", 2).attr("stroke-linecap", "round").attr("stroke-linejoin", "round").attr("pointer-events", "none");
182
+ ctx.set(halo, { d: (points) => path(points) }, { d: (points) => path(points) });
183
+ stroke.filter(function() {
184
+ return !this.getAttribute("d");
185
+ }).attr("d", (points) => path(points));
186
+ ctx.drawOn(stroke);
187
+ ctx.set(stroke, { d: (points) => path(points) });
188
+ const lone = ctx.marks("lone-point", "circle", showAverage && average.length === 1 ? average : []).attr("fill", averageInk).attr("stroke", ctx.theme.surface).attr("stroke-width", 2);
189
+ ctx.set(lone, {
190
+ cx: (point) => axes.x(point.x),
191
+ cy: (point) => axes.y(point.y),
192
+ r: 5
193
+ });
194
+ TrendHover.attach(ctx, average.map((point) => ({
195
+ x: axes.x(point.x),
196
+ title: `${this.options.xLabel ?? this.options.x} ${figure(point.x)}`,
197
+ rows: [["Average", figure(point.y)], ["Instances", point.instances]],
198
+ marks: showAverage ? [{
199
+ x: axes.x(point.x),
200
+ y: axes.y(point.y),
201
+ color: averageInk
202
+ }] : []
203
+ })), ctx.plot.width, ctx.plot.height);
204
+ }
205
+ drawAnchor(ctx, axes, centered) {
206
+ const rule = ctx.marks("anchor", "line", centered && this.model.curves.length ? [0] : []).attr("stroke", ctx.theme.axis).attr("stroke-width", 1).attr("stroke-dasharray", "4 3");
207
+ ctx.set(rule, {
208
+ x1: 0,
209
+ x2: ctx.plot.width,
210
+ y1: axes.y(0),
211
+ y2: axes.y(0)
212
+ });
213
+ }
214
+ drawRug(ctx, axes) {
215
+ const { height } = ctx.plot, deciles = this.options.rug === "deciles";
216
+ const ticks = ctx.marks("rug", "line", this.rugValues).attr("stroke", ctx.theme.inkSecondary).attr("stroke-width", deciles ? 2 : 1).attr("stroke-opacity", deciles ? 1 : .55).attr("pointer-events", "none");
217
+ ctx.set(ticks, {
218
+ x1: (value) => axes.x(value),
219
+ x2: (value) => axes.x(value),
220
+ y1: height,
221
+ y2: height - RUG_HEIGHT
222
+ });
223
+ }
224
+ };
225
+ export { PartialDependenceKind, PartialDependencePlot, PartialDependenceRug };