@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,279 @@
1
+ import { Chart2D } from "../../core/Chart2D.js";
2
+ import { ChartFamily, DataShape, Dimension } from "../../core/enums.js";
3
+ import { SampleData } from "../../data/SampleData.js";
4
+ import { NpsBand, NpsSurvey } from "./NpsSurvey.js";
5
+ import { arc, scaleBand, scaleLinear } from "d3";
6
+ // src/charts/frameworks/NpsGauge.ts
7
+ var NpsSweep = /* @__PURE__ */ function(NpsSweep) {
8
+ NpsSweep["Semi"] = "semi";
9
+ NpsSweep["ThreeQuarter"] = "three-quarter";
10
+ return NpsSweep;
11
+ }({});
12
+ var NpsIndicator = /* @__PURE__ */ function(NpsIndicator) {
13
+ NpsIndicator["Needle"] = "needle";
14
+ NpsIndicator["Fill"] = "fill";
15
+ return NpsIndicator;
16
+ }({});
17
+ var STRIP = 22;
18
+ var GAP = 8;
19
+ var CHAR = 6.2;
20
+ var BANDS = [
21
+ NpsBand.Detractors,
22
+ NpsBand.Passives,
23
+ NpsBand.Promoters
24
+ ];
25
+ var CELLS = Array.from({ length: 20 }, (_, i) => -100 + i * 10);
26
+ var percent = (value) => `${value.toFixed(1)}%`;
27
+ /** A bar with its own radius per corner, clockwise from top left. */
28
+ var bar = (x, y, w, h, [tl, tr, br, bl]) => `M${x + tl},${y}H${x + w - tr}Q${x + w},${y} ${x + w},${y + tr}V${y + h - br}Q${x + w},${y + h} ${x + w - br},${y + h}H${x + bl}Q${x},${y + h} ${x},${y + h - bl}V${y + tl}Q${x},${y} ${x + tl},${y}Z`;
29
+ /** Synthetic answers, invented for the sample: a left-skewed 0 to 10 spread like typical recommendation surveys. */
30
+ var syntheticResponses = (seed) => SampleData.distribution(seed, 400, 1).map((row) => {
31
+ const z = (row.value - 40) / 8;
32
+ return { score: Math.max(0, Math.min(10, Math.round(8.1 + 1.9 * z - .3 * z * z))) };
33
+ });
34
+ /**
35
+ * Net Promoter Score gauge: the score on a -100 to 100 dial with zero at the top, above a stacked breakdown
36
+ * of detractors, passives and promoters whose outer two parts are what the score subtracts. See `NpsSurvey`.
37
+ */
38
+ var NpsGauge = class extends Chart2D {
39
+ static meta = {
40
+ id: "nps-gauge",
41
+ name: "Net Promoter Score gauge",
42
+ family: ChartFamily.Indicator,
43
+ dimension: Dimension.Two,
44
+ shape: DataShape.Table,
45
+ description: "Shows Net Promoter Score from 0 to 10 answers on a -100 to 100 dial with its promoter, passive and detractor breakdown.",
46
+ tags: [
47
+ "nps",
48
+ "net promoter",
49
+ "customer loyalty",
50
+ "survey"
51
+ ],
52
+ sample: syntheticResponses,
53
+ variants: {
54
+ sweep: ["semi", "three-quarter"],
55
+ indicator: ["needle", "fill"],
56
+ scores: [false, true]
57
+ }
58
+ };
59
+ defaults() {
60
+ return {
61
+ score: "score",
62
+ sweep: "semi",
63
+ indicator: "needle",
64
+ scores: false
65
+ };
66
+ }
67
+ get survey() {
68
+ return new NpsSurvey(this.options.data, this.options.score, this.options.count);
69
+ }
70
+ /** Promoters take the good pole. Passives are a neutral, since the diverging midpoint nearly vanishes into the surface. */
71
+ #color(band) {
72
+ if (band === NpsBand.Passives) return this.theme.axis;
73
+ return this.palette.diverging(band === NpsBand.Promoters ? -1 : 1);
74
+ }
75
+ /** Blue above zero and red below, never so faint that a cell near zero disappears. */
76
+ #polarity(score) {
77
+ return this.palette.diverging(-Math.sign(score) * (.2 + .8 * Math.abs(score) / 100));
78
+ }
79
+ legendItems() {
80
+ return BANDS.map((key) => ({
81
+ key,
82
+ color: this.#color(key)
83
+ }));
84
+ }
85
+ draw(ctx) {
86
+ const { width, height } = ctx.plot;
87
+ const { survey } = this;
88
+ const panel = this.options.scores ? Math.round(width * .45) : width;
89
+ const room = height - STRIP - GAP;
90
+ this.#gauge(ctx, survey, this.#dial(panel, room));
91
+ this.#distribution(ctx, survey, panel, room);
92
+ this.#strip(ctx, survey, height - STRIP, width);
93
+ }
94
+ #dial(width, height) {
95
+ const needle = this.options.indicator === "needle", semi = this.options.sweep === "semi";
96
+ const top = 16;
97
+ const under = semi && needle ? 60 : 18;
98
+ const radius = Math.max(18, Math.min(width / 2 - 8, (height - top - under) / (semi ? 1 : 1 + Math.SQRT1_2)));
99
+ const cy = top + radius;
100
+ const size = Math.max(20, Math.min(40, radius * .5));
101
+ const number = needle ? semi ? cy + 12 + size * .75 : cy + radius * .56 : cy - (semi ? 14 : 0);
102
+ return {
103
+ cx: width / 2,
104
+ cy,
105
+ radius,
106
+ size,
107
+ number,
108
+ caption: number + 16,
109
+ room: radius * 1.5
110
+ };
111
+ }
112
+ #gauge(ctx, survey, dial) {
113
+ const { theme } = ctx;
114
+ const { cx, cy, radius } = dial;
115
+ const half = this.options.sweep === "semi" ? Math.PI / 2 : Math.PI * .75;
116
+ const needle = this.options.indicator === "needle";
117
+ const angle = scaleLinear().domain([-100, 100]).range([-half, half]);
118
+ const inner = radius * .82;
119
+ const ring = arc().innerRadius(inner).outerRadius(radius).startAngle((d) => angle(d.from)).endAngle((d) => angle(d.to));
120
+ const cell = arc().innerRadius(inner).outerRadius(radius).startAngle((d) => angle(d.from)).endAngle((d) => angle(d.to)).padAngle(2 / radius).padRadius(radius);
121
+ const stage = ctx.nest(`translate(${cx},${cy})`, "dial");
122
+ const { score } = survey;
123
+ const parts = needle ? CELLS.map((from) => ({
124
+ from,
125
+ to: from + 10
126
+ })) : [{
127
+ from: -100,
128
+ to: 100
129
+ }];
130
+ stage.marks("track", "path", parts, (d) => d.from).attr("d", (d) => (needle ? cell : ring)(d)).attr("fill", (d) => needle ? this.#polarity(d.from + 5) : theme.grid);
131
+ const value = !needle && score !== null ? [score] : [];
132
+ const fill = stage.marks("fill", "path", value).attr("fill", (d) => this.#polarity(d));
133
+ stage.set(fill, { d: (d) => ring(d < 0 ? {
134
+ from: d,
135
+ to: 0
136
+ } : {
137
+ from: 0,
138
+ to: d
139
+ }) }, { d: () => ring({
140
+ from: 0,
141
+ to: 0
142
+ }) });
143
+ stage.hover(fill, (d) => this.#tip(survey, d));
144
+ const zero = stage.marks("zero", "line", needle ? [] : [0]).attr("stroke", theme.ink).attr("stroke-width", 2);
145
+ stage.set(zero, {
146
+ x1: 0,
147
+ x2: 0,
148
+ y1: -radius,
149
+ y2: -inner
150
+ });
151
+ const length = needle ? inner - 4 : 0;
152
+ const pointer = stage.marks("needle", "path", needle && score !== null ? [score] : []).attr("fill", theme.ink);
153
+ stage.set(pointer, {
154
+ d: `M-3,0L0,${-length}L3,0Z`,
155
+ transform: (d) => `rotate(${angle(d) * 180 / Math.PI})`
156
+ }, { transform: "rotate(0)" });
157
+ stage.hover(pointer, (d) => this.#tip(survey, d));
158
+ const hub = stage.marks("hub", "circle", needle && score !== null ? [score] : []).attr("fill", theme.ink).attr("stroke", theme.surface).attr("stroke-width", 2);
159
+ stage.set(hub, { r: 6 });
160
+ stage.hover(hub, (d) => this.#tip(survey, d));
161
+ const reach = (radius + inner) / 2;
162
+ const ends = [
163
+ {
164
+ text: "−100",
165
+ x: Math.sin(-half) * reach,
166
+ y: -Math.cos(-half) * reach + 14
167
+ },
168
+ {
169
+ text: "0",
170
+ x: 0,
171
+ y: -radius - 5
172
+ },
173
+ {
174
+ text: "+100",
175
+ x: Math.sin(half) * reach,
176
+ y: -Math.cos(half) * reach + 14
177
+ }
178
+ ];
179
+ stage.text(stage.marks("ends", "text", ends, (d) => d.text).attr("text-anchor", "middle").attr("x", (d) => d.x).attr("y", (d) => d.y).text((d) => d.text), 11, "muted");
180
+ ctx.text(ctx.marks("score", "text", [score === null ? "–" : NpsSurvey.format(score)]).attr("text-anchor", "middle").attr("x", cx).attr("y", dial.number).attr("font-weight", 700).text((d) => d), dial.size, "ink");
181
+ ctx.text(ctx.marks("caption", "text", [this.#caption(survey, dial.room)]).attr("text-anchor", "middle").attr("x", cx).attr("y", dial.caption).text((d) => d), 11, "secondary");
182
+ }
183
+ #caption(survey, room) {
184
+ if (!survey.total) return "No valid responses";
185
+ const base = `n = ${survey.total.toLocaleString("en")}`;
186
+ const full = `${base} · ${survey.excluded.toLocaleString("en")} excluded`;
187
+ return survey.excluded && full.length * CHAR <= room ? full : base;
188
+ }
189
+ #tip(survey, score) {
190
+ const rows = BANDS.map((band) => [band, percent(survey.share(band))]);
191
+ return {
192
+ title: `NPS ${NpsSurvey.format(score)}`,
193
+ rows: [
194
+ ["Unrounded", score.toFixed(1)],
195
+ ...rows,
196
+ ["Responses", survey.total]
197
+ ]
198
+ };
199
+ }
200
+ /** The 0 to 10 answers themselves, so a score's shape shows: bars start at zero and share one scale. */
201
+ #distribution(ctx, survey, left, height) {
202
+ const bottom = height - 16;
203
+ const shown = this.options.scores && survey.total ? survey.counts.map((count, score) => ({
204
+ score,
205
+ count
206
+ })) : [];
207
+ const x = scaleBand().domain(shown.map((d) => d.score)).range([left + 12, ctx.plot.width]).paddingInner(0);
208
+ const peak = Math.max(1, ...survey.counts);
209
+ const y = scaleLinear().domain([0, peak]).range([bottom, 4]);
210
+ const shape = (d, to) => {
211
+ const w = Math.max(1, x.bandwidth() - 2), r = Math.min(4, w / 2, bottom - to);
212
+ return bar(x(d.score) + 1, to, w, bottom - to, [
213
+ r,
214
+ r,
215
+ 0,
216
+ 0
217
+ ]);
218
+ };
219
+ const bars = ctx.marks("scores", "path", shown.filter((d) => d.count), (d) => d.score).attr("fill", (d) => this.#color(NpsSurvey.band(d.score)));
220
+ ctx.set(bars, { d: (d) => shape(d, y(d.count)) }, { d: (d) => shape(d, bottom) }, 20);
221
+ ctx.hover(bars, (d) => ({
222
+ title: `Score ${d.score}`,
223
+ color: this.#color(NpsSurvey.band(d.score)),
224
+ rows: [
225
+ ["Band", NpsSurvey.band(d.score)],
226
+ ["Responses", d.count],
227
+ ["Share", percent(d.count / survey.total * 100)]
228
+ ]
229
+ }));
230
+ const step = x.bandwidth() >= 13 ? 1 : 2;
231
+ const ticks = ctx.marks("score-ticks", "text", shown.filter((d) => d.score % step === 0), (d) => d.score).attr("text-anchor", "middle").attr("x", (d) => x(d.score) + x.bandwidth() / 2).attr("y", height - 3).text((d) => d.score);
232
+ ctx.text(ticks, 11, "secondary");
233
+ }
234
+ /** Whole shares can round away from the score, as 41.25 - 22.5 reads 41 - 23 beside +19, so such a strip prints tenths. */
235
+ #tenths(survey) {
236
+ const whole = (band) => Math.round(survey.share(band));
237
+ return survey.score !== null && whole(NpsBand.Promoters) - whole(NpsBand.Detractors) !== NpsSurvey.rounded(survey.score);
238
+ }
239
+ /** Part-to-whole across 100% of valid responses, with a 2px gap taken out of the length, not added to it. */
240
+ #strip(ctx, survey, top, width) {
241
+ const present = BANDS.filter((band) => survey.count(band));
242
+ const room = width - 2 * Math.max(0, present.length - 1);
243
+ let at = 0;
244
+ const parts = present.map((band, i) => {
245
+ const w = survey.share(band) / 100 * room, x = at;
246
+ at += w + 2;
247
+ return {
248
+ band,
249
+ x,
250
+ w,
251
+ first: !i,
252
+ last: i === present.length - 1
253
+ };
254
+ });
255
+ const shape = (d, w) => {
256
+ const r = Math.min(4, w / 2);
257
+ return bar(d.x, top, w, STRIP, [
258
+ d.first ? r : 0,
259
+ d.last ? r : 0,
260
+ d.last ? r : 0,
261
+ d.first ? r : 0
262
+ ]);
263
+ };
264
+ const segments = ctx.marks("strip", "path", parts, (d) => d.band).attr("fill", (d) => this.#color(d.band));
265
+ ctx.set(segments, { d: (d) => shape(d, d.w) }, { d: (d) => shape(d, 0) }, 40);
266
+ ctx.hover(segments, (d) => ({
267
+ title: d.band,
268
+ color: this.#color(d.band),
269
+ rows: [["Responses", survey.count(d.band)], ["Share", percent(survey.share(d.band))]]
270
+ }));
271
+ const tenths = this.#tenths(survey);
272
+ const label = (d) => {
273
+ const share = `${tenths ? survey.share(d.band).toFixed(1) : Math.round(survey.share(d.band))}%`, long = `${d.band} ${share}`;
274
+ return long.length * CHAR + 12 <= d.w ? long : share.length * CHAR + 8 <= d.w ? share : "";
275
+ };
276
+ ctx.marks("strip-labels", "text", parts, (d) => d.band).attr("text-anchor", "middle").attr("x", (d) => d.x + d.w / 2).attr("y", top + STRIP / 2 + 4).attr("font-size", 11).attr("font-family", ctx.theme.fontFamily).attr("fill", (d) => ctx.palette.inkOn(this.#color(d.band))).text(label).style("pointer-events", "none");
277
+ }
278
+ };
279
+ export { NpsGauge, NpsIndicator, NpsSweep };
@@ -0,0 +1,58 @@
1
+ // src/charts/frameworks/NpsSurvey.ts
2
+ var NpsBand = /* @__PURE__ */ function(NpsBand) {
3
+ NpsBand["Detractors"] = "Detractors";
4
+ NpsBand["Passives"] = "Passives";
5
+ NpsBand["Promoters"] = "Promoters";
6
+ return NpsBand;
7
+ }({});
8
+ var SCORES = 11;
9
+ var number = (cell) => typeof cell === "number" ? cell : typeof cell === "string" && cell.trim() ? Number(cell) : NaN;
10
+ /**
11
+ * Net Promoter Score from 0 to 10 likelihood-to-recommend answers (Reichheld, "The One Number You Need to
12
+ * Grow", Harvard Business Review, 2003): 9 and 10 promote, 7 and 8 are passive, 0 to 6 detract, and the score
13
+ * is the percentage of promoters minus the percentage of detractors. Anything but a whole 0 to 10 is left out.
14
+ */
15
+ var NpsSurvey = class NpsSurvey {
16
+ /** Responses per score, 0 to 10. */
17
+ counts = Array.from({ length: SCORES }, () => 0);
18
+ excluded = 0;
19
+ /** `count` names a column of response counts, for data already tallied by score. */
20
+ constructor(rows, score, count) {
21
+ let excluded = 0;
22
+ for (const row of Array.isArray(rows) ? rows : []) {
23
+ const value = row !== null && typeof row === "object" ? number(row[score]) : NaN;
24
+ const weight = count === void 0 ? 1 : row !== null && typeof row === "object" ? number(row[count]) : NaN;
25
+ if (!Number.isFinite(weight) || weight < 0) excluded++;
26
+ else if (Number.isInteger(value) && value >= 0 && value < SCORES) this.counts[value] += weight;
27
+ else excluded += weight;
28
+ }
29
+ this.excluded = excluded;
30
+ }
31
+ static band(score) {
32
+ return score >= 9 ? "Promoters" : score >= 7 ? "Passives" : "Detractors";
33
+ }
34
+ /** Whole-number report form, rounding halves away from zero so the sign cannot bias it. */
35
+ static rounded(score) {
36
+ return Math.sign(score) * Math.round(Math.abs(score)) || 0;
37
+ }
38
+ /** Signed report form: `+42`, `−12`, `0`. */
39
+ static format(score) {
40
+ const whole = NpsSurvey.rounded(score);
41
+ return whole > 0 ? `+${whole}` : whole < 0 ? `−${-whole}` : "0";
42
+ }
43
+ get total() {
44
+ return this.counts.reduce((sum, count) => sum + count, 0);
45
+ }
46
+ count(band) {
47
+ return this.counts.reduce((sum, count, score) => NpsSurvey.band(score) === band ? sum + count : sum, 0);
48
+ }
49
+ /** Percentage of valid responses in `band`, 0 with none. */
50
+ share(band) {
51
+ return this.total ? this.count(band) / this.total * 100 : 0;
52
+ }
53
+ /** Unrounded, from -100 to 100, or `null` with no valid responses. */
54
+ get score() {
55
+ return this.total ? this.share("Promoters") - this.share("Detractors") : null;
56
+ }
57
+ };
58
+ export { NpsBand, NpsSurvey };