@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,130 @@
1
+ import { NetworkDeviceKind } from "./architectureEnums.js";
2
+ // src/charts/diagrams/ArchitectureShapes.ts
3
+ var f = (value) => +value.toFixed(2);
4
+ var circle = (cx, cy, r) => `M${f(cx - r)},${f(cy)}a${f(r)},${f(r)} 0 1,0 ${f(r * 2)},0a${f(r)},${f(r)} 0 1,0 ${f(-r * 2)},0Z`;
5
+ /** Path data for the diagram's boxes, connectors and device icons. Every shape stays inside the rect it is given. */
6
+ var ArchitectureShapes = class ArchitectureShapes {
7
+ static box({ x, y, width, height }, r = 6) {
8
+ const c = Math.min(r, width / 2, height / 2);
9
+ return `M${f(x + c)},${f(y)}h${f(width - 2 * c)}a${c},${c} 0 0,1 ${c},${c}v${f(height - 2 * c)}a${c},${c} 0 0,1 ${-c},${c}h${f(2 * c - width)}a${c},${c} 0 0,1 ${-c},${-c}v${f(2 * c - height)}a${c},${c} 0 0,1 ${c},${-c}Z`;
10
+ }
11
+ /** Head above a rounded body, split by a surface gap: the C4-PlantUML person. */
12
+ static person(rect, head) {
13
+ const r = head / 2 - 1;
14
+ return `${circle(rect.x + rect.width / 2, rect.y + r, r)}${ArchitectureShapes.box({
15
+ ...rect,
16
+ y: rect.y + head,
17
+ height: rect.height - head
18
+ }, 14)}`;
19
+ }
20
+ static cylinder({ x, y, width, height }, lid) {
21
+ const rx = width / 2;
22
+ return {
23
+ body: `M${f(x)},${f(y + lid)}a${f(rx)},${lid} 0 0,1 ${f(width)},0v${f(height - 2 * lid)}a${f(rx)},${lid} 0 0,1 ${f(-width)},0Z`,
24
+ lid: `M${f(x)},${f(y + lid)}a${f(rx)},${lid} 0 0,0 ${f(width)},0`
25
+ };
26
+ }
27
+ /** Polyline with its corners rounded by up to `r`, so orthogonal routes keep to their corridor. */
28
+ static route(points, r) {
29
+ let d = `M${f(points[0].x)},${f(points[0].y)}`;
30
+ for (let i = 1; i < points.length - 1; i++) {
31
+ const before = points[i - 1], at = points[i], after = points[i + 1];
32
+ const inward = Math.hypot(at.x - before.x, at.y - before.y), outward = Math.hypot(after.x - at.x, after.y - at.y);
33
+ const c = Math.min(r, inward / 2, outward / 2);
34
+ d += `L${f(at.x + (before.x - at.x) * c / inward)},${f(at.y + (before.y - at.y) * c / inward)}Q${f(at.x)},${f(at.y)} ${f(at.x + (after.x - at.x) * c / outward)},${f(at.y + (after.y - at.y) * c / outward)}`;
35
+ }
36
+ const last = points[points.length - 1];
37
+ return `${d}L${f(last.x)},${f(last.y)}`;
38
+ }
39
+ /** Filled triangle whose tip is on `tip`, pointing along the line from `from`. */
40
+ static arrow(from, tip, size) {
41
+ const angle = Math.atan2(tip.y - from.y, tip.x - from.x), wing = size * .55;
42
+ const bx = tip.x - Math.cos(angle) * size, by = tip.y - Math.sin(angle) * size;
43
+ const nx = -Math.sin(angle) * wing, ny = Math.cos(angle) * wing;
44
+ return `M${f(tip.x)},${f(tip.y)}L${f(bx + nx)},${f(by + ny)}L${f(bx - nx)},${f(by - ny)}Z`;
45
+ }
46
+ /** Network icons on the common Cisco conventions: a round router, a flat switch, a rack server, a cloud and a cylinder. */
47
+ static device(kind, cx, cy, size) {
48
+ const half = size / 2;
49
+ switch (kind) {
50
+ case NetworkDeviceKind.Router: return {
51
+ body: circle(cx, cy, half),
52
+ glyph: ArchitectureShapes.#crossArrows(cx, cy, half * .62)
53
+ };
54
+ case NetworkDeviceKind.Switch: {
55
+ const rect = {
56
+ x: cx - half * 1.2,
57
+ y: cy - half * .62,
58
+ width: size * 1.2,
59
+ height: size * .62
60
+ };
61
+ return {
62
+ body: ArchitectureShapes.box(rect, 4),
63
+ glyph: ArchitectureShapes.#laneArrows(cx, cy, half * .8, half * .24)
64
+ };
65
+ }
66
+ case NetworkDeviceKind.Server: {
67
+ const rect = {
68
+ x: cx - half * .72,
69
+ y: cy - half,
70
+ width: size * .72,
71
+ height: size
72
+ };
73
+ const rows = [
74
+ -.55,
75
+ -.2,
76
+ .15
77
+ ].map((row) => `M${f(cx - half * .46)},${f(cy + row * half)}h${f(half * .92)}`).join("");
78
+ return {
79
+ body: ArchitectureShapes.box(rect, 4),
80
+ glyph: `${rows}M${f(cx - half * .1)},${f(cy + half * .62)}h${f(half * .2)}`
81
+ };
82
+ }
83
+ case NetworkDeviceKind.Database: {
84
+ const lid = size * .16;
85
+ const drum = ArchitectureShapes.cylinder({
86
+ x: cx - half * .8,
87
+ y: cy - half,
88
+ width: size * .8,
89
+ height: size
90
+ }, lid);
91
+ return {
92
+ body: drum.body,
93
+ glyph: drum.lid
94
+ };
95
+ }
96
+ case NetworkDeviceKind.Cloud: {
97
+ const s = size / 32, x = (value) => f(cx + value * s), y = (value) => f(cy + value * s), r = (value) => f(value * s);
98
+ return {
99
+ body: `M${x(-14)},${y(9)}A${r(7)},${r(7)} 0 0,1 ${x(-15)},${y(-4.8)}A${r(9)},${r(9)} 0 0,1 ${x(-1)},${y(-8.6)}A${r(7)},${r(7)} 0 0,1 ${x(11)},${y(-4)}A${r(6.6)},${r(6.6)} 0 0,1 ${x(14)},${y(9)}Z`,
100
+ glyph: ""
101
+ };
102
+ }
103
+ }
104
+ }
105
+ static #crossArrows(cx, cy, r) {
106
+ const h = r * .4;
107
+ const point = (angle, inward) => {
108
+ const ax = Math.cos(angle), ay = Math.sin(angle);
109
+ const [from, to] = inward ? [r, r * .28] : [r * .28, r];
110
+ const tip = {
111
+ x: cx + ax * to,
112
+ y: cy + ay * to
113
+ }, sign = inward ? 1 : -1;
114
+ const wing = (side) => `M${f(tip.x + sign * ax * h + side * -ay * h)},${f(tip.y + sign * ay * h + side * ax * h)}L${f(tip.x)},${f(tip.y)}`;
115
+ return `M${f(cx + ax * from)},${f(cy + ay * from)}L${f(tip.x)},${f(tip.y)}${wing(1)}${wing(-1)}`;
116
+ };
117
+ return [
118
+ point(Math.PI / 4, false),
119
+ point(5 * Math.PI / 4, false),
120
+ point(3 * Math.PI / 4, true),
121
+ point(-Math.PI / 4, true)
122
+ ].join("");
123
+ }
124
+ static #laneArrows(cx, cy, reach, gap) {
125
+ const head = gap * .9;
126
+ const lane = (y, sign) => `M${f(cx - sign * reach)},${f(y)}H${f(cx + sign * reach)}m${f(-sign * head)},${f(-head)}l${f(sign * head)},${f(head)}l${f(-sign * head)},${f(head)}`;
127
+ return lane(cy - gap, 1) + lane(cy + gap, -1);
128
+ }
129
+ };
130
+ export { ArchitectureShapes };
@@ -0,0 +1,25 @@
1
+ import { DiagramText } from "./DiagramText.js";
2
+ // src/charts/diagrams/ArchitectureText.ts
3
+ var CHAR = .58;
4
+ /** Width estimates and wrapping for text fitted to boxes without measuring the DOM. */
5
+ var ArchitectureText = class ArchitectureText {
6
+ static width(text, size) {
7
+ return DiagramText.width(text, size, CHAR);
8
+ }
9
+ static fit(text, room, size) {
10
+ return DiagramText.fit(text, room, size, CHAR);
11
+ }
12
+ /** Breaks at spaces into at most `lines` lines, cutting the last one if text remains. */
13
+ static wrap(text, room, size, lines) {
14
+ const out = [];
15
+ let rest = text.trim().split(/\s+/).filter(Boolean);
16
+ while (rest.length && out.length < lines) {
17
+ let line = rest[0], used = 1;
18
+ while (used < rest.length && ArchitectureText.width(`${line} ${rest[used]}`, size) <= room) line += ` ${rest[used++]}`;
19
+ rest = rest.slice(used);
20
+ out.push(ArchitectureText.fit(out.length === lines - 1 && rest.length ? `${line} ${rest.join(" ")}` : line, room, size));
21
+ }
22
+ return out.filter(Boolean);
23
+ }
24
+ };
25
+ export { ArchitectureText };
@@ -0,0 +1,38 @@
1
+ // src/charts/diagrams/DiagramText.ts
2
+ var ASCII = /^[\x00-\x7f]*$/;
3
+ var NO_ADVANCE = /[\p{Mn}\p{Me}​-‍︀-️]/u;
4
+ var EMOJI = /\p{Extended_Pictographic}/u;
5
+ var FULL = /[ᄀ-ᅟ⺀-〾ぁ-꓏가-힣豈-﫿︰-﹯＀-⦆¢-₩\u{20000}-\u{3FFFD}]/u;
6
+ var EMOJI_EM = 1.35;
7
+ var FULL_EM = 1.05;
8
+ /**
9
+ * Width estimates for labels placed without measuring the DOM. A plain per-character estimate
10
+ * under-reads CJK and emoji by half or more, and cutting by UTF-16 unit can split a surrogate
11
+ * pair, so this works in code points. `narrow` is the em width of an ordinary character.
12
+ */
13
+ var DiagramText = class DiagramText {
14
+ static #advance(char, narrow) {
15
+ if (NO_ADVANCE.test(char)) return 0;
16
+ return FULL.test(char) ? FULL_EM : EMOJI.test(char) && char.length > 1 ? EMOJI_EM : narrow;
17
+ }
18
+ static width(text, size, narrow = .56) {
19
+ if (ASCII.test(text)) return text.length * size * narrow;
20
+ let ems = 0;
21
+ for (const char of text) ems += DiagramText.#advance(char, narrow);
22
+ return ems * size;
23
+ }
24
+ /** Cuts `text` to `room` pixels, marking the cut with an ellipsis, or returns nothing when under two characters would show. */
25
+ static fit(text, room, size, narrow = .56) {
26
+ if (DiagramText.width(text, size, narrow) <= room) return text;
27
+ const limit = room / size - narrow;
28
+ let used = 0, kept = "", count = 0;
29
+ for (const char of text) {
30
+ used += DiagramText.#advance(char, narrow);
31
+ if (used > limit + 1e-9) break;
32
+ kept += char;
33
+ count++;
34
+ }
35
+ return count && room >= 2 * size * narrow ? `${kept.trimEnd()}…` : "";
36
+ }
37
+ };
38
+ export { DiagramText };
@@ -0,0 +1,465 @@
1
+ import { Chart2D } from "../../core/Chart2D.js";
2
+ import { ChartFamily, DataShape, Dimension } from "../../core/enums.js";
3
+ import { UmlDirection } from "../structure/structureEnums.js";
4
+ import { UmlLayout } from "../structure/UmlLayout.js";
5
+ import { DiagramText } from "./DiagramText.js";
6
+ import { ER_RING, ErCrowsFoot } from "./ErCrowsFoot.js";
7
+ import { ErSamples } from "./ErSamples.js";
8
+ import { ErCardinality, ErSchema } from "./ErSchema.js";
9
+ // src/charts/diagrams/EntityRelationshipDiagram.ts
10
+ var ErDetail = /* @__PURE__ */ function(ErDetail) {
11
+ ErDetail["Full"] = "full";
12
+ ErDetail["Keys"] = "keys";
13
+ ErDetail["Names"] = "names";
14
+ return ErDetail;
15
+ }({});
16
+ var HEAD = 24;
17
+ var LINE = 14;
18
+ var TEXT = 10.5;
19
+ var BADGE = 9;
20
+ var PAD = 7;
21
+ var GAP = 10;
22
+ var MIN_WIDTH = 84;
23
+ var MAX_WIDTH = 236;
24
+ var MIN_TYPE = 44;
25
+ var LABEL = 10;
26
+ var LABEL_ROOM = 110;
27
+ /** Room for both end marks of a self-reference on one face, for UmlLayout's loop at 35% and 65%. */
28
+ var LOOP_HEIGHT = 50;
29
+ /** Face length each port takes when several self-references share a face. */
30
+ var LOOP_PORT = 16;
31
+ var CLEARANCE = 30;
32
+ var END_WORDS = {
33
+ [ErCardinality.ExactlyOne]: "Exactly one",
34
+ [ErCardinality.ZeroOrOne]: "Zero or one",
35
+ [ErCardinality.OneOrMany]: "One or many",
36
+ [ErCardinality.ZeroOrMany]: "Zero or many"
37
+ };
38
+ var overlaps = (a, b) => a.left < b.right && b.left < a.right && a.top < b.bottom && b.top < a.bottom;
39
+ /**
40
+ * Entity-relationship diagram of a relational schema in Information Engineering crow's foot notation
41
+ * (Martin, 1981; Everest, 1976). Takes one row per table column, as `information_schema` gives them. The parent
42
+ * end follows FK nullability and the child end whether the FK is unique. Identifying relationships are solid and
43
+ * non-identifying ones dashed. Use it to document a database; for object models use the UML class diagram.
44
+ */
45
+ var EntityRelationshipDiagram = class extends Chart2D {
46
+ static meta = {
47
+ id: "entity-relationship-diagram",
48
+ name: "Entity-relationship diagram",
49
+ family: ChartFamily.Network,
50
+ dimension: Dimension.Two,
51
+ shape: DataShape.Table,
52
+ description: "Lays out database tables with their columns and keys, joined by orthogonal foreign key lines with crow's foot cardinality marks.",
53
+ tags: [
54
+ "erd",
55
+ "database",
56
+ "schema",
57
+ "crows-foot",
58
+ "foreign-key"
59
+ ],
60
+ sample: (seed) => ErSamples.shop(seed),
61
+ variants: {
62
+ direction: [UmlDirection.LeftRight, UmlDirection.TopDown],
63
+ detail: [
64
+ "full",
65
+ "keys",
66
+ "names"
67
+ ],
68
+ labels: [true, false]
69
+ }
70
+ };
71
+ defaults() {
72
+ return {
73
+ direction: UmlDirection.LeftRight,
74
+ detail: "full",
75
+ labels: true,
76
+ table: "table",
77
+ column: "column",
78
+ dataType: "type",
79
+ key: "key",
80
+ references: "references",
81
+ nullable: "nullable",
82
+ constraint: "constraint",
83
+ relation: "relation",
84
+ parentEnd: "parentEnd",
85
+ childEnd: "childEnd"
86
+ };
87
+ }
88
+ plotMargin() {
89
+ return {
90
+ top: 8,
91
+ right: 12,
92
+ bottom: 8,
93
+ left: 12
94
+ };
95
+ }
96
+ legendItems() {
97
+ return [];
98
+ }
99
+ boxView(table, loops) {
100
+ const { detail } = this.options;
101
+ const shown = detail === "names" ? [] : table.columns.filter((column) => detail === "full" || column.primary || column.foreign || column.unique);
102
+ const ordered = [...shown.filter((column) => column.primary), ...shown.filter((column) => !column.primary)];
103
+ const keyed = shown.filter((column) => column.primary).length;
104
+ const badge = (column) => [
105
+ column.primary && "PK",
106
+ column.foreign && "FK",
107
+ column.unique && "UK"
108
+ ].filter(Boolean).join(" ");
109
+ const badgeWidth = Math.ceil(Math.max(0, ...ordered.map((column) => DiagramText.width(badge(column), BADGE) + (badge(column) ? 6 : 0))));
110
+ const nameWidth = Math.max(0, ...ordered.map((column) => DiagramText.width(column.name, TEXT)));
111
+ const typeWidth = Math.max(0, ...ordered.map((column) => DiagramText.width(column.type, TEXT)));
112
+ const inner = badgeWidth + nameWidth + (typeWidth ? typeWidth + GAP : 0);
113
+ const face = loops > 1 ? (2 * loops + 1) * LOOP_PORT : 0;
114
+ const width = Math.ceil(Math.max(face, Math.min(MAX_WIDTH, Math.max(MIN_WIDTH, DiagramText.width(table.id, 12) * 1.04 + 16, inner + 14))));
115
+ const room = width - 14 - badgeWidth - (typeWidth ? GAP : 0);
116
+ const typeRoom = Math.min(typeWidth, Math.max(Math.min(typeWidth, MIN_TYPE), room - nameWidth));
117
+ const rules = keyed && keyed < ordered.length ? [27 + keyed * LINE + 2.5] : [];
118
+ const rows = ordered.map((column, i) => ({
119
+ column,
120
+ badge: badge(column),
121
+ y: 27 + i * LINE + (keyed && i >= keyed ? 5 : 0)
122
+ }));
123
+ const body = rows.length ? rows.at(-1).y + LINE + 3 : HEAD;
124
+ return {
125
+ table,
126
+ id: table.id,
127
+ width,
128
+ height: Math.max(body, loops ? Math.max(LOOP_HEIGHT, face) : 0),
129
+ rows,
130
+ rules,
131
+ badgeWidth,
132
+ nameRoom: room - typeRoom,
133
+ typeRoom
134
+ };
135
+ }
136
+ draw(ctx) {
137
+ const { direction, labels } = this.options;
138
+ const fields = this.options;
139
+ const { tables, relationships } = ErSchema.read(Array.isArray(this.options.data) ? this.options.data : [], fields);
140
+ const loops = /* @__PURE__ */ new Map();
141
+ for (const link of relationships) if (link.parent === link.child) loops.set(link.child, (loops.get(link.child) ?? 0) + 1);
142
+ const views = tables.map((table) => this.boxView(table, loops.get(table.id) ?? 0));
143
+ const edges = relationships.map((link) => ({
144
+ upper: link.parent,
145
+ lower: link.child,
146
+ priority: link.identifying ? 0 : 1,
147
+ trunk: null
148
+ }));
149
+ const plan = UmlLayout.place(views, edges, {
150
+ horizontal: direction === UmlDirection.LeftRight,
151
+ orthogonal: true,
152
+ clearance: CLEARANCE
153
+ });
154
+ const scale = plan.width ? Math.min(1, (ctx.plot.width - 2) / plan.width, (ctx.plot.height - 2) / plan.height) : 1;
155
+ const left = (ctx.plot.width - plan.width * scale) / 2, top = (ctx.plot.height - plan.height * scale) / 2;
156
+ const stage = ctx.nest(`translate(${left},${top}) scale(${scale})`, "erd");
157
+ const byId = new Map(views.map((view) => [view.id, view]));
158
+ const placed = plan.boxes.map((box) => ({
159
+ view: byId.get(box.id),
160
+ box
161
+ }));
162
+ const turns = /* @__PURE__ */ new Map();
163
+ const routes = relationships.map((relationship, i) => {
164
+ const points = this.square(plan.routes[i]), { child, parent, key } = relationship;
165
+ if (child !== parent || loops.get(child) < 2) return {
166
+ key,
167
+ relationship,
168
+ points
169
+ };
170
+ const turn = turns.get(child) ?? 0;
171
+ turns.set(child, turn + 1);
172
+ return {
173
+ key,
174
+ relationship,
175
+ points: this.stackLoop(points, placed.find(({ box }) => box.id === child).box, turn, loops.get(child))
176
+ };
177
+ });
178
+ const lines = stage.marks("edges", "path", routes, (d) => d.key).attr("fill", "none").attr("stroke", ctx.theme.inkSecondary).attr("stroke-width", 2).attr("stroke-linejoin", "round").attr("stroke-dasharray", (d) => d.relationship.identifying ? null : "6 4");
179
+ ctx.set(lines, { d: (d) => `M${d.points.map((point) => `${point.x},${point.y}`).join("L")}` });
180
+ ctx.hover(lines, (d) => this.linkTip(d.relationship));
181
+ const marks = routes.flatMap(({ key, relationship, points }) => [{
182
+ key: `${key}:p`,
183
+ ...ErCrowsFoot.mark(points[0], points[1], relationship.parentEnd)
184
+ }, {
185
+ key: `${key}:c`,
186
+ ...ErCrowsFoot.mark(points.at(-1), points.at(-2), relationship.childEnd)
187
+ }]);
188
+ const feet = stage.marks("feet", "path", marks, (d) => d.key).attr("fill", "none").attr("stroke", ctx.theme.inkSecondary).attr("stroke-width", 2).attr("pointer-events", "none");
189
+ ctx.set(feet, { d: (d) => d.d });
190
+ const rings = stage.marks("rings", "circle", marks.filter((d) => d.ring), (d) => d.key).attr("fill", ctx.theme.surface).attr("stroke", ctx.theme.inkSecondary).attr("stroke-width", 2).attr("pointer-events", "none");
191
+ ctx.set(rings, {
192
+ cx: (d) => d.ring.x,
193
+ cy: (d) => d.ring.y,
194
+ r: ER_RING
195
+ });
196
+ const boxes = stage.marks("boxes", "rect", placed, (d) => d.box.id).attr("fill", ctx.theme.surface).attr("stroke", ctx.theme.axis).attr("stroke-width", 1.5).attr("rx", 3);
197
+ ctx.set(boxes, {
198
+ x: (d) => d.box.x,
199
+ y: (d) => d.box.y,
200
+ width: (d) => d.box.width,
201
+ height: (d) => d.box.height
202
+ });
203
+ ctx.hover(boxes, (d) => this.tableTip(d, relationships));
204
+ const titles = stage.marks("titles", "rect", placed, (d) => d.box.id).attr("fill", ctx.theme.grid).attr("rx", 3).attr("pointer-events", "none");
205
+ ctx.set(titles, {
206
+ x: (d) => d.box.x + 1,
207
+ y: (d) => d.box.y + 1,
208
+ width: (d) => d.box.width - 2,
209
+ height: (d) => (d.view.rows.length ? HEAD : d.box.height) - 2
210
+ });
211
+ const rules = placed.flatMap(({ box, view }) => [...view.rows.length ? [HEAD] : [], ...view.rules].map((at, i) => ({
212
+ key: `${box.id}:${i}`,
213
+ x1: box.x,
214
+ x2: box.x + box.width,
215
+ y: box.y + at
216
+ })));
217
+ const dividers = stage.marks("dividers", "line", rules, (d) => d.key).attr("stroke", ctx.theme.axis).attr("stroke-width", 1).attr("pointer-events", "none");
218
+ ctx.set(dividers, {
219
+ x1: (d) => d.x1,
220
+ x2: (d) => d.x2,
221
+ y1: (d) => d.y,
222
+ y2: (d) => d.y
223
+ });
224
+ const rows = placed.flatMap(({ box, view }) => view.rows.map((row) => ({
225
+ row,
226
+ box,
227
+ table: view.id
228
+ })));
229
+ const strips = stage.marks("rows", "rect", rows, (d) => `${d.table}:${d.row.column.name}`).attr("fill", "transparent");
230
+ ctx.set(strips, {
231
+ x: (d) => d.box.x + 1,
232
+ y: (d) => d.box.y + d.row.y,
233
+ width: (d) => d.box.width - 2,
234
+ height: LINE
235
+ });
236
+ ctx.hover(strips, (d) => this.columnTip(d.table, d.row.column));
237
+ const texts = stage.marks("text", "text", placed.flatMap((item) => this.lines(item)), (d) => d.key).attr("pointer-events", "none").attr("font-family", ctx.theme.fontFamily).attr("font-size", (d) => d.size).attr("font-weight", (d) => d.bold ? 600 : 400).attr("text-anchor", (d) => d.anchor).attr("fill", (d) => d.tone === "ink" ? ctx.theme.ink : ctx.theme.inkSecondary).text((d) => d.text);
238
+ ctx.set(texts, {
239
+ x: (d) => d.x,
240
+ y: (d) => d.y
241
+ });
242
+ const names = labels ? this.relationLabels(routes, plan.boxes, plan.width, plan.height) : [];
243
+ const captions = ctx.text(stage.marks("captions", "text", names, (d) => d.key), LABEL).attr("text-anchor", (d) => d.anchor).attr("pointer-events", "none").attr("font-style", "italic").attr("stroke", ctx.theme.surface).attr("stroke-width", 3).attr("paint-order", "stroke").text((d) => d.text);
244
+ ctx.set(captions, {
245
+ x: (d) => d.x,
246
+ y: (d) => d.y
247
+ });
248
+ }
249
+ /** UmlLayout draws every self-reference of a table on the same two ports, so each after the first would hide under it. */
250
+ stackLoop(points, box, turn, count) {
251
+ const upright = points[0].x === points.at(-1).x;
252
+ const [start, length] = upright ? [box.y, box.height] : [box.x, box.width];
253
+ const port = (slot) => start + length * (slot + 1) / (2 * count + 1);
254
+ return points.map((point, i) => {
255
+ const at = port(2 * turn + (i < 2 ? 0 : 1));
256
+ return upright ? {
257
+ ...point,
258
+ y: at
259
+ } : {
260
+ ...point,
261
+ x: at
262
+ };
263
+ });
264
+ }
265
+ /** UmlLayout leaves bends under half a pixel unrouted, which would slant a line. */
266
+ square(points) {
267
+ const out = [points[0]];
268
+ for (const { x, y } of points.slice(1)) {
269
+ const last = out.at(-1), flat = {
270
+ x: Math.abs(x - last.x) < 1 ? last.x : x,
271
+ y: Math.abs(y - last.y) < 1 ? last.y : y
272
+ };
273
+ if (flat.x !== last.x || flat.y !== last.y) out.push(flat);
274
+ }
275
+ return out.length > 1 ? out : points;
276
+ }
277
+ lines({ box, view }) {
278
+ const drop = view.rows.length ? 0 : (box.height - HEAD) / 2;
279
+ const out = [{
280
+ key: `${box.id}:n`,
281
+ text: DiagramText.fit(box.id, box.width - 10, 12),
282
+ x: box.x + box.width / 2,
283
+ y: box.y + drop + 16,
284
+ size: 12,
285
+ bold: true,
286
+ anchor: "middle",
287
+ tone: "ink"
288
+ }];
289
+ for (const { column, y, badge } of view.rows) {
290
+ const base = box.y + y + 10.5, name = column.name, key = `${box.id}:${name}`;
291
+ if (badge) out.push({
292
+ key: `${key}:k`,
293
+ text: badge,
294
+ x: box.x + PAD,
295
+ y: base,
296
+ size: BADGE,
297
+ bold: true,
298
+ anchor: "start",
299
+ tone: "secondary"
300
+ });
301
+ out.push({
302
+ key: `${key}:c`,
303
+ text: DiagramText.fit(name, view.nameRoom, TEXT),
304
+ x: box.x + PAD + view.badgeWidth,
305
+ y: base,
306
+ size: TEXT,
307
+ bold: column.primary,
308
+ anchor: "start",
309
+ tone: "ink"
310
+ });
311
+ if (column.type) out.push({
312
+ key: `${key}:t`,
313
+ text: DiagramText.fit(column.type, view.typeRoom, TEXT),
314
+ x: box.x + box.width - PAD,
315
+ y: base,
316
+ size: TEXT,
317
+ bold: false,
318
+ anchor: "end",
319
+ tone: "secondary"
320
+ });
321
+ }
322
+ return out;
323
+ }
324
+ /** Puts each name beside its own line, keeping clear of boxes, end marks and other names. A name with no free spot stays in the tooltip. */
325
+ relationLabels(routes, boxes, width, height) {
326
+ const taken = boxes.map((box) => ({
327
+ left: box.x,
328
+ top: box.y,
329
+ right: box.x + box.width,
330
+ bottom: box.y + box.height
331
+ }));
332
+ const reach = 21;
333
+ for (const { points } of routes) for (const [tip, next] of [[points[0], points[1]], [points.at(-1), points.at(-2)]]) {
334
+ const size = Math.hypot(next.x - tip.x, next.y - tip.y) || 1;
335
+ const end = {
336
+ x: tip.x + (next.x - tip.x) / size * reach,
337
+ y: tip.y + (next.y - tip.y) / size * reach
338
+ };
339
+ taken.push({
340
+ left: Math.min(tip.x, end.x) - 7,
341
+ top: Math.min(tip.y, end.y) - 7,
342
+ right: Math.max(tip.x, end.x) + 7,
343
+ bottom: Math.max(tip.y, end.y) + 7
344
+ });
345
+ }
346
+ const wires = routes.flatMap(({ points }) => points.slice(1).map((point, i) => {
347
+ const from = points[i];
348
+ return {
349
+ left: Math.min(from.x, point.x) - 1,
350
+ top: Math.min(from.y, point.y) - 1,
351
+ right: Math.max(from.x, point.x) + 1,
352
+ bottom: Math.max(from.y, point.y) + 1
353
+ };
354
+ }));
355
+ const out = [];
356
+ for (const { key, relationship, points } of routes) {
357
+ if (!relationship.label) continue;
358
+ const text = DiagramText.fit(relationship.label, LABEL_ROOM, LABEL);
359
+ const spot = this.spots(points, DiagramText.width(text, LABEL)).find(({ area }) => area.left >= 0 && area.top >= 0 && area.right <= width && area.bottom <= height && !taken.some((other) => overlaps(area, other)) && !wires.some((other) => overlaps(area, other)));
360
+ if (!spot || !text) continue;
361
+ taken.push(spot.area);
362
+ out.push({
363
+ key,
364
+ text,
365
+ x: spot.x,
366
+ y: spot.y,
367
+ anchor: spot.anchor
368
+ });
369
+ }
370
+ return out;
371
+ }
372
+ /** Candidate name positions along a route, longest segment and middle first. */
373
+ spots(points, size) {
374
+ return points.slice(1).map((point, i) => ({
375
+ a: points[i],
376
+ b: point,
377
+ length: Math.hypot(point.x - points[i].x, point.y - points[i].y)
378
+ })).sort((p, q) => q.length - p.length).flatMap(({ a, b }) => [
379
+ .5,
380
+ .3,
381
+ .7
382
+ ].flatMap((t) => {
383
+ const x = a.x + (b.x - a.x) * t, y = a.y + (b.y - a.y) * t;
384
+ if (Math.abs(b.y - a.y) >= Math.abs(b.x - a.x)) return [{
385
+ x: x + 6,
386
+ y: y + 3.5,
387
+ anchor: "start",
388
+ area: {
389
+ left: x + 5,
390
+ top: y - 7,
391
+ right: x + 7 + size,
392
+ bottom: y + 6
393
+ }
394
+ }, {
395
+ x: x - 6,
396
+ y: y + 3.5,
397
+ anchor: "end",
398
+ area: {
399
+ left: x - 7 - size,
400
+ top: y - 7,
401
+ right: x - 5,
402
+ bottom: y + 6
403
+ }
404
+ }];
405
+ const half = size / 2 + 1;
406
+ return [{
407
+ x,
408
+ y: y - 5,
409
+ anchor: "middle",
410
+ area: {
411
+ left: x - half,
412
+ top: y - 15,
413
+ right: x + half,
414
+ bottom: y - 2
415
+ }
416
+ }, {
417
+ x,
418
+ y: y + 12,
419
+ anchor: "middle",
420
+ area: {
421
+ left: x - half,
422
+ top: y + 2,
423
+ right: x + half,
424
+ bottom: y + 15
425
+ }
426
+ }];
427
+ }));
428
+ }
429
+ linkTip(link) {
430
+ const rows = [
431
+ [`${link.parent} (parent) end`, END_WORDS[link.parentEnd]],
432
+ [`${link.child} (child) end`, END_WORDS[link.childEnd]],
433
+ ["Kind", link.identifying ? "Identifying" : "Non-identifying"]
434
+ ];
435
+ if (link.label) rows.push(["Name", link.label]);
436
+ if (link.constraint) rows.push(["Constraint", link.constraint]);
437
+ return {
438
+ title: `${link.child}.${link.columns.join(", ")} → ${link.references.join(", ")}`,
439
+ rows
440
+ };
441
+ }
442
+ tableTip({ view }, links) {
443
+ const { columns } = view.table;
444
+ return {
445
+ title: view.id,
446
+ rows: [
447
+ ["Columns", columns.length],
448
+ ["Primary key", columns.filter((column) => column.primary).map((column) => column.name).join(", ") || "None"],
449
+ ["References", links.filter((link) => link.child === view.id).length],
450
+ ["Referenced by", links.filter((link) => link.parent === view.id).length]
451
+ ]
452
+ };
453
+ }
454
+ columnTip(table, column) {
455
+ const rows = [["Type", column.type || "Unknown"], ["Nullable", column.nullable ? "Yes" : "No"]];
456
+ if (column.primary) rows.push(["Key", "Primary"]);
457
+ else if (column.unique) rows.push(["Key", "Unique"]);
458
+ if (column.foreign) rows.push(["References", column.references]);
459
+ return {
460
+ title: `${table}.${column.name}`,
461
+ rows
462
+ };
463
+ }
464
+ };
465
+ export { EntityRelationshipDiagram, ErDetail };
@@ -0,0 +1,33 @@
1
+ import { ErCardinality } from "./ErSchema.js";
2
+ // src/charts/diagrams/ErCrowsFoot.ts
3
+ var SPREAD = 6;
4
+ var APEX = 11;
5
+ var NEAR = 5;
6
+ var FAR = 10;
7
+ var ER_RING = 3.5;
8
+ /**
9
+ * Information Engineering end marks. Reading out from the entity, the first symbol is the maximum
10
+ * (a bar for one, three prongs for many) and the second the minimum (a ring for zero, a bar for one).
11
+ */
12
+ var ErCrowsFoot = class {
13
+ /** `tip` lies on the entity's face and `next` is the following route point. */
14
+ static mark(tip, next, cardinality) {
15
+ const length = Math.hypot(next.x - tip.x, next.y - tip.y) || 1;
16
+ const ux = (next.x - tip.x) / length, uy = (next.y - tip.y) / length;
17
+ const at = (along, across) => `${tip.x + ux * along - uy * across},${tip.y + uy * along + ux * across}`;
18
+ const bar = (along) => `M${at(along, SPREAD)}L${at(along, -6)}`;
19
+ const many = cardinality === ErCardinality.OneOrMany || cardinality === ErCardinality.ZeroOrMany;
20
+ const optional = cardinality === ErCardinality.ZeroOrOne || cardinality === ErCardinality.ZeroOrMany;
21
+ const start = many ? APEX : NEAR;
22
+ const maximum = many ? `M${at(0, SPREAD)}L${at(APEX, 0)}L${at(0, -6)}` : bar(NEAR);
23
+ const center = start + ER_RING + 1;
24
+ return {
25
+ d: maximum + (optional ? "" : bar(start + FAR - NEAR)),
26
+ ring: optional ? {
27
+ x: tip.x + ux * center,
28
+ y: tip.y + uy * center
29
+ } : null
30
+ };
31
+ }
32
+ };
33
+ export { ER_RING, ErCrowsFoot };