@diagrammo/dgmo 0.2.8 → 0.2.10

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.2.8",
3
+ "version": "0.2.10",
4
4
  "description": "DGMO diagram markup language — parser, renderer, and color system",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/chart.ts CHANGED
@@ -23,6 +23,7 @@ export interface ChartDataPoint {
23
23
  export interface ParsedChart {
24
24
  type: ChartType;
25
25
  title?: string;
26
+ titleLineNumber?: number;
26
27
  series?: string;
27
28
  xlabel?: string;
28
29
  ylabel?: string;
@@ -31,6 +32,7 @@ export interface ParsedChart {
31
32
  orientation?: 'horizontal' | 'vertical';
32
33
  color?: string;
33
34
  label?: string;
35
+ labels?: 'name' | 'value' | 'percent' | 'full';
34
36
  data: ChartDataPoint[];
35
37
  error?: string;
36
38
  }
@@ -120,6 +122,7 @@ export function parseChart(
120
122
 
121
123
  if (key === 'title') {
122
124
  result.title = value;
125
+ result.titleLineNumber = lineNumber;
123
126
  continue;
124
127
  }
125
128
 
@@ -138,6 +141,14 @@ export function parseChart(
138
141
  continue;
139
142
  }
140
143
 
144
+ if (key === 'labels') {
145
+ const v = value.toLowerCase();
146
+ if (v === 'name' || v === 'value' || v === 'percent' || v === 'full') {
147
+ result.labels = v;
148
+ }
149
+ continue;
150
+ }
151
+
141
152
  if (key === 'orientation') {
142
153
  const v = value.toLowerCase();
143
154
  if (v === 'horizontal' || v === 'vertical') {
package/src/cli.ts CHANGED
@@ -1,10 +1,7 @@
1
1
  import { existsSync, readFileSync, writeFileSync } from 'node:fs';
2
2
  import { resolve, basename, extname } from 'node:path';
3
- import { JSDOM } from 'jsdom';
4
3
  import { Resvg } from '@resvg/resvg-js';
5
- import { renderD3ForExport } from './d3';
6
- import { renderEChartsForExport } from './echarts';
7
- import { parseDgmoChartType, getDgmoFramework } from './dgmo-router';
4
+ import { render } from './render';
8
5
  import { getPalette } from './palettes/registry';
9
6
  import { DEFAULT_FONT_NAME } from './fonts';
10
7
 
@@ -108,18 +105,6 @@ function parseArgs(argv: string[]): {
108
105
  return result;
109
106
  }
110
107
 
111
- function setupDom(): void {
112
- const dom = new JSDOM('<!DOCTYPE html><html><body></body></html>');
113
- const win = dom.window;
114
-
115
- // Expose DOM globals needed by d3-selection and renderers
116
- Object.defineProperty(globalThis, 'document', { value: win.document, configurable: true });
117
- Object.defineProperty(globalThis, 'window', { value: win, configurable: true });
118
- Object.defineProperty(globalThis, 'navigator', { value: win.navigator, configurable: true });
119
- Object.defineProperty(globalThis, 'HTMLElement', { value: win.HTMLElement, configurable: true });
120
- Object.defineProperty(globalThis, 'SVGElement', { value: win.SVGElement, configurable: true });
121
- }
122
-
123
108
  function inferFormat(outputPath: string | undefined): 'svg' | 'png' {
124
109
  if (outputPath && extname(outputPath).toLowerCase() === '.svg') {
125
110
  return 'svg';
@@ -217,28 +202,23 @@ async function main(): Promise<void> {
217
202
  noInput();
218
203
  }
219
204
 
220
- const isDark = opts.theme === 'dark';
221
- const paletteColors = isDark
222
- ? getPalette(opts.palette).dark
223
- : getPalette(opts.palette).light;
224
-
225
- // Determine which rendering framework to use
226
- const chartType = parseDgmoChartType(content);
227
- const framework = chartType ? getDgmoFramework(chartType) : null;
228
-
229
- let svg: string;
205
+ const paletteColors = getPalette(opts.palette)[opts.theme === 'dark' ? 'dark' : 'light'];
230
206
 
231
- if (framework === 'echart') {
232
- svg = await renderEChartsForExport(content, opts.theme, paletteColors);
233
- } else if (framework === 'd3' || framework === null) {
234
- // Set up jsdom before any d3/renderer code runs
235
- setupDom();
236
- svg = await renderD3ForExport(content, opts.theme, paletteColors);
237
- } else {
238
- console.error(`Error: Unknown chart framework "${framework}".`);
207
+ // Word clouds require Canvas APIs (HTMLCanvasElement.getContext('2d'))
208
+ // which are unavailable in Node.js — check before attempting render.
209
+ const wordcloudRe = /^\s*chart\s*:\s*wordcloud\b/im;
210
+ if (wordcloudRe.test(content)) {
211
+ console.error(
212
+ 'Error: Word clouds are not supported in the CLI (requires Canvas). Use the desktop app or browser instead.'
213
+ );
239
214
  process.exit(1);
240
215
  }
241
216
 
217
+ const svg = await render(content, {
218
+ theme: opts.theme,
219
+ palette: opts.palette,
220
+ });
221
+
242
222
  if (!svg) {
243
223
  console.error(
244
224
  'Error: Failed to render diagram. The input may be empty, invalid, or use an unsupported chart type.'
package/src/d3.ts CHANGED
@@ -139,6 +139,7 @@ export interface D3ExportDimensions {
139
139
  export interface ParsedD3 {
140
140
  type: D3ChartType | null;
141
141
  title: string | null;
142
+ titleLineNumber: number | null;
142
143
  orientation: 'horizontal' | 'vertical';
143
144
  periods: string[];
144
145
  data: D3DataItem[];
@@ -265,6 +266,7 @@ export function parseD3(content: string, palette?: PaletteColors): ParsedD3 {
265
266
  const result: ParsedD3 = {
266
267
  type: null,
267
268
  title: null,
269
+ titleLineNumber: null,
268
270
  orientation: 'horizontal',
269
271
  periods: [],
270
272
  data: [],
@@ -609,6 +611,7 @@ export function parseD3(content: string, palette?: PaletteColors): ParsedD3 {
609
611
 
610
612
  if (key === 'title') {
611
613
  result.title = line.substring(colonIndex + 1).trim();
614
+ result.titleLineNumber = lineNumber;
612
615
  if (result.type === 'quadrant') {
613
616
  result.quadrantTitleLineNumber = lineNumber;
614
617
  }
@@ -1120,15 +1123,27 @@ export function renderSlopeChart(
1120
1123
 
1121
1124
  // Title
1122
1125
  if (title) {
1123
- svg
1126
+ const titleEl = svg
1124
1127
  .append('text')
1128
+ .attr('class', 'chart-title')
1125
1129
  .attr('x', width / 2)
1126
1130
  .attr('y', 30)
1127
1131
  .attr('text-anchor', 'middle')
1128
1132
  .attr('fill', textColor)
1129
1133
  .attr('font-size', '20px')
1130
1134
  .attr('font-weight', '700')
1135
+ .style('cursor', onClickItem && parsed.titleLineNumber ? 'pointer' : 'default')
1131
1136
  .text(title);
1137
+
1138
+ if (parsed.titleLineNumber) {
1139
+ titleEl.attr('data-line-number', parsed.titleLineNumber);
1140
+ if (onClickItem) {
1141
+ titleEl
1142
+ .on('click', () => onClickItem(parsed.titleLineNumber!))
1143
+ .on('mouseenter', function () { d3Selection.select(this).attr('opacity', 0.7); })
1144
+ .on('mouseleave', function () { d3Selection.select(this).attr('opacity', 1); });
1145
+ }
1146
+ }
1132
1147
  }
1133
1148
 
1134
1149
  // Period column headers
@@ -1164,6 +1179,12 @@ export function renderSlopeChart(
1164
1179
  data.forEach((item, idx) => {
1165
1180
  const color = item.color ?? colors[idx % colors.length];
1166
1181
 
1182
+ // Wrap each series in a group with data-line-number for sync adapter
1183
+ const seriesG = g
1184
+ .append('g')
1185
+ .attr('class', 'slope-series')
1186
+ .attr('data-line-number', String(item.lineNumber));
1187
+
1167
1188
  // Tooltip content – overall change for this series
1168
1189
  const firstVal = item.values[0];
1169
1190
  const lastVal = item.values[item.values.length - 1];
@@ -1178,7 +1199,7 @@ export function renderSlopeChart(
1178
1199
  `Change: ${sign}${absChange}${pctPart}`;
1179
1200
 
1180
1201
  // Line
1181
- g.append('path')
1202
+ seriesG.append('path')
1182
1203
  .datum(item.values)
1183
1204
  .attr('fill', 'none')
1184
1205
  .attr('stroke', color)
@@ -1186,7 +1207,7 @@ export function renderSlopeChart(
1186
1207
  .attr('d', lineGen);
1187
1208
 
1188
1209
  // Invisible wider path for easier hover targeting
1189
- g.append('path')
1210
+ seriesG.append('path')
1190
1211
  .datum(item.values)
1191
1212
  .attr('fill', 'none')
1192
1213
  .attr('stroke', 'transparent')
@@ -1210,7 +1231,7 @@ export function renderSlopeChart(
1210
1231
  const y = yScale(val);
1211
1232
 
1212
1233
  // Point circle
1213
- g.append('circle')
1234
+ seriesG.append('circle')
1214
1235
  .attr('cx', x)
1215
1236
  .attr('cy', y)
1216
1237
  .attr('r', 4)
@@ -1233,7 +1254,7 @@ export function renderSlopeChart(
1233
1254
  const isFirst = i === 0;
1234
1255
  const isLast = i === periods.length - 1;
1235
1256
  if (!isLast) {
1236
- g.append('text')
1257
+ seriesG.append('text')
1237
1258
  .attr('x', isFirst ? x - 10 : x)
1238
1259
  .attr('y', y)
1239
1260
  .attr('dy', '0.35em')
@@ -1251,7 +1272,7 @@ export function renderSlopeChart(
1251
1272
  const availableWidth = rightMargin - 15;
1252
1273
  const maxChars = Math.floor(availableWidth / SLOPE_CHAR_WIDTH);
1253
1274
 
1254
- const labelEl = g
1275
+ const labelEl = seriesG
1255
1276
  .append('text')
1256
1277
  .attr('x', lastX + 10)
1257
1278
  .attr('y', lastY)
@@ -1501,15 +1522,27 @@ export function renderArcDiagram(
1501
1522
 
1502
1523
  // Title
1503
1524
  if (title) {
1504
- svg
1525
+ const titleEl = svg
1505
1526
  .append('text')
1527
+ .attr('class', 'chart-title')
1506
1528
  .attr('x', width / 2)
1507
1529
  .attr('y', 30)
1508
1530
  .attr('text-anchor', 'middle')
1509
1531
  .attr('fill', textColor)
1510
1532
  .attr('font-size', '20px')
1511
1533
  .attr('font-weight', '700')
1534
+ .style('cursor', onClickItem && parsed.titleLineNumber ? 'pointer' : 'default')
1512
1535
  .text(title);
1536
+
1537
+ if (parsed.titleLineNumber) {
1538
+ titleEl.attr('data-line-number', parsed.titleLineNumber);
1539
+ if (onClickItem) {
1540
+ titleEl
1541
+ .on('click', () => onClickItem(parsed.titleLineNumber!))
1542
+ .on('mouseenter', function () { d3Selection.select(this).attr('opacity', 0.7); })
1543
+ .on('mouseleave', function () { d3Selection.select(this).attr('opacity', 1); });
1544
+ }
1545
+ }
1513
1546
  }
1514
1547
 
1515
1548
  // Build adjacency map for hover interactions
@@ -1613,6 +1646,7 @@ export function renderArcDiagram(
1613
1646
  g.append('rect')
1614
1647
  .attr('class', 'arc-group-band')
1615
1648
  .attr('data-group', group.name)
1649
+ .attr('data-line-number', String(group.lineNumber))
1616
1650
  .attr('x', baseX - bandHalfW)
1617
1651
  .attr('y', minY)
1618
1652
  .attr('width', bandHalfW * 2)
@@ -1630,6 +1664,7 @@ export function renderArcDiagram(
1630
1664
  g.append('text')
1631
1665
  .attr('class', 'arc-group-label')
1632
1666
  .attr('data-group', group.name)
1667
+ .attr('data-line-number', String(group.lineNumber))
1633
1668
  .attr('x', baseX - bandHalfW + 6)
1634
1669
  .attr('y', minY + 14)
1635
1670
  .attr('fill', textColor)
@@ -1669,6 +1704,7 @@ export function renderArcDiagram(
1669
1704
  .attr('class', 'arc-link')
1670
1705
  .attr('data-source', link.source)
1671
1706
  .attr('data-target', link.target)
1707
+ .attr('data-line-number', String(link.lineNumber))
1672
1708
  .attr('d', `M ${baseX},${y1} Q ${controlX},${midY} ${baseX},${y2}`)
1673
1709
  .attr('fill', 'none')
1674
1710
  .attr('stroke', color)
@@ -1693,6 +1729,7 @@ export function renderArcDiagram(
1693
1729
  .append('g')
1694
1730
  .attr('class', 'arc-node')
1695
1731
  .attr('data-node', node)
1732
+ .attr('data-line-number', nodeLink?.lineNumber ? String(nodeLink.lineNumber) : null)
1696
1733
  .style('cursor', 'pointer')
1697
1734
  .on('mouseenter', () => handleMouseEnter(node))
1698
1735
  .on('mouseleave', handleMouseLeave)
@@ -1746,6 +1783,7 @@ export function renderArcDiagram(
1746
1783
  g.append('rect')
1747
1784
  .attr('class', 'arc-group-band')
1748
1785
  .attr('data-group', group.name)
1786
+ .attr('data-line-number', String(group.lineNumber))
1749
1787
  .attr('x', minX)
1750
1788
  .attr('y', baseY - bandHalfH)
1751
1789
  .attr('width', maxX - minX)
@@ -1763,6 +1801,7 @@ export function renderArcDiagram(
1763
1801
  g.append('text')
1764
1802
  .attr('class', 'arc-group-label')
1765
1803
  .attr('data-group', group.name)
1804
+ .attr('data-line-number', String(group.lineNumber))
1766
1805
  .attr('x', (minX + maxX) / 2)
1767
1806
  .attr('y', baseY + bandHalfH - 4)
1768
1807
  .attr('text-anchor', 'middle')
@@ -1803,6 +1842,7 @@ export function renderArcDiagram(
1803
1842
  .attr('class', 'arc-link')
1804
1843
  .attr('data-source', link.source)
1805
1844
  .attr('data-target', link.target)
1845
+ .attr('data-line-number', String(link.lineNumber))
1806
1846
  .attr('d', `M ${x1},${baseY} Q ${midX},${controlY} ${x2},${baseY}`)
1807
1847
  .attr('fill', 'none')
1808
1848
  .attr('stroke', color)
@@ -1827,6 +1867,7 @@ export function renderArcDiagram(
1827
1867
  .append('g')
1828
1868
  .attr('class', 'arc-node')
1829
1869
  .attr('data-node', node)
1870
+ .attr('data-line-number', nodeLink?.lineNumber ? String(nodeLink.lineNumber) : null)
1830
1871
  .style('cursor', 'pointer')
1831
1872
  .on('mouseenter', () => handleMouseEnter(node))
1832
1873
  .on('mouseleave', handleMouseLeave)
@@ -1992,6 +2033,7 @@ function renderMarkers(
1992
2033
  .append('g')
1993
2034
  .attr('class', 'tl-marker')
1994
2035
  .attr('data-marker-date', String(dateVal))
2036
+ .attr('data-line-number', String(marker.lineNumber))
1995
2037
  .style('cursor', 'pointer')
1996
2038
  .on('mouseenter', function (event: MouseEvent) {
1997
2039
  if (tooltip) {
@@ -2745,15 +2787,27 @@ export function renderTimeline(
2745
2787
  .attr('transform', `translate(${margin.left},${margin.top})`);
2746
2788
 
2747
2789
  if (title) {
2748
- svg
2790
+ const titleEl = svg
2749
2791
  .append('text')
2792
+ .attr('class', 'chart-title')
2750
2793
  .attr('x', width / 2)
2751
2794
  .attr('y', 30)
2752
2795
  .attr('text-anchor', 'middle')
2753
2796
  .attr('fill', textColor)
2754
2797
  .attr('font-size', '20px')
2755
2798
  .attr('font-weight', '700')
2799
+ .style('cursor', onClickItem && parsed.titleLineNumber ? 'pointer' : 'default')
2756
2800
  .text(title);
2801
+
2802
+ if (parsed.titleLineNumber) {
2803
+ titleEl.attr('data-line-number', parsed.titleLineNumber);
2804
+ if (onClickItem) {
2805
+ titleEl
2806
+ .on('click', () => onClickItem(parsed.titleLineNumber!))
2807
+ .on('mouseenter', function () { d3Selection.select(this).attr('opacity', 0.7); })
2808
+ .on('mouseleave', function () { d3Selection.select(this).attr('opacity', 1); });
2809
+ }
2810
+ }
2757
2811
  }
2758
2812
 
2759
2813
  renderEras(
@@ -2841,6 +2895,7 @@ export function renderTimeline(
2841
2895
  .append('g')
2842
2896
  .attr('class', 'tl-event')
2843
2897
  .attr('data-group', laneName)
2898
+ .attr('data-line-number', String(ev.lineNumber))
2844
2899
  .attr('data-date', String(parseTimelineDate(ev.date)))
2845
2900
  .attr(
2846
2901
  'data-end-date',
@@ -2936,15 +2991,27 @@ export function renderTimeline(
2936
2991
  .attr('transform', `translate(${margin.left},${margin.top})`);
2937
2992
 
2938
2993
  if (title) {
2939
- svg
2994
+ const titleEl = svg
2940
2995
  .append('text')
2996
+ .attr('class', 'chart-title')
2941
2997
  .attr('x', width / 2)
2942
2998
  .attr('y', 30)
2943
2999
  .attr('text-anchor', 'middle')
2944
3000
  .attr('fill', textColor)
2945
3001
  .attr('font-size', '20px')
2946
3002
  .attr('font-weight', '700')
3003
+ .style('cursor', onClickItem && parsed.titleLineNumber ? 'pointer' : 'default')
2947
3004
  .text(title);
3005
+
3006
+ if (parsed.titleLineNumber) {
3007
+ titleEl.attr('data-line-number', parsed.titleLineNumber);
3008
+ if (onClickItem) {
3009
+ titleEl
3010
+ .on('click', () => onClickItem(parsed.titleLineNumber!))
3011
+ .on('mouseenter', function () { d3Selection.select(this).attr('opacity', 0.7); })
3012
+ .on('mouseleave', function () { d3Selection.select(this).attr('opacity', 1); });
3013
+ }
3014
+ }
2948
3015
  }
2949
3016
 
2950
3017
  renderEras(
@@ -3039,6 +3106,7 @@ export function renderTimeline(
3039
3106
  .append('g')
3040
3107
  .attr('class', 'tl-event')
3041
3108
  .attr('data-group', ev.group || '')
3109
+ .attr('data-line-number', String(ev.lineNumber))
3042
3110
  .attr('data-date', String(parseTimelineDate(ev.date)))
3043
3111
  .attr(
3044
3112
  'data-end-date',
@@ -3188,15 +3256,27 @@ export function renderTimeline(
3188
3256
  .attr('transform', `translate(${margin.left},${margin.top})`);
3189
3257
 
3190
3258
  if (title) {
3191
- svg
3259
+ const titleEl = svg
3192
3260
  .append('text')
3261
+ .attr('class', 'chart-title')
3193
3262
  .attr('x', width / 2)
3194
3263
  .attr('y', 30)
3195
3264
  .attr('text-anchor', 'middle')
3196
3265
  .attr('fill', textColor)
3197
3266
  .attr('font-size', '20px')
3198
3267
  .attr('font-weight', '700')
3268
+ .style('cursor', onClickItem && parsed.titleLineNumber ? 'pointer' : 'default')
3199
3269
  .text(title);
3270
+
3271
+ if (parsed.titleLineNumber) {
3272
+ titleEl.attr('data-line-number', parsed.titleLineNumber);
3273
+ if (onClickItem) {
3274
+ titleEl
3275
+ .on('click', () => onClickItem(parsed.titleLineNumber!))
3276
+ .on('mouseenter', function () { d3Selection.select(this).attr('opacity', 0.7); })
3277
+ .on('mouseleave', function () { d3Selection.select(this).attr('opacity', 1); });
3278
+ }
3279
+ }
3200
3280
  }
3201
3281
 
3202
3282
  renderEras(
@@ -3300,6 +3380,7 @@ export function renderTimeline(
3300
3380
  .append('g')
3301
3381
  .attr('class', 'tl-event')
3302
3382
  .attr('data-group', lane.name)
3383
+ .attr('data-line-number', String(ev.lineNumber))
3303
3384
  .attr('data-date', String(parseTimelineDate(ev.date)))
3304
3385
  .attr(
3305
3386
  'data-end-date',
@@ -3472,15 +3553,27 @@ export function renderTimeline(
3472
3553
  .attr('transform', `translate(${margin.left},${margin.top})`);
3473
3554
 
3474
3555
  if (title) {
3475
- svg
3556
+ const titleEl = svg
3476
3557
  .append('text')
3558
+ .attr('class', 'chart-title')
3477
3559
  .attr('x', width / 2)
3478
3560
  .attr('y', 30)
3479
3561
  .attr('text-anchor', 'middle')
3480
3562
  .attr('fill', textColor)
3481
3563
  .attr('font-size', '20px')
3482
3564
  .attr('font-weight', '700')
3565
+ .style('cursor', onClickItem && parsed.titleLineNumber ? 'pointer' : 'default')
3483
3566
  .text(title);
3567
+
3568
+ if (parsed.titleLineNumber) {
3569
+ titleEl.attr('data-line-number', parsed.titleLineNumber);
3570
+ if (onClickItem) {
3571
+ titleEl
3572
+ .on('click', () => onClickItem(parsed.titleLineNumber!))
3573
+ .on('mouseenter', function () { d3Selection.select(this).attr('opacity', 0.7); })
3574
+ .on('mouseleave', function () { d3Selection.select(this).attr('opacity', 1); });
3575
+ }
3576
+ }
3484
3577
  }
3485
3578
 
3486
3579
  renderEras(
@@ -3568,6 +3661,7 @@ export function renderTimeline(
3568
3661
  .append('g')
3569
3662
  .attr('class', 'tl-event')
3570
3663
  .attr('data-group', ev.group || '')
3664
+ .attr('data-line-number', String(ev.lineNumber))
3571
3665
  .attr('data-date', String(parseTimelineDate(ev.date)))
3572
3666
  .attr(
3573
3667
  'data-end-date',
@@ -3767,15 +3861,27 @@ export function renderWordCloud(
3767
3861
  .style('background', bgColor);
3768
3862
 
3769
3863
  if (title) {
3770
- svg
3864
+ const titleEl = svg
3771
3865
  .append('text')
3866
+ .attr('class', 'chart-title')
3772
3867
  .attr('x', width / 2)
3773
3868
  .attr('y', 30)
3774
3869
  .attr('text-anchor', 'middle')
3775
3870
  .attr('fill', textColor)
3776
3871
  .attr('font-size', '20px')
3777
3872
  .attr('font-weight', '700')
3873
+ .style('cursor', onClickItem && parsed.titleLineNumber ? 'pointer' : 'default')
3778
3874
  .text(title);
3875
+
3876
+ if (parsed.titleLineNumber) {
3877
+ titleEl.attr('data-line-number', parsed.titleLineNumber);
3878
+ if (onClickItem) {
3879
+ titleEl
3880
+ .on('click', () => onClickItem(parsed.titleLineNumber!))
3881
+ .on('mouseenter', function () { d3Selection.select(this).attr('opacity', 0.7); })
3882
+ .on('mouseleave', function () { d3Selection.select(this).attr('opacity', 1); });
3883
+ }
3884
+ }
3779
3885
  }
3780
3886
 
3781
3887
  const g = svg
@@ -3808,6 +3914,10 @@ export function renderWordCloud(
3808
3914
  'transform',
3809
3915
  (d) => `translate(${d.x},${d.y}) rotate(${d.rotate})`
3810
3916
  )
3917
+ .attr('data-line-number', (d) => {
3918
+ const ln = (d as WordCloudWord).lineNumber;
3919
+ return ln ? String(ln) : null;
3920
+ })
3811
3921
  .text((d) => d.text!)
3812
3922
  .on('click', (_event, d) => {
3813
3923
  const ln = (d as WordCloudWord).lineNumber;
@@ -3872,8 +3982,9 @@ function renderWordCloudAsync(
3872
3982
  .style('background', bgColor);
3873
3983
 
3874
3984
  if (title) {
3875
- svg
3985
+ const titleEl = svg
3876
3986
  .append('text')
3987
+ .attr('class', 'chart-title')
3877
3988
  .attr('x', width / 2)
3878
3989
  .attr('y', 30)
3879
3990
  .attr('text-anchor', 'middle')
@@ -3881,6 +3992,10 @@ function renderWordCloudAsync(
3881
3992
  .attr('font-size', '20px')
3882
3993
  .attr('font-weight', '700')
3883
3994
  .text(title);
3995
+
3996
+ if (parsed.titleLineNumber) {
3997
+ titleEl.attr('data-line-number', parsed.titleLineNumber);
3998
+ }
3884
3999
  }
3885
4000
 
3886
4001
  const g = svg
@@ -4217,15 +4332,27 @@ export function renderVenn(
4217
4332
 
4218
4333
  // Title
4219
4334
  if (title) {
4220
- svg
4335
+ const titleEl = svg
4221
4336
  .append('text')
4337
+ .attr('class', 'chart-title')
4222
4338
  .attr('x', width / 2)
4223
4339
  .attr('y', 30)
4224
4340
  .attr('text-anchor', 'middle')
4225
4341
  .attr('fill', textColor)
4226
4342
  .attr('font-size', '20px')
4227
4343
  .attr('font-weight', '700')
4344
+ .style('cursor', onClickItem && parsed.titleLineNumber ? 'pointer' : 'default')
4228
4345
  .text(title);
4346
+
4347
+ if (parsed.titleLineNumber) {
4348
+ titleEl.attr('data-line-number', parsed.titleLineNumber);
4349
+ if (onClickItem) {
4350
+ titleEl
4351
+ .on('click', () => onClickItem(parsed.titleLineNumber!))
4352
+ .on('mouseenter', function () { d3Selection.select(this).attr('opacity', 0.7); })
4353
+ .on('mouseleave', function () { d3Selection.select(this).attr('opacity', 1); });
4354
+ }
4355
+ }
4229
4356
  }
4230
4357
 
4231
4358
  // ── Clip-path definitions ──
@@ -4490,6 +4617,7 @@ export function renderVenn(
4490
4617
  .attr('cy', c.y)
4491
4618
  .attr('r', c.r)
4492
4619
  .attr('fill', 'transparent')
4620
+ .attr('data-line-number', String(vennSets[i].lineNumber))
4493
4621
  .style('cursor', onClickItem ? 'pointer' : 'default')
4494
4622
  .on('mouseenter', (event: MouseEvent) => {
4495
4623
  for (const rg of regionGroups) {
@@ -4593,6 +4721,7 @@ export function renderQuadrant(
4593
4721
  if (title) {
4594
4722
  const titleText = svg
4595
4723
  .append('text')
4724
+ .attr('class', 'chart-title')
4596
4725
  .attr('x', width / 2)
4597
4726
  .attr('y', 30)
4598
4727
  .attr('text-anchor', 'middle')
@@ -4605,6 +4734,10 @@ export function renderQuadrant(
4605
4734
  )
4606
4735
  .text(title);
4607
4736
 
4737
+ if (quadrantTitleLineNumber) {
4738
+ titleText.attr('data-line-number', quadrantTitleLineNumber);
4739
+ }
4740
+
4608
4741
  if (onClickItem && quadrantTitleLineNumber) {
4609
4742
  titleText
4610
4743
  .on('click', () => onClickItem(quadrantTitleLineNumber))
@@ -4739,6 +4872,9 @@ export function renderQuadrant(
4739
4872
  .attr('fill', (d) => getQuadrantLabelColor(d))
4740
4873
  .attr('font-size', '48px')
4741
4874
  .attr('font-weight', '700')
4875
+ .attr('data-line-number', (d) =>
4876
+ d.label?.lineNumber ? String(d.label.lineNumber) : null
4877
+ )
4742
4878
  .style('cursor', (d) =>
4743
4879
  onClickItem && d.label?.lineNumber ? 'pointer' : 'default'
4744
4880
  )
@@ -4762,11 +4898,13 @@ export function renderQuadrant(
4762
4898
  // Low label (centered on left half)
4763
4899
  const xLowLabel = svg
4764
4900
  .append('text')
4901
+ .attr('class', 'quadrant-axis-label')
4765
4902
  .attr('x', margin.left + chartWidth / 4)
4766
4903
  .attr('y', height - 20)
4767
4904
  .attr('text-anchor', 'middle')
4768
4905
  .attr('fill', textColor)
4769
4906
  .attr('font-size', '18px')
4907
+ .attr('data-line-number', quadrantXAxisLineNumber ? String(quadrantXAxisLineNumber) : null)
4770
4908
  .style(
4771
4909
  'cursor',
4772
4910
  onClickItem && quadrantXAxisLineNumber ? 'pointer' : 'default'
@@ -4776,11 +4914,13 @@ export function renderQuadrant(
4776
4914
  // High label (centered on right half)
4777
4915
  const xHighLabel = svg
4778
4916
  .append('text')
4917
+ .attr('class', 'quadrant-axis-label')
4779
4918
  .attr('x', margin.left + (chartWidth * 3) / 4)
4780
4919
  .attr('y', height - 20)
4781
4920
  .attr('text-anchor', 'middle')
4782
4921
  .attr('fill', textColor)
4783
4922
  .attr('font-size', '18px')
4923
+ .attr('data-line-number', quadrantXAxisLineNumber ? String(quadrantXAxisLineNumber) : null)
4784
4924
  .style(
4785
4925
  'cursor',
4786
4926
  onClickItem && quadrantXAxisLineNumber ? 'pointer' : 'default'
@@ -4809,12 +4949,14 @@ export function renderQuadrant(
4809
4949
  // Low label (centered on bottom half)
4810
4950
  const yLowLabel = svg
4811
4951
  .append('text')
4952
+ .attr('class', 'quadrant-axis-label')
4812
4953
  .attr('x', 22)
4813
4954
  .attr('y', yMidBottom)
4814
4955
  .attr('text-anchor', 'middle')
4815
4956
  .attr('fill', textColor)
4816
4957
  .attr('font-size', '18px')
4817
4958
  .attr('transform', `rotate(-90, 22, ${yMidBottom})`)
4959
+ .attr('data-line-number', quadrantYAxisLineNumber ? String(quadrantYAxisLineNumber) : null)
4818
4960
  .style(
4819
4961
  'cursor',
4820
4962
  onClickItem && quadrantYAxisLineNumber ? 'pointer' : 'default'
@@ -4824,12 +4966,14 @@ export function renderQuadrant(
4824
4966
  // High label (centered on top half)
4825
4967
  const yHighLabel = svg
4826
4968
  .append('text')
4969
+ .attr('class', 'quadrant-axis-label')
4827
4970
  .attr('x', 22)
4828
4971
  .attr('y', yMidTop)
4829
4972
  .attr('text-anchor', 'middle')
4830
4973
  .attr('fill', textColor)
4831
4974
  .attr('font-size', '18px')
4832
4975
  .attr('transform', `rotate(-90, 22, ${yMidTop})`)
4976
+ .attr('data-line-number', quadrantYAxisLineNumber ? String(quadrantYAxisLineNumber) : null)
4833
4977
  .style(
4834
4978
  'cursor',
4835
4979
  onClickItem && quadrantYAxisLineNumber ? 'pointer' : 'default'
@@ -4888,7 +5032,8 @@ export function renderQuadrant(
4888
5032
  const pointColor =
4889
5033
  quadDef?.label?.color ?? defaultColors[quadDef?.colorIdx ?? 0];
4890
5034
 
4891
- const pointG = pointsG.append('g').attr('class', 'point-group');
5035
+ const pointG = pointsG.append('g').attr('class', 'point-group')
5036
+ .attr('data-line-number', String(point.lineNumber));
4892
5037
 
4893
5038
  // Circle with white fill and colored border for visibility on opaque quadrants
4894
5039
  pointG