@transitrix/diagrams 1.8.4 → 1.8.6

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.
@@ -44,4 +44,12 @@ describe('horizontalCubicEdgePath', () => {
44
44
  it('omitting entryCurvature gives symmetric handles (historical behaviour)', () => {
45
45
  expect(horizontalCubicEdgePath(0, 0, 40, 0, 0.5)).toBe(horizontalCubicEdgePath(0, 0, 40, 0, 0.5, 0.5));
46
46
  });
47
+ it('caps the handle for a tall edge with a narrow column gap, instead of overshooting the target', () => {
48
+ // A network-view edge spanning several rows (|dy|=500) with a typical
49
+ // adjacent-column gap (dx=80): uncapped, |dy|*0.8=400 would push the exit
50
+ // control point 320px past the target's x, producing an exaggerated
51
+ // S-bow. Capped at 160, the control points stay close to the endpoints.
52
+ const d = horizontalCubicEdgePath(0, 0, 80, 500, 1);
53
+ expect(d).toBe('M0,0 C160,0 -80,500 80,500');
54
+ });
47
55
  });
package/dist/edge-path.js CHANGED
@@ -15,6 +15,15 @@
15
15
  export const EDGE_MIN_HANDLE = 64;
16
16
  const DX_FACTOR = 0.5;
17
17
  const DY_FACTOR = 0.8;
18
+ /**
19
+ * Ceiling on each span factor's contribution. Uncapped, a tall edge with a
20
+ * narrow column gap (large dy, small dx — the common case in a many-row
21
+ * network view) grows a handle far past the endpoints' actual x-distance;
22
+ * the two control points then overshoot each other, producing an
23
+ * exaggerated S-bow instead of a gentle curve. Capping keeps typical spans
24
+ * (within the existing historical range) untouched while reining in outliers.
25
+ */
26
+ const MAX_HANDLE = 160;
18
27
  /** Multiplier applied to the base handle length. 1 = historical appearance. */
19
28
  export const DEFAULT_EDGE_CURVATURE = 1;
20
29
  /**
@@ -33,7 +42,7 @@ export const DEFAULT_EDGE_CURVATURE = 1;
33
42
  export function horizontalCubicEdgePath(sx, sy, tx, ty, curvature = DEFAULT_EDGE_CURVATURE, entryCurvature) {
34
43
  const dx = tx - sx;
35
44
  const dy = ty - sy;
36
- const baseHandle = Math.max(EDGE_MIN_HANDLE, Math.abs(dx) * DX_FACTOR, Math.abs(dy) * DY_FACTOR);
45
+ const baseHandle = Math.max(EDGE_MIN_HANDLE, Math.min(Math.abs(dx) * DX_FACTOR, MAX_HANDLE), Math.min(Math.abs(dy) * DY_FACTOR, MAX_HANDLE));
37
46
  const exitHandle = baseHandle * curvature;
38
47
  const entryHandle = baseHandle * (entryCurvature ?? curvature);
39
48
  return `M${sx},${sy} C${sx + exitHandle},${sy} ${tx - entryHandle},${ty} ${tx},${ty}`;
@@ -7,7 +7,7 @@ import { type NodeSizePreset } from '../node-size-presets.js';
7
7
  * network subset. Embedded alongside the shared theme CSS so the SVG is
8
8
  * self-contained for the JCEF host.
9
9
  */
10
- export declare const ACTIVITIES_NETWORK_CSS = "\n .act-node { fill: var(--ts-layer-activity, #d4edda); stroke: var(--ts-node-stroke, #004d67); stroke-width: 1; }\n .critical-node { fill: var(--ts-brand-orange-tint, #ffeee5); stroke: var(--ts-brand-orange, #ff4d00); stroke-width: 2.5; }\n .milestone-node { fill: #ecfeff; stroke: var(--ts-text-muted, #64748b); stroke-dasharray: 4 2; }\n .critical-edge { stroke: var(--ts-brand-orange, #ff4d00); stroke-width: 2; }\n .arrow-fill-critical { fill: var(--ts-brand-orange, #ff4d00); }\n";
10
+ export declare const ACTIVITIES_NETWORK_CSS = "\n .act-node { fill: var(--ts-layer-activity, #d4edda); stroke: var(--ts-node-stroke, #004d67); stroke-width: 1; }\n .critical-node { fill: var(--ts-brand-orange-tint, #ffeee5); stroke: var(--ts-brand-orange, #ff4d00); stroke-width: 1; }\n .milestone-node { fill: #ecfeff; stroke: var(--ts-text-muted, #64748b); stroke-dasharray: 4 2; }\n .critical-edge { stroke: var(--ts-brand-orange, #ff4d00); stroke-width: 1.5; }\n .arrow-fill-critical { fill: var(--ts-brand-orange, #ff4d00); }\n";
11
11
  /** Arrowhead marker defs shared by both hosts for the network view. */
12
12
  export declare const ACTIVITIES_NETWORK_DEFS = "<defs>\n <marker id=\"arrow\" markerWidth=\"8\" markerHeight=\"8\" refX=\"8\" refY=\"3\" orient=\"auto\">\n <path d=\"M0,0 L0,6 L8,3 z\" class=\"arrow-fill\"/>\n </marker>\n <marker id=\"arrow-crit\" markerWidth=\"8\" markerHeight=\"8\" refX=\"8\" refY=\"3\" orient=\"auto\">\n <path d=\"M0,0 L0,6 L8,3 z\" class=\"arrow-fill-critical\"/>\n </marker>\n</defs>";
13
13
  /**
@@ -32,9 +32,9 @@ const N_PAD = 24;
32
32
  */
33
33
  export const ACTIVITIES_NETWORK_CSS = `
34
34
  .act-node { fill: var(--ts-layer-activity, #d4edda); stroke: var(--ts-node-stroke, #004d67); stroke-width: 1; }
35
- .critical-node { fill: var(--ts-brand-orange-tint, #ffeee5); stroke: var(--ts-brand-orange, #ff4d00); stroke-width: 2.5; }
35
+ .critical-node { fill: var(--ts-brand-orange-tint, #ffeee5); stroke: var(--ts-brand-orange, #ff4d00); stroke-width: 1; }
36
36
  .milestone-node { fill: #ecfeff; stroke: var(--ts-text-muted, #64748b); stroke-dasharray: 4 2; }
37
- .critical-edge { stroke: var(--ts-brand-orange, #ff4d00); stroke-width: 2; }
37
+ .critical-edge { stroke: var(--ts-brand-orange, #ff4d00); stroke-width: 1.5; }
38
38
  .arrow-fill-critical { fill: var(--ts-brand-orange, #ff4d00); }
39
39
  `;
40
40
  /** Arrowhead marker defs shared by both hosts for the network view. */
@@ -38,10 +38,7 @@ export function renderFgcaBody(columns, nodes, edges, curvature, entryCurvature,
38
38
  const nodeHeight = bodyOptions.nodeHeight ?? FGCA_NODE_H;
39
39
  const headerTruncate = Math.max(8, Math.floor((nodeWidth - 16) / 7));
40
40
  const headerSvg = columns
41
- .map(({ col, x }) => [
42
- `<rect class="diagram-node layer-${col}" x="${x}" y="${PAD}" width="${nodeWidth}" height="${HEADER_H}" rx="6"/>`,
43
- `<text class="text-header" x="${x + nodeWidth / 2}" y="${PAD + HEADER_H / 2}" text-anchor="middle" dominant-baseline="central">${escXml(truncateLine(COL_LABELS[col], headerTruncate))}</text>`,
44
- ].join('\n'))
41
+ .map(({ col, x }) => `<text class="text-header" x="${x + nodeWidth / 2}" y="${PAD + HEADER_H / 2}" text-anchor="middle" dominant-baseline="central">${escXml(truncateLine(COL_LABELS[col], headerTruncate))}</text>`)
45
42
  .join('\n');
46
43
  const edgeSvg = edges
47
44
  .map((e) => `<path d="${horizontalCubicEdgePath(e.sx, e.sy, e.tx, e.ty, curvature, entryCurvature)}" class="diagram-edge" marker-end="url(#arrow)"/>`)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@transitrix/diagrams",
3
- "version": "1.8.4",
3
+ "version": "1.8.6",
4
4
  "type": "module",
5
5
  "description": "Shared rendering, validation, and pure mutations for Transitrix custom diagram formats.",
6
6
  "license": "MIT",
@@ -55,4 +55,13 @@ describe('horizontalCubicEdgePath', () => {
55
55
  horizontalCubicEdgePath(0, 0, 40, 0, 0.5, 0.5),
56
56
  );
57
57
  });
58
+
59
+ it('caps the handle for a tall edge with a narrow column gap, instead of overshooting the target', () => {
60
+ // A network-view edge spanning several rows (|dy|=500) with a typical
61
+ // adjacent-column gap (dx=80): uncapped, |dy|*0.8=400 would push the exit
62
+ // control point 320px past the target's x, producing an exaggerated
63
+ // S-bow. Capped at 160, the control points stay close to the endpoints.
64
+ const d = horizontalCubicEdgePath(0, 0, 80, 500, 1);
65
+ expect(d).toBe('M0,0 C160,0 -80,500 80,500');
66
+ });
58
67
  });
package/src/edge-path.ts CHANGED
@@ -17,6 +17,16 @@ export const EDGE_MIN_HANDLE = 64;
17
17
  const DX_FACTOR = 0.5;
18
18
  const DY_FACTOR = 0.8;
19
19
 
20
+ /**
21
+ * Ceiling on each span factor's contribution. Uncapped, a tall edge with a
22
+ * narrow column gap (large dy, small dx — the common case in a many-row
23
+ * network view) grows a handle far past the endpoints' actual x-distance;
24
+ * the two control points then overshoot each other, producing an
25
+ * exaggerated S-bow instead of a gentle curve. Capping keeps typical spans
26
+ * (within the existing historical range) untouched while reining in outliers.
27
+ */
28
+ const MAX_HANDLE = 160;
29
+
20
30
  /** Multiplier applied to the base handle length. 1 = historical appearance. */
21
31
  export const DEFAULT_EDGE_CURVATURE = 1;
22
32
 
@@ -43,7 +53,11 @@ export function horizontalCubicEdgePath(
43
53
  ): string {
44
54
  const dx = tx - sx;
45
55
  const dy = ty - sy;
46
- const baseHandle = Math.max(EDGE_MIN_HANDLE, Math.abs(dx) * DX_FACTOR, Math.abs(dy) * DY_FACTOR);
56
+ const baseHandle = Math.max(
57
+ EDGE_MIN_HANDLE,
58
+ Math.min(Math.abs(dx) * DX_FACTOR, MAX_HANDLE),
59
+ Math.min(Math.abs(dy) * DY_FACTOR, MAX_HANDLE),
60
+ );
47
61
  const exitHandle = baseHandle * curvature;
48
62
  const entryHandle = baseHandle * (entryCurvature ?? curvature);
49
63
  return `M${sx},${sy} C${sx + exitHandle},${sy} ${tx - entryHandle},${ty} ${tx},${ty}`;
@@ -39,9 +39,9 @@ const N_PAD = 24;
39
39
  */
40
40
  export const ACTIVITIES_NETWORK_CSS = `
41
41
  .act-node { fill: var(--ts-layer-activity, #d4edda); stroke: var(--ts-node-stroke, #004d67); stroke-width: 1; }
42
- .critical-node { fill: var(--ts-brand-orange-tint, #ffeee5); stroke: var(--ts-brand-orange, #ff4d00); stroke-width: 2.5; }
42
+ .critical-node { fill: var(--ts-brand-orange-tint, #ffeee5); stroke: var(--ts-brand-orange, #ff4d00); stroke-width: 1; }
43
43
  .milestone-node { fill: #ecfeff; stroke: var(--ts-text-muted, #64748b); stroke-dasharray: 4 2; }
44
- .critical-edge { stroke: var(--ts-brand-orange, #ff4d00); stroke-width: 2; }
44
+ .critical-edge { stroke: var(--ts-brand-orange, #ff4d00); stroke-width: 1.5; }
45
45
  .arrow-fill-critical { fill: var(--ts-brand-orange, #ff4d00); }
46
46
  `;
47
47
 
@@ -64,10 +64,7 @@ export function renderFgcaBody(
64
64
 
65
65
  const headerSvg = columns
66
66
  .map(({ col, x }) =>
67
- [
68
- `<rect class="diagram-node layer-${col}" x="${x}" y="${PAD}" width="${nodeWidth}" height="${HEADER_H}" rx="6"/>`,
69
- `<text class="text-header" x="${x + nodeWidth / 2}" y="${PAD + HEADER_H / 2}" text-anchor="middle" dominant-baseline="central">${escXml(truncateLine(COL_LABELS[col], headerTruncate))}</text>`,
70
- ].join('\n'),
67
+ `<text class="text-header" x="${x + nodeWidth / 2}" y="${PAD + HEADER_H / 2}" text-anchor="middle" dominant-baseline="central">${escXml(truncateLine(COL_LABELS[col], headerTruncate))}</text>`,
71
68
  )
72
69
  .join('\n');
73
70