@diagrammo/dgmo 0.2.19 → 0.2.21

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.
@@ -15,6 +15,7 @@ import { layoutOrg } from './layout';
15
15
  // ============================================================
16
16
 
17
17
  const DIAGRAM_PADDING = 20;
18
+ const MAX_SCALE = 3;
18
19
  const TITLE_HEIGHT = 30;
19
20
  const TITLE_FONT_SIZE = 18;
20
21
  const LABEL_FONT_SIZE = 13;
@@ -35,29 +36,17 @@ const CONTAINER_HEADER_HEIGHT = 28;
35
36
  const COLLAPSE_BAR_HEIGHT = 6;
36
37
  const COLLAPSE_BAR_INSET = 0;
37
38
 
38
- // Legend
39
- const LEGEND_RADIUS = 6;
40
- const LEGEND_DOT_R = 5;
41
- const LEGEND_DOT_TEXT_GAP = 6;
42
- const LEGEND_ENTRY_GAP = 12;
43
- const LEGEND_PAD = 10;
44
- const LEGEND_HEADER_H = 20;
45
- const LEGEND_ENTRY_H = 18;
46
- const LEGEND_FONT_SIZE = 11;
47
- const LEGEND_MAX_PER_ROW = 3;
48
- const LEGEND_CHAR_WIDTH = 7.5;
49
-
50
- // Eye icon (12×12 viewBox, scaled from 0,0 to 12,12)
51
- const EYE_ICON_SIZE = 12;
52
- const EYE_ICON_GAP = 6;
53
- // Open eye: elliptical outline + circle pupil
54
- const EYE_OPEN_PATH =
55
- 'M1 6C1 6 3 2 6 2C9 2 11 6 11 6C11 6 9 10 6 10C3 10 1 6 1 6Z';
56
- const EYE_PUPIL_CX = 6;
57
- const EYE_PUPIL_CY = 6;
58
- const EYE_PUPIL_R = 1.8;
59
- // Closed eye: same outline + diagonal slash
60
- const EYE_SLASH_PATH = 'M2 2L10 10';
39
+ // Legend (kanban-style pills)
40
+ const LEGEND_HEIGHT = 28;
41
+ const LEGEND_PILL_PAD = 16;
42
+ const LEGEND_PILL_FONT_SIZE = 11;
43
+ const LEGEND_PILL_FONT_W = LEGEND_PILL_FONT_SIZE * 0.6;
44
+ const LEGEND_CAPSULE_PAD = 4;
45
+ const LEGEND_DOT_R = 4;
46
+ const LEGEND_ENTRY_FONT_SIZE = 10;
47
+ const LEGEND_ENTRY_FONT_W = LEGEND_ENTRY_FONT_SIZE * 0.6;
48
+ const LEGEND_ENTRY_DOT_GAP = 4;
49
+ const LEGEND_ENTRY_TRAIL = 8;
61
50
 
62
51
  // ============================================================
63
52
  // Color helpers (inline to avoid cross-module import issues)
@@ -144,7 +133,7 @@ export function renderOrg(
144
133
  const diagramH = layout.height + titleOffset;
145
134
  const scaleX = (width - DIAGRAM_PADDING * 2) / diagramW;
146
135
  const scaleY = (height - DIAGRAM_PADDING * 2) / diagramH;
147
- const scale = Math.min(scaleX, scaleY);
136
+ const scale = Math.min(MAX_SCALE, scaleX, scaleY);
148
137
 
149
138
  // Center the diagram
150
139
  const scaledW = diagramW * scale;
@@ -209,6 +198,7 @@ export function renderOrg(
209
198
  }
210
199
 
211
200
  // Render container backgrounds (bottom layer)
201
+ const colorOff = parsed.options?.color === 'off';
212
202
  for (const c of layout.containers) {
213
203
  const cG = contentG
214
204
  .append('g')
@@ -234,8 +224,8 @@ export function renderOrg(
234
224
  });
235
225
  }
236
226
 
237
- const fill = containerFill(palette, isDark, c.color);
238
- const stroke = containerStroke(palette, c.color);
227
+ const fill = containerFill(palette, isDark, colorOff ? undefined : c.color);
228
+ const stroke = containerStroke(palette, colorOff ? undefined : c.color);
239
229
 
240
230
  // Background rect
241
231
  cG.append('rect')
@@ -301,7 +291,7 @@ export function renderOrg(
301
291
  .attr('y', c.height - COLLAPSE_BAR_HEIGHT)
302
292
  .attr('width', c.width - COLLAPSE_BAR_INSET * 2)
303
293
  .attr('height', COLLAPSE_BAR_HEIGHT)
304
- .attr('fill', containerStroke(palette, c.color))
294
+ .attr('fill', containerStroke(palette, colorOff ? undefined : c.color))
305
295
  .attr('clip-path', `url(#${clipId})`)
306
296
  .attr('class', 'org-collapse-bar');
307
297
  }
@@ -360,8 +350,8 @@ export function renderOrg(
360
350
  }
361
351
 
362
352
  // Card background
363
- const fill = nodeFill(palette, isDark, node.color);
364
- const stroke = nodeStroke(palette, node.color);
353
+ const fill = nodeFill(palette, isDark, colorOff ? undefined : node.color);
354
+ const stroke = nodeStroke(palette, colorOff ? undefined : node.color);
365
355
 
366
356
  const rect = nodeG
367
357
  .append('rect')
@@ -448,22 +438,30 @@ export function renderOrg(
448
438
  .attr('y', node.height - COLLAPSE_BAR_HEIGHT)
449
439
  .attr('width', node.width - COLLAPSE_BAR_INSET * 2)
450
440
  .attr('height', COLLAPSE_BAR_HEIGHT)
451
- .attr('fill', nodeStroke(palette, node.color))
441
+ .attr('fill', nodeStroke(palette, colorOff ? undefined : node.color))
452
442
  .attr('clip-path', `url(#${clipId})`)
453
443
  .attr('class', 'org-collapse-bar');
454
444
  }
455
445
 
456
446
  }
457
447
 
458
- // Render legend — skip entirely in export mode; hide non-active groups when one is active
459
- if (!exportDims) for (const group of layout.legend) {
448
+ // Render legend — kanban-style pills.
449
+ // Skip in export mode (unless legend-only chart).
450
+ // Active group: only that group rendered as capsule (pill + entries).
451
+ // No active group: all groups rendered as standalone pills.
452
+ if (!exportDims || layout.nodes.length === 0) for (const group of layout.legend) {
460
453
  const isActive =
461
454
  activeTagGroup != null &&
462
455
  group.name.toLowerCase() === activeTagGroup.toLowerCase();
463
456
 
464
- // When a group is active, skip rendering all other groups
457
+ // When a group is active, skip all other groups entirely
465
458
  if (activeTagGroup != null && !isActive) continue;
466
459
 
460
+ const groupBg = mix(palette.surface, palette.bg, isDark ? 35 : 20);
461
+
462
+ const pillWidth =
463
+ group.name.length * LEGEND_PILL_FONT_W + LEGEND_PILL_PAD;
464
+
467
465
  const gEl = contentG
468
466
  .append('g')
469
467
  .attr('transform', `translate(${group.x}, ${group.y})`)
@@ -471,143 +469,77 @@ export function renderOrg(
471
469
  .attr('data-legend-group', group.name.toLowerCase())
472
470
  .style('cursor', 'pointer');
473
471
 
474
- // Background rect
475
- const legendFill = mix(palette.surface, palette.bg, 40);
476
- const bgRect = gEl
477
- .append('rect')
478
- .attr('x', 0)
479
- .attr('y', 0)
480
- .attr('width', group.width)
481
- .attr('height', group.height)
482
- .attr('rx', LEGEND_RADIUS)
483
- .attr('fill', legendFill);
472
+ // Outer capsule background (active only)
473
+ if (isActive) {
474
+ gEl
475
+ .append('rect')
476
+ .attr('width', group.width)
477
+ .attr('height', LEGEND_HEIGHT)
478
+ .attr('rx', LEGEND_HEIGHT / 2)
479
+ .attr('fill', groupBg);
480
+ }
484
481
 
482
+ const pillX = isActive ? LEGEND_CAPSULE_PAD : 0;
483
+ const pillY = isActive ? LEGEND_CAPSULE_PAD : 0;
484
+ const pillH = LEGEND_HEIGHT - (isActive ? LEGEND_CAPSULE_PAD * 2 : 0);
485
+
486
+ // Pill background
487
+ gEl
488
+ .append('rect')
489
+ .attr('x', pillX)
490
+ .attr('y', pillY)
491
+ .attr('width', pillWidth)
492
+ .attr('height', pillH)
493
+ .attr('rx', pillH / 2)
494
+ .attr('fill', isActive ? palette.bg : groupBg);
495
+
496
+ // Active pill border
485
497
  if (isActive) {
486
- bgRect
487
- .attr('stroke', palette.primary)
488
- .attr('stroke-opacity', 0.8)
489
- .attr('stroke-width', 2);
490
- } else {
491
- bgRect
492
- .attr('stroke', palette.textMuted)
493
- .attr('stroke-opacity', 0.35)
494
- .attr('stroke-width', NODE_STROKE_WIDTH);
498
+ gEl
499
+ .append('rect')
500
+ .attr('x', pillX)
501
+ .attr('y', pillY)
502
+ .attr('width', pillWidth)
503
+ .attr('height', pillH)
504
+ .attr('rx', pillH / 2)
505
+ .attr('fill', 'none')
506
+ .attr('stroke', mix(palette.textMuted, palette.bg, 50))
507
+ .attr('stroke-width', 0.75);
495
508
  }
496
509
 
497
- // Group name header
510
+ // Pill text
498
511
  gEl
499
512
  .append('text')
500
- .attr('x', LEGEND_PAD)
501
- .attr('y', LEGEND_HEADER_H / 2 + LEGEND_FONT_SIZE / 2 - 2)
502
- .attr('fill', palette.text)
503
- .attr('font-size', LEGEND_FONT_SIZE)
504
- .attr('font-weight', 'bold')
513
+ .attr('x', pillX + pillWidth / 2)
514
+ .attr('y', LEGEND_HEIGHT / 2 + LEGEND_PILL_FONT_SIZE / 2 - 2)
515
+ .attr('font-size', LEGEND_PILL_FONT_SIZE)
516
+ .attr('font-weight', '500')
517
+ .attr('fill', isActive ? palette.text : palette.textMuted)
518
+ .attr('text-anchor', 'middle')
505
519
  .text(group.name);
506
520
 
507
- // Eye icon for visibility toggle (interactive only, not export)
508
- if (hiddenAttributes !== undefined && !exportDims) {
509
- const groupKey = group.name.toLowerCase();
510
- const isHidden = hiddenAttributes.has(groupKey);
511
- const eyeX =
512
- LEGEND_PAD + group.name.length * LEGEND_CHAR_WIDTH + EYE_ICON_GAP;
513
- const eyeY = (LEGEND_HEADER_H - EYE_ICON_SIZE) / 2;
514
-
515
- const eyeG = gEl
516
- .append('g')
517
- .attr('class', 'org-legend-eye')
518
- .attr('data-legend-visibility', groupKey)
519
- .attr('transform', `translate(${eyeX}, ${eyeY})`);
520
-
521
- // Transparent hit area
522
- eyeG
523
- .append('rect')
524
- .attr('x', -4)
525
- .attr('y', -4)
526
- .attr('width', EYE_ICON_SIZE + 8)
527
- .attr('height', EYE_ICON_SIZE + 8)
528
- .attr('fill', 'transparent');
529
-
530
- // Eye outline
531
- eyeG
532
- .append('path')
533
- .attr('d', EYE_OPEN_PATH)
534
- .attr('fill', isHidden ? 'none' : palette.textMuted)
535
- .attr('fill-opacity', isHidden ? 0 : 0.15)
536
- .attr('stroke', palette.textMuted)
537
- .attr('stroke-width', 1.2)
538
- .attr('opacity', isHidden ? 0.5 : 0.7);
539
-
540
- if (!isHidden) {
541
- // Pupil (only when visible)
542
- eyeG
521
+ // Entries inside capsule (active only)
522
+ if (isActive) {
523
+ let entryX = pillX + pillWidth + 4;
524
+ for (const entry of group.entries) {
525
+ gEl
543
526
  .append('circle')
544
- .attr('cx', EYE_PUPIL_CX)
545
- .attr('cy', EYE_PUPIL_CY)
546
- .attr('r', EYE_PUPIL_R)
527
+ .attr('cx', entryX + LEGEND_DOT_R)
528
+ .attr('cy', LEGEND_HEIGHT / 2)
529
+ .attr('r', LEGEND_DOT_R)
530
+ .attr('fill', entry.color);
531
+
532
+ const textX = entryX + LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP;
533
+ gEl
534
+ .append('text')
535
+ .attr('x', textX)
536
+ .attr('y', LEGEND_HEIGHT / 2 + LEGEND_ENTRY_FONT_SIZE / 2 - 1)
537
+ .attr('font-size', LEGEND_ENTRY_FONT_SIZE)
547
538
  .attr('fill', palette.textMuted)
548
- .attr('opacity', 0.7);
549
- } else {
550
- // Slash through the eye (hidden state)
551
- eyeG
552
- .append('line')
553
- .attr('x1', 2)
554
- .attr('y1', 2)
555
- .attr('x2', 10)
556
- .attr('y2', 10)
557
- .attr('stroke', palette.textMuted)
558
- .attr('stroke-width', 1.5)
559
- .attr('opacity', 0.5);
560
- }
561
- }
539
+ .text(entry.value);
562
540
 
563
- // Pre-compute column widths so dots align across rows
564
- const entryWidths = group.entries.map(
565
- (e) =>
566
- LEGEND_DOT_R * 2 + LEGEND_DOT_TEXT_GAP + e.value.length * LEGEND_CHAR_WIDTH
567
- );
568
- const numRows = Math.ceil(group.entries.length / LEGEND_MAX_PER_ROW);
569
- const colWidths: number[] = [];
570
- for (let col = 0; col < LEGEND_MAX_PER_ROW; col++) {
571
- let maxW = 0;
572
- for (let r = 0; r < numRows; r++) {
573
- const idx = r * LEGEND_MAX_PER_ROW + col;
574
- if (idx < entryWidths.length && entryWidths[idx] > maxW) {
575
- maxW = entryWidths[idx];
576
- }
541
+ entryX = textX + entry.value.length * LEGEND_ENTRY_FONT_W + LEGEND_ENTRY_TRAIL;
577
542
  }
578
- if (maxW > 0) colWidths.push(maxW);
579
- }
580
- const colX: number[] = [LEGEND_PAD];
581
- for (let c = 1; c < colWidths.length; c++) {
582
- colX.push(colX[c - 1] + colWidths[c - 1] + LEGEND_ENTRY_GAP);
583
- }
584
-
585
- // Entries: colored dot + value label
586
- for (let i = 0; i < group.entries.length; i++) {
587
- const entry = group.entries[i];
588
- const row = Math.floor(i / LEGEND_MAX_PER_ROW);
589
- const col = i % LEGEND_MAX_PER_ROW;
590
- const entryX = colX[col];
591
-
592
- const entryY =
593
- LEGEND_HEADER_H + row * LEGEND_ENTRY_H + LEGEND_ENTRY_H / 2;
594
-
595
- // Colored dot
596
- gEl
597
- .append('circle')
598
- .attr('cx', entryX + LEGEND_DOT_R)
599
- .attr('cy', entryY)
600
- .attr('r', LEGEND_DOT_R)
601
- .attr('fill', entry.color);
602
-
603
- // Value label
604
- gEl
605
- .append('text')
606
- .attr('x', entryX + LEGEND_DOT_R * 2 + LEGEND_DOT_TEXT_GAP)
607
- .attr('y', entryY + LEGEND_FONT_SIZE / 2 - 2)
608
- .attr('fill', palette.text)
609
- .attr('font-size', LEGEND_FONT_SIZE)
610
- .text(entry.value);
611
543
  }
612
544
  }
613
545
  }
@@ -622,7 +554,7 @@ export function renderOrgForExport(
622
554
  palette: PaletteColors
623
555
  ): string {
624
556
  const parsed = parseOrg(content, palette);
625
- if (parsed.error || parsed.roots.length === 0) return '';
557
+ if (parsed.error) return '';
626
558
 
627
559
  // Extract hide option for export: cards sized without hidden attributes
628
560
  const hideOption = parsed.options?.['hide'];