@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,81 @@
1
+ import { FrameworkText } from "./FrameworkText.js";
2
+ import { RaciIssue, RaciLetter } from "./raciEnums.js";
3
+ // src/charts/frameworks/RaciPlan.ts
4
+ var ORDER = [
5
+ RaciLetter.Accountable,
6
+ RaciLetter.Responsible,
7
+ RaciLetter.Consulted,
8
+ RaciLetter.Informed
9
+ ];
10
+ var WORDS = {
11
+ responsible: RaciLetter.Responsible,
12
+ accountable: RaciLetter.Accountable,
13
+ consulted: RaciLetter.Consulted,
14
+ informed: RaciLetter.Informed
15
+ };
16
+ var UNPHASED = "Other";
17
+ /** Reads long RACI rows into tasks and roles, and audits each task for exactly one A and at least one R. */
18
+ var RaciPlan = class RaciPlan {
19
+ tasks = [];
20
+ roles = [];
21
+ /** Rows with no task, no role, or an assignment that is not R, A, C or I. */
22
+ skipped = 0;
23
+ constructor(data, fields) {
24
+ const byName = /* @__PURE__ */ new Map();
25
+ for (const raw of Array.isArray(data) ? data : []) {
26
+ const row = raw ?? {};
27
+ const name = FrameworkText.clean(row[fields.task]), role = FrameworkText.clean(row[fields.role]);
28
+ if (!name || !role) {
29
+ this.skipped++;
30
+ continue;
31
+ }
32
+ let task = byName.get(name);
33
+ if (!task) {
34
+ task = {
35
+ name,
36
+ phase: FrameworkText.clean(row[fields.phase]) || UNPHASED,
37
+ cells: /* @__PURE__ */ new Map(),
38
+ accountable: [],
39
+ responsible: [],
40
+ issues: []
41
+ };
42
+ byName.set(name, task);
43
+ this.tasks.push(task);
44
+ }
45
+ if (!this.roles.includes(role)) this.roles.push(role);
46
+ const letters = RaciPlan.letters(row[fields.assignment]);
47
+ if (!letters) {
48
+ this.skipped++;
49
+ continue;
50
+ }
51
+ if (letters.length) task.cells.set(role, ORDER.filter((letter) => letters.includes(letter) || task.cells.get(role)?.includes(letter)));
52
+ }
53
+ for (const task of this.tasks) RaciPlan.#audit(task);
54
+ }
55
+ /** Letters in a cell such as `R`, `A/R`, `ar` or `Accountable`. Blank is none; null means unreadable. */
56
+ static letters(value) {
57
+ const text = FrameworkText.clean(value).toLowerCase();
58
+ const found = /* @__PURE__ */ new Set();
59
+ for (const token of text.split(/[^a-z]+/).filter(Boolean)) if (WORDS[token]) found.add(WORDS[token]);
60
+ else if (/^[raci]{1,4}$/.test(token)) for (const char of token) found.add(char.toUpperCase());
61
+ else return null;
62
+ return [...found];
63
+ }
64
+ static #audit(task) {
65
+ const holding = (letter) => [...task.cells].filter(([, letters]) => letters.includes(letter)).map(([role]) => role);
66
+ task.accountable = holding(RaciLetter.Accountable);
67
+ task.responsible = holding(RaciLetter.Responsible);
68
+ if (!task.accountable.length) task.issues.push(RaciIssue.NoAccountable);
69
+ if (task.accountable.length > 1) task.issues.push(RaciIssue.SeveralAccountable);
70
+ if (!task.responsible.length) task.issues.push(RaciIssue.NoResponsible);
71
+ }
72
+ get flagged() {
73
+ return this.tasks.filter((task) => task.issues.length);
74
+ }
75
+ /** How many tasks have this role, by letter. */
76
+ load(role) {
77
+ const count = (letter) => this.tasks.filter((task) => task.cells.get(role)?.includes(letter)).length;
78
+ return Object.fromEntries(ORDER.map((letter) => [letter, count(letter)]));
79
+ }
80
+ };
81
+ export { RaciPlan };
@@ -0,0 +1,76 @@
1
+ // src/charts/frameworks/RaciSamples.ts
2
+ /**
3
+ * SYNTHETIC: an invented product launch, with three faults planted so the validation has something
4
+ * to find: one task with no A, one with two, one with no R.
5
+ */
6
+ var LAUNCH = [
7
+ [
8
+ "Plan",
9
+ "Agree launch scope",
10
+ {
11
+ PM: "A/R",
12
+ Engineering: "C",
13
+ Design: "C",
14
+ Marketing: "I"
15
+ }
16
+ ],
17
+ [
18
+ "Plan",
19
+ "Estimate engineering effort and staffing",
20
+ {
21
+ PM: "C",
22
+ Engineering: "R",
23
+ Finance: "I"
24
+ }
25
+ ],
26
+ [
27
+ "Plan",
28
+ "Approve launch budget",
29
+ {
30
+ PM: "A",
31
+ Finance: "A",
32
+ Marketing: "R"
33
+ }
34
+ ],
35
+ [
36
+ "Ship",
37
+ "Build and test release candidate",
38
+ {
39
+ PM: "A",
40
+ Engineering: "R",
41
+ Design: "C"
42
+ }
43
+ ],
44
+ [
45
+ "Ship",
46
+ "Write launch announcement",
47
+ {
48
+ PM: "A",
49
+ Design: "C",
50
+ Marketing: "C",
51
+ Support: "I"
52
+ }
53
+ ],
54
+ [
55
+ "Ship",
56
+ "Brief customer support",
57
+ {
58
+ PM: "A",
59
+ Marketing: "I",
60
+ Support: "R"
61
+ }
62
+ ]
63
+ ];
64
+ var RaciSamples = class {
65
+ /** The invented launch as long rows. A new seed rotates its tasks. */
66
+ static launch(seed = 1) {
67
+ const turn = (seed - 1) % LAUNCH.length;
68
+ return [...LAUNCH.slice(turn), ...LAUNCH.slice(0, turn)].flatMap(([phase, task, assignments]) => Object.entries(assignments).map(([role, assignment]) => ({
69
+ phase,
70
+ task,
71
+ role,
72
+ assignment
73
+ })));
74
+ }
75
+ };
76
+ export { RaciSamples };
@@ -0,0 +1,358 @@
1
+ import { Chart2D } from "../../core/Chart2D.js";
2
+ import { ChartFamily, DataShape, Dimension, LegendPosition } from "../../core/enums.js";
3
+ import { FrameworkText } from "./FrameworkText.js";
4
+ import { SwotItemMark, SwotLayout, SwotOrder, SwotQuadrant, SwotSizing } from "./swotEnums.js";
5
+ import { SwotSamples } from "./SwotSamples.js";
6
+ // src/charts/frameworks/SwotMatrix.ts
7
+ var QUADRANTS = Object.values(SwotQuadrant);
8
+ var HEADER = 24;
9
+ var PAD = 6;
10
+ var SIDE = 124;
11
+ var GAP = 6;
12
+ var RAIL = 18;
13
+ var NOTE = 16;
14
+ var ITEM_GAP = 4;
15
+ var DOT = 4;
16
+ var SIZES = [
17
+ 12,
18
+ 11,
19
+ 10
20
+ ];
21
+ var INTERNAL = /* @__PURE__ */ new Set([SwotQuadrant.Strengths, SwotQuadrant.Weaknesses]);
22
+ var HELPFUL = /* @__PURE__ */ new Set([SwotQuadrant.Strengths, SwotQuadrant.Opportunities]);
23
+ var NAMES = /* @__PURE__ */ new Map([
24
+ ["s", SwotQuadrant.Strengths],
25
+ ["strength", SwotQuadrant.Strengths],
26
+ ["strengths", SwotQuadrant.Strengths],
27
+ ["w", SwotQuadrant.Weaknesses],
28
+ ["weakness", SwotQuadrant.Weaknesses],
29
+ ["weaknesses", SwotQuadrant.Weaknesses],
30
+ ["o", SwotQuadrant.Opportunities],
31
+ ["opportunity", SwotQuadrant.Opportunities],
32
+ ["opportunities", SwotQuadrant.Opportunities],
33
+ ["t", SwotQuadrant.Threats],
34
+ ["threat", SwotQuadrant.Threats],
35
+ ["threats", SwotQuadrant.Threats]
36
+ ]);
37
+ /**
38
+ * Lists a situation's internal strengths and weaknesses against its external opportunities and
39
+ * threats. Method: SWOT analysis, usually traced to the Harvard business policy school (Learned,
40
+ * Christensen, Andrews and Guth, 1965): rows are origin, columns are effect. Items that do not fit
41
+ * are counted in a "+N more" line and text shrinks no further than 10px; only a lone item too long
42
+ * for its whole box ends in an ellipsis. Hidden and cut text stays in the tooltip and the CSV.
43
+ */
44
+ var SwotMatrix = class extends Chart2D {
45
+ static meta = {
46
+ id: "swot-matrix",
47
+ name: "SWOT matrix",
48
+ family: ChartFamily.Matrix,
49
+ dimension: Dimension.Two,
50
+ shape: DataShape.Table,
51
+ description: "Sets out strengths, weaknesses, opportunities and threats as four lists of wrapped text for strategy reviews.",
52
+ tags: [
53
+ "framework",
54
+ "strategy",
55
+ "swot",
56
+ "text",
57
+ "quadrant"
58
+ ],
59
+ sample: (seed) => SwotSamples.bakery(seed),
60
+ variants: {
61
+ layout: [
62
+ SwotLayout.Grid,
63
+ SwotLayout.Columns,
64
+ SwotLayout.Rows
65
+ ],
66
+ sizing: [SwotSizing.Equal, SwotSizing.Content],
67
+ framing: [true, false],
68
+ itemMark: [SwotItemMark.Bullet, SwotItemMark.Code]
69
+ },
70
+ /** Side-by-side columns share one height, so there is nothing to size. */
71
+ valid: (v) => v.layout !== SwotLayout.Columns || v.sizing === SwotSizing.Equal
72
+ };
73
+ defaults() {
74
+ return {
75
+ quadrant: "quadrant",
76
+ item: "item",
77
+ weight: "weight",
78
+ layout: SwotLayout.Grid,
79
+ sizing: SwotSizing.Equal,
80
+ framing: true,
81
+ itemMark: SwotItemMark.Bullet,
82
+ order: SwotOrder.Input
83
+ };
84
+ }
85
+ plotMargin() {
86
+ return {
87
+ top: 8,
88
+ right: 16,
89
+ bottom: 12,
90
+ left: 16
91
+ };
92
+ }
93
+ /** Each quadrant is named on its own header. */
94
+ get legendPosition() {
95
+ return LegendPosition.None;
96
+ }
97
+ /** Rows dropped for naming no SWOT quadrant or no item text. */
98
+ get skipped() {
99
+ return this.#parse().skipped;
100
+ }
101
+ #parse() {
102
+ const { quadrant, item, weight, data, order } = this.options;
103
+ const rows = Array.isArray(data) ? data : [];
104
+ const lists = Object.fromEntries(QUADRANTS.map((name) => [name, []]));
105
+ let skipped = 0;
106
+ for (const row of rows) {
107
+ const cells = row ?? {};
108
+ const name = NAMES.get(FrameworkText.clean(cells[quadrant]).toLowerCase()), text = FrameworkText.clean(cells[item]);
109
+ if (!name || !text) {
110
+ skipped++;
111
+ continue;
112
+ }
113
+ const raw = cells[weight], score = raw === null || raw === "" ? NaN : Number(raw);
114
+ lists[name].push({
115
+ quadrant: name,
116
+ text,
117
+ weight: Number.isFinite(score) ? score : null,
118
+ code: ""
119
+ });
120
+ }
121
+ for (const name of QUADRANTS) {
122
+ if (order === SwotOrder.Weight) lists[name].sort((a, b) => (b.weight ?? -Infinity) - (a.weight ?? -Infinity));
123
+ lists[name].forEach((entry, i) => {
124
+ entry.code = `${name[0]}${i + 1}`;
125
+ });
126
+ }
127
+ return {
128
+ lists,
129
+ skipped
130
+ };
131
+ }
132
+ #indent(size) {
133
+ return this.options.itemMark === SwotItemMark.Code ? Math.ceil(FrameworkText.width("T00", size)) + 6 : 16;
134
+ }
135
+ /** Rows put the quadrant's heading beside its text, where the other layouts put it above. */
136
+ #side(width) {
137
+ return this.options.layout === SwotLayout.Rows ? Math.min(SIDE, width * .4) : 0;
138
+ }
139
+ #chrome() {
140
+ return (this.options.layout === SwotLayout.Rows ? 0 : HEADER) + 12;
141
+ }
142
+ /** Height a quadrant wants at this size and width. */
143
+ #wants(list, width, size) {
144
+ const lead = Math.round(size * 1.3), wrap = width - this.#side(width) - 12 - this.#indent(size);
145
+ const text = list.reduce((sum, entry) => sum + FrameworkText.wrap(entry.text, wrap, size).length * lead + ITEM_GAP, -4);
146
+ return this.#chrome() + Math.max(text, lead);
147
+ }
148
+ #heights(frame, grid, lists, width, size) {
149
+ const room = frame.height - GAP * (grid.length - 1);
150
+ const equal = room / grid.length;
151
+ if (this.options.sizing !== SwotSizing.Content) return grid.map(() => equal);
152
+ const floor = this.#chrome() + Math.round(size * 1.3) * 2 + ITEM_GAP;
153
+ const wants = grid.map((row) => Math.max(0, Math.max(...row.map((name) => this.#wants(lists[name], width, size))) - floor));
154
+ const total = wants.reduce((sum, want) => sum + want, 0), spare = room - floor * grid.length;
155
+ return total > 0 && spare > 0 ? wants.map((want) => floor + spare * (want / total)) : grid.map(() => equal);
156
+ }
157
+ #boxes(frame, lists, size) {
158
+ const { layout } = this.options;
159
+ const grid = layout === SwotLayout.Columns ? [QUADRANTS] : layout === SwotLayout.Rows ? QUADRANTS.map((name) => [name]) : [QUADRANTS.slice(0, 2), QUADRANTS.slice(2)];
160
+ const across = grid[0].length, width = (frame.width - GAP * (across - 1)) / across;
161
+ let y = frame.y;
162
+ return this.#heights(frame, grid, lists, width, size).flatMap((height, r) => {
163
+ const row = grid[r].map((quadrant, c) => ({
164
+ quadrant,
165
+ x: frame.x + c * (width + GAP),
166
+ y,
167
+ width,
168
+ height,
169
+ count: lists[quadrant].length,
170
+ side: this.#side(width)
171
+ }));
172
+ y += height + GAP;
173
+ return row;
174
+ });
175
+ }
176
+ /** Lists whole items while they fit, keeping a line for the count of the rest. Only a lone item too long for the box is cut. */
177
+ #fill(box, list, size) {
178
+ const lead = Math.round(size * 1.3), indent = this.#indent(size), inner = box.width - box.side - 12, width = inner - indent;
179
+ const left = box.x + box.side + PAD, x = left + indent, room = box.height - this.#chrome();
180
+ const wrapped = list.map((entry) => FrameworkText.wrap(entry.text, width, size));
181
+ const cost = (count) => wrapped.slice(0, count).reduce((sum, lines) => sum + lines.length * lead + ITEM_GAP, 0);
182
+ let shown = list.length;
183
+ if (cost(shown) - ITEM_GAP > room) {
184
+ shown--;
185
+ while (shown > 0 && cost(shown) + lead > room) shown--;
186
+ }
187
+ let cut = false;
188
+ if (shown === 0 && list.length) {
189
+ const rest = list.length > 1 ? lead + ITEM_GAP : 0, rows = Math.floor((room - rest) / lead);
190
+ if (rows >= 1) {
191
+ cut = wrapped[0].length > rows;
192
+ wrapped[0] = FrameworkText.clamp(wrapped[0], rows, width, size);
193
+ shown = 1;
194
+ }
195
+ }
196
+ let top = box.y + this.#chrome() - PAD;
197
+ const place = (entry, hidden, lines, key) => {
198
+ const block = {
199
+ key,
200
+ quadrant: box.quadrant,
201
+ entry,
202
+ hidden,
203
+ lines,
204
+ x,
205
+ y: top + size,
206
+ lead,
207
+ left,
208
+ top,
209
+ width: inner,
210
+ height: lines.length * lead,
211
+ size
212
+ };
213
+ top += block.height + ITEM_GAP;
214
+ return block;
215
+ };
216
+ const blocks = list.slice(0, shown).map((entry, i) => place(entry, [], wrapped[i], `${box.quadrant}:${i}`));
217
+ const hidden = list.slice(shown);
218
+ if (hidden.length && room >= lead + (shown ? cost(shown) : 0)) blocks.push(place(null, hidden, [`+${hidden.length} more`], `${box.quadrant}:more`));
219
+ return {
220
+ blocks,
221
+ whole: !hidden.length && !cut
222
+ };
223
+ }
224
+ #plan(frame, lists) {
225
+ let boxes = [], blocks = [];
226
+ for (const size of SIZES) {
227
+ boxes = this.#boxes(frame, lists, size);
228
+ const filled = boxes.map((box) => this.#fill(box, lists[box.quadrant], size));
229
+ blocks = filled.flatMap((fill) => fill.blocks);
230
+ if (filled.every((fill) => fill.whole)) break;
231
+ }
232
+ return {
233
+ boxes,
234
+ blocks
235
+ };
236
+ }
237
+ #rails(frame) {
238
+ const { layout, framing } = this.options;
239
+ if (!framing) return [];
240
+ const across = (frame.width - GAP) / 2, down = (frame.height - GAP) / 2;
241
+ const top = (key, i) => ({
242
+ key,
243
+ label: key,
244
+ x: frame.x + (across + GAP) * i + across / 2,
245
+ y: 11,
246
+ along: across,
247
+ turned: false
248
+ });
249
+ const side = (key, i) => ({
250
+ key,
251
+ label: key,
252
+ x: 11,
253
+ y: frame.y + (down + GAP) * i + down / 2,
254
+ along: down,
255
+ turned: true
256
+ });
257
+ if (layout === SwotLayout.Columns) return ["Internal", "External"].map(top);
258
+ if (layout === SwotLayout.Rows) return ["Internal", "External"].map(side);
259
+ return [...["Helpful", "Harmful"].map(top), ...["Internal", "External"].map(side)];
260
+ }
261
+ draw(ctx) {
262
+ const { framing, layout, itemMark } = this.options;
263
+ const { lists, skipped } = this.#parse(), { plot, theme, palette } = ctx;
264
+ const colors = new Map(QUADRANTS.map((name) => [name, palette.color(name)]));
265
+ const left = framing && layout !== SwotLayout.Columns ? RAIL : 0, top = framing && layout !== SwotLayout.Rows ? RAIL : 0;
266
+ const frame = {
267
+ x: left,
268
+ y: top,
269
+ width: Math.max(0, plot.width - left),
270
+ height: Math.max(0, plot.height - top - (skipped ? NOTE : 0))
271
+ };
272
+ const { boxes, blocks } = this.#plan(frame, lists);
273
+ const rails = ctx.text(ctx.marks("rails", "text", this.#rails(frame), (d) => d.key), 11).attr("text-anchor", "middle").attr("font-weight", 600);
274
+ rails.text((d) => FrameworkText.fit(d.label, d.along, 11));
275
+ ctx.set(rails, { transform: (d) => `translate(${d.x},${d.y})${d.turned ? " rotate(-90)" : ""}` });
276
+ const rect = {
277
+ x: (d) => d.x,
278
+ y: (d) => d.y,
279
+ width: (d) => d.width
280
+ };
281
+ const panels = ctx.marks("panels", "rect", boxes, (d) => d.quadrant).attr("rx", 4).attr("fill", (d) => colors.get(d.quadrant)).attr("fill-opacity", .1);
282
+ ctx.set(panels, {
283
+ ...rect,
284
+ height: (d) => d.height
285
+ });
286
+ const bands = ctx.marks("bands", "rect", boxes, (d) => d.quadrant).attr("rx", 4).attr("fill", (d) => colors.get(d.quadrant));
287
+ ctx.set(bands, {
288
+ x: (d) => d.x,
289
+ y: (d) => d.y,
290
+ width: (d) => d.side || d.width,
291
+ height: (d) => d.side ? d.height : Math.min(HEADER, d.height)
292
+ });
293
+ const shown = boxes.filter((box) => box.height >= HEADER);
294
+ const titles = ctx.text(ctx.marks("titles", "text", shown, (d) => d.quadrant), 12, "ink").attr("font-weight", 600).attr("fill", (d) => palette.inkOn(colors.get(d.quadrant)));
295
+ const headed = (d) => d.side || d.width;
296
+ titles.text((d) => FrameworkText.fit(d.quadrant, headed(d) - 12 - (headed(d) >= 70 ? 22 : 0), 12));
297
+ ctx.set(titles, {
298
+ x: (d) => d.x + PAD,
299
+ y: (d) => d.y + 16
300
+ });
301
+ const counts = ctx.text(ctx.marks("counts", "text", shown.filter((box) => headed(box) >= 70), (d) => d.quadrant), 12, "ink").attr("text-anchor", "end").attr("fill", (d) => palette.inkOn(colors.get(d.quadrant)));
302
+ counts.text((d) => d.count);
303
+ ctx.set(counts, {
304
+ x: (d) => d.x + headed(d) - PAD,
305
+ y: (d) => d.y + 16
306
+ });
307
+ const empties = boxes.filter((box) => !box.count && box.height >= this.#chrome() + 12);
308
+ const none = ctx.text(ctx.marks("none", "text", empties, (d) => d.quadrant), 11, "muted").text("None listed");
309
+ ctx.set(none, {
310
+ x: (d) => d.x + d.side + PAD,
311
+ y: (d) => d.y + this.#chrome() - PAD + 11
312
+ });
313
+ const items = ctx.marks("items", "g", blocks, (d) => d.key);
314
+ ctx.hover(items, (d) => {
315
+ const color = colors.get(d.quadrant);
316
+ if (!d.entry) return {
317
+ title: `${d.hidden.length} more ${d.quadrant}`,
318
+ color,
319
+ rows: d.hidden.flatMap((entry) => FrameworkText.wrap(entry.text, 320, 12).map((line) => [line, ""]))
320
+ };
321
+ const score = d.entry.weight === null ? [] : [["Weight", d.entry.weight]];
322
+ return FrameworkText.tip(d.entry.text, [
323
+ ["Quadrant", d.quadrant],
324
+ ["Origin", INTERNAL.has(d.quadrant) ? "Internal" : "External"],
325
+ ["Effect", HELPFUL.has(d.quadrant) ? "Helpful" : "Harmful"],
326
+ ...score
327
+ ], color);
328
+ });
329
+ const hits = items.selectAll(":scope > rect").data((d) => [d]).join("rect").attr("fill", "transparent");
330
+ ctx.set(hits, {
331
+ x: (d) => d.left,
332
+ y: (d) => d.top,
333
+ width: (d) => d.width,
334
+ height: (d) => d.height
335
+ });
336
+ const bulleted = (d) => itemMark === SwotItemMark.Bullet && d.entry ? [d] : [];
337
+ const dots = items.selectAll("circle").data(bulleted).join("circle").attr("r", DOT).attr("fill", (d) => colors.get(d.quadrant)).attr("stroke", theme.surface).attr("stroke-width", 2);
338
+ ctx.set(dots, {
339
+ cx: (d) => d.left + DOT + 1,
340
+ cy: (d) => d.top + d.lead / 2
341
+ });
342
+ const coded = (d) => itemMark === SwotItemMark.Code && d.entry ? [d] : [];
343
+ const codes = ctx.text(items.selectAll("text.code").data(coded).join("text").attr("class", "code")).attr("font-weight", 600).attr("font-size", (d) => d.size).text((d) => d.entry.code);
344
+ ctx.set(codes, {
345
+ x: (d) => d.left,
346
+ y: (d) => d.y
347
+ });
348
+ const body = ctx.text(items.selectAll("text.body").data((d) => [d]).join("text").attr("class", "body"), 11, "ink").attr("font-size", (d) => d.size).attr("pointer-events", "none");
349
+ FrameworkText.write(ctx, body);
350
+ const notes = skipped ? [`${skipped} ${skipped === 1 ? "row" : "rows"} left out: no SWOT quadrant or no item text`] : [];
351
+ const note = ctx.text(ctx.marks("note", "text", notes), 10.5, "muted").text((d) => FrameworkText.fit(d, plot.width, 10.5));
352
+ ctx.set(note, {
353
+ x: 0,
354
+ y: plot.height - 3
355
+ });
356
+ }
357
+ };
358
+ export { SwotMatrix };
@@ -0,0 +1,85 @@
1
+ import { SwotQuadrant } from "./swotEnums.js";
2
+ // src/charts/frameworks/SwotSamples.ts
3
+ /** SYNTHETIC: an invented neighbourhood bakery. Nothing here describes a real business or measurement. */
4
+ var BAKERY = [
5
+ [
6
+ SwotQuadrant.Strengths,
7
+ "Loyal weekday breakfast regulars",
8
+ 8
9
+ ],
10
+ [
11
+ SwotQuadrant.Strengths,
12
+ "Sourdough starter and recipes nobody nearby can copy",
13
+ 9
14
+ ],
15
+ [
16
+ SwotQuadrant.Strengths,
17
+ "Corner site with heavy foot traffic",
18
+ 7
19
+ ],
20
+ [
21
+ SwotQuadrant.Strengths,
22
+ "Staff who have stayed for years",
23
+ 5
24
+ ],
25
+ [
26
+ SwotQuadrant.Strengths,
27
+ "Own delivery bike",
28
+ 2
29
+ ],
30
+ [
31
+ SwotQuadrant.Weaknesses,
32
+ "Oven capacity caps the morning bake",
33
+ 8
34
+ ],
35
+ [
36
+ SwotQuadrant.Weaknesses,
37
+ "No online ordering or card-on-file for wholesale accounts",
38
+ 6
39
+ ],
40
+ [
41
+ SwotQuadrant.Weaknesses,
42
+ "Owner does all the buying",
43
+ 7
44
+ ],
45
+ [
46
+ SwotQuadrant.Opportunities,
47
+ "Office lunch catering within walking distance of the shop",
48
+ 7
49
+ ],
50
+ [
51
+ SwotQuadrant.Opportunities,
52
+ "Weekend baking classes",
53
+ 4
54
+ ],
55
+ [
56
+ SwotQuadrant.Opportunities,
57
+ "Wholesale bread for two new cafes",
58
+ 8
59
+ ],
60
+ [
61
+ SwotQuadrant.Threats,
62
+ "Flour and butter prices keep rising",
63
+ 9
64
+ ],
65
+ [
66
+ SwotQuadrant.Threats,
67
+ "A supermarket in-store bakery opening nearby next spring",
68
+ 7
69
+ ]
70
+ ];
71
+ var SwotSamples = class {
72
+ /** The invented bakery, with items rotated by seed so a new seed reorders each quadrant. */
73
+ static bakery(seed = 1) {
74
+ return Object.values(SwotQuadrant).flatMap((quadrant) => {
75
+ const items = BAKERY.filter((item) => item[0] === quadrant);
76
+ const turn = (seed - 1) % items.length;
77
+ return [...items.slice(turn), ...items.slice(0, turn)].map(([, item, weight]) => ({
78
+ quadrant,
79
+ item,
80
+ weight
81
+ }));
82
+ });
83
+ }
84
+ };
85
+ export { SwotSamples };