@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.
- package/README.md +111 -14
- package/dist/ExcelWriter-3ZSYUH4T.cjs +154 -0
- package/dist/GraphType.js +24 -38
- package/dist/PdfWriter-6HJFYMCO.cjs +205 -0
- package/dist/catalog-YWX6FHRM.cjs +8 -0
- package/dist/catalog.js +5 -1
- package/dist/charts/bars/index.js +2 -12
- package/dist/charts/comparison/EffectForestPlot.js +1 -1
- package/dist/charts/comparison/index.js +1 -8
- package/dist/charts/demography/Nomogram.js +3 -3
- package/dist/charts/density/HexbinChart.js +6 -4
- package/dist/charts/diagrams/ArchitectureCanvas.js +68 -0
- package/dist/charts/diagrams/ArchitectureDiagram.js +331 -0
- package/dist/charts/diagrams/ArchitectureLayout.js +106 -0
- package/dist/charts/diagrams/ArchitectureLinkLabels.js +78 -0
- package/dist/charts/diagrams/ArchitectureModel.js +108 -0
- package/dist/charts/diagrams/ArchitectureRouter.js +348 -0
- package/dist/charts/diagrams/ArchitectureSamples.js +312 -0
- package/dist/charts/diagrams/ArchitectureShapes.js +130 -0
- package/dist/charts/diagrams/ArchitectureText.js +25 -0
- package/dist/charts/diagrams/DiagramText.js +38 -0
- package/dist/charts/diagrams/EntityRelationshipDiagram.js +465 -0
- package/dist/charts/diagrams/ErCrowsFoot.js +33 -0
- package/dist/charts/diagrams/ErSamples.js +205 -0
- package/dist/charts/diagrams/ErSchema.js +122 -0
- package/dist/charts/diagrams/LogicCircuit.js +154 -0
- package/dist/charts/diagrams/LogicCircuitLayout.js +600 -0
- package/dist/charts/diagrams/LogicCircuitSamples.js +66 -0
- package/dist/charts/diagrams/LogicGateDiagram.js +216 -0
- package/dist/charts/diagrams/LogicGateSymbol.js +113 -0
- package/dist/charts/diagrams/NetworkTopologyDiagram.js +247 -0
- package/dist/charts/diagrams/SequenceDiagram.js +351 -0
- package/dist/charts/diagrams/SequenceLayout.js +422 -0
- package/dist/charts/diagrams/SequenceModel.js +221 -0
- package/dist/charts/diagrams/TopologyLayout.js +143 -0
- package/dist/charts/diagrams/architectureEnums.js +50 -0
- package/dist/charts/diagrams/index.js +26 -0
- package/dist/charts/diagrams/logicEnums.js +42 -0
- package/dist/charts/diagrams/sequenceEnums.js +40 -0
- package/dist/charts/distribution/HistogramChart.js +25 -3
- package/dist/charts/distribution/index.js +1 -3
- package/dist/charts/earthscience/HovmollerDiagram.js +477 -0
- package/dist/charts/earthscience/HovmollerEnums.js +42 -0
- package/dist/charts/earthscience/HovmollerField.js +180 -0
- package/dist/charts/earthscience/HovmollerSample.js +40 -0
- package/dist/charts/earthscience/HovmollerSpeed.js +71 -0
- package/dist/charts/earthscience/index.js +10 -2
- package/dist/charts/engineering/ConstellationDiagram.js +26 -6
- package/dist/charts/engineering/EyeDiagram.js +2 -1
- package/dist/charts/engineering/PoleZeroPlot.js +32 -8
- package/dist/charts/financial/RelativeStrengthRotation.js +434 -0
- package/dist/charts/financial/RelativeStrengthRotationMath.js +71 -0
- package/dist/charts/financial/RelativeStrengthRotationSamples.js +27 -0
- package/dist/charts/financial/index.js +5 -3
- package/dist/charts/flow/index.js +1 -5
- package/dist/charts/frameworks/AnsoffMatrix.js +523 -0
- package/dist/charts/frameworks/FrameworkText.js +98 -0
- package/dist/charts/frameworks/NpsGauge.js +279 -0
- package/dist/charts/frameworks/NpsSurvey.js +58 -0
- package/dist/charts/frameworks/PerceptualMap.js +419 -0
- package/dist/charts/frameworks/PerceptualProjection.js +142 -0
- package/dist/charts/frameworks/PositioningSamples.js +191 -0
- package/dist/charts/frameworks/RaciMatrix.js +458 -0
- package/dist/charts/frameworks/RaciPlan.js +81 -0
- package/dist/charts/frameworks/RaciSamples.js +76 -0
- package/dist/charts/frameworks/SwotMatrix.js +358 -0
- package/dist/charts/frameworks/SwotSamples.js +85 -0
- package/dist/charts/frameworks/VanWestendorpChart.js +187 -0
- package/dist/charts/frameworks/VanWestendorpSurvey.js +197 -0
- package/dist/charts/frameworks/index.js +26 -0
- package/dist/charts/frameworks/raciEnums.js +30 -0
- package/dist/charts/frameworks/swotEnums.js +34 -0
- package/dist/charts/genomics/CircosPlot.js +1 -1
- package/dist/charts/geoflows/GeoFlowChart.js +2 -2
- package/dist/charts/geoflows/GeoFlowSample.js +2 -1
- package/dist/charts/geomaps/FloorPlanMap.js +324 -0
- package/dist/charts/geomaps/FloorPlanModel.js +179 -0
- package/dist/charts/geomaps/FloorPlanSamples.js +416 -0
- package/dist/charts/geomaps/GeoLand.js +2 -2
- package/dist/charts/geomaps/GeoSamples.js +2 -1
- package/dist/charts/geomaps/GraticuleMap.js +2 -2
- package/dist/charts/geomaps/RoomLabelFit.js +200 -0
- package/dist/charts/geomaps/TissotMap.js +2 -2
- package/dist/charts/geomaps/index.js +8 -4
- package/dist/charts/hierarchy/SunburstChart.js +17 -10
- package/dist/charts/hierarchy/index.js +2 -11
- package/dist/charts/indicators/BulletChart.js +8 -2
- package/dist/charts/indicators/SpeedometerChart.js +1 -1
- package/dist/charts/indicators/ThermometerChart.js +1 -1
- package/dist/charts/lifescience/LoessFit.js +113 -0
- package/dist/charts/lifescience/MaPlot.js +337 -0
- package/dist/charts/lifescience/MaPlotSamples.js +35 -0
- package/dist/charts/lifescience/OncoGlyph.js +137 -0
- package/dist/charts/lifescience/OncoPrint.js +373 -0
- package/dist/charts/lifescience/OncoPrintMatrix.js +107 -0
- package/dist/charts/lifescience/OncoPrintSamples.js +142 -0
- package/dist/charts/lifescience/PedigreeChart.js +306 -0
- package/dist/charts/lifescience/PedigreeLayout.js +261 -0
- package/dist/charts/lifescience/PedigreeRows.js +70 -0
- package/dist/charts/lifescience/PedigreeSamples.js +161 -0
- package/dist/charts/lifescience/PedigreeSymbol.js +47 -0
- package/dist/charts/lifescience/RamachandranPlot.js +364 -0
- package/dist/charts/lifescience/RamachandranRegions.js +2712 -0
- package/dist/charts/lifescience/RamachandranSamples.js +235 -0
- package/dist/charts/lifescience/index.js +25 -0
- package/dist/charts/lifescience/pedigreeEnums.js +21 -0
- package/dist/charts/lifescience/ramachandranEnums.js +34 -0
- package/dist/charts/linesareas/DualAxisChart.js +1 -1
- package/dist/charts/linesareas/LineEnums.js +1 -15
- package/dist/charts/linesareas/MultiLineChart.js +1 -1
- package/dist/charts/linesareas/TrendChart.js +15 -5
- package/dist/charts/linesareas/index.js +2 -6
- package/dist/charts/massive/MassiveChart.js +3 -2
- package/dist/charts/mathematical/FourierSeriesChart.js +7 -1
- package/dist/charts/mathematical/index.js +2 -6
- package/dist/charts/matrix/GridPlot.js +27 -2
- package/dist/charts/matrix/TilegramChart.js +2 -2
- package/dist/charts/mlcharts/AttentionGrid.js +79 -0
- package/dist/charts/mlcharts/AttentionHeatmap.js +329 -0
- package/dist/charts/mlcharts/AttentionModel.js +132 -0
- package/dist/charts/mlcharts/AttentionSamples.js +82 -0
- package/dist/charts/mlcharts/CellNumber.js +8 -0
- package/dist/charts/mlcharts/CurveDiagnosticSamples.js +95 -0
- package/dist/charts/mlcharts/CurvePickMark.js +31 -0
- package/dist/charts/mlcharts/DecisionBoundaryChart.js +316 -0
- package/dist/charts/mlcharts/DecisionSamples.js +60 -0
- package/dist/charts/mlcharts/DecisionSurface.js +130 -0
- package/dist/charts/mlcharts/DistinctTicks.js +13 -0
- package/dist/charts/mlcharts/ElbowMath.js +153 -0
- package/dist/charts/mlcharts/ElbowPlot.js +273 -0
- package/dist/charts/mlcharts/ExplainabilitySamples.js +70 -0
- package/dist/charts/mlcharts/FeatureImportanceChart.js +150 -0
- package/dist/charts/mlcharts/ImportanceRanking.js +98 -0
- package/dist/charts/mlcharts/LearningCurveChart.js +276 -0
- package/dist/charts/mlcharts/LearningCurveMath.js +92 -0
- package/dist/charts/mlcharts/PartialDependence.js +92 -0
- package/dist/charts/mlcharts/PartialDependencePlot.js +225 -0
- package/dist/charts/mlcharts/WideLabel.js +28 -0
- package/dist/charts/mlcharts/curveDiagnosticEnums.js +37 -0
- package/dist/charts/mlcharts/index.js +32 -0
- package/dist/charts/proportion/index.js +5 -9
- package/dist/charts/scatter/CategoryScatterBase.js +1 -4
- package/dist/charts/scatter/ConnectedScatter.js +7 -4
- package/dist/charts/scatter/MarginalScatter.js +13 -8
- package/dist/charts/scatter/index.js +4 -17
- package/dist/charts/scientific/SpectrogramChart.js +9 -3
- package/dist/charts/scientific/index.js +2 -2
- package/dist/charts/signals/index.js +3 -13
- package/dist/charts/statistics/LorenzCurve.js +7 -4
- package/dist/charts/threebars/Candlestick3DChart.js +18 -2
- package/dist/charts/threeglobe/GlobeChart.js +2 -2
- package/dist/charts/threeglobe/GlobeSamples.js +2 -1
- package/dist/charts/threeglobe/RingGlobe.js +1 -1
- package/dist/charts/threenetwork/ConeTreeChart.js +2 -1
- package/dist/charts/threenetwork/Gantt3DChart.js +3 -2
- package/dist/charts/threesurfaces/Ridgeline3DChart.js +22 -4
- package/dist/charts/threevolume/LorenzAttractor.js +19 -0
- package/dist/charts/threevolume/VolumeRaycastChart.js +4 -3
- package/dist/charts/threevolume/volumeEnums.js +1 -0
- package/dist/charts/timeline/index.js +2 -10
- package/dist/chunk-753UBLV2.cjs +33 -0
- package/dist/chunk-VX54A7KA.cjs +17 -0
- package/dist/{chunk-W5GAAAZY.cjs → chunk-WAODDPYX.cjs} +24874 -17620
- package/dist/chunk-Z3JPIJSC.cjs +338 -0
- package/dist/core/CartesianChart.js +87 -11
- package/dist/core/Chart.js +63 -15
- package/dist/core/Chart2D.js +54 -17
- package/dist/core/Chart3D.js +56 -20
- package/dist/core/ChartExportError.js +2 -0
- package/dist/core/ChartExporter.js +50 -8
- package/dist/core/ColumnCheck.js +53 -0
- package/dist/core/Context2D.js +29 -15
- package/dist/core/Context3D.js +7 -2
- package/dist/core/ExcelWriter.js +152 -0
- package/dist/core/ExportText.js +28 -0
- package/dist/core/MarkOrigin.js +88 -0
- package/dist/core/MarkStyle.js +294 -0
- package/dist/core/Palette.js +27 -7
- package/dist/core/PdfPage.js +13 -0
- package/dist/core/PdfWriter.js +245 -0
- package/dist/core/Scales.js +52 -2
- package/dist/core/SceneOverlay.js +4 -4
- package/dist/core/TextFit.js +37 -0
- package/dist/core/Theme.js +28 -1
- package/dist/core/enums.js +33 -1
- package/dist/core/pathMorph.js +15 -0
- package/dist/data/SampleData.js +1 -17
- package/dist/data/WorldSample.js +23 -0
- package/dist/index.cjs +810 -691
- package/dist/index.d.cts +8390 -2925
- package/dist/index.d.ts +8392 -2927
- package/dist/index.js +106 -56
- package/dist/react/ChartView.js +18 -8
- package/dist/react/GraphHandle.js +2 -0
- package/dist/react/components.js +74 -116
- package/package.json +13 -3
- package/dist/catalog-4UP2YFNR.cjs +0 -7
- package/dist/charts/bars/BulletBar.js +0 -184
- package/dist/charts/bars/ErrorBarColumn.js +0 -147
- package/dist/charts/bars/HistogramBar.js +0 -217
- package/dist/charts/bars/MarimekkoBar.js +0 -191
- package/dist/charts/bars/ParetoBar.js +0 -143
- package/dist/charts/comparison/PanelChart.js +0 -70
- package/dist/charts/comparison/SmallMultipleBars.js +0 -134
- package/dist/charts/comparison/SpanChart.js +0 -118
- package/dist/charts/comparison/TrellisChart.js +0 -158
- package/dist/charts/distribution/RidgeSlabs3DChart.js +0 -169
- package/dist/charts/financial/Candlestick3D.js +0 -157
- package/dist/charts/flow/BumpFlow.js +0 -212
- package/dist/charts/flow/StreamFlow.js +0 -174
- package/dist/charts/geomaps/ThiessenMap.js +0 -106
- package/dist/charts/hierarchy/ConeTree3D.js +0 -135
- package/dist/charts/hierarchy/Hierarchy3DChart.js +0 -27
- package/dist/charts/hierarchy/SpherePack3D.js +0 -81
- package/dist/charts/hierarchy/SunburstTower3D.js +0 -93
- package/dist/charts/hierarchy/TreemapSkyline3D.js +0 -98
- package/dist/charts/linesareas/ConnectedScatterTime.js +0 -206
- package/dist/charts/linesareas/SmallMultipleLines.js +0 -153
- package/dist/charts/mathematical/ParametricSurface3D.js +0 -234
- package/dist/charts/mathematical/StrangeAttractor3D.js +0 -251
- package/dist/charts/proportion/ConeFunnel.js +0 -126
- package/dist/charts/proportion/ExtrudedPieChart.js +0 -123
- package/dist/charts/scatter/BeeswarmScatter.js +0 -151
- package/dist/charts/scatter/BinnedScatterBase.js +0 -51
- package/dist/charts/scatter/ContourScatter.js +0 -116
- package/dist/charts/scatter/GridBinScatter.js +0 -98
- package/dist/charts/scatter/HexbinScatter.js +0 -74
- package/dist/charts/scatter/StripScatter.js +0 -107
- package/dist/charts/scatter/SunflowerScatter.js +0 -109
- package/dist/charts/signals/ConstellationChart.js +0 -153
- package/dist/charts/signals/CorrelogramChart.js +0 -171
- package/dist/charts/signals/EyeDiagramChart.js +0 -155
- package/dist/charts/signals/PoleZeroChart.js +0 -164
- package/dist/charts/signals/SpectrogramChart.js +0 -79
- package/dist/charts/timeline/CalendarSkylineChart.js +0 -128
- package/dist/charts/timeline/ScheduleBlocksChart.js +0 -119
- package/dist/charts/timeline/SpiralTimeline.js +0 -154
- package/dist/charts/timeline/StreamTimeline.js +0 -141
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import { CartesianChart } from "../../core/CartesianChart.js";
|
|
2
|
+
import { ChartFamily, DataShape, Dimension } from "../../core/enums.js";
|
|
3
|
+
import { ZoomAxis } from "../../core/ChartViewport.js";
|
|
4
|
+
import { TrendHover } from "../linesareas/TrendHover.js";
|
|
5
|
+
import { cellNumber } from "./CellNumber.js";
|
|
6
|
+
import { CurveDiagnosticSamples } from "./CurveDiagnosticSamples.js";
|
|
7
|
+
import { ElbowMethod, SilhouetteView } from "./curveDiagnosticEnums.js";
|
|
8
|
+
import { CurvePickMark } from "./CurvePickMark.js";
|
|
9
|
+
import { DistinctTicks } from "./DistinctTicks.js";
|
|
10
|
+
import { ElbowMath } from "./ElbowMath.js";
|
|
11
|
+
import { WideLabel } from "./WideLabel.js";
|
|
12
|
+
import { axisLeft, axisRight, extent, line, scaleLinear } from "d3";
|
|
13
|
+
// src/charts/mlcharts/ElbowPlot.ts
|
|
14
|
+
var PANEL_SHARE = .6;
|
|
15
|
+
var PANEL_GAP = 20;
|
|
16
|
+
var RIGHT = 62;
|
|
17
|
+
var LEGEND_INSET = 64;
|
|
18
|
+
var LEGEND_CHAR = 6.4;
|
|
19
|
+
/** Keeps end dots off the axes, in units of k. */
|
|
20
|
+
var K_PAD = .35;
|
|
21
|
+
/** Tallest a rescaled trace reaches, leaving the top for the elbow label. */
|
|
22
|
+
var TRACE_REACH = .85;
|
|
23
|
+
var METHOD_NAME = {
|
|
24
|
+
[ElbowMethod.Kneedle]: "Kneedle",
|
|
25
|
+
[ElbowMethod.LMethod]: "L method",
|
|
26
|
+
[ElbowMethod.Chord]: "Chord distance",
|
|
27
|
+
[ElbowMethod.SecondDifference]: "Second difference"
|
|
28
|
+
};
|
|
29
|
+
var GUIDE_NAME = {
|
|
30
|
+
[ElbowMethod.Kneedle]: "Difference curve (rescaled)",
|
|
31
|
+
[ElbowMethod.LMethod]: "Two-line fit",
|
|
32
|
+
[ElbowMethod.Chord]: "Chord and distance",
|
|
33
|
+
[ElbowMethod.SecondDifference]: "Second difference (rescaled)"
|
|
34
|
+
};
|
|
35
|
+
var NO_GUIDE = {
|
|
36
|
+
index: null,
|
|
37
|
+
trace: [],
|
|
38
|
+
segments: []
|
|
39
|
+
};
|
|
40
|
+
var average = (values) => values.length ? values.reduce((a, b) => a + b, 0) / values.length : null;
|
|
41
|
+
/**
|
|
42
|
+
* Chooses a cluster count from inertia or another score by k. The elbow comes from a named method: Kneedle (Satopää et al.
|
|
43
|
+
* 2011), the L method (Salvador and Chan 2004), the chord distance of Rosin (2001), or the largest second difference.
|
|
44
|
+
* The silhouette (Rousseeuw 1987) is an independent second opinion on k. A curve with no bend gets no elbow: the elbow
|
|
45
|
+
* is a heuristic, and an honest plot says so instead of always naming a k.
|
|
46
|
+
*/
|
|
47
|
+
var ElbowPlot = class extends CartesianChart {
|
|
48
|
+
static meta = {
|
|
49
|
+
id: "elbow-plot",
|
|
50
|
+
name: "Elbow plot",
|
|
51
|
+
family: ChartFamily.Statistical,
|
|
52
|
+
dimension: Dimension.Two,
|
|
53
|
+
shape: DataShape.Table,
|
|
54
|
+
description: "Plots inertia or another score by number of clusters, marks the elbow found by a named method, and can compare it with the silhouette.",
|
|
55
|
+
tags: [
|
|
56
|
+
"machine learning",
|
|
57
|
+
"k-means",
|
|
58
|
+
"clustering",
|
|
59
|
+
"kneedle",
|
|
60
|
+
"silhouette",
|
|
61
|
+
"model selection"
|
|
62
|
+
],
|
|
63
|
+
sample: (seed) => CurveDiagnosticSamples.clusterSweep(seed),
|
|
64
|
+
variants: {
|
|
65
|
+
method: Object.values(ElbowMethod),
|
|
66
|
+
silhouetteView: Object.values(SilhouetteView)
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
#points = [];
|
|
70
|
+
#fit = NO_GUIDE;
|
|
71
|
+
defaults() {
|
|
72
|
+
return {
|
|
73
|
+
x: "k",
|
|
74
|
+
y: "inertia",
|
|
75
|
+
silhouette: "silhouette",
|
|
76
|
+
method: ElbowMethod.Kneedle,
|
|
77
|
+
sensitivity: 1,
|
|
78
|
+
silhouetteView: SilhouetteView.None,
|
|
79
|
+
guide: true,
|
|
80
|
+
xLabel: "Clusters (k)",
|
|
81
|
+
yLabel: "Inertia"
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
read() {
|
|
85
|
+
const { data, x, y, silhouette } = this.options;
|
|
86
|
+
const rows = /* @__PURE__ */ new Map();
|
|
87
|
+
for (const row of Array.isArray(data) ? data : []) {
|
|
88
|
+
const k = cellNumber(row?.[x]), score = cellNumber(row?.[y]);
|
|
89
|
+
if (k === null || score === null) continue;
|
|
90
|
+
const entry = rows.get(k) ?? rows.set(k, {
|
|
91
|
+
score: [],
|
|
92
|
+
silhouette: []
|
|
93
|
+
}).get(k);
|
|
94
|
+
entry.score.push(score);
|
|
95
|
+
const width = cellNumber(row[silhouette]);
|
|
96
|
+
if (width !== null) entry.silhouette.push(width);
|
|
97
|
+
}
|
|
98
|
+
return [...rows].sort(([a], [b]) => a - b).map(([k, entry]) => ({
|
|
99
|
+
k,
|
|
100
|
+
score: average(entry.score),
|
|
101
|
+
silhouette: average(entry.silhouette)
|
|
102
|
+
}));
|
|
103
|
+
}
|
|
104
|
+
/** The chosen view, or `None` when the data has no silhouette column. */
|
|
105
|
+
get view() {
|
|
106
|
+
const known = this.options.silhouetteView;
|
|
107
|
+
const { data, silhouette } = this.options;
|
|
108
|
+
return Array.isArray(data) && data.some((row) => cellNumber(row?.[silhouette]) !== null) ? known : SilhouetteView.None;
|
|
109
|
+
}
|
|
110
|
+
zoomAxes() {
|
|
111
|
+
return this.view === SilhouetteView.None ? ZoomAxis.XY : ZoomAxis.X;
|
|
112
|
+
}
|
|
113
|
+
plotMargin() {
|
|
114
|
+
return {
|
|
115
|
+
...super.plotMargin(),
|
|
116
|
+
...this.view === SilhouetteView.SecondAxis ? { right: RIGHT } : {}
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
xTicks() {
|
|
120
|
+
const [lo = 0, hi = 1] = extent(this.#points, (point) => point.k);
|
|
121
|
+
return lo === hi ? 2 : Math.min(7, Math.ceil(hi - lo));
|
|
122
|
+
}
|
|
123
|
+
tickFormat(scale) {
|
|
124
|
+
return DistinctTicks.format(scale, super.tickFormat(scale));
|
|
125
|
+
}
|
|
126
|
+
yTicks() {
|
|
127
|
+
return this.view === SilhouetteView.Panel ? 4 : 6;
|
|
128
|
+
}
|
|
129
|
+
legendItems() {
|
|
130
|
+
const shown = this.options.guide && (this.#fit.segments.length || this.#fit.trace.length);
|
|
131
|
+
const room = this.size.width - LEGEND_INSET;
|
|
132
|
+
return [...super.legendItems().map((item) => ({
|
|
133
|
+
...item,
|
|
134
|
+
label: WideLabel.fit(item.key, room, LEGEND_CHAR)
|
|
135
|
+
})), ...shown ? [{
|
|
136
|
+
key: GUIDE_NAME[this.options.method],
|
|
137
|
+
color: this.theme.inkMuted
|
|
138
|
+
}] : []];
|
|
139
|
+
}
|
|
140
|
+
scales(ctx) {
|
|
141
|
+
const { width, height } = ctx.plot;
|
|
142
|
+
const points = this.#points = this.read();
|
|
143
|
+
const fit = this.#fit = ElbowMath.fit(points, this.options.method, this.options.sensitivity);
|
|
144
|
+
const bottom = this.view === SilhouetteView.Panel ? Math.round(height * PANEL_SHARE) : height;
|
|
145
|
+
const guide = this.options.guide ? fit.segments.flatMap((segment) => [segment.y1, segment.y2]) : [];
|
|
146
|
+
let [left, right] = extent(points, (point) => point.k), [lo, hi] = extent([...points.map((point) => point.score), ...guide]);
|
|
147
|
+
if (left === void 0 || right === void 0) [left, right] = [0, 1];
|
|
148
|
+
if (left === right) [left, right] = [left - 1, right + 1];
|
|
149
|
+
if (lo === void 0 || hi === void 0) [lo, hi] = [0, 1];
|
|
150
|
+
if (lo === hi) {
|
|
151
|
+
const pad = Math.abs(lo) * .1 || 1;
|
|
152
|
+
[lo, hi] = [lo - pad, hi + pad];
|
|
153
|
+
}
|
|
154
|
+
return {
|
|
155
|
+
x: scaleLinear([left - K_PAD, right + K_PAD], [0, width]),
|
|
156
|
+
y: scaleLinear([lo, hi], [bottom, 0]).nice(this.yTicks())
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
/** Silhouette scale, with headroom on a shared plot so the peak label stays clear of the elbow label. */
|
|
160
|
+
silhouetteScale(ctx, values) {
|
|
161
|
+
const { height } = ctx.plot, panel = this.view === SilhouetteView.Panel;
|
|
162
|
+
let [lo = 0, hi = 1] = extent(values);
|
|
163
|
+
if (lo === hi) {
|
|
164
|
+
lo -= .1;
|
|
165
|
+
hi += .1;
|
|
166
|
+
}
|
|
167
|
+
const head = panel ? 0 : (hi - lo) * .4;
|
|
168
|
+
const range = panel ? [height, Math.round(height * PANEL_SHARE) + PANEL_GAP] : [height, 0];
|
|
169
|
+
return scaleLinear([lo, hi + head], range).nice(panel ? 3 : 5);
|
|
170
|
+
}
|
|
171
|
+
styleAxis(ctx, axis) {
|
|
172
|
+
axis.select(".domain").attr("stroke", "none");
|
|
173
|
+
axis.selectAll("text").attr("fill", ctx.theme.inkSecondary).attr("font-size", 11).attr("font-family", ctx.theme.fontFamily);
|
|
174
|
+
}
|
|
175
|
+
plotMarks(ctx, axes) {
|
|
176
|
+
const { method, guide } = this.options;
|
|
177
|
+
const { width, height } = ctx.plot;
|
|
178
|
+
const points = this.#points, fit = this.#fit, view = this.view;
|
|
179
|
+
const px = (k) => axes.x(k) ?? 0, py = (value) => axes.y(value) ?? 0;
|
|
180
|
+
const name = this.options.yLabel || "Score", ink = ctx.palette.color(name);
|
|
181
|
+
this.drawGuide(ctx, guide ? fit : NO_GUIDE, points, axes);
|
|
182
|
+
const curve = line().x((d) => px(d.k)).y((d) => py(d.score));
|
|
183
|
+
const path = ctx.marks("curve", "path", points.length ? [points] : []).attr("fill", "none").attr("stroke", ink).attr("stroke-width", 2).attr("stroke-linejoin", "round").attr("stroke-linecap", "round");
|
|
184
|
+
ctx.set(path, { d: (d) => curve(d) ?? "" });
|
|
185
|
+
ctx.drawOn(path);
|
|
186
|
+
const dots = ctx.marks("dots", "circle", points, (d) => d.k).attr("fill", ink).attr("stroke", ctx.theme.surface).attr("stroke-width", 2);
|
|
187
|
+
ctx.set(dots, {
|
|
188
|
+
cx: (d) => px(d.k),
|
|
189
|
+
cy: (d) => py(d.score),
|
|
190
|
+
r: 5
|
|
191
|
+
}, { r: 0 }, 20);
|
|
192
|
+
const silhouette = view === SilhouetteView.None ? null : this.drawSilhouette(ctx, points, px);
|
|
193
|
+
const elbow = fit.index === null ? null : points[fit.index];
|
|
194
|
+
const pick = elbow && {
|
|
195
|
+
x: px(elbow.k),
|
|
196
|
+
y: py(elbow.score),
|
|
197
|
+
text: `Elbow k = ${TrendHover.title(elbow.k)} · ${METHOD_NAME[method]}`
|
|
198
|
+
};
|
|
199
|
+
CurvePickMark.draw(ctx, "elbow", pick, 12);
|
|
200
|
+
ctx.text(ctx.marks("no-elbow", "text", points.length && !elbow ? [`No elbow found by ${METHOD_NAME[method]}`] : []), 11, "muted").attr("text-anchor", "end").attr("x", width - 2).attr("y", 12).text((d) => d).attr("paint-order", "stroke").attr("stroke", ctx.theme.surface).attr("stroke-width", 3);
|
|
201
|
+
const slices = points.map((point) => ({
|
|
202
|
+
x: px(point.k),
|
|
203
|
+
title: `k = ${TrendHover.title(point.k)}`,
|
|
204
|
+
rows: [[name, TrendHover.value(point.score)], ...view === SilhouetteView.None ? [] : [["Silhouette", TrendHover.value(point.silhouette)]]],
|
|
205
|
+
marks: [{
|
|
206
|
+
x: px(point.k),
|
|
207
|
+
y: py(point.score),
|
|
208
|
+
color: ink
|
|
209
|
+
}, ...silhouette && point.silhouette !== null ? [{
|
|
210
|
+
x: px(point.k),
|
|
211
|
+
y: silhouette(point.silhouette),
|
|
212
|
+
color: ctx.palette.color("Silhouette")
|
|
213
|
+
}] : []]
|
|
214
|
+
}));
|
|
215
|
+
TrendHover.attach(ctx, slices, width, height);
|
|
216
|
+
}
|
|
217
|
+
/** Traces have their own units, so they are stretched to the value axis, which the legend says. */
|
|
218
|
+
drawGuide(ctx, fit, points, { x, y }) {
|
|
219
|
+
const px = (k) => x(k) ?? 0, py = (value) => y(value) ?? 0;
|
|
220
|
+
const dashed = (selection) => selection.attr("stroke", ctx.theme.inkMuted).attr("stroke-width", 1.5).attr("stroke-dasharray", "6 4");
|
|
221
|
+
const lines = dashed(ctx.marks("guide-lines", "line", fit.segments));
|
|
222
|
+
ctx.set(lines, {
|
|
223
|
+
x1: (d) => px(d.x1),
|
|
224
|
+
y1: (d) => py(d.y1),
|
|
225
|
+
x2: (d) => px(d.x2),
|
|
226
|
+
y2: (d) => py(d.y2)
|
|
227
|
+
});
|
|
228
|
+
const rows = fit.segments.length ? [] : fit.trace.map((value, i) => ({
|
|
229
|
+
k: points[i].k,
|
|
230
|
+
value
|
|
231
|
+
}));
|
|
232
|
+
const [low = 0, high = 1] = extent(rows, (d) => d.value);
|
|
233
|
+
const [floor = 0, ceiling = 1] = y.domain();
|
|
234
|
+
const lift = (value) => floor + (high > low ? (value - low) / (high - low) : 0) * TRACE_REACH * (ceiling - floor);
|
|
235
|
+
const trace = line().defined((d) => d.value !== null).x((d) => px(d.k)).y((d) => py(lift(d.value)));
|
|
236
|
+
const path = dashed(ctx.marks("guide-trace", "path", rows.length ? [rows] : [])).attr("fill", "none");
|
|
237
|
+
ctx.set(path, { d: (d) => trace(d) ?? "" });
|
|
238
|
+
}
|
|
239
|
+
/** Draws the silhouette on its own axis and returns that axis, for the hover dots. */
|
|
240
|
+
drawSilhouette(ctx, points, px) {
|
|
241
|
+
const { width, height } = ctx.plot, panel = this.view === SilhouetteView.Panel;
|
|
242
|
+
const known = points.filter((point) => point.silhouette !== null);
|
|
243
|
+
const scale = this.silhouetteScale(ctx, known.map((point) => point.silhouette));
|
|
244
|
+
const ink = ctx.palette.color("Silhouette");
|
|
245
|
+
const axis = (panel ? axisLeft(scale) : axisRight(scale)).ticks(5).tickSize(0).tickFormat((value) => (+value).toFixed(2));
|
|
246
|
+
if (panel && Math.abs(scale.range()[0] - scale.range()[1]) < 60) axis.tickValues(scale.domain());
|
|
247
|
+
this.styleAxis(ctx, ctx.group("axis-silhouette").attr("transform", panel ? "" : `translate(${width},0)`).call(axis));
|
|
248
|
+
const top = scale.range()[1];
|
|
249
|
+
const title = ctx.text(ctx.marks("silhouette-title", "text", ["Silhouette"]), 11, "secondary").text((d) => d);
|
|
250
|
+
if (panel) title.attr("text-anchor", "end").attr("transform", `translate(${width - 2},${top + 2})`);
|
|
251
|
+
else title.attr("text-anchor", "middle").attr("transform", `rotate(90) translate(${height / 2},${-width - 48})`);
|
|
252
|
+
const curve = line().x((d) => px(d.k)).y((d) => scale(d.silhouette));
|
|
253
|
+
const path = ctx.marks("silhouette", "path", known.length ? [known] : []).attr("fill", "none").attr("stroke", ink).attr("stroke-width", 2).attr("stroke-linejoin", "round").attr("stroke-linecap", "round");
|
|
254
|
+
ctx.set(path, { d: (d) => curve(d) ?? "" });
|
|
255
|
+
ctx.drawOn(path);
|
|
256
|
+
const dots = ctx.marks("silhouette-dots", "circle", known, (d) => d.k).attr("fill", ink).attr("stroke", ctx.theme.surface).attr("stroke-width", 2);
|
|
257
|
+
ctx.set(dots, {
|
|
258
|
+
cx: (d) => px(d.k),
|
|
259
|
+
cy: (d) => scale(d.silhouette),
|
|
260
|
+
r: 5
|
|
261
|
+
}, { r: 0 }, 20);
|
|
262
|
+
const peak = known.reduce((top, point) => !top || point.silhouette > top.silhouette ? point : top, null);
|
|
263
|
+
const text = peak && `Peak k = ${TrendHover.title(peak.k)}`;
|
|
264
|
+
const reach = (text?.length ?? 0) * 6.2 / 2;
|
|
265
|
+
const note = ctx.text(ctx.marks("silhouette-peak", "text", peak ? [peak] : []), 11, "ink").attr("text-anchor", "middle").attr("paint-order", "stroke").attr("stroke", ctx.theme.surface).attr("stroke-width", 3).text(text ?? "");
|
|
266
|
+
ctx.set(note, {
|
|
267
|
+
x: (d) => Math.max(reach, Math.min(width - reach, px(d.k))),
|
|
268
|
+
y: (d) => scale(d.silhouette) - 11
|
|
269
|
+
});
|
|
270
|
+
return scale;
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
export { ElbowPlot };
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { SampleData } from "../../data/SampleData.js";
|
|
2
|
+
// src/charts/mlcharts/ExplainabilitySamples.ts
|
|
3
|
+
/** Feature names and the sign of their synthetic coefficient, strongest first. */
|
|
4
|
+
var FEATURES = [
|
|
5
|
+
["debt_to_income", 1],
|
|
6
|
+
["annual_income", -1],
|
|
7
|
+
["credit_utilization", 1],
|
|
8
|
+
["months_since_last_delinquency", -1],
|
|
9
|
+
["loan_amount", 1],
|
|
10
|
+
["employment_length", -1],
|
|
11
|
+
["open_accounts", 1],
|
|
12
|
+
["recent_inquiries", 1],
|
|
13
|
+
["mortgage_accounts", -1],
|
|
14
|
+
["revolving_balance", 1],
|
|
15
|
+
["total_accounts", -1],
|
|
16
|
+
["installment", 1],
|
|
17
|
+
["home_ownership", -1],
|
|
18
|
+
["loan_term", 1],
|
|
19
|
+
["public_records", 1],
|
|
20
|
+
["savings_balance", -1],
|
|
21
|
+
["age", -1],
|
|
22
|
+
["region_index", 1],
|
|
23
|
+
["verified_income", -1],
|
|
24
|
+
["card_count", 1],
|
|
25
|
+
["autopay_enrolled", -1],
|
|
26
|
+
["joint_application", -1],
|
|
27
|
+
["purpose_code", 1],
|
|
28
|
+
["application_hour", 1]
|
|
29
|
+
];
|
|
30
|
+
var CURVES = 36;
|
|
31
|
+
var GRID = 28;
|
|
32
|
+
var RESPONSE_MIDPOINT = 50;
|
|
33
|
+
var RESPONSE_WIDTH = 14;
|
|
34
|
+
/** Synthetic model output for demos. Nothing here was measured or came from a fitted model. */
|
|
35
|
+
var ExplainabilitySamples = class {
|
|
36
|
+
static #normals(seed, count) {
|
|
37
|
+
const values = SampleData.distribution(seed, count, 1).map((row) => (row.value - 40) / 8);
|
|
38
|
+
let i = 0;
|
|
39
|
+
return () => values[i++ % values.length];
|
|
40
|
+
}
|
|
41
|
+
/** Synthetic standardised coefficients: `{ feature, importance, error }`, a power-law tail with noise. */
|
|
42
|
+
static importance(seed) {
|
|
43
|
+
const z = this.#normals(seed, FEATURES.length * 2);
|
|
44
|
+
return FEATURES.map(([feature, sign], rank) => {
|
|
45
|
+
const size = .9 / (rank + 1) ** .85 * (1 + z() * .08);
|
|
46
|
+
return {
|
|
47
|
+
feature,
|
|
48
|
+
importance: +(sign * size).toFixed(4),
|
|
49
|
+
error: +(size * .12 + .01 * Math.abs(z())).toFixed(4)
|
|
50
|
+
};
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
/** Synthetic ICE sweeps: `{ instance, x, prediction, observed }`, one saturating response per instance, whose slope flips for some. */
|
|
54
|
+
static iceCurves(seed) {
|
|
55
|
+
const z = this.#normals(seed, 72);
|
|
56
|
+
const observed = SampleData.points(seed, CURVES).map((row) => row.x);
|
|
57
|
+
const low = Math.min(...observed), high = Math.max(...observed);
|
|
58
|
+
const grid = Array.from({ length: GRID }, (_, i) => +(low + (high - low) * i / 27).toFixed(2));
|
|
59
|
+
return observed.flatMap((at, i) => {
|
|
60
|
+
const level = z(), slope = z();
|
|
61
|
+
return grid.map((x) => ({
|
|
62
|
+
instance: `row ${i + 1}`,
|
|
63
|
+
x,
|
|
64
|
+
observed: at,
|
|
65
|
+
prediction: +(.45 + .1 * level + (.2 + .22 * slope) * Math.tanh((x - RESPONSE_MIDPOINT) / RESPONSE_WIDTH)).toFixed(4)
|
|
66
|
+
}));
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
export { ExplainabilitySamples };
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { ChartFamily, DataShape, Dimension, Orientation } from "../../core/enums.js";
|
|
2
|
+
import { BandChart } from "../bars/BandChart.js";
|
|
3
|
+
import { ExplainabilitySamples } from "./ExplainabilitySamples.js";
|
|
4
|
+
import { FeatureImportanceOther, FeatureImportanceSign, ImportanceRanking } from "./ImportanceRanking.js";
|
|
5
|
+
import { WideLabel } from "./WideLabel.js";
|
|
6
|
+
import { format, max } from "d3";
|
|
7
|
+
// src/charts/mlcharts/FeatureImportanceChart.ts
|
|
8
|
+
var CAP = 4;
|
|
9
|
+
/** Widest band label, in px: 14 Latin characters. */
|
|
10
|
+
var LABEL_ROOM = 86.8;
|
|
11
|
+
var LABEL_LINE = 13;
|
|
12
|
+
var TICK_GAP = 12;
|
|
13
|
+
var TITLE_GAP = 18;
|
|
14
|
+
var figure = format(".3~g");
|
|
15
|
+
/**
|
|
16
|
+
* Ranked feature scores from a fitted model: impurity importance (Breiman 2001), permutation
|
|
17
|
+
* importance (Fisher et al. 2019), mean |SHAP| (Lundberg & Lee 2017) or coefficients, which need
|
|
18
|
+
* standardised inputs for their sizes to compare. Use `FeatureImportanceSign.Value` to rank
|
|
19
|
+
* permutation scores, where a negative is noise rather than an effect. A summed Other row follows `shap.plots.bar`; it is only meaningful where the scores add up.
|
|
20
|
+
*/
|
|
21
|
+
var FeatureImportanceChart = class extends BandChart {
|
|
22
|
+
static meta = {
|
|
23
|
+
id: "feature-importance",
|
|
24
|
+
name: "Feature importance",
|
|
25
|
+
family: ChartFamily.Statistical,
|
|
26
|
+
dimension: Dimension.Two,
|
|
27
|
+
shape: DataShape.Table,
|
|
28
|
+
description: "Ranks the features of a fitted model by importance or coefficient, with uncertainty whiskers and the tail folded into an Other row.",
|
|
29
|
+
tags: [
|
|
30
|
+
"machine learning",
|
|
31
|
+
"explainability",
|
|
32
|
+
"permutation importance",
|
|
33
|
+
"shap",
|
|
34
|
+
"coefficients"
|
|
35
|
+
],
|
|
36
|
+
sample: (seed) => ExplainabilitySamples.importance(seed),
|
|
37
|
+
variants: {
|
|
38
|
+
sign: Object.values(FeatureImportanceSign),
|
|
39
|
+
whiskers: [true, false],
|
|
40
|
+
other: Object.values(FeatureImportanceOther)
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
defaults() {
|
|
44
|
+
return {
|
|
45
|
+
x: "feature",
|
|
46
|
+
y: "importance",
|
|
47
|
+
error: "error",
|
|
48
|
+
low: "low",
|
|
49
|
+
high: "high",
|
|
50
|
+
orientation: Orientation.Horizontal,
|
|
51
|
+
sign: FeatureImportanceSign.Signed,
|
|
52
|
+
whiskers: true,
|
|
53
|
+
top: 10,
|
|
54
|
+
other: FeatureImportanceOther.Sum
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
get bars() {
|
|
58
|
+
const { data, x, y, error, low, high, sign, other, top } = this.options;
|
|
59
|
+
return ImportanceRanking.rank(Array.isArray(data) ? data : [], {
|
|
60
|
+
feature: x,
|
|
61
|
+
value: y,
|
|
62
|
+
error,
|
|
63
|
+
low,
|
|
64
|
+
high
|
|
65
|
+
}, {
|
|
66
|
+
sign,
|
|
67
|
+
other,
|
|
68
|
+
top
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
/** Scores that are all positive have no polarity to show, so they take one identity colour. */
|
|
72
|
+
get signColoured() {
|
|
73
|
+
return this.options.sign !== FeatureImportanceSign.Value && this.bars.some((bar) => bar.polarity < 0);
|
|
74
|
+
}
|
|
75
|
+
bandNames() {
|
|
76
|
+
return [...new Set(this.bars.map((bar) => bar.band))];
|
|
77
|
+
}
|
|
78
|
+
valueExtent() {
|
|
79
|
+
const { whiskers } = this.options;
|
|
80
|
+
const reach = this.bars.flatMap((bar) => [bar.value, ...whiskers ? [bar.low ?? 0, bar.high ?? 0] : []]);
|
|
81
|
+
return [Math.min(0, ...reach), Math.max(0, ...reach)];
|
|
82
|
+
}
|
|
83
|
+
/** Room for the widest label: core's gutter counts characters, which is too few for CJK. */
|
|
84
|
+
plotMargin() {
|
|
85
|
+
const base = super.plotMargin();
|
|
86
|
+
if (this.vertical) return base;
|
|
87
|
+
const widest = max(this.bandNames(), (name) => WideLabel.width(WideLabel.fit(name, LABEL_ROOM))) ?? 0;
|
|
88
|
+
return {
|
|
89
|
+
...base,
|
|
90
|
+
left: Math.max(base.left, Math.ceil(widest) + TICK_GAP + (this.options.yLabel ? TITLE_GAP : 0))
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
tickFormat(scale) {
|
|
94
|
+
const names = scale.domain?.() ?? [];
|
|
95
|
+
if (!this.vertical && typeof names[0] === "string") {
|
|
96
|
+
const every = Math.ceil(LABEL_LINE / Math.max(1, scale.step()));
|
|
97
|
+
return (name) => names.indexOf(name) % every ? "" : WideLabel.fit(String(name), LABEL_ROOM);
|
|
98
|
+
}
|
|
99
|
+
const base = super.tickFormat(scale);
|
|
100
|
+
if (!base || !("ticks" in scale)) return base;
|
|
101
|
+
const fine = scale;
|
|
102
|
+
return (max(fine.domain(), Math.abs) ?? 0) < 10 ? (value) => fine.tickFormat(this.xTicks())(+value) : base;
|
|
103
|
+
}
|
|
104
|
+
legendItems() {
|
|
105
|
+
if (!this.signColoured) return super.legendItems();
|
|
106
|
+
const present = new Set(this.bars.map((bar) => bar.polarity));
|
|
107
|
+
return [{
|
|
108
|
+
key: "Positive",
|
|
109
|
+
color: this.palette.diverging(1)
|
|
110
|
+
}, {
|
|
111
|
+
key: "Negative",
|
|
112
|
+
color: this.palette.diverging(-1)
|
|
113
|
+
}].filter((_, i) => present.has(i ? -1 : 1));
|
|
114
|
+
}
|
|
115
|
+
fill(ctx, bar) {
|
|
116
|
+
if (bar.folded && (!bar.polarity || !this.signColoured)) return ctx.theme.inkMuted;
|
|
117
|
+
return this.signColoured ? ctx.palette.diverging(bar.polarity) : ctx.palette.color(this.options.y);
|
|
118
|
+
}
|
|
119
|
+
drawBands(ctx, frame) {
|
|
120
|
+
const bars = this.bars;
|
|
121
|
+
const band = (d) => frame.band(d.band) ?? 0;
|
|
122
|
+
const shape = (d, to) => frame.bar(band(d), frame.thick, 0, to, 4);
|
|
123
|
+
const marks = ctx.marks("bars", "path", bars, (d) => d.key).attr("fill", (d) => this.fill(ctx, d));
|
|
124
|
+
ctx.set(marks, { d: (d) => shape(d, d.value) }, { d: (d) => shape(d, 0) }, 12);
|
|
125
|
+
ctx.hover(marks, (d) => ({
|
|
126
|
+
title: d.band,
|
|
127
|
+
color: this.fill(ctx, d),
|
|
128
|
+
rows: this.tipRows(d)
|
|
129
|
+
}));
|
|
130
|
+
this.drawWhiskers(ctx, frame, this.options.whiskers ? bars.filter((d) => d.low !== null) : []);
|
|
131
|
+
}
|
|
132
|
+
tipRows(d) {
|
|
133
|
+
if (d.folded) return [[this.options.other === FeatureImportanceOther.Mean ? "Mean" : "Sum", figure(d.source)], ["Features", d.folded]];
|
|
134
|
+
return [["Value", figure(d.source)], ...d.low === null ? [] : [["Interval", `${figure(d.low)} to ${figure(d.high)}`]]];
|
|
135
|
+
}
|
|
136
|
+
drawWhiskers(ctx, frame, bars) {
|
|
137
|
+
const cap = Math.min(CAP, frame.thick / 3);
|
|
138
|
+
const tick = (d, value) => {
|
|
139
|
+
const at = frame.center(d.band), { x1, y1, x2, y2 } = frame.rule(value, at - cap, at + cap);
|
|
140
|
+
return `M${x1},${y1}L${x2},${y2}`;
|
|
141
|
+
};
|
|
142
|
+
const stem = (d, low, high) => {
|
|
143
|
+
const { x1, y1, x2, y2 } = frame.stem(frame.center(d.band), low, high);
|
|
144
|
+
return `M${x1},${y1}L${x2},${y2}${tick(d, low)}${tick(d, high)}`;
|
|
145
|
+
};
|
|
146
|
+
const whiskers = ctx.marks("whiskers", "path", bars, (d) => d.key).attr("fill", "none").attr("stroke", ctx.theme.ink).attr("stroke-width", 2).attr("pointer-events", "none");
|
|
147
|
+
ctx.set(whiskers, { d: (d) => stem(d, d.low, d.high) }, { d: (d) => stem(d, d.value, d.value) }, 12);
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
export { FeatureImportanceChart };
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { BarRows } from "../bars/BarRows.js";
|
|
2
|
+
import { group, mean, sum } from "d3";
|
|
3
|
+
// src/charts/mlcharts/ImportanceRanking.ts
|
|
4
|
+
/** How a signed score maps to bars and rank. */
|
|
5
|
+
var FeatureImportanceSign = /* @__PURE__ */ function(FeatureImportanceSign) {
|
|
6
|
+
/** Ranked by size, drawn either side of zero: the usual read of coefficients. */
|
|
7
|
+
FeatureImportanceSign["Signed"] = "signed";
|
|
8
|
+
/** Ranked high to low as supplied, so a negative permutation importance stays below zero. */
|
|
9
|
+
FeatureImportanceSign["Value"] = "value";
|
|
10
|
+
/** Ranked and drawn by size, with the sign kept as colour. */
|
|
11
|
+
FeatureImportanceSign["Magnitude"] = "magnitude";
|
|
12
|
+
return FeatureImportanceSign;
|
|
13
|
+
}({});
|
|
14
|
+
/** What the row for features past `top` shows. */
|
|
15
|
+
var FeatureImportanceOther = /* @__PURE__ */ function(FeatureImportanceOther) {
|
|
16
|
+
FeatureImportanceOther["Sum"] = "sum";
|
|
17
|
+
FeatureImportanceOther["Mean"] = "mean";
|
|
18
|
+
FeatureImportanceOther["None"] = "none";
|
|
19
|
+
return FeatureImportanceOther;
|
|
20
|
+
}({});
|
|
21
|
+
/** Turns rows into ranked bars: repeated features merged, the tail folded into an Other row. */
|
|
22
|
+
var ImportanceRanking = class {
|
|
23
|
+
/**
|
|
24
|
+
* Repeated rows for a feature, as from CV folds or permutation repeats, are averaged. Without
|
|
25
|
+
* interval columns they get the population SD across repeats, as scikit-learn's `importances_std`.
|
|
26
|
+
*/
|
|
27
|
+
static merge(rows, columns) {
|
|
28
|
+
const named = (row) => row?.[columns.feature] !== null && row?.[columns.feature] !== void 0 && row[columns.feature] !== "";
|
|
29
|
+
const usable = rows.filter((row) => named(row) && BarRows.number(row[columns.value]) !== null);
|
|
30
|
+
return [...group(usable, (row) => String(row[columns.feature]))].map(([feature, set]) => {
|
|
31
|
+
const values = set.map((row) => BarRows.number(row[columns.value]));
|
|
32
|
+
const value = mean(values);
|
|
33
|
+
const low = mean(set, (row) => BarRows.number(row[columns.low])), high = mean(set, (row) => BarRows.number(row[columns.high]));
|
|
34
|
+
if (low !== void 0 && high !== void 0) return {
|
|
35
|
+
feature,
|
|
36
|
+
value,
|
|
37
|
+
low: Math.min(low, high),
|
|
38
|
+
high: Math.max(low, high)
|
|
39
|
+
};
|
|
40
|
+
const reach = mean(set, (row) => {
|
|
41
|
+
const cell = BarRows.number(row[columns.error]);
|
|
42
|
+
return cell === null ? null : Math.abs(cell);
|
|
43
|
+
}) ?? (values.length > 1 ? Math.sqrt(mean(values, (v) => (v - value) ** 2)) : null);
|
|
44
|
+
return {
|
|
45
|
+
feature,
|
|
46
|
+
value,
|
|
47
|
+
low: reach === null ? null : value - reach,
|
|
48
|
+
high: reach === null ? null : value + reach
|
|
49
|
+
};
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
static rank(rows, columns, { sign, other, top }) {
|
|
53
|
+
const size = sign === "value" ? (d) => d.value : (d) => Math.abs(d.value);
|
|
54
|
+
const sorted = this.merge(rows, columns).sort((a, b) => size(b) - size(a));
|
|
55
|
+
const limit = top >= 1 ? Math.floor(top) : 1;
|
|
56
|
+
const cut = other !== "none" && sorted.length === limit + 1 ? sorted.length : limit;
|
|
57
|
+
return [...sorted.slice(0, cut).map((d) => this.bar(d, sign)), ...this.folded(sorted.slice(cut), sign, other)];
|
|
58
|
+
}
|
|
59
|
+
static bar({ feature, value, low, high }, sign) {
|
|
60
|
+
const entry = {
|
|
61
|
+
key: `feature ${feature}`,
|
|
62
|
+
band: feature,
|
|
63
|
+
source: value,
|
|
64
|
+
polarity: Math.sign(value),
|
|
65
|
+
folded: 0
|
|
66
|
+
};
|
|
67
|
+
if (sign !== "magnitude" || low === null || high === null) return {
|
|
68
|
+
...entry,
|
|
69
|
+
value: sign === "magnitude" ? Math.abs(value) : value,
|
|
70
|
+
low,
|
|
71
|
+
high
|
|
72
|
+
};
|
|
73
|
+
const near = low <= 0 && high >= 0 ? 0 : Math.min(Math.abs(low), Math.abs(high));
|
|
74
|
+
return {
|
|
75
|
+
...entry,
|
|
76
|
+
value: Math.abs(value),
|
|
77
|
+
low: near,
|
|
78
|
+
high: Math.max(Math.abs(low), Math.abs(high))
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
/** No interval here: the spread of a sum needs covariances the rows do not hold. A signed Other splits into its two sides. */
|
|
82
|
+
static folded(rest, sign, other) {
|
|
83
|
+
if (!rest.length || other === "none") return [];
|
|
84
|
+
const band = `Other (${rest.length})`, scale = other === "mean" ? 1 / rest.length : 1;
|
|
85
|
+
const take = (pick) => sum(rest, pick) * scale;
|
|
86
|
+
return (sign === "signed" ? [take((d) => Math.max(0, d.value)), take((d) => Math.min(0, d.value))].filter(Boolean) : [take(sign === "magnitude" ? (d) => Math.abs(d.value) : (d) => d.value)]).map((value) => ({
|
|
87
|
+
key: `other ${Math.sign(value)}`,
|
|
88
|
+
band,
|
|
89
|
+
value,
|
|
90
|
+
source: value,
|
|
91
|
+
polarity: sign === "signed" ? Math.sign(value) : 0,
|
|
92
|
+
low: null,
|
|
93
|
+
high: null,
|
|
94
|
+
folded: rest.length
|
|
95
|
+
}));
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
export { FeatureImportanceOther, FeatureImportanceSign, ImportanceRanking };
|