@diagrammo/dgmo 0.2.2 → 0.2.4

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/src/d3.ts CHANGED
@@ -130,6 +130,12 @@ export interface QuadrantLabels {
130
130
  bottomRight: QuadrantLabel | null;
131
131
  }
132
132
 
133
+ /** Optional explicit dimensions for CLI/export rendering (bypasses DOM layout). */
134
+ export interface D3ExportDimensions {
135
+ width?: number;
136
+ height?: number;
137
+ }
138
+
133
139
  export interface ParsedD3 {
134
140
  type: D3ChartType | null;
135
141
  title: string | null;
@@ -1036,8 +1042,8 @@ function tokenizeFreeformText(text: string): WordCloudWord[] {
1036
1042
  // ============================================================
1037
1043
 
1038
1044
  const SLOPE_MARGIN = { top: 80, bottom: 40, left: 80 };
1039
- const SLOPE_LABEL_FONT_SIZE = 12;
1040
- const SLOPE_CHAR_WIDTH = 7; // approximate px per character at 12px
1045
+ const SLOPE_LABEL_FONT_SIZE = 14;
1046
+ const SLOPE_CHAR_WIDTH = 8; // approximate px per character at 14px
1041
1047
 
1042
1048
  /**
1043
1049
  * Renders a slope chart into the given container using D3.
@@ -1047,7 +1053,8 @@ export function renderSlopeChart(
1047
1053
  parsed: ParsedD3,
1048
1054
  palette: PaletteColors,
1049
1055
  isDark: boolean,
1050
- onClickItem?: (lineNumber: number) => void
1056
+ onClickItem?: (lineNumber: number) => void,
1057
+ exportDims?: D3ExportDimensions
1051
1058
  ): void {
1052
1059
  // Clear existing content
1053
1060
  d3Selection.select(container).selectAll(':not([data-d3-tooltip])').remove();
@@ -1055,8 +1062,8 @@ export function renderSlopeChart(
1055
1062
  const { periods, data, title } = parsed;
1056
1063
  if (data.length === 0 || periods.length < 2) return;
1057
1064
 
1058
- const width = container.clientWidth;
1059
- const height = container.clientHeight;
1065
+ const width = exportDims?.width ?? container.clientWidth;
1066
+ const height = exportDims?.height ?? container.clientHeight;
1060
1067
  if (width <= 0 || height <= 0) return;
1061
1068
 
1062
1069
  // Compute right margin from the longest end-of-line label
@@ -1067,7 +1074,7 @@ export function renderSlopeChart(
1067
1074
  const estimatedLabelWidth = maxLabelText.length * SLOPE_CHAR_WIDTH;
1068
1075
  const maxRightMargin = Math.floor(width * 0.35);
1069
1076
  const rightMargin = Math.min(
1070
- Math.max(estimatedLabelWidth + 20, 100),
1077
+ Math.max(estimatedLabelWidth + 30, 120),
1071
1078
  maxRightMargin
1072
1079
  );
1073
1080
 
@@ -1077,7 +1084,7 @@ export function renderSlopeChart(
1077
1084
  // Theme colors
1078
1085
  const textColor = palette.text;
1079
1086
  const mutedColor = palette.border;
1080
- const bgColor = palette.overlay;
1087
+ const bgColor = palette.bg;
1081
1088
  const colors = getSeriesColors(palette);
1082
1089
 
1083
1090
  // Scales
@@ -1119,7 +1126,7 @@ export function renderSlopeChart(
1119
1126
  .attr('y', 30)
1120
1127
  .attr('text-anchor', 'middle')
1121
1128
  .attr('fill', textColor)
1122
- .attr('font-size', '18px')
1129
+ .attr('font-size', '20px')
1123
1130
  .attr('font-weight', '700')
1124
1131
  .text(title);
1125
1132
  }
@@ -1132,7 +1139,7 @@ export function renderSlopeChart(
1132
1139
  .attr('y', -15)
1133
1140
  .attr('text-anchor', 'middle')
1134
1141
  .attr('fill', textColor)
1135
- .attr('font-size', '13px')
1142
+ .attr('font-size', '18px')
1136
1143
  .attr('font-weight', '600')
1137
1144
  .text(period);
1138
1145
 
@@ -1232,7 +1239,7 @@ export function renderSlopeChart(
1232
1239
  .attr('dy', '0.35em')
1233
1240
  .attr('text-anchor', isFirst ? 'end' : 'middle')
1234
1241
  .attr('fill', textColor)
1235
- .attr('font-size', '12px')
1242
+ .attr('font-size', '16px')
1236
1243
  .text(val.toString());
1237
1244
  }
1238
1245
  });
@@ -1420,15 +1427,16 @@ export function renderArcDiagram(
1420
1427
  parsed: ParsedD3,
1421
1428
  palette: PaletteColors,
1422
1429
  _isDark: boolean,
1423
- onClickItem?: (lineNumber: number) => void
1430
+ onClickItem?: (lineNumber: number) => void,
1431
+ exportDims?: D3ExportDimensions
1424
1432
  ): void {
1425
1433
  d3Selection.select(container).selectAll(':not([data-d3-tooltip])').remove();
1426
1434
 
1427
1435
  const { links, title, orientation, arcOrder, arcNodeGroups } = parsed;
1428
1436
  if (links.length === 0) return;
1429
1437
 
1430
- const width = container.clientWidth;
1431
- const height = container.clientHeight;
1438
+ const width = exportDims?.width ?? container.clientWidth;
1439
+ const height = exportDims?.height ?? container.clientHeight;
1432
1440
  if (width <= 0 || height <= 0) return;
1433
1441
 
1434
1442
  const isVertical = orientation === 'vertical';
@@ -1447,7 +1455,7 @@ export function renderArcDiagram(
1447
1455
  // Theme colors
1448
1456
  const textColor = palette.text;
1449
1457
  const mutedColor = palette.border;
1450
- const bgColor = palette.overlay;
1458
+ const bgColor = palette.bg;
1451
1459
  const colors = getSeriesColors(palette);
1452
1460
 
1453
1461
  // Order nodes by selected strategy
@@ -1499,7 +1507,7 @@ export function renderArcDiagram(
1499
1507
  .attr('y', 30)
1500
1508
  .attr('text-anchor', 'middle')
1501
1509
  .attr('fill', textColor)
1502
- .attr('font-size', '18px')
1510
+ .attr('font-size', '20px')
1503
1511
  .attr('font-weight', '700')
1504
1512
  .text(title);
1505
1513
  }
@@ -1541,11 +1549,11 @@ export function renderArcDiagram(
1541
1549
  g.selectAll<SVGGElement, unknown>('.arc-node').attr('opacity', 1);
1542
1550
  g.selectAll<SVGRectElement, unknown>('.arc-group-band').attr(
1543
1551
  'fill-opacity',
1544
- 0.08
1552
+ 0.06
1545
1553
  );
1546
1554
  g.selectAll<SVGTextElement, unknown>('.arc-group-label').attr(
1547
1555
  'fill-opacity',
1548
- 0.7
1556
+ 0.5
1549
1557
  );
1550
1558
  }
1551
1559
 
@@ -1610,8 +1618,8 @@ export function renderArcDiagram(
1610
1618
  .attr('width', bandHalfW * 2)
1611
1619
  .attr('height', maxY - minY)
1612
1620
  .attr('rx', 4)
1613
- .attr('fill', bandColor)
1614
- .attr('fill-opacity', 0.08)
1621
+ .attr('fill', textColor)
1622
+ .attr('fill-opacity', 0.06)
1615
1623
  .style('cursor', 'pointer')
1616
1624
  .on('mouseenter', () => handleGroupEnter(group.name))
1617
1625
  .on('mouseleave', handleMouseLeave)
@@ -1623,11 +1631,11 @@ export function renderArcDiagram(
1623
1631
  .attr('class', 'arc-group-label')
1624
1632
  .attr('data-group', group.name)
1625
1633
  .attr('x', baseX - bandHalfW + 6)
1626
- .attr('y', minY + 12)
1627
- .attr('fill', bandColor)
1628
- .attr('font-size', '10px')
1634
+ .attr('y', minY + 14)
1635
+ .attr('fill', textColor)
1636
+ .attr('font-size', '12px')
1629
1637
  .attr('font-weight', '600')
1630
- .attr('fill-opacity', 0.7)
1638
+ .attr('fill-opacity', 0.5)
1631
1639
  .style('cursor', onClickItem ? 'pointer' : 'default')
1632
1640
  .text(group.name)
1633
1641
  .on('mouseenter', () => handleGroupEnter(group.name))
@@ -1743,8 +1751,8 @@ export function renderArcDiagram(
1743
1751
  .attr('width', maxX - minX)
1744
1752
  .attr('height', bandHalfH * 2)
1745
1753
  .attr('rx', 4)
1746
- .attr('fill', bandColor)
1747
- .attr('fill-opacity', 0.08)
1754
+ .attr('fill', textColor)
1755
+ .attr('fill-opacity', 0.06)
1748
1756
  .style('cursor', 'pointer')
1749
1757
  .on('mouseenter', () => handleGroupEnter(group.name))
1750
1758
  .on('mouseleave', handleMouseLeave)
@@ -1758,10 +1766,10 @@ export function renderArcDiagram(
1758
1766
  .attr('x', (minX + maxX) / 2)
1759
1767
  .attr('y', baseY + bandHalfH - 4)
1760
1768
  .attr('text-anchor', 'middle')
1761
- .attr('fill', bandColor)
1762
- .attr('font-size', '10px')
1769
+ .attr('fill', textColor)
1770
+ .attr('font-size', '12px')
1763
1771
  .attr('font-weight', '600')
1764
- .attr('fill-opacity', 0.7)
1772
+ .attr('fill-opacity', 0.5)
1765
1773
  .style('cursor', onClickItem ? 'pointer' : 'default')
1766
1774
  .text(group.name)
1767
1775
  .on('mouseenter', () => handleGroupEnter(group.name))
@@ -2549,7 +2557,8 @@ export function renderTimeline(
2549
2557
  parsed: ParsedD3,
2550
2558
  palette: PaletteColors,
2551
2559
  isDark: boolean,
2552
- onClickItem?: (lineNumber: number) => void
2560
+ onClickItem?: (lineNumber: number) => void,
2561
+ exportDims?: D3ExportDimensions
2553
2562
  ): void {
2554
2563
  d3Selection.select(container).selectAll(':not([data-d3-tooltip])').remove();
2555
2564
 
@@ -2568,8 +2577,8 @@ export function renderTimeline(
2568
2577
 
2569
2578
  const tooltip = createTooltip(container, palette, isDark);
2570
2579
 
2571
- const width = container.clientWidth;
2572
- const height = container.clientHeight;
2580
+ const width = exportDims?.width ?? container.clientWidth;
2581
+ const height = exportDims?.height ?? container.clientHeight;
2573
2582
  if (width <= 0 || height <= 0) return;
2574
2583
 
2575
2584
  const isVertical = orientation === 'vertical';
@@ -2577,7 +2586,7 @@ export function renderTimeline(
2577
2586
  // Theme colors
2578
2587
  const textColor = palette.text;
2579
2588
  const mutedColor = palette.border;
2580
- const bgColor = palette.overlay;
2589
+ const bgColor = palette.bg;
2581
2590
  const colors = getSeriesColors(palette);
2582
2591
 
2583
2592
  // Assign colors to groups
@@ -2732,7 +2741,7 @@ export function renderTimeline(
2732
2741
  .attr('y', 30)
2733
2742
  .attr('text-anchor', 'middle')
2734
2743
  .attr('fill', textColor)
2735
- .attr('font-size', '18px')
2744
+ .attr('font-size', '20px')
2736
2745
  .attr('font-weight', '700')
2737
2746
  .text(title);
2738
2747
  }
@@ -2923,7 +2932,7 @@ export function renderTimeline(
2923
2932
  .attr('y', 30)
2924
2933
  .attr('text-anchor', 'middle')
2925
2934
  .attr('fill', textColor)
2926
- .attr('font-size', '18px')
2935
+ .attr('font-size', '20px')
2927
2936
  .attr('font-weight', '700')
2928
2937
  .text(title);
2929
2938
  }
@@ -3175,7 +3184,7 @@ export function renderTimeline(
3175
3184
  .attr('y', 30)
3176
3185
  .attr('text-anchor', 'middle')
3177
3186
  .attr('fill', textColor)
3178
- .attr('font-size', '18px')
3187
+ .attr('font-size', '20px')
3179
3188
  .attr('font-weight', '700')
3180
3189
  .text(title);
3181
3190
  }
@@ -3371,8 +3380,8 @@ export function renderTimeline(
3371
3380
  .attr('dy', '0.35em')
3372
3381
  .attr('text-anchor', 'start')
3373
3382
  .attr('fill', '#ffffff')
3374
- .attr('font-size', '13px')
3375
- .attr('font-weight', '500')
3383
+ .attr('font-size', '14px')
3384
+ .attr('font-weight', '700')
3376
3385
  .text(ev.label);
3377
3386
  } else {
3378
3387
  // Text outside bar - check if it fits on left or must go right
@@ -3459,7 +3468,7 @@ export function renderTimeline(
3459
3468
  .attr('y', 30)
3460
3469
  .attr('text-anchor', 'middle')
3461
3470
  .attr('fill', textColor)
3462
- .attr('font-size', '18px')
3471
+ .attr('font-size', '20px')
3463
3472
  .attr('font-weight', '700')
3464
3473
  .text(title);
3465
3474
  }
@@ -3639,8 +3648,8 @@ export function renderTimeline(
3639
3648
  .attr('dy', '0.35em')
3640
3649
  .attr('text-anchor', 'start')
3641
3650
  .attr('fill', '#ffffff')
3642
- .attr('font-size', '13px')
3643
- .attr('font-weight', '500')
3651
+ .attr('font-size', '14px')
3652
+ .attr('font-weight', '700')
3644
3653
  .text(ev.label);
3645
3654
  } else {
3646
3655
  // Text outside bar - check if it fits on left or must go right
@@ -3708,22 +3717,23 @@ export function renderWordCloud(
3708
3717
  parsed: ParsedD3,
3709
3718
  palette: PaletteColors,
3710
3719
  _isDark: boolean,
3711
- onClickItem?: (lineNumber: number) => void
3720
+ onClickItem?: (lineNumber: number) => void,
3721
+ exportDims?: D3ExportDimensions
3712
3722
  ): void {
3713
3723
  d3Selection.select(container).selectAll(':not([data-d3-tooltip])').remove();
3714
3724
 
3715
3725
  const { words, title, cloudOptions } = parsed;
3716
3726
  if (words.length === 0) return;
3717
3727
 
3718
- const width = container.clientWidth;
3719
- const height = container.clientHeight;
3728
+ const width = exportDims?.width ?? container.clientWidth;
3729
+ const height = exportDims?.height ?? container.clientHeight;
3720
3730
  if (width <= 0 || height <= 0) return;
3721
3731
 
3722
3732
  const titleHeight = title ? 40 : 0;
3723
3733
  const cloudHeight = height - titleHeight;
3724
3734
 
3725
3735
  const textColor = palette.text;
3726
- const bgColor = palette.overlay;
3736
+ const bgColor = palette.bg;
3727
3737
  const colors = getSeriesColors(palette);
3728
3738
 
3729
3739
  const { minSize, maxSize } = cloudOptions;
@@ -3750,10 +3760,10 @@ export function renderWordCloud(
3750
3760
  svg
3751
3761
  .append('text')
3752
3762
  .attr('x', width / 2)
3753
- .attr('y', 28)
3763
+ .attr('y', 30)
3754
3764
  .attr('text-anchor', 'middle')
3755
3765
  .attr('fill', textColor)
3756
- .attr('font-size', '18px')
3766
+ .attr('font-size', '20px')
3757
3767
  .attr('font-weight', '700')
3758
3768
  .text(title);
3759
3769
  }
@@ -3805,7 +3815,8 @@ function renderWordCloudAsync(
3805
3815
  container: HTMLDivElement,
3806
3816
  parsed: ParsedD3,
3807
3817
  palette: PaletteColors,
3808
- _isDark: boolean
3818
+ _isDark: boolean,
3819
+ exportDims?: D3ExportDimensions
3809
3820
  ): Promise<void> {
3810
3821
  return new Promise((resolve) => {
3811
3822
  d3Selection.select(container).selectAll(':not([data-d3-tooltip])').remove();
@@ -3816,8 +3827,8 @@ function renderWordCloudAsync(
3816
3827
  return;
3817
3828
  }
3818
3829
 
3819
- const width = container.clientWidth;
3820
- const height = container.clientHeight;
3830
+ const width = exportDims?.width ?? container.clientWidth;
3831
+ const height = exportDims?.height ?? container.clientHeight;
3821
3832
  if (width <= 0 || height <= 0) {
3822
3833
  resolve();
3823
3834
  return;
@@ -3827,7 +3838,7 @@ function renderWordCloudAsync(
3827
3838
  const cloudHeight = height - titleHeight;
3828
3839
 
3829
3840
  const textColor = palette.text;
3830
- const bgColor = palette.overlay;
3841
+ const bgColor = palette.bg;
3831
3842
  const colors = getSeriesColors(palette);
3832
3843
 
3833
3844
  const { minSize, maxSize } = cloudOptions;
@@ -3854,10 +3865,10 @@ function renderWordCloudAsync(
3854
3865
  svg
3855
3866
  .append('text')
3856
3867
  .attr('x', width / 2)
3857
- .attr('y', 28)
3868
+ .attr('y', 30)
3858
3869
  .attr('text-anchor', 'middle')
3859
3870
  .attr('fill', textColor)
3860
- .attr('font-size', '18px')
3871
+ .attr('font-size', '20px')
3861
3872
  .attr('font-weight', '700')
3862
3873
  .text(title);
3863
3874
  }
@@ -4096,19 +4107,20 @@ export function renderVenn(
4096
4107
  parsed: ParsedD3,
4097
4108
  palette: PaletteColors,
4098
4109
  isDark: boolean,
4099
- onClickItem?: (lineNumber: number) => void
4110
+ onClickItem?: (lineNumber: number) => void,
4111
+ exportDims?: D3ExportDimensions
4100
4112
  ): void {
4101
4113
  d3Selection.select(container).selectAll(':not([data-d3-tooltip])').remove();
4102
4114
 
4103
4115
  const { vennSets, vennOverlaps, vennShowValues, title } = parsed;
4104
4116
  if (vennSets.length < 2) return;
4105
4117
 
4106
- const width = container.clientWidth;
4107
- const height = container.clientHeight;
4118
+ const width = exportDims?.width ?? container.clientWidth;
4119
+ const height = exportDims?.height ?? container.clientHeight;
4108
4120
  if (width <= 0 || height <= 0) return;
4109
4121
 
4110
4122
  const textColor = palette.text;
4111
- const bgColor = palette.overlay;
4123
+ const bgColor = palette.bg;
4112
4124
  const colors = getSeriesColors(palette);
4113
4125
  const titleHeight = title ? 40 : 0;
4114
4126
 
@@ -4198,10 +4210,10 @@ export function renderVenn(
4198
4210
  svg
4199
4211
  .append('text')
4200
4212
  .attr('x', width / 2)
4201
- .attr('y', 28)
4213
+ .attr('y', 30)
4202
4214
  .attr('text-anchor', 'middle')
4203
4215
  .attr('fill', textColor)
4204
- .attr('font-size', '18px')
4216
+ .attr('font-size', '20px')
4205
4217
  .attr('font-weight', '700')
4206
4218
  .text(title);
4207
4219
  }
@@ -4510,7 +4522,8 @@ export function renderQuadrant(
4510
4522
  parsed: ParsedD3,
4511
4523
  palette: PaletteColors,
4512
4524
  isDark: boolean,
4513
- onClickItem?: (lineNumber: number) => void
4525
+ onClickItem?: (lineNumber: number) => void,
4526
+ exportDims?: D3ExportDimensions
4514
4527
  ): void {
4515
4528
  d3Selection.select(container).selectAll(':not([data-d3-tooltip])').remove();
4516
4529
 
@@ -4527,13 +4540,13 @@ export function renderQuadrant(
4527
4540
 
4528
4541
  if (quadrantPoints.length === 0) return;
4529
4542
 
4530
- const width = container.clientWidth;
4531
- const height = container.clientHeight;
4543
+ const width = exportDims?.width ?? container.clientWidth;
4544
+ const height = exportDims?.height ?? container.clientHeight;
4532
4545
  if (width <= 0 || height <= 0) return;
4533
4546
 
4534
4547
  const textColor = palette.text;
4535
4548
  const mutedColor = palette.textMuted;
4536
- const bgColor = palette.overlay;
4549
+ const bgColor = palette.bg;
4537
4550
  const borderColor = palette.border;
4538
4551
 
4539
4552
  // Default quadrant colors with alpha
@@ -4545,7 +4558,9 @@ export function renderQuadrant(
4545
4558
  ];
4546
4559
 
4547
4560
  // Margins
4548
- const margin = { top: title ? 60 : 30, right: 30, bottom: 50, left: 60 };
4561
+ const hasXAxis = !!quadrantXAxis;
4562
+ const hasYAxis = !!quadrantYAxis;
4563
+ const margin = { top: title ? 60 : 30, right: 30, bottom: hasXAxis ? 70 : 40, left: hasYAxis ? 80 : 40 };
4549
4564
  const chartWidth = width - margin.left - margin.right;
4550
4565
  const chartHeight = height - margin.top - margin.bottom;
4551
4566
 
@@ -4572,7 +4587,7 @@ export function renderQuadrant(
4572
4587
  .attr('y', 30)
4573
4588
  .attr('text-anchor', 'middle')
4574
4589
  .attr('fill', textColor)
4575
- .attr('font-size', '18px')
4590
+ .attr('font-size', '20px')
4576
4591
  .attr('font-weight', '700')
4577
4592
  .style(
4578
4593
  'cursor',
@@ -4597,7 +4612,19 @@ export function renderQuadrant(
4597
4612
  .append('g')
4598
4613
  .attr('transform', `translate(${margin.left}, ${margin.top})`);
4599
4614
 
4600
- // Get fill color for each quadrant (solid, no transparency)
4615
+ // Mix two hex colors: pct=100 all `a`, pct=0 → all `b`
4616
+ const mixHex = (a: string, b: string, pct: number): string => {
4617
+ const parse = (h: string) => {
4618
+ const r = h.replace('#', '');
4619
+ const f = r.length === 3 ? r[0]+r[0]+r[1]+r[1]+r[2]+r[2] : r;
4620
+ return [parseInt(f.substring(0,2),16), parseInt(f.substring(2,4),16), parseInt(f.substring(4,6),16)];
4621
+ };
4622
+ const [ar,ag,ab] = parse(a), [br,bg,bb] = parse(b), t = pct/100;
4623
+ const c = (x: number, y: number) => Math.round(x*t + y*(1-t)).toString(16).padStart(2,'0');
4624
+ return `#${c(ar,br)}${c(ag,bg)}${c(ab,bb)}`;
4625
+ };
4626
+
4627
+ // Opaque quadrant fills using the assigned color directly
4601
4628
  const getQuadrantFill = (
4602
4629
  label: QuadrantLabel | null,
4603
4630
  defaultIdx: number
@@ -4678,11 +4705,17 @@ export function renderQuadrant(
4678
4705
  .attr('stroke', borderColor)
4679
4706
  .attr('stroke-width', 0.5);
4680
4707
 
4681
- // Contrast color for text/points on colored backgrounds
4682
- const contrastColor = isDark ? '#ffffff' : '#333333';
4683
- const shadowColor = isDark ? 'rgba(0,0,0,0.3)' : 'rgba(255,255,255,0.5)';
4708
+ // White text for points; quadrant labels use a darkened shade of their fill
4709
+ const contrastColor = '#ffffff';
4710
+ const shadowColor = 'rgba(0,0,0,0.4)';
4684
4711
 
4685
- // Draw quadrant labels (large, centered, contrasting color for readability)
4712
+ // Darken the quadrant fill to create a watermark-style label color
4713
+ const getQuadrantLabelColor = (d: (typeof quadrantDefs)[number]): string => {
4714
+ const fill = getQuadrantFill(d.label, d.colorIdx);
4715
+ return mixHex('#000000', fill, 40);
4716
+ };
4717
+
4718
+ // Draw quadrant labels (large, centered, darkened shade of fill — recedes behind points)
4686
4719
  const quadrantLabelTexts = chartG
4687
4720
  .selectAll('text.quadrant-label')
4688
4721
  .data(quadrantDefs.filter((d) => d.label !== null))
@@ -4693,10 +4726,9 @@ export function renderQuadrant(
4693
4726
  .attr('y', (d) => d.labelY)
4694
4727
  .attr('text-anchor', 'middle')
4695
4728
  .attr('dominant-baseline', 'central')
4696
- .attr('fill', contrastColor)
4697
- .attr('font-size', '16px')
4698
- .attr('font-weight', '600')
4699
- .style('text-shadow', `0 1px 2px ${shadowColor}`)
4729
+ .attr('fill', (d) => getQuadrantLabelColor(d))
4730
+ .attr('font-size', '48px')
4731
+ .attr('font-weight', '700')
4700
4732
  .style('cursor', (d) =>
4701
4733
  onClickItem && d.label?.lineNumber ? 'pointer' : 'default'
4702
4734
  )
@@ -4715,46 +4747,36 @@ export function renderQuadrant(
4715
4747
  });
4716
4748
  }
4717
4749
 
4718
- // X-axis labels
4750
+ // X-axis labels — centered on left/right halves
4719
4751
  if (quadrantXAxis) {
4720
- // Low label (left)
4752
+ // Low label (centered on left half)
4721
4753
  const xLowLabel = svg
4722
4754
  .append('text')
4723
- .attr('x', margin.left)
4724
- .attr('y', height - 15)
4725
- .attr('text-anchor', 'start')
4755
+ .attr('x', margin.left + chartWidth / 4)
4756
+ .attr('y', height - 20)
4757
+ .attr('text-anchor', 'middle')
4726
4758
  .attr('fill', textColor)
4727
- .attr('font-size', '12px')
4759
+ .attr('font-size', '18px')
4728
4760
  .style(
4729
4761
  'cursor',
4730
4762
  onClickItem && quadrantXAxisLineNumber ? 'pointer' : 'default'
4731
4763
  )
4732
4764
  .text(quadrantXAxis[0]);
4733
4765
 
4734
- // High label (right)
4766
+ // High label (centered on right half)
4735
4767
  const xHighLabel = svg
4736
4768
  .append('text')
4737
- .attr('x', width - margin.right)
4738
- .attr('y', height - 15)
4739
- .attr('text-anchor', 'end')
4769
+ .attr('x', margin.left + (chartWidth * 3) / 4)
4770
+ .attr('y', height - 20)
4771
+ .attr('text-anchor', 'middle')
4740
4772
  .attr('fill', textColor)
4741
- .attr('font-size', '12px')
4773
+ .attr('font-size', '18px')
4742
4774
  .style(
4743
4775
  'cursor',
4744
4776
  onClickItem && quadrantXAxisLineNumber ? 'pointer' : 'default'
4745
4777
  )
4746
4778
  .text(quadrantXAxis[1]);
4747
4779
 
4748
- // Arrow in the middle
4749
- svg
4750
- .append('text')
4751
- .attr('x', width / 2)
4752
- .attr('y', height - 15)
4753
- .attr('text-anchor', 'middle')
4754
- .attr('fill', mutedColor)
4755
- .attr('font-size', '12px')
4756
- .text('→');
4757
-
4758
4780
  if (onClickItem && quadrantXAxisLineNumber) {
4759
4781
  [xLowLabel, xHighLabel].forEach((label) => {
4760
4782
  label
@@ -4769,49 +4791,41 @@ export function renderQuadrant(
4769
4791
  }
4770
4792
  }
4771
4793
 
4772
- // Y-axis labels
4794
+ // Y-axis labels — centered on top/bottom halves
4773
4795
  if (quadrantYAxis) {
4774
- // Low label (bottom)
4796
+ const yMidBottom = margin.top + (chartHeight * 3) / 4;
4797
+ const yMidTop = margin.top + chartHeight / 4;
4798
+
4799
+ // Low label (centered on bottom half)
4775
4800
  const yLowLabel = svg
4776
4801
  .append('text')
4777
- .attr('x', 15)
4778
- .attr('y', height - margin.bottom)
4779
- .attr('text-anchor', 'start')
4802
+ .attr('x', 22)
4803
+ .attr('y', yMidBottom)
4804
+ .attr('text-anchor', 'middle')
4780
4805
  .attr('fill', textColor)
4781
- .attr('font-size', '12px')
4782
- .attr('transform', `rotate(-90, 15, ${height - margin.bottom})`)
4806
+ .attr('font-size', '18px')
4807
+ .attr('transform', `rotate(-90, 22, ${yMidBottom})`)
4783
4808
  .style(
4784
4809
  'cursor',
4785
4810
  onClickItem && quadrantYAxisLineNumber ? 'pointer' : 'default'
4786
4811
  )
4787
4812
  .text(quadrantYAxis[0]);
4788
4813
 
4789
- // High label (top)
4814
+ // High label (centered on top half)
4790
4815
  const yHighLabel = svg
4791
4816
  .append('text')
4792
- .attr('x', 15)
4793
- .attr('y', margin.top)
4794
- .attr('text-anchor', 'end')
4817
+ .attr('x', 22)
4818
+ .attr('y', yMidTop)
4819
+ .attr('text-anchor', 'middle')
4795
4820
  .attr('fill', textColor)
4796
- .attr('font-size', '12px')
4797
- .attr('transform', `rotate(-90, 15, ${margin.top})`)
4821
+ .attr('font-size', '18px')
4822
+ .attr('transform', `rotate(-90, 22, ${yMidTop})`)
4798
4823
  .style(
4799
4824
  'cursor',
4800
4825
  onClickItem && quadrantYAxisLineNumber ? 'pointer' : 'default'
4801
4826
  )
4802
4827
  .text(quadrantYAxis[1]);
4803
4828
 
4804
- // Arrow in the middle
4805
- svg
4806
- .append('text')
4807
- .attr('x', 15)
4808
- .attr('y', height / 2)
4809
- .attr('text-anchor', 'middle')
4810
- .attr('fill', mutedColor)
4811
- .attr('font-size', '12px')
4812
- .attr('transform', `rotate(-90, 15, ${height / 2})`)
4813
- .text('→');
4814
-
4815
4829
  if (onClickItem && quadrantYAxisLineNumber) {
4816
4830
  [yLowLabel, yHighLabel].forEach((label) => {
4817
4831
  label
@@ -4866,13 +4880,13 @@ export function renderQuadrant(
4866
4880
 
4867
4881
  const pointG = pointsG.append('g').attr('class', 'point-group');
4868
4882
 
4869
- // Circle (contrasting fill with colored border for visibility)
4883
+ // Circle with white fill and colored border for visibility on opaque quadrants
4870
4884
  pointG
4871
4885
  .append('circle')
4872
4886
  .attr('cx', cx)
4873
4887
  .attr('cy', cy)
4874
4888
  .attr('r', 6)
4875
- .attr('fill', contrastColor)
4889
+ .attr('fill', '#ffffff')
4876
4890
  .attr('stroke', pointColor)
4877
4891
  .attr('stroke-width', 2);
4878
4892
 
@@ -4883,7 +4897,8 @@ export function renderQuadrant(
4883
4897
  .attr('y', cy - 10)
4884
4898
  .attr('text-anchor', 'middle')
4885
4899
  .attr('fill', contrastColor)
4886
- .attr('font-size', '11px')
4900
+ .attr('font-size', '12px')
4901
+ .attr('font-weight', '700')
4887
4902
  .style('text-shadow', `0 1px 2px ${shadowColor}`)
4888
4903
  .text(point.label);
4889
4904
 
@@ -4958,7 +4973,15 @@ export async function renderD3ForExport(
4958
4973
  palette?: PaletteColors
4959
4974
  ): Promise<string> {
4960
4975
  const parsed = parseD3(content, palette);
4961
- if (parsed.error) return '';
4976
+ // Allow sequence diagrams through even if parseD3 errors
4977
+ // sequence is parsed by its own dedicated parser (parseSequenceDgmo)
4978
+ // and may not have a "chart:" line (auto-detected from arrow syntax).
4979
+ if (parsed.error && parsed.type !== 'sequence') {
4980
+ // Check if content looks like a sequence diagram (has arrows but no chart: line)
4981
+ const looksLikeSequence = /->|~>|<-/.test(content);
4982
+ if (!looksLikeSequence) return '';
4983
+ parsed.type = 'sequence';
4984
+ }
4962
4985
  if (parsed.type === 'wordcloud' && parsed.words.length === 0) return '';
4963
4986
  if (parsed.type === 'slope' && parsed.data.length === 0) return '';
4964
4987
  if (parsed.type === 'arc' && parsed.links.length === 0) return '';
@@ -4983,33 +5006,39 @@ export async function renderD3ForExport(
4983
5006
  container.style.left = '-9999px';
4984
5007
  document.body.appendChild(container);
4985
5008
 
5009
+ const dims: D3ExportDimensions = { width: EXPORT_WIDTH, height: EXPORT_HEIGHT };
5010
+
4986
5011
  try {
4987
5012
  if (parsed.type === 'sequence') {
4988
5013
  const { parseSequenceDgmo } = await import('./sequence/parser');
4989
5014
  const { renderSequenceDiagram } = await import('./sequence/renderer');
4990
5015
  const seqParsed = parseSequenceDgmo(content);
4991
5016
  if (seqParsed.error || seqParsed.participants.length === 0) return '';
4992
- renderSequenceDiagram(container, seqParsed, effectivePalette, isDark);
5017
+ renderSequenceDiagram(container, seqParsed, effectivePalette, isDark, undefined, {
5018
+ exportWidth: EXPORT_WIDTH,
5019
+ });
4993
5020
  } else if (parsed.type === 'wordcloud') {
4994
- await renderWordCloudAsync(container, parsed, effectivePalette, isDark);
5021
+ await renderWordCloudAsync(container, parsed, effectivePalette, isDark, dims);
4995
5022
  } else if (parsed.type === 'arc') {
4996
- renderArcDiagram(container, parsed, effectivePalette, isDark);
5023
+ renderArcDiagram(container, parsed, effectivePalette, isDark, undefined, dims);
4997
5024
  } else if (parsed.type === 'timeline') {
4998
- renderTimeline(container, parsed, effectivePalette, isDark);
5025
+ renderTimeline(container, parsed, effectivePalette, isDark, undefined, dims);
4999
5026
  } else if (parsed.type === 'venn') {
5000
- renderVenn(container, parsed, effectivePalette, isDark);
5027
+ renderVenn(container, parsed, effectivePalette, isDark, undefined, dims);
5001
5028
  } else if (parsed.type === 'quadrant') {
5002
- renderQuadrant(container, parsed, effectivePalette, isDark);
5029
+ renderQuadrant(container, parsed, effectivePalette, isDark, undefined, dims);
5003
5030
  } else {
5004
- renderSlopeChart(container, parsed, effectivePalette, isDark);
5031
+ renderSlopeChart(container, parsed, effectivePalette, isDark, undefined, dims);
5005
5032
  }
5006
5033
 
5007
5034
  const svgEl = container.querySelector('svg');
5008
5035
  if (!svgEl) return '';
5009
5036
 
5010
- // For transparent theme, remove the background
5037
+ // Ensure all chart types have a consistent background
5011
5038
  if (theme === 'transparent') {
5012
5039
  svgEl.style.background = 'none';
5040
+ } else if (!svgEl.style.background) {
5041
+ svgEl.style.background = effectivePalette.bg;
5013
5042
  }
5014
5043
 
5015
5044
  // Add xmlns for standalone SVG