@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,316 @@
1
+ import { CartesianChart } from "../../core/CartesianChart.js";
2
+ import { ChartFamily, DataShape, Dimension } from "../../core/enums.js";
3
+ import { FieldContours } from "../density/FieldContours.js";
4
+ import { DistinctTicks } from "./DistinctTicks.js";
5
+ import { DecisionSamples } from "./DecisionSamples.js";
6
+ import { DecisionSurface } from "./DecisionSurface.js";
7
+ import { extent, scaleLinear, symbol, symbolCircle, symbolCross } from "d3";
8
+ // src/charts/mlcharts/DecisionBoundaryChart.ts
9
+ /** How the feature plane shows the model. `Confidence` deepens each region with the margin; `Cells` shows the evaluated grid unsmoothed. */
10
+ var DecisionFill = /* @__PURE__ */ function(DecisionFill) {
11
+ DecisionFill["Regions"] = "regions";
12
+ DecisionFill["Confidence"] = "confidence";
13
+ DecisionFill["Cells"] = "cells";
14
+ DecisionFill["None"] = "no-fill";
15
+ return DecisionFill;
16
+ }({});
17
+ /** Which observations sit on the surface. */
18
+ var DecisionOverlay = /* @__PURE__ */ function(DecisionOverlay) {
19
+ DecisionOverlay["All"] = "all-points";
20
+ DecisionOverlay["Errors"] = "errors-only";
21
+ DecisionOverlay["None"] = "no-points";
22
+ return DecisionOverlay;
23
+ }({});
24
+ var MARGINS = [
25
+ .25,
26
+ .5,
27
+ .75
28
+ ];
29
+ var REGION_TINT = .24;
30
+ var BAND_TINT = .13;
31
+ var LABEL_ROOM = 24;
32
+ var dot = symbol(symbolCircle, 64)();
33
+ var cross = symbol(symbolCross, 130)();
34
+ var text = (value) => value === null || value === void 0 || value === "" ? null : String(value);
35
+ var number = (value) => value === null || value === void 0 || value === "" ? NaN : Number(value);
36
+ var percent = (value) => `${(value * 100).toFixed(1)}%`;
37
+ /**
38
+ * Draws what a classifier decides across two features: regions, the boundary where the top two classes tie, and points marked right or wrong.
39
+ * Same reading as Hastie, Tibshirani & Friedman, ESL ch. 2, and scikit-learn's `DecisionBoundaryDisplay`. Each region is the zero contour of
40
+ * `p(class) − max p(other)`, so it works for any number of classes; `Confidence` adds that margin's upper contours (best-versus-second-best).
41
+ * A model with more than two features is shown only through the slice the caller evaluated.
42
+ */
43
+ var DecisionBoundaryChart = class extends CartesianChart {
44
+ static meta = {
45
+ id: "decision-boundary",
46
+ name: "Decision boundary",
47
+ family: ChartFamily.Statistical,
48
+ dimension: Dimension.Two,
49
+ shape: DataShape.Grid,
50
+ description: "Maps where a classifier predicts each class across two features, with its boundary and the points it gets right or wrong.",
51
+ tags: [
52
+ "classifier",
53
+ "machine learning",
54
+ "decision region",
55
+ "misclassification",
56
+ "predict_proba"
57
+ ],
58
+ sample: (seed) => DecisionSamples.clusters(seed),
59
+ variants: {
60
+ fill: [
61
+ "regions",
62
+ "confidence",
63
+ "cells",
64
+ "no-fill"
65
+ ],
66
+ overlay: [
67
+ "all-points",
68
+ "errors-only",
69
+ "no-points"
70
+ ],
71
+ boundary: [true, false]
72
+ },
73
+ valid: ({ fill, boundary }) => fill !== "no-fill" || boundary === true
74
+ };
75
+ #memo = null;
76
+ #key = [];
77
+ defaults() {
78
+ return {
79
+ x: "x",
80
+ y: "y",
81
+ label: "group",
82
+ fill: "regions",
83
+ overlay: "all-points",
84
+ boundary: true,
85
+ resolution: 80
86
+ };
87
+ }
88
+ legendItems() {
89
+ const short = (name) => name.length > LABEL_ROOM ? `${name.slice(0, 23)}…` : name;
90
+ return [...super.legendItems().map((item) => ({
91
+ ...item,
92
+ label: short(item.key)
93
+ })), ...this.#key];
94
+ }
95
+ /** `predict` may be slow, and a render paints more than once, so the sweep lasts until its inputs change. */
96
+ #view() {
97
+ const { data, x, y, label, predicted, resolution, xDomain, yDomain } = this.options;
98
+ const key = [
99
+ data,
100
+ x,
101
+ y,
102
+ label,
103
+ predicted,
104
+ resolution,
105
+ xDomain,
106
+ yDomain
107
+ ];
108
+ if (this.#memo?.key.every((value, i) => value === key[i])) return this.#memo.view;
109
+ const view = this.#build();
110
+ this.#memo = {
111
+ key,
112
+ view
113
+ };
114
+ return view;
115
+ }
116
+ #build() {
117
+ const { data, x, y, label, predicted, resolution, xDomain, yDomain } = this.options;
118
+ const rows = (data?.points ?? []).flatMap((row) => {
119
+ const vx = number(row?.[x]), vy = number(row?.[y]), actual = text(row?.[label]);
120
+ return Number.isFinite(vx) && Number.isFinite(vy) && actual !== null ? [{
121
+ x: vx,
122
+ y: vy,
123
+ actual,
124
+ stated: predicted ? text(row[predicted]) : null
125
+ }] : [];
126
+ });
127
+ const given = data?.grid ? DecisionSurface.fromGrid(data.grid) : null;
128
+ const reach = (values, frame) => {
129
+ const [low = 0, high = 1] = extent([...values, ...frame ?? []].filter(Number.isFinite));
130
+ if (low === high) return [low - 1, high + 1];
131
+ const pad = frame ? 0 : (high - low) * .05;
132
+ return [low - pad, high + pad];
133
+ };
134
+ const rangeX = reach(rows.map((row) => row.x), given?.x ?? xDomain), rangeY = reach(rows.map((row) => row.y), given?.y ?? yDomain);
135
+ const known = [...new Set(rows.map((row) => row.actual))];
136
+ const surface = given ?? (data?.predict ? DecisionSurface.fromPredict(data.predict, rangeX, rangeY, resolution, known) : null);
137
+ const points = rows.map(({ stated, ...row }, index) => {
138
+ const call = surface?.classify(row.x, row.y) ?? null;
139
+ const label = stated ?? call?.label ?? null;
140
+ return {
141
+ ...row,
142
+ index,
143
+ predicted: label,
144
+ probability: call?.label === label ? call?.probability ?? null : null,
145
+ correct: label === null ? null : label === row.actual
146
+ };
147
+ });
148
+ const named = points.flatMap((point) => [point.actual, ...point.predicted === null ? [] : [point.predicted]]);
149
+ return {
150
+ surface,
151
+ points,
152
+ classes: [.../* @__PURE__ */ new Set([...surface?.classes ?? [], ...named])],
153
+ x: rangeX,
154
+ y: rangeY
155
+ };
156
+ }
157
+ tickFormat(scale) {
158
+ return DistinctTicks.format(scale, super.tickFormat(scale));
159
+ }
160
+ scales(ctx) {
161
+ const { x, y } = this.#view();
162
+ return {
163
+ x: scaleLinear(x, [0, ctx.plot.width]),
164
+ y: scaleLinear(y, [ctx.plot.height, 0])
165
+ };
166
+ }
167
+ plotMarks(ctx, axes) {
168
+ const { surface, classes } = this.#view();
169
+ for (const name of classes) ctx.palette.color(name);
170
+ this.#regions(ctx, axes, surface);
171
+ this.#cells(ctx, axes, surface);
172
+ this.#boundary(ctx, axes, surface);
173
+ this.#points(ctx, axes);
174
+ }
175
+ /** `FieldContours` drops line vertices on its outer nodes, so a repeated ring of nodes lets the boundary reach the edge of the grid. */
176
+ #contours(axes, surface, index) {
177
+ const { columns, rows } = surface, margin = surface.margin(index);
178
+ const across = scaleLinear(surface.x.map((_, i) => i), surface.x.map((v) => axes.x(v))).clamp(true);
179
+ const down = scaleLinear(surface.y.map((_, j) => j), surface.y.map((v) => axes.y(v))).clamp(true);
180
+ const inner = (value, size) => Math.max(0, Math.min(size - 1, value - 1));
181
+ const ringed = Array.from({ length: (rows + 2) * (columns + 2) }, (_, node) => margin[inner(node % (columns + 2), columns) + inner(Math.floor(node / (columns + 2)), rows) * columns]);
182
+ return new FieldContours(ringed, columns + 2, rows + 2, (i, j) => [across(i - 1), down(j - 1)]);
183
+ }
184
+ #regions(ctx, axes, surface) {
185
+ const { fill } = this.options;
186
+ const levels = fill === "confidence" ? [0, ...MARGINS] : fill === "regions" ? [0] : [];
187
+ const bands = !surface || !levels.length ? [] : surface.classes.flatMap((name, index) => {
188
+ const contours = this.#contours(axes, surface, index);
189
+ return levels.map((level) => ({
190
+ name,
191
+ index,
192
+ level,
193
+ d: contours.at(level).area
194
+ }));
195
+ });
196
+ const tint = fill === "confidence" ? BAND_TINT : REGION_TINT;
197
+ const paths = ctx.marks("regions", "path", bands, (d) => `${d.name}/${d.level}`).attr("fill", (d) => ctx.palette.color(d.name)).attr("fill-opacity", tint);
198
+ ctx.set(paths, {
199
+ d: (d) => d.d,
200
+ opacity: 1
201
+ }, {
202
+ d: (d) => d.d,
203
+ opacity: 0
204
+ });
205
+ ctx.hover(paths, (d) => ({
206
+ title: `Predicted ${d.name}`,
207
+ color: ctx.palette.color(d.name),
208
+ rows: [["Share of grid", percent(surface.share(d.index))], ...d.level ? [["Margin at least", d.level]] : []]
209
+ }));
210
+ }
211
+ #cells(ctx, axes, surface) {
212
+ const tiles = surface && this.options.fill === "cells" ? surface.tiles(axes.x, axes.y) : [];
213
+ const name = (index) => surface.classes[index];
214
+ const paths = ctx.marks("cells", "path", tiles, (d) => d.index).attr("fill", (d) => ctx.palette.color(name(d.index))).attr("fill-opacity", REGION_TINT);
215
+ ctx.set(paths, {
216
+ d: (d) => d.d,
217
+ opacity: 1
218
+ }, {
219
+ d: (d) => d.d,
220
+ opacity: 0
221
+ });
222
+ ctx.hover(paths, (d) => ({
223
+ title: `Predicted ${name(d.index)}`,
224
+ color: ctx.palette.color(name(d.index)),
225
+ rows: [["Share of grid", percent(surface.share(d.index))]]
226
+ }));
227
+ }
228
+ /** Every stretch of boundary has a class on each side, so the last class's outline would only repeat the others. */
229
+ #boundary(ctx, axes, surface) {
230
+ const edges = surface && this.options.boundary ? surface.classes.slice(0, -1).map((name, index) => ({
231
+ name,
232
+ d: this.#contours(axes, surface, index).at(0).line
233
+ })) : [];
234
+ const lines = ctx.marks("boundary", "path", edges.filter((edge) => edge.d), (d) => d.name).attr("fill", "none").attr("stroke", ctx.theme.ink).attr("stroke-width", 2).attr("stroke-linejoin", "round").attr("stroke-linecap", "round");
235
+ ctx.set(lines, { d: (d) => d.d });
236
+ ctx.drawOn(lines);
237
+ ctx.hover(lines, () => ({
238
+ title: "Decision boundary",
239
+ rows: [["Meaning", "Top two classes tie"]]
240
+ }));
241
+ }
242
+ #points(ctx, axes) {
243
+ const { overlay } = this.options, { points, classes } = this.#view();
244
+ const wrong = points.filter((point) => point.correct === false), right = points.filter((point) => point.correct === true);
245
+ const unscored = points.length - wrong.length - right.length, scored = points.length > unscored;
246
+ const shown = overlay === "no-points" ? [] : overlay === "errors-only" ? wrong : [...points.filter((point) => point.correct !== false), ...wrong];
247
+ const hollow = (d) => d.correct === null;
248
+ const marks = ctx.marks("points", "path", shown, (d) => d.index).attr("fill", (d) => hollow(d) ? ctx.theme.surface : ctx.palette.color(d.actual)).attr("stroke", (d) => hollow(d) ? ctx.palette.color(d.actual) : ctx.theme.surface).attr("stroke-width", 2).attr("paint-order", "stroke").attr("stroke-linejoin", "round");
249
+ const place = (d) => `translate(${axes.x(d.x)},${axes.y(d.y)})${d.correct === false ? " rotate(45)" : ""}`;
250
+ const shape = (d) => d.correct === false ? cross : dot;
251
+ ctx.set(marks, {
252
+ d: shape,
253
+ transform: place,
254
+ opacity: 1
255
+ }, {
256
+ d: shape,
257
+ transform: place,
258
+ opacity: 0
259
+ });
260
+ ctx.hover(marks, (d) => ({
261
+ title: d.correct === null ? "Not scored" : d.correct ? "Correct" : "Wrong",
262
+ color: ctx.palette.color(d.actual),
263
+ rows: [
264
+ ["Actual", d.actual],
265
+ ["Predicted", d.predicted ?? "None"],
266
+ ...d.probability === null ? [] : [["Probability", percent(d.probability)]],
267
+ [this.options.xLabel ?? this.options.x, d.x],
268
+ [this.options.yLabel ?? this.options.y, d.y]
269
+ ]
270
+ }));
271
+ const legend = overlay === "no-points" ? [] : [
272
+ ...scored && overlay === "all-points" ? [{
273
+ key: "right",
274
+ label: `● correct ${right.length}`
275
+ }] : [],
276
+ ...scored ? [{
277
+ key: "wrong",
278
+ label: `✕ wrong ${wrong.length}`
279
+ }] : [],
280
+ ...unscored && overlay === "all-points" ? [{
281
+ key: "unscored",
282
+ label: `○ not scored ${unscored}`
283
+ }] : []
284
+ ];
285
+ const band = this.options.fill === "confidence" && classes.length ? [{
286
+ key: "margin",
287
+ label: "deeper tint: larger margin"
288
+ }] : [];
289
+ this.#key = [...legend, ...band].map((item) => ({
290
+ ...item,
291
+ color: "none"
292
+ }));
293
+ }
294
+ table() {
295
+ const { x, y, label, predicted } = this.options, { points } = this.#view();
296
+ return {
297
+ columns: [
298
+ x,
299
+ y,
300
+ label,
301
+ predicted ?? "predicted",
302
+ "probability",
303
+ "correct"
304
+ ],
305
+ rows: points.map((point) => [
306
+ point.x,
307
+ point.y,
308
+ point.actual,
309
+ point.predicted,
310
+ point.probability,
311
+ point.correct === null ? null : String(point.correct)
312
+ ])
313
+ };
314
+ }
315
+ };
316
+ export { DecisionBoundaryChart, DecisionFill, DecisionOverlay };
@@ -0,0 +1,60 @@
1
+ import { SampleData } from "../../data/SampleData.js";
2
+ import { extent, group, mean, range, variance } from "d3";
3
+ // src/charts/mlcharts/DecisionSamples.ts
4
+ var GRID = 61;
5
+ var FLOOR = 1e-6;
6
+ /**
7
+ * Synthetic demo only: seeded `SampleData` clusters and a toy classifier fitted to them. Not a real model or measurement.
8
+ * The classifier is Gaussian naive Bayes (Hastie, Tibshirani & Friedman, ESL §6.6.3): per-class axis-aligned normals, priors from class counts.
9
+ */
10
+ var DecisionSamples = class {
11
+ /** Fits Gaussian naive Bayes on two numeric columns. */
12
+ static naiveBayes(rows, x, y, label) {
13
+ const classes = [...group(rows, (row) => String(row[label]))].map(([name, members]) => {
14
+ const fit = (column) => {
15
+ const values = members.map((row) => Number(row[column]));
16
+ return {
17
+ mean: mean(values) ?? 0,
18
+ variance: Math.max(FLOOR, variance(values) ?? FLOOR)
19
+ };
20
+ };
21
+ return {
22
+ name,
23
+ prior: Math.log(members.length / rows.length),
24
+ x: fit(x),
25
+ y: fit(y)
26
+ };
27
+ });
28
+ const logNormal = (value, fit) => -.5 * (Math.log(2 * Math.PI * fit.variance) + (value - fit.mean) ** 2 / fit.variance);
29
+ return (vx, vy) => {
30
+ const scores = classes.map((c) => c.prior + logNormal(vx, c.x) + logNormal(vy, c.y));
31
+ const top = Math.max(...scores);
32
+ return Object.fromEntries(classes.map((c, k) => [c.name, Math.exp(scores[k] - top)]));
33
+ };
34
+ }
35
+ /** Three overlapping clusters with that classifier's probabilities evaluated onto a grid. */
36
+ static clusters(seed) {
37
+ const points = SampleData.points(seed, 150, 3);
38
+ const predict = this.naiveBayes(points, "x", "y", "group");
39
+ const sweep = (column) => {
40
+ const [low = 0, high = 1] = extent(points, (row) => Number(row[column])), pad = (high - low) * .05;
41
+ return range(GRID).map((i) => low - pad + (high - low + 2 * pad) * i / 60);
42
+ };
43
+ const x = sweep("x"), y = sweep("y");
44
+ const classes = [...new Set(points.map((row) => String(row.group)))];
45
+ return {
46
+ points,
47
+ grid: {
48
+ x,
49
+ y,
50
+ classes,
51
+ probabilities: y.map((vy) => x.map((vx) => {
52
+ const scores = predict(vx, vy);
53
+ const total = classes.reduce((sum, name) => sum + scores[name], 0);
54
+ return classes.map((name) => scores[name] / total);
55
+ }))
56
+ }
57
+ };
58
+ }
59
+ };
60
+ export { DecisionSamples };
@@ -0,0 +1,130 @@
1
+ import { ascending, bisectRight, range } from "d3";
2
+ // src/charts/mlcharts/DecisionSurface.ts
3
+ var answer = (value) => typeof value === "object" ? Object.entries(value ?? {}) : value === void 0 ? [] : [[String(value), 1]];
4
+ /** Ranks scores. Anything but finite non-negative numbers with a positive total means no call. */
5
+ var rank = (scores) => {
6
+ if (!scores.length || scores.some(([, p]) => !Number.isFinite(p) || p < 0)) return null;
7
+ const total = scores.reduce((sum, [, p]) => sum + p, 0);
8
+ if (!(total > 0)) return null;
9
+ const [first, second] = [...scores].sort((a, b) => b[1] - a[1]);
10
+ return {
11
+ label: first[0],
12
+ probability: first[1] / total,
13
+ margin: (first[1] - (second?.[1] ?? 0)) / total
14
+ };
15
+ };
16
+ /** Class probabilities on a rectilinear grid, in ascending x and y. Where a class leads is its decision region. */
17
+ var DecisionSurface = class DecisionSurface {
18
+ x;
19
+ y;
20
+ classes;
21
+ cells;
22
+ exact;
23
+ columns;
24
+ rows;
25
+ /** `cells[(i + j * columns) * classes.length + k]` holds a probability, NaN over a gap. */
26
+ constructor(x, y, classes, cells, exact) {
27
+ this.x = x;
28
+ this.y = y;
29
+ this.classes = classes;
30
+ this.cells = cells;
31
+ this.exact = exact;
32
+ this.columns = x.length;
33
+ this.rows = y.length;
34
+ }
35
+ /** A grid that can be read at all: two or more distinct finite coordinates per axis, any order, and at least one class. */
36
+ static fromGrid({ x, y, classes, probabilities }) {
37
+ const across = DecisionSurface.axis(x), down = DecisionSurface.axis(y), labels = (classes ?? []).map(String);
38
+ if (!across || !down || !labels.length) return null;
39
+ const width = labels.length, cells = new Float64Array(across.length * down.length * width).fill(NaN);
40
+ down.forEach((j, row) => across.forEach((i, column) => {
41
+ const source = probabilities?.[j]?.[i];
42
+ if (source?.length !== width) return;
43
+ DecisionSurface.write(cells, (column + row * across.length) * width, source.map((p, k) => [labels[k], p ?? NaN]));
44
+ }));
45
+ return new DecisionSurface(across.map((i) => x[i]), down.map((j) => y[j]), labels, cells);
46
+ }
47
+ /** Sweeps `predict` over a `resolution × resolution` grid. Points are then scored by `predict` itself, not read off the grid. */
48
+ static fromPredict(predict, xDomain, yDomain, resolution, known = []) {
49
+ const size = Math.max(2, Math.min(400, Math.round(resolution) || 2));
50
+ const steps = ([low, high]) => range(size).map((i) => low + (high - low) * i / (size - 1));
51
+ const x = steps(xDomain), y = steps(yDomain);
52
+ const seen = new Set(known);
53
+ const answers = y.flatMap((vy) => x.map((vx) => answer(predict(vx, vy))));
54
+ for (const scores of answers) for (const [label] of scores) seen.add(label);
55
+ const labels = [...seen], cells = new Float64Array(size * size * labels.length).fill(NaN);
56
+ answers.forEach((scores, cell) => {
57
+ const given = new Map(scores);
58
+ DecisionSurface.write(cells, cell * labels.length, labels.map((label) => [label, given.get(label) ?? 0]));
59
+ });
60
+ return new DecisionSurface(x, y, labels, cells, predict);
61
+ }
62
+ /** Indexes that sort a coordinate array, or null when it cannot be a grid axis. */
63
+ static axis(values) {
64
+ if (!values || values.length < 2 || !values.every(Number.isFinite)) return null;
65
+ const order = range(values.length).sort((a, b) => ascending(values[a], values[b]));
66
+ return order.every((index, i) => !i || values[index] > values[order[i - 1]]) ? order : null;
67
+ }
68
+ static write(cells, offset, scores) {
69
+ if (!rank(scores)) return;
70
+ const total = scores.reduce((sum, [, p]) => sum + p, 0);
71
+ scores.forEach(([, p], k) => {
72
+ cells[offset + k] = p / total;
73
+ });
74
+ }
75
+ /** `p(class) − max p(other)` per node: its zero contour is the edge of that class's region. NaN over gaps. */
76
+ margin(index) {
77
+ const { classes: { length: width }, cells } = this;
78
+ return Array.from({ length: this.columns * this.rows }, (_, node) => {
79
+ let rival = Number.NEGATIVE_INFINITY;
80
+ for (let k = 0; k < width; k++) if (k !== index) rival = Math.max(rival, cells[node * width + k]);
81
+ const own = cells[node * width + index];
82
+ return width > 1 ? own - rival : own;
83
+ });
84
+ }
85
+ /** Index of the leading class per node, -1 over gaps. */
86
+ leaders() {
87
+ const { classes: { length: width }, cells } = this;
88
+ return Int32Array.from({ length: this.columns * this.rows }, (_, node) => {
89
+ if (Number.isNaN(cells[node * width])) return -1;
90
+ let best = 0;
91
+ for (let k = 1; k < width; k++) if (cells[node * width + k] > cells[node * width + best]) best = k;
92
+ return best;
93
+ });
94
+ }
95
+ /** The model's call at a location. A grid is read bilinearly and makes no call outside its extent or next to a gap. */
96
+ classify(x, y) {
97
+ if (this.exact) return rank(answer(this.exact(x, y)));
98
+ const { x: xs, y: ys, classes, columns, cells } = this, width = classes.length;
99
+ if (x < xs[0] || x > xs.at(-1) || y < ys[0] || y > ys.at(-1)) return null;
100
+ const i = Math.min(columns - 2, bisectRight(xs, x) - 1), j = Math.min(this.rows - 2, bisectRight(ys, y) - 1);
101
+ const u = (x - xs[i]) / (xs[i + 1] - xs[i]), v = (y - ys[j]) / (ys[j + 1] - ys[j]);
102
+ const at = (a, b, k) => cells[(a + b * columns) * width + k];
103
+ return rank(classes.map((label, k) => [label, (at(i, j, k) * (1 - u) + at(i + 1, j, k) * u) * (1 - v) + (at(i, j + 1, k) * (1 - u) + at(i + 1, j + 1, k) * u) * v]));
104
+ }
105
+ /** One crisp path per class with a node in it, each node owning the area nearest it. Edges land on whole pixels so tiles leave no seams. */
106
+ tiles(sx, sy) {
107
+ const { columns, rows } = this, leaders = this.leaders();
108
+ const edges = (values, scale) => values.map((value, i) => Math.round(scale(i ? (values[i - 1] + value) / 2 : value))).concat(Math.round(scale(values.at(-1))));
109
+ const across = edges(this.x, sx), down = edges(this.y, sy);
110
+ const runs = /* @__PURE__ */ new Map();
111
+ for (let j = 0; j < rows; j++) for (let i = 0, end; i < columns; i = end) {
112
+ const leader = leaders[i + j * columns];
113
+ end = i + 1;
114
+ while (end < columns && leaders[end + j * columns] === leader) end++;
115
+ if (leader < 0) continue;
116
+ if (!runs.has(leader)) runs.set(leader, []);
117
+ runs.get(leader).push(`M${across[i]} ${down[j]}H${across[end]}V${down[j + 1]}H${across[i]}Z`);
118
+ }
119
+ return [...runs].sort(([a], [b]) => a - b).map(([index, list]) => ({
120
+ index,
121
+ d: list.join("")
122
+ }));
123
+ }
124
+ /** Share of scored nodes won by a class. */
125
+ share(index) {
126
+ const leaders = this.leaders(), scored = leaders.filter((leader) => leader >= 0).length;
127
+ return scored ? leaders.filter((leader) => leader === index).length / scored : 0;
128
+ }
129
+ };
130
+ export { DecisionSurface };
@@ -0,0 +1,13 @@
1
+ // src/charts/mlcharts/DistinctTicks.ts
2
+ var COUNT = 10;
3
+ /** The axis format from core rounds to whole numbers from 10 up, so a narrow range would repeat a label. */
4
+ var DistinctTicks = class {
5
+ /** Keeps `format` unless it gives two ticks one label; then uses the precision of the scale's own tick step. */
6
+ static format(scale, format) {
7
+ if (!format || !("ticks" in scale) || !("tickFormat" in scale)) return format;
8
+ const fine = scale;
9
+ const labels = fine.ticks(COUNT).map(format);
10
+ return new Set(labels).size < labels.length ? (value) => fine.tickFormat(COUNT)(+value) : format;
11
+ }
12
+ };
13
+ export { DistinctTicks };
@@ -0,0 +1,153 @@
1
+ import { ElbowMethod } from "./curveDiagnosticEnums.js";
2
+ // src/charts/mlcharts/ElbowMath.ts
3
+ var TINY = 1e-12;
4
+ var NONE = {
5
+ index: null,
6
+ trace: [],
7
+ segments: []
8
+ };
9
+ /** Elbow detectors for a score-by-k curve, in either direction. Points must be sorted by k with distinct k. */
10
+ var ElbowMath = class {
11
+ /** Unit-square form with x rising and the curve bending like a concave rise. `null` if x or y is constant. */
12
+ static unit(points) {
13
+ if (points.length < 3) return null;
14
+ const falling = points.at(-1).score <= points[0].score;
15
+ const scores = points.map((point) => falling ? -point.score : point.score);
16
+ const low = Math.min(...scores), high = Math.max(...scores), first = points[0].k, last = points.at(-1).k;
17
+ if (high - low < TINY || last - first < TINY) return null;
18
+ return {
19
+ x: points.map((point) => (point.k - first) / (last - first)),
20
+ y: scores.map((score) => (score - low) / (high - low))
21
+ };
22
+ }
23
+ /**
24
+ * Kneedle (Satopää, Albrecht, Irwin and Raghavan 2011), offline form without the smoothing step. Local maxima of the
25
+ * difference curve are candidates; the first whose difference drops below `d - S / (n - 1)` before the next maximum is the knee.
26
+ */
27
+ static kneedle(points, sensitivity = 1) {
28
+ const unit = this.unit(points);
29
+ if (!unit) return NONE;
30
+ const { x, y } = unit, n = x.length;
31
+ const trace = y.map((value, i) => value - x[i]);
32
+ const maxima = [];
33
+ for (let i = 1; i < n - 1; i++) if (trace[i] > TINY && trace[i] > trace[i - 1] && trace[i] >= trace[i + 1]) maxima.push(i);
34
+ const step = 1 / (n - 1);
35
+ for (const [slot, candidate] of maxima.entries()) {
36
+ const threshold = trace[candidate] - sensitivity * step;
37
+ for (let j = candidate + 1; j < (maxima[slot + 1] ?? n); j++) if (trace[j] < threshold) return {
38
+ index: candidate,
39
+ trace,
40
+ segments: []
41
+ };
42
+ }
43
+ return {
44
+ index: null,
45
+ trace,
46
+ segments: []
47
+ };
48
+ }
49
+ /**
50
+ * L method (Salvador and Chan 2004): two least-squares lines, one per side of each split c, keeping the split with the
51
+ * lowest size-weighted RMSE. Each side has at least two points, and the elbow is c, the last point of the left line.
52
+ * Only splits where the curve flattens count, so a straight line has no elbow. The paper's iterative cutoff is not applied.
53
+ */
54
+ static lMethod(points) {
55
+ const n = points.length;
56
+ if (n < 4 || !this.unit(points)) return NONE;
57
+ const sign = points.at(-1).score <= points[0].score ? -1 : 1;
58
+ let top = null;
59
+ for (let c = 1; c <= n - 3; c++) {
60
+ const left = this.#fit(points.slice(0, c + 1)), right = this.#fit(points.slice(c + 1));
61
+ const error = ((c + 1) * left.rmse + (n - c - 1) * right.rmse) / n;
62
+ if (sign * (left.slope - right.slope) > 1e-9 * (Math.abs(left.slope) + Math.abs(right.slope)) && (!top || error < top.error)) top = {
63
+ index: c,
64
+ error,
65
+ left: left.segment,
66
+ right: right.segment
67
+ };
68
+ }
69
+ return top ? {
70
+ index: top.index,
71
+ trace: [],
72
+ segments: [top.left, top.right]
73
+ } : NONE;
74
+ }
75
+ static #fit(points) {
76
+ const n = points.length, meanX = points.reduce((a, p) => a + p.k, 0) / n, meanY = points.reduce((a, p) => a + p.score, 0) / n;
77
+ const spread = points.reduce((a, p) => a + (p.k - meanX) ** 2, 0);
78
+ const slope = points.reduce((a, p) => a + (p.k - meanX) * (p.score - meanY), 0) / spread;
79
+ const at = (k) => meanY + slope * (k - meanX);
80
+ const first = points[0].k, last = points.at(-1).k;
81
+ return {
82
+ slope,
83
+ rmse: Math.sqrt(points.reduce((a, p) => a + (p.score - at(p.k)) ** 2, 0) / n),
84
+ segment: {
85
+ x1: first,
86
+ y1: at(first),
87
+ x2: last,
88
+ y2: at(last)
89
+ }
90
+ };
91
+ }
92
+ /** Point furthest above the chord from first to last in the unit square: the triangle method of Rosin (2001). */
93
+ static chord(points) {
94
+ const unit = this.unit(points);
95
+ if (!unit) return NONE;
96
+ const { x, y } = unit, rise = y.at(-1) - y[0], length = Math.hypot(1, rise);
97
+ const trace = y.map((value, i) => (value - y[0] - rise * x[i]) / length);
98
+ const index = this.#peak(trace), first = points[0], last = points.at(-1);
99
+ const chord = {
100
+ x1: first.k,
101
+ y1: first.score,
102
+ x2: last.k,
103
+ y2: last.score
104
+ };
105
+ if (index === null) return {
106
+ index,
107
+ trace,
108
+ segments: [chord]
109
+ };
110
+ const { k, score } = points[index];
111
+ return {
112
+ index,
113
+ trace,
114
+ segments: [chord, {
115
+ x1: k,
116
+ y1: score,
117
+ x2: k,
118
+ y2: first.score + (last.score - first.score) * (k - first.k) / (last.k - first.k)
119
+ }]
120
+ };
121
+ }
122
+ /** Sharpest bend: the largest second divided difference of the unit curve, which handles uneven k. Ends have none. */
123
+ static secondDifference(points) {
124
+ const unit = this.unit(points);
125
+ if (!unit) return NONE;
126
+ const { x, y } = unit;
127
+ const trace = y.map((value, i) => {
128
+ if (i === 0 || i === y.length - 1) return null;
129
+ return 2 * ((value - y[i - 1]) / (x[i] - x[i - 1]) - (y[i + 1] - value) / (x[i + 1] - x[i])) / (x[i + 1] - x[i - 1]);
130
+ });
131
+ return {
132
+ index: this.#peak(trace),
133
+ trace,
134
+ segments: []
135
+ };
136
+ }
137
+ static #peak(values) {
138
+ let top = null;
139
+ values.forEach((value, i) => {
140
+ if (value !== null && value > TINY && (top === null || value > values[top])) top = i;
141
+ });
142
+ return top;
143
+ }
144
+ static fit(points, method, sensitivity = 1) {
145
+ switch (method) {
146
+ case ElbowMethod.LMethod: return this.lMethod(points);
147
+ case ElbowMethod.Chord: return this.chord(points);
148
+ case ElbowMethod.SecondDifference: return this.secondDifference(points);
149
+ default: return this.kneedle(points, sensitivity);
150
+ }
151
+ }
152
+ };
153
+ export { ElbowMath };