@diagrammo/dgmo 0.8.10 → 0.8.11

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diagrammo/dgmo",
3
- "version": "0.8.10",
3
+ "version": "0.8.11",
4
4
  "description": "DGMO diagram markup language — parser, renderer, and color system",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -74,10 +74,10 @@
74
74
  "prepare": "husky"
75
75
  },
76
76
  "dependencies": {
77
- "@dagrejs/dagre": "^2.0.4",
77
+ "@dagrejs/dagre": "^3.0.0",
78
78
  "@resvg/resvg-js": "^2.6.2",
79
79
  "d3-array": "^3.2.4",
80
- "d3-cloud": "^1.2.7",
80
+ "d3-cloud": "^1.2.9",
81
81
  "d3-hierarchy": "^3.1.2",
82
82
  "d3-scale": "^4.0.2",
83
83
  "d3-selection": "^3.0.0",
@@ -85,32 +85,30 @@
85
85
  "echarts": "^6.0.0",
86
86
  "lz-string": "^1.5.0"
87
87
  },
88
- "optionalDependencies": {
89
- "jsdom": "^28.1.0"
90
- },
91
88
  "devDependencies": {
92
89
  "@codemirror/language": "^6.12.3",
93
- "@codemirror/state": "^6.6.0",
94
90
  "@eslint/js": "^10.0.1",
95
91
  "@lezer/generator": "^1.8.0",
96
- "@types/d3-array": "^3.2.1",
92
+ "@types/d3-array": "^3.2.2",
97
93
  "@types/d3-cloud": "^1.2.9",
98
94
  "@types/d3-hierarchy": "^3.1.7",
99
- "@types/d3-scale": "^4.0.8",
95
+ "@types/d3-scale": "^4.0.9",
100
96
  "@types/d3-selection": "^3.0.11",
101
- "@types/d3-shape": "^3.1.7",
102
- "@types/jsdom": "^28.0.0",
97
+ "@types/d3-shape": "^3.1.8",
98
+ "@types/jsdom": "^28.0.1",
103
99
  "cspell": "^9.7.0",
104
- "eslint": "^10.1.0",
100
+ "esbuild": "^0.28.0",
101
+ "eslint": "^10.2.0",
105
102
  "husky": "^9.1.7",
106
103
  "jscpd": "^4.0.8",
107
- "knip": "^6.0.1",
104
+ "jsdom": "^29.0.1",
105
+ "knip": "^6.3.0",
108
106
  "lint-staged": "^16.4.0",
109
107
  "prettier": "^3.8.1",
110
108
  "tsup": "^8.5.1",
111
- "typescript": "^5.7.3",
112
- "typescript-eslint": "^8.57.2",
113
- "vitest": "^4.0.18"
109
+ "typescript": "^6.0.2",
110
+ "typescript-eslint": "^8.58.0",
111
+ "vitest": "^4.1.2"
114
112
  },
115
113
  "lint-staged": {
116
114
  "*.ts": [
@@ -120,7 +118,6 @@
120
118
  },
121
119
  "peerDependencies": {
122
120
  "@codemirror/language": "^6.12.3",
123
- "@codemirror/state": "^6.6.0",
124
121
  "@lezer/common": "^1.5.1",
125
122
  "@lezer/highlight": "^1.2.3",
126
123
  "@lezer/lr": "^1.4.8"
@@ -292,7 +292,7 @@ function resolveEdgeLabelOverlaps(
292
292
 
293
293
  // ── Main render function ───────────────────────────────────
294
294
 
295
- export interface BLRenderOptions {
295
+ interface BLRenderOptions {
296
296
  onClickItem?: (lineNumber: number) => void;
297
297
  exportDims?: { width?: number; height?: number };
298
298
  activeTagGroup?: string | null;
package/src/c4/layout.ts CHANGED
@@ -18,6 +18,27 @@ import {
18
18
  measureLegendText,
19
19
  } from '../utils/legend-constants';
20
20
 
21
+ /** dagre node label shape after layout(). */
22
+ interface DagreNodeLabel {
23
+ x: number;
24
+ y: number;
25
+ width: number;
26
+ height: number;
27
+ [key: string]: unknown;
28
+ }
29
+
30
+ /** dagre edge label shape after layout(). */
31
+ interface DagreEdgeLabel {
32
+ points: { x: number; y: number }[];
33
+ [key: string]: unknown;
34
+ }
35
+
36
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
37
+ const gNode = (g: any, name: string): DagreNodeLabel => g.node(name);
38
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
39
+ const gEdge = (g: any, v: string, w: string): DagreEdgeLabel | undefined =>
40
+ g.edge(v, w);
41
+
21
42
  // ============================================================
22
43
  // Types
23
44
  // ============================================================
@@ -213,7 +234,7 @@ function computeEdgePenalty(
213
234
  * closer to their neighbors, producing cleaner visual layouts.
214
235
  */
215
236
  function reduceCrossings(
216
- g: dagre.graphlib.Graph,
237
+ g: InstanceType<typeof dagre.graphlib.Graph>,
217
238
  edgeList: { source: string; target: string }[],
218
239
  nodeGroupMap?: Map<string, string>
219
240
  ): void {
@@ -229,7 +250,7 @@ function reduceCrossings(
229
250
  // Build geometry map for edge-node collision scoring
230
251
  const nodeGeometry = new Map<string, NodeGeometry>();
231
252
  for (const name of g.nodes()) {
232
- const pos = g.node(name);
253
+ const pos = gNode(g, name);
233
254
  if (pos)
234
255
  nodeGeometry.set(name, {
235
256
  y: pos.y,
@@ -241,7 +262,7 @@ function reduceCrossings(
241
262
  // Group nodes by rank
242
263
  const rankMap = new Map<number, string[]>();
243
264
  for (const name of g.nodes()) {
244
- const pos = g.node(name);
265
+ const pos = gNode(g, name);
245
266
  if (!pos) continue;
246
267
  const rankY = Math.round(pos.y);
247
268
  if (!rankMap.has(rankY)) rankMap.set(rankY, []);
@@ -250,7 +271,7 @@ function reduceCrossings(
250
271
 
251
272
  // Sort each rank by current x position
252
273
  for (const [, rankNodes] of rankMap) {
253
- rankNodes.sort((a, b) => g.node(a).x - g.node(b).x);
274
+ rankNodes.sort((a, b) => gNode(g, a).x - gNode(g, b).x);
254
275
  }
255
276
 
256
277
  let anyMoved = false;
@@ -285,13 +306,13 @@ function reduceCrossings(
285
306
 
286
307
  // Collect the x-slots for this partition (sorted)
287
308
  const xSlots = partition
288
- .map((name) => g.node(name).x)
309
+ .map((name) => gNode(g, name).x)
289
310
  .sort((a, b) => a - b);
290
311
 
291
312
  // Build position map snapshot
292
313
  const basePositions = new Map<string, number>();
293
314
  for (const name of g.nodes()) {
294
- const pos = g.node(name);
315
+ const pos = gNode(g, name);
295
316
  if (pos) basePositions.set(name, pos.x);
296
317
  }
297
318
 
@@ -379,7 +400,7 @@ function reduceCrossings(
379
400
  // Apply best permutation if it differs from current
380
401
  if (bestPerm.some((name, i) => name !== partition[i])) {
381
402
  for (let i = 0; i < bestPerm.length; i++) {
382
- g.node(bestPerm[i]!).x = xSlots[i]!;
403
+ gNode(g, bestPerm[i]!).x = xSlots[i]!;
383
404
  // Update in the original rankNodes too
384
405
  const rankIdx = rankNodes.indexOf(partition[i]!);
385
406
  if (rankIdx >= 0) rankNodes[rankIdx] = bestPerm[i]!;
@@ -392,10 +413,10 @@ function reduceCrossings(
392
413
  // Recompute edge waypoints if any positions changed
393
414
  if (anyMoved) {
394
415
  for (const edge of edgeList) {
395
- const edgeData = g.edge(edge.source, edge.target);
416
+ const edgeData = gEdge(g, edge.source, edge.target);
396
417
  if (!edgeData) continue;
397
- const srcPos = g.node(edge.source);
398
- const tgtPos = g.node(edge.target);
418
+ const srcPos = gNode(g, edge.source);
419
+ const tgtPos = gNode(g, edge.target);
399
420
  if (!srcPos || !tgtPos) continue;
400
421
 
401
422
  const srcBottom = { x: srcPos.x, y: srcPos.y + srcPos.height / 2 };
package/src/d3.ts CHANGED
@@ -5,6 +5,7 @@ import * as d3Array from 'd3-array';
5
5
  import cloud from 'd3-cloud';
6
6
  import { FONT_FAMILY } from './fonts';
7
7
  import { injectBranding } from './branding';
8
+ import { computeQuadrantPointLabels, type LabelRect } from './label-layout';
8
9
 
9
10
  // ============================================================
10
11
  // Types
@@ -19,22 +20,22 @@ export type VisualizationType =
19
20
  | 'quadrant'
20
21
  | 'sequence';
21
22
 
22
- export interface D3DataItem {
23
+ interface D3DataItem {
23
24
  label: string;
24
25
  values: number[];
25
26
  color: string | null;
26
27
  lineNumber: number;
27
28
  }
28
29
 
29
- export interface WordCloudWord {
30
+ interface WordCloudWord {
30
31
  text: string;
31
32
  weight: number;
32
33
  lineNumber: number;
33
34
  }
34
35
 
35
- export type WordCloudRotate = 'none' | 'mixed' | 'angled';
36
+ type WordCloudRotate = 'none' | 'mixed' | 'angled';
36
37
 
37
- export interface WordCloudOptions {
38
+ interface WordCloudOptions {
38
39
  rotate: WordCloudRotate;
39
40
  max: number;
40
41
  minSize: number;
@@ -56,7 +57,7 @@ export interface ArcLink {
56
57
  lineNumber: number;
57
58
  }
58
59
 
59
- export type ArcOrder = 'appearance' | 'name' | 'group' | 'degree';
60
+ type ArcOrder = 'appearance' | 'name' | 'group' | 'degree';
60
61
 
61
62
  export interface ArcNodeGroup {
62
63
  name: string;
@@ -65,9 +66,9 @@ export interface ArcNodeGroup {
65
66
  lineNumber: number;
66
67
  }
67
68
 
68
- export type TimelineSort = 'time' | 'group' | 'tag';
69
+ type TimelineSort = 'time' | 'group' | 'tag';
69
70
 
70
- export interface TimelineEvent {
71
+ interface TimelineEvent {
71
72
  date: string;
72
73
  endDate: string | null;
73
74
  label: string;
@@ -77,13 +78,13 @@ export interface TimelineEvent {
77
78
  uncertain?: boolean;
78
79
  }
79
80
 
80
- export interface TimelineGroup {
81
+ interface TimelineGroup {
81
82
  name: string;
82
83
  color: string | null;
83
84
  lineNumber: number;
84
85
  }
85
86
 
86
- export interface TimelineEra {
87
+ interface TimelineEra {
87
88
  startDate: string;
88
89
  endDate: string;
89
90
  label: string;
@@ -91,40 +92,40 @@ export interface TimelineEra {
91
92
  lineNumber: number;
92
93
  }
93
94
 
94
- export interface TimelineMarker {
95
+ interface TimelineMarker {
95
96
  date: string;
96
97
  label: string;
97
98
  color: string | null;
98
99
  lineNumber: number;
99
100
  }
100
101
 
101
- export interface VennSet {
102
+ interface VennSet {
102
103
  name: string;
103
104
  alias: string | null;
104
105
  color: string | null;
105
106
  lineNumber: number;
106
107
  }
107
108
 
108
- export interface VennOverlap {
109
+ interface VennOverlap {
109
110
  sets: string[];
110
111
  label: string | null;
111
112
  lineNumber: number;
112
113
  }
113
114
 
114
- export interface QuadrantLabel {
115
+ interface QuadrantLabel {
115
116
  text: string;
116
117
  color: string | null;
117
118
  lineNumber: number;
118
119
  }
119
120
 
120
- export interface QuadrantPoint {
121
+ interface QuadrantPoint {
121
122
  label: string;
122
123
  x: number;
123
124
  y: number;
124
125
  lineNumber: number;
125
126
  }
126
127
 
127
- export interface QuadrantLabels {
128
+ interface QuadrantLabels {
128
129
  topRight: QuadrantLabel | null;
129
130
  topLeft: QuadrantLabel | null;
130
131
  bottomLeft: QuadrantLabel | null;
@@ -4525,8 +4526,7 @@ export function renderTimeline(
4525
4526
  .attr('dy', '0.35em')
4526
4527
  .attr('text-anchor', 'start')
4527
4528
  .attr('fill', textColor)
4528
- .attr('font-size', '14px')
4529
- .attr('font-weight', '700')
4529
+ .attr('font-size', '13px')
4530
4530
  .text(ev.label);
4531
4531
  } else {
4532
4532
  // Text outside bar - check if it fits on left or must go right
@@ -4815,8 +4815,7 @@ export function renderTimeline(
4815
4815
  .attr('dy', '0.35em')
4816
4816
  .attr('text-anchor', 'start')
4817
4817
  .attr('fill', textColor)
4818
- .attr('font-size', '14px')
4819
- .attr('font-weight', '700')
4818
+ .attr('font-size', '13px')
4820
4819
  .text(ev.label);
4821
4820
  } else {
4822
4821
  // Text outside bar - check if it fits on left or must go right
@@ -5461,7 +5460,7 @@ export function renderVenn(
5461
5460
  exportDims?: D3ExportDimensions
5462
5461
  ): void {
5463
5462
  const { vennSets, vennOverlaps, title } = parsed;
5464
- if (vennSets.length < 2) return;
5463
+ if (vennSets.length < 2 || vennSets.length > 3) return;
5465
5464
 
5466
5465
  const init = initD3Chart(container, palette, exportDims);
5467
5466
  if (!init) return;
@@ -5539,7 +5538,9 @@ export function renderVenn(
5539
5538
  // Suppress WebKit focus ring on interactive SVG elements
5540
5539
  svg
5541
5540
  .append('style')
5542
- .text('circle:focus, circle:focus-visible { outline: none !important; }');
5541
+ .text(
5542
+ 'circle:focus, circle:focus-visible { outline-solid: none !important; }'
5543
+ );
5543
5544
 
5544
5545
  // Title
5545
5546
  renderChartTitle(
@@ -5870,7 +5871,7 @@ export function renderVenn(
5870
5871
  .attr('class', 'venn-hit-target')
5871
5872
  .attr('data-line-number', String(vennSets[i].lineNumber))
5872
5873
  .style('cursor', onClickItem ? 'pointer' : 'default')
5873
- .style('outline', 'none')
5874
+ .style('outline-solid', 'none')
5874
5875
  .on('mouseenter', () => {
5875
5876
  showRegionOverlay([i]);
5876
5877
  })
@@ -5926,7 +5927,7 @@ export function renderVenn(
5926
5927
  .attr('class', 'venn-hit-target')
5927
5928
  .attr('data-line-number', declaredOv ? String(declaredOv.lineNumber) : '')
5928
5929
  .style('cursor', onClickItem && declaredOv ? 'pointer' : 'default')
5929
- .style('outline', 'none')
5930
+ .style('outline-solid', 'none')
5930
5931
  .on('mouseenter', () => {
5931
5932
  showRegionOverlay(idxs);
5932
5933
  })
@@ -6416,40 +6417,88 @@ export function renderQuadrant(
6416
6417
  return 'bottom-right';
6417
6418
  };
6418
6419
 
6420
+ // Build obstacle rects from quadrant watermark labels for collision avoidance
6421
+ const POINT_RADIUS = 6;
6422
+ const POINT_LABEL_FONT_SIZE = 12;
6423
+ const quadrantLabelObstacles: LabelRect[] = quadrantDefsWithLabel.map((d) => {
6424
+ const layout = labelLayouts.get(d.label!.text)!;
6425
+ const totalW =
6426
+ Math.max(...layout.lines.map((l) => l.length)) *
6427
+ layout.fontSize *
6428
+ CHAR_WIDTH_RATIO;
6429
+ const totalH = layout.lines.length * layout.fontSize * 1.2;
6430
+ return {
6431
+ x: d.labelX - totalW / 2,
6432
+ y: d.labelY - totalH / 2,
6433
+ w: totalW,
6434
+ h: totalH,
6435
+ };
6436
+ });
6437
+
6438
+ // Compute collision-free label positions for all points
6439
+ const pointPixels = quadrantPoints.map((point) => ({
6440
+ label: point.label,
6441
+ cx: xScale(point.x),
6442
+ cy: yScale(point.y),
6443
+ }));
6444
+
6445
+ const placedPointLabels = computeQuadrantPointLabels(
6446
+ pointPixels,
6447
+ { left: 0, top: 0, right: chartWidth, bottom: chartHeight },
6448
+ quadrantLabelObstacles,
6449
+ POINT_RADIUS,
6450
+ POINT_LABEL_FONT_SIZE
6451
+ );
6452
+
6419
6453
  // Draw data points (circles and labels)
6420
6454
  const pointsG = chartG.append('g').attr('class', 'points');
6421
6455
 
6422
- quadrantPoints.forEach((point) => {
6456
+ quadrantPoints.forEach((point, i) => {
6423
6457
  const cx = xScale(point.x);
6424
6458
  const cy = yScale(point.y);
6425
6459
  const quadrant = getPointQuadrant(point.x, point.y);
6426
6460
  const quadDef = quadrantDefs.find((d) => d.position === quadrant);
6427
6461
  const pointColor =
6428
6462
  quadDef?.label?.color ?? defaultColors[quadDef?.colorIdx ?? 0];
6463
+ const placed = placedPointLabels[i];
6429
6464
 
6430
6465
  const pointG = pointsG
6431
6466
  .append('g')
6432
6467
  .attr('class', 'point-group')
6433
6468
  .attr('data-line-number', String(point.lineNumber));
6434
6469
 
6470
+ // Connector line (drawn first so it renders behind circle and label)
6471
+ if (placed.connectorLine) {
6472
+ pointG
6473
+ .append('line')
6474
+ .attr('x1', placed.connectorLine.x1)
6475
+ .attr('y1', placed.connectorLine.y1)
6476
+ .attr('x2', placed.connectorLine.x2)
6477
+ .attr('y2', placed.connectorLine.y2)
6478
+ .attr('stroke', pointColor)
6479
+ .attr('stroke-width', 1)
6480
+ .attr('opacity', 0.5);
6481
+ }
6482
+
6435
6483
  // Circle with white fill and colored border for visibility on opaque quadrants
6436
6484
  pointG
6437
6485
  .append('circle')
6438
6486
  .attr('cx', cx)
6439
6487
  .attr('cy', cy)
6440
- .attr('r', 6)
6488
+ .attr('r', POINT_RADIUS)
6441
6489
  .attr('fill', '#ffffff')
6442
6490
  .attr('stroke', pointColor)
6443
6491
  .attr('stroke-width', 2);
6444
6492
 
6445
- // Label (palette text color adapts to light/dark mode)
6493
+ // Label at computed position
6446
6494
  pointG
6447
6495
  .append('text')
6448
- .attr('x', cx)
6449
- .attr('y', cy - 10)
6450
- .attr('text-anchor', 'middle')
6496
+ .attr('x', placed.x)
6497
+ .attr('y', placed.y)
6498
+ .attr('text-anchor', placed.anchor)
6499
+ .attr('dominant-baseline', 'central')
6451
6500
  .attr('fill', textColor)
6452
- .attr('font-size', '12px')
6501
+ .attr('font-size', `${POINT_LABEL_FONT_SIZE}px`)
6453
6502
  .attr('font-weight', '700')
6454
6503
  .style('text-shadow', `0 1px 2px ${shadowColor}`)
6455
6504
  .text(point.label);
@@ -6468,7 +6517,7 @@ export function renderQuadrant(
6468
6517
  })
6469
6518
  .on('mouseleave', () => {
6470
6519
  hideTooltip(tooltip);
6471
- pointG.select('circle').attr('r', 6);
6520
+ pointG.select('circle').attr('r', POINT_RADIUS);
6472
6521
  })
6473
6522
  .on('click', () => {
6474
6523
  if (onClickItem && point.lineNumber) onClickItem(point.lineNumber);