@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,523 @@
1
+ import { Chart2D } from "../../core/Chart2D.js";
2
+ import { ChartFamily, DataShape, Dimension } from "../../core/enums.js";
3
+ import { Scales } from "../../core/Scales.js";
4
+ import { FrameworkText } from "./FrameworkText.js";
5
+ import { PositioningSamples } from "./PositioningSamples.js";
6
+ import { max, packEnclose, packSiblings, sum } from "d3";
7
+ // src/charts/frameworks/AnsoffMatrix.ts
8
+ var AnsoffStrategy = /* @__PURE__ */ function(AnsoffStrategy) {
9
+ AnsoffStrategy["MarketPenetration"] = "Market Penetration";
10
+ AnsoffStrategy["ProductDevelopment"] = "Product Development";
11
+ AnsoffStrategy["MarketDevelopment"] = "Market Development";
12
+ AnsoffStrategy["Diversification"] = "Diversification";
13
+ return AnsoffStrategy;
14
+ }({});
15
+ var AnsoffDisplay = /* @__PURE__ */ function(AnsoffDisplay) {
16
+ AnsoffDisplay["List"] = "list";
17
+ AnsoffDisplay["Bars"] = "bars";
18
+ AnsoffDisplay["Bubbles"] = "bubbles";
19
+ return AnsoffDisplay;
20
+ }({});
21
+ var AnsoffMarketOrder = /* @__PURE__ */ function(AnsoffMarketOrder) {
22
+ AnsoffMarketOrder["ExistingTop"] = "existing-top";
23
+ AnsoffMarketOrder["ExistingBottom"] = "existing-bottom";
24
+ return AnsoffMarketOrder;
25
+ }({});
26
+ var AnsoffStage = /* @__PURE__ */ function(AnsoffStage) {
27
+ AnsoffStage["Existing"] = "Existing";
28
+ AnsoffStage["New"] = "New";
29
+ return AnsoffStage;
30
+ }({});
31
+ var GUTTER_LEFT = 38;
32
+ var GUTTER_TOP = 36;
33
+ var GAP = 2;
34
+ var PAD = 8;
35
+ var HEAD = 40;
36
+ var COMPACT_HEAD = 24;
37
+ var ROW = 18;
38
+ var CHAR = 6.2;
39
+ var PIP = {
40
+ width: 11,
41
+ height: 7,
42
+ gap: 2
43
+ };
44
+ var MIN_RADIUS = 5;
45
+ var QUADRANTS = [
46
+ {
47
+ strategy: "Market Penetration",
48
+ market: "Existing",
49
+ product: "Existing",
50
+ risk: 1
51
+ },
52
+ {
53
+ strategy: "Product Development",
54
+ market: "Existing",
55
+ product: "New",
56
+ risk: 2
57
+ },
58
+ {
59
+ strategy: "Market Development",
60
+ market: "New",
61
+ product: "Existing",
62
+ risk: 2
63
+ },
64
+ {
65
+ strategy: "Diversification",
66
+ market: "New",
67
+ product: "New",
68
+ risk: 3
69
+ }
70
+ ];
71
+ var RISK_NAME = [
72
+ "",
73
+ "Lowest risk",
74
+ "Medium risk",
75
+ "Highest risk"
76
+ ];
77
+ var RISK_SHADE = [
78
+ 0,
79
+ .08,
80
+ .2,
81
+ .36
82
+ ];
83
+ var EXISTING = /^(existing|present|current)\b/;
84
+ var NEW = /^new\b/;
85
+ var clip = (text, room) => FrameworkText.fit(text, room, 11);
86
+ /**
87
+ * Sorts growth initiatives by Ansoff's product-market matrix (Ansoff, Strategies for Diversification, HBR 1957): market penetration,
88
+ * product development, market development and diversification. Choose it to see where a growth plan's bets sit and how far they stray
89
+ * from the present business. Risk rises with distance from it: penetration lowest, diversification highest, the other two between.
90
+ */
91
+ var AnsoffMatrix = class AnsoffMatrix extends Chart2D {
92
+ static meta = {
93
+ id: "ansoff-matrix",
94
+ name: "Ansoff matrix",
95
+ family: ChartFamily.Matrix,
96
+ dimension: Dimension.Two,
97
+ shape: DataShape.Table,
98
+ description: "Places growth initiatives in the four product-market strategies, from market penetration to diversification.",
99
+ tags: [
100
+ "strategy",
101
+ "growth",
102
+ "product market",
103
+ "2x2",
104
+ "diversification"
105
+ ],
106
+ sample: (seed) => PositioningSamples.initiatives(seed),
107
+ variants: {
108
+ display: [
109
+ "list",
110
+ "bars",
111
+ "bubbles"
112
+ ],
113
+ marketOrder: ["existing-top", "existing-bottom"],
114
+ risk: [false, true]
115
+ }
116
+ };
117
+ /** Assigns rows to quadrants, largest first when values exist. Rows naming neither stage are left out. */
118
+ static classify(rows, columns) {
119
+ const stage = (cell) => {
120
+ const word = String(cell ?? "").trim().toLowerCase();
121
+ return EXISTING.test(word) ? "Existing" : NEW.test(word) ? "New" : null;
122
+ };
123
+ const clean = rows.filter((row) => row !== null && typeof row === "object");
124
+ return QUADRANTS.map((quadrant) => {
125
+ const items = clean.filter((row) => stage(row[columns.market]) === quadrant.market && stage(row[columns.product]) === quadrant.product).map((row) => {
126
+ const raw = row[columns.value];
127
+ const value = typeof raw === "number" ? raw : typeof raw === "string" && raw.trim() ? Number(raw) : NaN;
128
+ return {
129
+ name: String(row[columns.item] ?? "Unnamed"),
130
+ value: Number.isFinite(value) ? value : null,
131
+ row
132
+ };
133
+ });
134
+ const ranked = items.map((item, i) => [item, i]).sort(([a, i], [b, j]) => (b.value ?? -Infinity) - (a.value ?? -Infinity) || i - j).map(([item]) => item);
135
+ return {
136
+ ...quadrant,
137
+ items: ranked,
138
+ total: sum(items, (item) => item.value ?? 0)
139
+ };
140
+ });
141
+ }
142
+ defaults() {
143
+ return {
144
+ item: "initiative",
145
+ market: "market",
146
+ product: "product",
147
+ value: "value",
148
+ display: "list",
149
+ marketOrder: "existing-top",
150
+ risk: false
151
+ };
152
+ }
153
+ plotMargin() {
154
+ return {
155
+ top: 6,
156
+ right: 12,
157
+ bottom: 8,
158
+ left: 8
159
+ };
160
+ }
161
+ /** Quadrant headers name the colours directly. */
162
+ legendItems() {
163
+ return [];
164
+ }
165
+ #cells(ctx) {
166
+ const { data, item, market, product, value, marketOrder, risk } = this.options;
167
+ const { width, height } = ctx.plot;
168
+ const cellWidth = (width - GUTTER_LEFT - GAP) / 2, cellHeight = (height - GUTTER_TOP - GAP) / 2;
169
+ const existingRow = marketOrder === "existing-top" ? 0 : 1;
170
+ return AnsoffMatrix.classify(Array.isArray(data) ? data : [], {
171
+ item,
172
+ market,
173
+ product,
174
+ value
175
+ }).map((quadrant) => {
176
+ const row = quadrant.market === "Existing" ? existingRow : 1 - existingRow;
177
+ const fill = risk ? ctx.palette.sequential(RISK_SHADE[quadrant.risk]) : ctx.theme.grid;
178
+ return {
179
+ ...quadrant,
180
+ x: GUTTER_LEFT + (quadrant.product === "New" ? cellWidth + GAP : 0),
181
+ y: GUTTER_TOP + row * (cellHeight + GAP),
182
+ width: cellWidth,
183
+ height: cellHeight,
184
+ fill,
185
+ ink: risk ? ctx.palette.inkOn(fill) : ctx.theme.ink
186
+ };
187
+ });
188
+ }
189
+ draw(ctx) {
190
+ const cells = this.#cells(ctx);
191
+ const quadrants = ctx.marks("quadrants", "rect", cells, (cell) => cell.strategy).attr("rx", 4).attr("fill", (cell) => cell.fill).attr("fill-opacity", this.options.risk ? 1 : .45);
192
+ const place = {
193
+ x: (d) => d.x,
194
+ y: (d) => d.y,
195
+ width: (d) => d.width,
196
+ height: (d) => d.height
197
+ };
198
+ ctx.set(quadrants, place, place);
199
+ ctx.hover(quadrants, (cell) => ({
200
+ title: cell.strategy,
201
+ rows: [
202
+ ["Markets", cell.market],
203
+ ["Products", cell.product],
204
+ ["Items", cell.items.length],
205
+ ...this.#valued(cells) ? [["Total", +cell.total.toFixed(2)]] : [],
206
+ ...this.options.risk ? [["Risk", RISK_NAME[cell.risk]]] : []
207
+ ]
208
+ }));
209
+ const words = [...this.#frameWords(ctx, cells), ...cells.flatMap((cell) => this.#headWords(ctx, cell, cells))];
210
+ words.push(...this.#drawItems(ctx, cells));
211
+ const texts = ctx.marks("words", "text", words, (word) => word.key).attr("text-anchor", (word) => word.anchor).attr("dy", "0.32em").attr("font-size", (word) => word.size).attr("font-weight", (word) => word.weight).attr("font-family", ctx.theme.fontFamily).attr("fill", (word) => word.fill).text((word) => word.text);
212
+ const at = (word) => `translate(${word.x},${word.y})${word.rotate ? " rotate(-90)" : ""}`;
213
+ ctx.set(texts, { transform: at }, { transform: at });
214
+ this.#drawPips(ctx, cells);
215
+ }
216
+ #valued(cells) {
217
+ return cells.some((cell) => cell.items.some((item) => item.value !== null));
218
+ }
219
+ #frameWords(ctx, cells) {
220
+ const { inkSecondary: fill, ink } = ctx.theme;
221
+ const left = cells.find((cell) => cell.product === "Existing");
222
+ const stages = [...new Map(cells.map((cell) => [cell.market, cell])).values()];
223
+ const columns = [...new Map(cells.map((cell) => [cell.product, cell])).values()];
224
+ const midHeight = left.height + GAP / 2 + GUTTER_TOP;
225
+ return [
226
+ {
227
+ key: "products",
228
+ text: "Products",
229
+ x: left.width + GAP / 2 + GUTTER_LEFT,
230
+ y: 8,
231
+ anchor: "middle",
232
+ size: 11,
233
+ weight: 600,
234
+ fill: ink
235
+ },
236
+ {
237
+ key: "markets",
238
+ text: "Markets",
239
+ x: 8,
240
+ y: midHeight,
241
+ anchor: "middle",
242
+ size: 11,
243
+ weight: 600,
244
+ fill: ink,
245
+ rotate: true
246
+ },
247
+ ...columns.map((cell) => ({
248
+ key: `product/${cell.product}`,
249
+ text: cell.product,
250
+ x: cell.x + cell.width / 2,
251
+ y: 25,
252
+ anchor: "middle",
253
+ size: 11,
254
+ weight: 400,
255
+ fill
256
+ })),
257
+ ...stages.map((cell) => ({
258
+ key: `market/${cell.market}`,
259
+ text: cell.market,
260
+ x: 25,
261
+ y: cell.y + cell.height / 2,
262
+ anchor: "middle",
263
+ size: 11,
264
+ weight: 400,
265
+ fill,
266
+ rotate: true
267
+ }))
268
+ ];
269
+ }
270
+ #headWords(ctx, cell, cells) {
271
+ const room = cell.width - 16 - (this.options.risk ? PIP.width * 3 + PIP.gap * 2 + PAD : 0);
272
+ const count = `${cell.items.length} ${cell.items.length === 1 ? "item" : "items"}`;
273
+ const total = this.#valued(cells) ? ` · ${Scales.format(cell.total, max(cells, (other) => other.total) ?? 0)}` : "";
274
+ const note = this.options.risk ? `${RISK_NAME[cell.risk]} · ${count}` : `${count}${total}`;
275
+ const words = [{
276
+ key: `head/${cell.strategy}`,
277
+ text: clip(cell.strategy, room / 1.1),
278
+ x: cell.x + PAD,
279
+ y: cell.y + 14,
280
+ anchor: "start",
281
+ size: 12,
282
+ weight: 600,
283
+ fill: cell.ink
284
+ }];
285
+ if (this.#roomy(cell)) words.push({
286
+ key: `note/${cell.strategy}`,
287
+ text: clip(note, cell.width - 16),
288
+ x: cell.x + PAD,
289
+ y: cell.y + 29,
290
+ anchor: "start",
291
+ size: 11,
292
+ weight: 400,
293
+ fill: this.options.risk ? cell.ink : ctx.theme.inkSecondary
294
+ });
295
+ return words;
296
+ }
297
+ #drawPips(ctx, cells) {
298
+ const pips = !this.options.risk ? [] : cells.flatMap((cell) => [
299
+ 1,
300
+ 2,
301
+ 3
302
+ ].map((step) => ({
303
+ key: `${cell.strategy}/${step}`,
304
+ x: cell.x + cell.width - PAD - (4 - step) * PIP.width - (3 - step) * PIP.gap,
305
+ y: cell.y + 11,
306
+ on: step <= cell.risk,
307
+ cell
308
+ })));
309
+ const marks = ctx.marks("risk-meter", "rect", pips, (pip) => pip.key).attr("rx", 2).attr("fill", (pip) => pip.cell.ink).attr("fill-opacity", (pip) => pip.on ? .85 : .2);
310
+ const place = {
311
+ x: (d) => d.x,
312
+ y: (d) => d.y,
313
+ width: PIP.width,
314
+ height: PIP.height
315
+ };
316
+ ctx.set(marks, place, place);
317
+ }
318
+ /** A short quadrant gives its second header line to the items. */
319
+ #roomy(cell) {
320
+ return cell.height >= 94;
321
+ }
322
+ #stack(cell) {
323
+ const head = this.#roomy(cell) ? HEAD : COMPACT_HEAD;
324
+ return {
325
+ x: cell.x + PAD,
326
+ y: cell.y + head,
327
+ width: cell.width - 16,
328
+ rows: Math.max(0, Math.floor((cell.height - head - 2) / ROW))
329
+ };
330
+ }
331
+ /** The rows that fit, with a final line counting the rest. */
332
+ #rows(cells) {
333
+ const rows = [], words = [];
334
+ for (const cell of cells) {
335
+ const stack = this.#stack(cell);
336
+ const overflow = cell.items.length > stack.rows;
337
+ const shown = overflow ? Math.max(0, stack.rows - 1) : cell.items.length;
338
+ cell.items.slice(0, shown).forEach((item, i) => rows.push({
339
+ key: `${cell.strategy}/${i}`,
340
+ cell,
341
+ item,
342
+ x: stack.x,
343
+ y: stack.y + i * ROW + ROW / 2,
344
+ width: stack.width
345
+ }));
346
+ if (overflow && stack.rows) words.push({
347
+ key: `more/${cell.strategy}`,
348
+ text: `+${cell.items.length - shown} more`,
349
+ x: stack.x,
350
+ y: stack.y + shown * ROW + ROW / 2,
351
+ anchor: "start",
352
+ size: 11,
353
+ weight: 400,
354
+ fill: cell.ink
355
+ });
356
+ }
357
+ return {
358
+ rows,
359
+ words
360
+ };
361
+ }
362
+ #drawItems(ctx, cells) {
363
+ const { display } = this.options;
364
+ if (display === "bubbles") return this.#drawBubbles(ctx, cells);
365
+ return display === "bars" ? this.#drawBars(ctx, cells) : this.#drawList(ctx, cells);
366
+ }
367
+ #tip(ctx, cell, item) {
368
+ return {
369
+ title: item.name,
370
+ color: ctx.palette.color(cell.strategy),
371
+ rows: [["Strategy", cell.strategy], ...item.value === null ? [] : [[this.options.value, item.value]]]
372
+ };
373
+ }
374
+ #drawList(ctx, cells) {
375
+ const { rows, words } = this.#rows(cells);
376
+ const reach = max(cells, (cell) => max(cell.items, (item) => Math.abs(item.value ?? 0)) ?? 0) ?? 0;
377
+ const dots = ctx.marks("item-dots", "circle", rows, (row) => row.key).attr("fill", (row) => ctx.palette.color(row.cell.strategy));
378
+ ctx.set(dots, {
379
+ cx: (row) => row.x + 4,
380
+ cy: (row) => row.y,
381
+ r: 4
382
+ }, { r: 0 }, 20);
383
+ ctx.hover(dots, (row) => this.#tip(ctx, row.cell, row.item));
384
+ return [...words, ...rows.flatMap((row) => {
385
+ const text = row.item.value === null ? "" : Scales.format(row.item.value, reach);
386
+ const room = row.width - 14 - (text ? text.length * CHAR + 8 : 0);
387
+ return [{
388
+ key: `name/${row.key}`,
389
+ text: clip(row.item.name, room),
390
+ x: row.x + 14,
391
+ y: row.y,
392
+ anchor: "start",
393
+ size: 11,
394
+ weight: 400,
395
+ fill: row.cell.ink
396
+ }, ...text && room > 48 ? [{
397
+ key: `amount/${row.key}`,
398
+ text,
399
+ x: row.x + row.width,
400
+ y: row.y,
401
+ anchor: "end",
402
+ size: 11,
403
+ weight: 400,
404
+ fill: row.cell.ink
405
+ }] : []];
406
+ })];
407
+ }
408
+ #drawBars(ctx, cells) {
409
+ const { rows, words } = this.#rows(cells);
410
+ const top = max(rows, (row) => Math.max(0, row.item.value ?? 0)) ?? 0;
411
+ const labelRoom = (row) => Math.min(row.width * .5, 110);
412
+ const barRoom = (row) => Math.max(0, row.width - labelRoom(row) - 4);
413
+ const length = (row, share = 1) => top ? barRoom(row) * Math.max(0, row.item.value ?? 0) * share / top : 0;
414
+ const bar = (row, width) => {
415
+ const x = row.x + labelRoom(row) + 4, y = row.y - 6, h = 12, r = Math.min(4, width);
416
+ return `M${x},${y}h${width - r}a${r},${r} 0 0 1 ${r},${r}v${h - 2 * r}a${r},${r} 0 0 1 ${-r},${r}h${r - width}z`;
417
+ };
418
+ const bars = ctx.marks("item-bars", "path", rows, (row) => row.key).attr("fill", (row) => ctx.palette.color(row.cell.strategy));
419
+ ctx.set(bars, { d: (row) => bar(row, length(row)) }, { d: (row) => bar(row, 0) }, 20);
420
+ ctx.hover(bars, (row) => this.#tip(ctx, row.cell, row.item));
421
+ return [...words, ...rows.map((row) => ({
422
+ key: `name/${row.key}`,
423
+ text: clip(row.item.name, labelRoom(row)),
424
+ x: row.x,
425
+ y: row.y,
426
+ anchor: "start",
427
+ size: 11,
428
+ weight: 400,
429
+ fill: row.cell.ink
430
+ }))];
431
+ }
432
+ #pack(cell, scale, counted) {
433
+ const circles = counted.map((item) => ({
434
+ item,
435
+ x: 0,
436
+ y: 0,
437
+ r: Math.max(MIN_RADIUS, scale * Math.sqrt(Math.max(0, item.value ?? 0))) + GAP / 2
438
+ }));
439
+ if (!circles.length) return {
440
+ circles: [],
441
+ reach: 0
442
+ };
443
+ packSiblings(circles);
444
+ return {
445
+ circles,
446
+ reach: packEnclose(circles).r
447
+ };
448
+ }
449
+ /** One area scale spans all four quadrants, so a bubble compares with any other. */
450
+ #drawBubbles(ctx, cells) {
451
+ const valued = this.#valued(cells);
452
+ const lists = cells.map((cell) => valued ? cell.items : cell.items.map((item) => ({
453
+ ...item,
454
+ value: 1
455
+ })));
456
+ const region = (cell) => {
457
+ const y = this.#stack(cell).y;
458
+ return {
459
+ x: cell.x,
460
+ y,
461
+ width: cell.width,
462
+ height: cell.y + cell.height - y - 4
463
+ };
464
+ };
465
+ const limit = (cell) => {
466
+ const { width, height } = region(cell);
467
+ return Math.min(width, height) / 2 - 2;
468
+ };
469
+ const shown = cells.map((cell, i) => lists[i].slice(0, Math.max(1, Math.floor((limit(cell) / 6) ** 2 * .55))));
470
+ const top = max(shown, (list) => max(list, (item) => Math.max(0, item.value ?? 0)) ?? 0) ?? 0;
471
+ let scale = top ? Math.max(...cells.map(limit)) / Math.sqrt(top) : 0;
472
+ while (cells.some((cell, i) => this.#pack(cell, scale, shown[i]).reach > limit(cell)) && scale > .01) scale *= .94;
473
+ const bubbles = [], words = [];
474
+ cells.forEach((cell, i) => {
475
+ const { x, y, width, height } = region(cell);
476
+ const packed = this.#pack(cell, scale, shown[i]);
477
+ const center = packed.circles.length ? packEnclose(packed.circles) : {
478
+ x: 0,
479
+ y: 0
480
+ };
481
+ for (const [j, circle] of packed.circles.entries()) bubbles.push({
482
+ key: `${cell.strategy}/${j}`,
483
+ cell,
484
+ item: circle.item,
485
+ x: x + width / 2 + circle.x - center.x,
486
+ y: y + height / 2 + circle.y - center.y,
487
+ r: circle.r - GAP / 2
488
+ });
489
+ const hidden = cell.items.length - shown[i].length;
490
+ if (hidden > 0) words.push({
491
+ key: `more/${cell.strategy}`,
492
+ text: `+${hidden} more`,
493
+ x: cell.x + cell.width - PAD,
494
+ y: cell.y + cell.height - 10,
495
+ anchor: "end",
496
+ size: 11,
497
+ weight: 400,
498
+ fill: cell.ink
499
+ });
500
+ });
501
+ const circles = ctx.marks("item-bubbles", "circle", bubbles, (bubble) => bubble.key).attr("fill", (bubble) => ctx.palette.color(bubble.cell.strategy));
502
+ ctx.set(circles, {
503
+ cx: (bubble) => bubble.x,
504
+ cy: (bubble) => bubble.y,
505
+ r: (bubble) => bubble.r
506
+ }, { r: 0 }, 20);
507
+ ctx.hover(circles, (bubble) => this.#tip(ctx, bubble.cell, bubble.item));
508
+ return [...words, ...bubbles.flatMap((bubble) => {
509
+ const text = clip(bubble.item.name, bubble.r * 2 - 6);
510
+ return text && !text.endsWith("…") ? [{
511
+ key: `name/${bubble.key}`,
512
+ text,
513
+ x: bubble.x,
514
+ y: bubble.y,
515
+ anchor: "middle",
516
+ size: 10.5,
517
+ weight: 400,
518
+ fill: ctx.palette.inkOn(ctx.palette.color(bubble.cell.strategy))
519
+ }] : [];
520
+ })];
521
+ }
522
+ };
523
+ export { AnsoffDisplay, AnsoffMarketOrder, AnsoffMatrix, AnsoffStage, AnsoffStrategy };
@@ -0,0 +1,98 @@
1
+ // src/charts/frameworks/FrameworkText.ts
2
+ var NARROW = /[fijlrtI.,:;'’!|()[\] ]/;
3
+ var WIDE = /[mwMW@%—]/;
4
+ var TALL = /[A-Z0-9&#+]/;
5
+ var FULL = /[ᄀ-ᅟ⺀-꓏가-힣豈-﫿︰-﹯＀-⦆]|\p{Extended_Pictographic}/u;
6
+ var ELLIPSIS = "…";
7
+ var TIP_WIDTH = 320;
8
+ /** Word-wraps text for framework layouts, whose text is the data. Widths are estimates by character class, so they work without a layout engine. */
9
+ var FrameworkText = class FrameworkText {
10
+ static width(text, size) {
11
+ let em = 0;
12
+ for (const char of text) em += FULL.test(char) ? 1 : WIDE.test(char) ? .9 : TALL.test(char) ? .68 : NARROW.test(char) ? .34 : .59;
13
+ return em * size;
14
+ }
15
+ /** Collapses whitespace; null, undefined and blank text become ''. */
16
+ static clean(value) {
17
+ if (value === null || value === void 0) return "";
18
+ return String(value).replace(/\s+/g, " ").trim();
19
+ }
20
+ /** Greedy word wrap. A word wider than the line breaks at a hyphen, so no line is ever wider than `width`. */
21
+ static wrap(text, width, size) {
22
+ const lines = [];
23
+ let line = "";
24
+ for (const word of FrameworkText.clean(text).split(" ").filter(Boolean)) {
25
+ const joined = line ? `${line} ${word}` : word;
26
+ if (FrameworkText.width(joined, size) <= width) {
27
+ line = joined;
28
+ continue;
29
+ }
30
+ if (line) lines.push(line);
31
+ line = "";
32
+ if (FrameworkText.width(word, size) <= width) {
33
+ line = word;
34
+ continue;
35
+ }
36
+ const pieces = FrameworkText.#split(word, width, size);
37
+ line = pieces.pop();
38
+ lines.push(...pieces);
39
+ }
40
+ if (line) lines.push(line);
41
+ return lines;
42
+ }
43
+ /** At least one character per piece, so a line narrower than a letter still ends. */
44
+ static #split(word, width, size) {
45
+ const pieces = [];
46
+ let piece = "";
47
+ for (const char of word) {
48
+ if (piece && FrameworkText.width(`${piece}${char}-`, size) > width) {
49
+ pieces.push(`${piece}-`);
50
+ piece = "";
51
+ }
52
+ piece += char;
53
+ }
54
+ pieces.push(piece);
55
+ return pieces;
56
+ }
57
+ /** The largest prefix of `text` that fits on one line, ending in an ellipsis if anything was cut. */
58
+ static fit(text, width, size) {
59
+ if (FrameworkText.width(text, size) <= width) return text;
60
+ let kept = "";
61
+ for (const char of text) {
62
+ if (FrameworkText.width(`${kept}${char}${ELLIPSIS}`, size) > width) break;
63
+ kept += char;
64
+ }
65
+ return kept ? `${kept.trimEnd()}${ELLIPSIS}` : "";
66
+ }
67
+ /** Keeps the first `count` lines, marking the last with an ellipsis when lines were dropped. */
68
+ static clamp(lines, count, width, size) {
69
+ if (lines.length <= count) return [...lines];
70
+ const kept = lines.slice(0, Math.max(0, count));
71
+ if (kept.length) kept[kept.length - 1] = FrameworkText.fit(`${kept.at(-1).replace(/-$/, "")}${ELLIPSIS}`, width, size);
72
+ return kept;
73
+ }
74
+ /** Sets a `<text>` per block's tspans, one per line. */
75
+ static write(ctx, selection) {
76
+ const spans = selection.selectAll("tspan").data((block) => block.lines.map((text, i) => ({
77
+ text,
78
+ x: block.x,
79
+ y: block.y + i * block.lead
80
+ }))).join("tspan");
81
+ const place = {
82
+ x: (line) => line.x,
83
+ y: (line) => line.y
84
+ };
85
+ ctx.set(spans, place, place).text((line) => line.text);
86
+ return selection;
87
+ }
88
+ /** Tooltip with long text on several lines: the tooltip box itself never wraps. */
89
+ static tip(text, rows, color) {
90
+ const [title, ...rest] = FrameworkText.wrap(text, TIP_WIDTH, 12);
91
+ return {
92
+ title,
93
+ color,
94
+ rows: [...rest.map((line) => [line, ""]), ...rows]
95
+ };
96
+ }
97
+ };
98
+ export { FrameworkText };