@mstone6969/graph 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +111 -14
- package/dist/ExcelWriter-3ZSYUH4T.cjs +154 -0
- package/dist/GraphType.js +24 -38
- package/dist/PdfWriter-6HJFYMCO.cjs +205 -0
- package/dist/catalog-YWX6FHRM.cjs +8 -0
- package/dist/catalog.js +5 -1
- package/dist/charts/bars/index.js +2 -12
- package/dist/charts/comparison/EffectForestPlot.js +1 -1
- package/dist/charts/comparison/index.js +1 -8
- package/dist/charts/demography/Nomogram.js +3 -3
- package/dist/charts/density/HexbinChart.js +6 -4
- package/dist/charts/diagrams/ArchitectureCanvas.js +68 -0
- package/dist/charts/diagrams/ArchitectureDiagram.js +331 -0
- package/dist/charts/diagrams/ArchitectureLayout.js +106 -0
- package/dist/charts/diagrams/ArchitectureLinkLabels.js +78 -0
- package/dist/charts/diagrams/ArchitectureModel.js +108 -0
- package/dist/charts/diagrams/ArchitectureRouter.js +348 -0
- package/dist/charts/diagrams/ArchitectureSamples.js +312 -0
- package/dist/charts/diagrams/ArchitectureShapes.js +130 -0
- package/dist/charts/diagrams/ArchitectureText.js +25 -0
- package/dist/charts/diagrams/DiagramText.js +38 -0
- package/dist/charts/diagrams/EntityRelationshipDiagram.js +465 -0
- package/dist/charts/diagrams/ErCrowsFoot.js +33 -0
- package/dist/charts/diagrams/ErSamples.js +205 -0
- package/dist/charts/diagrams/ErSchema.js +122 -0
- package/dist/charts/diagrams/LogicCircuit.js +154 -0
- package/dist/charts/diagrams/LogicCircuitLayout.js +600 -0
- package/dist/charts/diagrams/LogicCircuitSamples.js +66 -0
- package/dist/charts/diagrams/LogicGateDiagram.js +216 -0
- package/dist/charts/diagrams/LogicGateSymbol.js +113 -0
- package/dist/charts/diagrams/NetworkTopologyDiagram.js +247 -0
- package/dist/charts/diagrams/SequenceDiagram.js +351 -0
- package/dist/charts/diagrams/SequenceLayout.js +422 -0
- package/dist/charts/diagrams/SequenceModel.js +221 -0
- package/dist/charts/diagrams/TopologyLayout.js +143 -0
- package/dist/charts/diagrams/architectureEnums.js +50 -0
- package/dist/charts/diagrams/index.js +26 -0
- package/dist/charts/diagrams/logicEnums.js +42 -0
- package/dist/charts/diagrams/sequenceEnums.js +40 -0
- package/dist/charts/distribution/HistogramChart.js +25 -3
- package/dist/charts/distribution/index.js +1 -3
- package/dist/charts/earthscience/HovmollerDiagram.js +477 -0
- package/dist/charts/earthscience/HovmollerEnums.js +42 -0
- package/dist/charts/earthscience/HovmollerField.js +180 -0
- package/dist/charts/earthscience/HovmollerSample.js +40 -0
- package/dist/charts/earthscience/HovmollerSpeed.js +71 -0
- package/dist/charts/earthscience/index.js +10 -2
- package/dist/charts/engineering/ConstellationDiagram.js +26 -6
- package/dist/charts/engineering/EyeDiagram.js +2 -1
- package/dist/charts/engineering/PoleZeroPlot.js +32 -8
- package/dist/charts/financial/RelativeStrengthRotation.js +434 -0
- package/dist/charts/financial/RelativeStrengthRotationMath.js +71 -0
- package/dist/charts/financial/RelativeStrengthRotationSamples.js +27 -0
- package/dist/charts/financial/index.js +5 -3
- package/dist/charts/flow/index.js +1 -5
- package/dist/charts/frameworks/AnsoffMatrix.js +523 -0
- package/dist/charts/frameworks/FrameworkText.js +98 -0
- package/dist/charts/frameworks/NpsGauge.js +279 -0
- package/dist/charts/frameworks/NpsSurvey.js +58 -0
- package/dist/charts/frameworks/PerceptualMap.js +419 -0
- package/dist/charts/frameworks/PerceptualProjection.js +142 -0
- package/dist/charts/frameworks/PositioningSamples.js +191 -0
- package/dist/charts/frameworks/RaciMatrix.js +458 -0
- package/dist/charts/frameworks/RaciPlan.js +81 -0
- package/dist/charts/frameworks/RaciSamples.js +76 -0
- package/dist/charts/frameworks/SwotMatrix.js +358 -0
- package/dist/charts/frameworks/SwotSamples.js +85 -0
- package/dist/charts/frameworks/VanWestendorpChart.js +187 -0
- package/dist/charts/frameworks/VanWestendorpSurvey.js +197 -0
- package/dist/charts/frameworks/index.js +26 -0
- package/dist/charts/frameworks/raciEnums.js +30 -0
- package/dist/charts/frameworks/swotEnums.js +34 -0
- package/dist/charts/genomics/CircosPlot.js +1 -1
- package/dist/charts/geoflows/GeoFlowChart.js +2 -2
- package/dist/charts/geoflows/GeoFlowSample.js +2 -1
- package/dist/charts/geomaps/FloorPlanMap.js +324 -0
- package/dist/charts/geomaps/FloorPlanModel.js +179 -0
- package/dist/charts/geomaps/FloorPlanSamples.js +416 -0
- package/dist/charts/geomaps/GeoLand.js +2 -2
- package/dist/charts/geomaps/GeoSamples.js +2 -1
- package/dist/charts/geomaps/GraticuleMap.js +2 -2
- package/dist/charts/geomaps/RoomLabelFit.js +200 -0
- package/dist/charts/geomaps/TissotMap.js +2 -2
- package/dist/charts/geomaps/index.js +8 -4
- package/dist/charts/hierarchy/SunburstChart.js +17 -10
- package/dist/charts/hierarchy/index.js +2 -11
- package/dist/charts/indicators/BulletChart.js +8 -2
- package/dist/charts/indicators/SpeedometerChart.js +1 -1
- package/dist/charts/indicators/ThermometerChart.js +1 -1
- package/dist/charts/lifescience/LoessFit.js +113 -0
- package/dist/charts/lifescience/MaPlot.js +337 -0
- package/dist/charts/lifescience/MaPlotSamples.js +35 -0
- package/dist/charts/lifescience/OncoGlyph.js +137 -0
- package/dist/charts/lifescience/OncoPrint.js +373 -0
- package/dist/charts/lifescience/OncoPrintMatrix.js +107 -0
- package/dist/charts/lifescience/OncoPrintSamples.js +142 -0
- package/dist/charts/lifescience/PedigreeChart.js +306 -0
- package/dist/charts/lifescience/PedigreeLayout.js +261 -0
- package/dist/charts/lifescience/PedigreeRows.js +70 -0
- package/dist/charts/lifescience/PedigreeSamples.js +161 -0
- package/dist/charts/lifescience/PedigreeSymbol.js +47 -0
- package/dist/charts/lifescience/RamachandranPlot.js +364 -0
- package/dist/charts/lifescience/RamachandranRegions.js +2712 -0
- package/dist/charts/lifescience/RamachandranSamples.js +235 -0
- package/dist/charts/lifescience/index.js +25 -0
- package/dist/charts/lifescience/pedigreeEnums.js +21 -0
- package/dist/charts/lifescience/ramachandranEnums.js +34 -0
- package/dist/charts/linesareas/DualAxisChart.js +1 -1
- package/dist/charts/linesareas/LineEnums.js +1 -15
- package/dist/charts/linesareas/MultiLineChart.js +1 -1
- package/dist/charts/linesareas/TrendChart.js +15 -5
- package/dist/charts/linesareas/index.js +2 -6
- package/dist/charts/massive/MassiveChart.js +3 -2
- package/dist/charts/mathematical/FourierSeriesChart.js +7 -1
- package/dist/charts/mathematical/index.js +2 -6
- package/dist/charts/matrix/GridPlot.js +27 -2
- package/dist/charts/matrix/TilegramChart.js +2 -2
- package/dist/charts/mlcharts/AttentionGrid.js +79 -0
- package/dist/charts/mlcharts/AttentionHeatmap.js +329 -0
- package/dist/charts/mlcharts/AttentionModel.js +132 -0
- package/dist/charts/mlcharts/AttentionSamples.js +82 -0
- package/dist/charts/mlcharts/CellNumber.js +8 -0
- package/dist/charts/mlcharts/CurveDiagnosticSamples.js +95 -0
- package/dist/charts/mlcharts/CurvePickMark.js +31 -0
- package/dist/charts/mlcharts/DecisionBoundaryChart.js +316 -0
- package/dist/charts/mlcharts/DecisionSamples.js +60 -0
- package/dist/charts/mlcharts/DecisionSurface.js +130 -0
- package/dist/charts/mlcharts/DistinctTicks.js +13 -0
- package/dist/charts/mlcharts/ElbowMath.js +153 -0
- package/dist/charts/mlcharts/ElbowPlot.js +273 -0
- package/dist/charts/mlcharts/ExplainabilitySamples.js +70 -0
- package/dist/charts/mlcharts/FeatureImportanceChart.js +150 -0
- package/dist/charts/mlcharts/ImportanceRanking.js +98 -0
- package/dist/charts/mlcharts/LearningCurveChart.js +276 -0
- package/dist/charts/mlcharts/LearningCurveMath.js +92 -0
- package/dist/charts/mlcharts/PartialDependence.js +92 -0
- package/dist/charts/mlcharts/PartialDependencePlot.js +225 -0
- package/dist/charts/mlcharts/WideLabel.js +28 -0
- package/dist/charts/mlcharts/curveDiagnosticEnums.js +37 -0
- package/dist/charts/mlcharts/index.js +32 -0
- package/dist/charts/proportion/index.js +5 -9
- package/dist/charts/scatter/CategoryScatterBase.js +1 -4
- package/dist/charts/scatter/ConnectedScatter.js +7 -4
- package/dist/charts/scatter/MarginalScatter.js +13 -8
- package/dist/charts/scatter/index.js +4 -17
- package/dist/charts/scientific/SpectrogramChart.js +9 -3
- package/dist/charts/scientific/index.js +2 -2
- package/dist/charts/signals/index.js +3 -13
- package/dist/charts/statistics/LorenzCurve.js +7 -4
- package/dist/charts/threebars/Candlestick3DChart.js +18 -2
- package/dist/charts/threeglobe/GlobeChart.js +2 -2
- package/dist/charts/threeglobe/GlobeSamples.js +2 -1
- package/dist/charts/threeglobe/RingGlobe.js +1 -1
- package/dist/charts/threenetwork/ConeTreeChart.js +2 -1
- package/dist/charts/threenetwork/Gantt3DChart.js +3 -2
- package/dist/charts/threesurfaces/Ridgeline3DChart.js +22 -4
- package/dist/charts/threevolume/LorenzAttractor.js +19 -0
- package/dist/charts/threevolume/VolumeRaycastChart.js +4 -3
- package/dist/charts/threevolume/volumeEnums.js +1 -0
- package/dist/charts/timeline/index.js +2 -10
- package/dist/chunk-753UBLV2.cjs +33 -0
- package/dist/chunk-VX54A7KA.cjs +17 -0
- package/dist/{chunk-W5GAAAZY.cjs → chunk-WAODDPYX.cjs} +24874 -17620
- package/dist/chunk-Z3JPIJSC.cjs +338 -0
- package/dist/core/CartesianChart.js +87 -11
- package/dist/core/Chart.js +63 -15
- package/dist/core/Chart2D.js +54 -17
- package/dist/core/Chart3D.js +56 -20
- package/dist/core/ChartExportError.js +2 -0
- package/dist/core/ChartExporter.js +50 -8
- package/dist/core/ColumnCheck.js +53 -0
- package/dist/core/Context2D.js +29 -15
- package/dist/core/Context3D.js +7 -2
- package/dist/core/ExcelWriter.js +152 -0
- package/dist/core/ExportText.js +28 -0
- package/dist/core/MarkOrigin.js +88 -0
- package/dist/core/MarkStyle.js +294 -0
- package/dist/core/Palette.js +27 -7
- package/dist/core/PdfPage.js +13 -0
- package/dist/core/PdfWriter.js +245 -0
- package/dist/core/Scales.js +52 -2
- package/dist/core/SceneOverlay.js +4 -4
- package/dist/core/TextFit.js +37 -0
- package/dist/core/Theme.js +28 -1
- package/dist/core/enums.js +33 -1
- package/dist/core/pathMorph.js +15 -0
- package/dist/data/SampleData.js +1 -17
- package/dist/data/WorldSample.js +23 -0
- package/dist/index.cjs +810 -691
- package/dist/index.d.cts +8390 -2925
- package/dist/index.d.ts +8392 -2927
- package/dist/index.js +106 -56
- package/dist/react/ChartView.js +18 -8
- package/dist/react/GraphHandle.js +2 -0
- package/dist/react/components.js +74 -116
- package/package.json +13 -3
- package/dist/catalog-4UP2YFNR.cjs +0 -7
- package/dist/charts/bars/BulletBar.js +0 -184
- package/dist/charts/bars/ErrorBarColumn.js +0 -147
- package/dist/charts/bars/HistogramBar.js +0 -217
- package/dist/charts/bars/MarimekkoBar.js +0 -191
- package/dist/charts/bars/ParetoBar.js +0 -143
- package/dist/charts/comparison/PanelChart.js +0 -70
- package/dist/charts/comparison/SmallMultipleBars.js +0 -134
- package/dist/charts/comparison/SpanChart.js +0 -118
- package/dist/charts/comparison/TrellisChart.js +0 -158
- package/dist/charts/distribution/RidgeSlabs3DChart.js +0 -169
- package/dist/charts/financial/Candlestick3D.js +0 -157
- package/dist/charts/flow/BumpFlow.js +0 -212
- package/dist/charts/flow/StreamFlow.js +0 -174
- package/dist/charts/geomaps/ThiessenMap.js +0 -106
- package/dist/charts/hierarchy/ConeTree3D.js +0 -135
- package/dist/charts/hierarchy/Hierarchy3DChart.js +0 -27
- package/dist/charts/hierarchy/SpherePack3D.js +0 -81
- package/dist/charts/hierarchy/SunburstTower3D.js +0 -93
- package/dist/charts/hierarchy/TreemapSkyline3D.js +0 -98
- package/dist/charts/linesareas/ConnectedScatterTime.js +0 -206
- package/dist/charts/linesareas/SmallMultipleLines.js +0 -153
- package/dist/charts/mathematical/ParametricSurface3D.js +0 -234
- package/dist/charts/mathematical/StrangeAttractor3D.js +0 -251
- package/dist/charts/proportion/ConeFunnel.js +0 -126
- package/dist/charts/proportion/ExtrudedPieChart.js +0 -123
- package/dist/charts/scatter/BeeswarmScatter.js +0 -151
- package/dist/charts/scatter/BinnedScatterBase.js +0 -51
- package/dist/charts/scatter/ContourScatter.js +0 -116
- package/dist/charts/scatter/GridBinScatter.js +0 -98
- package/dist/charts/scatter/HexbinScatter.js +0 -74
- package/dist/charts/scatter/StripScatter.js +0 -107
- package/dist/charts/scatter/SunflowerScatter.js +0 -109
- package/dist/charts/signals/ConstellationChart.js +0 -153
- package/dist/charts/signals/CorrelogramChart.js +0 -171
- package/dist/charts/signals/EyeDiagramChart.js +0 -155
- package/dist/charts/signals/PoleZeroChart.js +0 -164
- package/dist/charts/signals/SpectrogramChart.js +0 -79
- package/dist/charts/timeline/CalendarSkylineChart.js +0 -128
- package/dist/charts/timeline/ScheduleBlocksChart.js +0 -119
- package/dist/charts/timeline/SpiralTimeline.js +0 -154
- package/dist/charts/timeline/StreamTimeline.js +0 -141
|
@@ -0,0 +1,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 };
|