@diagrammo/dgmo 0.2.21 → 0.2.22

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.21",
3
+ "version": "0.2.22",
4
4
  "description": "DGMO diagram markup language — parser, renderer, and color system",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/echarts.ts CHANGED
@@ -1198,16 +1198,15 @@ function buildFunnelOption(
1198
1198
  const val = p.value;
1199
1199
  const prev = prevValueMap.get(p.name) ?? val;
1200
1200
  const isFirst = p.dataIndex === 0;
1201
- let html = `<strong>${p.name}</strong>: ${val}`;
1202
- if (!isFirst) {
1203
- const stepDrop = ((1 - val / prev) * 100).toFixed(1);
1204
- html += `<br/>Step drop-off: ${stepDrop}%`;
1205
- }
1206
- if (!isFirst && topValue > 0) {
1201
+ if (isFirst) return '';
1202
+ const parts: string[] = [];
1203
+ const stepDrop = ((1 - val / prev) * 100).toFixed(1);
1204
+ parts.push(`Step drop-off: ${stepDrop}%`);
1205
+ if (topValue > 0) {
1207
1206
  const totalDrop = ((1 - val / topValue) * 100).toFixed(1);
1208
- html += `<br/>Overall drop-off: ${totalDrop}%`;
1207
+ parts.push(`Overall drop-off: ${totalDrop}%`);
1209
1208
  }
1210
- return html;
1209
+ return parts.join('<br/>');
1211
1210
  },
1212
1211
  },
1213
1212
  series: [
@@ -267,7 +267,9 @@ export function renderKanban(
267
267
  ? parsed.title.length * TITLE_FONT_SIZE * 0.6 + 16
268
268
  : 0;
269
269
  let legendX = DIAGRAM_PADDING + titleTextWidth;
270
- const groupBg = mix(palette.surface, palette.bg, isDark ? 35 : 20);
270
+ const groupBg = isDark
271
+ ? mix(palette.surface, palette.bg, 50)
272
+ : mix(palette.surface, palette.bg, 30);
271
273
  const capsulePad = 4;
272
274
 
273
275
  for (const group of parsed.tagGroups) {
package/src/org/layout.ts CHANGED
@@ -52,10 +52,12 @@ export interface OrgContainerBounds {
52
52
  export interface OrgLegendEntry {
53
53
  value: string;
54
54
  color: string;
55
+ isDefault?: boolean;
55
56
  }
56
57
 
57
58
  export interface OrgLegendGroup {
58
59
  name: string;
60
+ alias?: string;
59
61
  entries: OrgLegendEntry[];
60
62
  x: number;
61
63
  y: number;
@@ -276,15 +278,23 @@ function computeLegendGroups(tagGroups: OrgTagGroup[], _showEyeIcons: boolean):
276
278
  for (const group of tagGroups) {
277
279
  if (group.entries.length === 0) continue;
278
280
 
279
- const pillWidth = group.name.length * LEGEND_PILL_FONT_W + LEGEND_PILL_PAD;
281
+ // Pill label includes alias if present (e.g., "Rank (r)")
282
+ const pillLabel = group.alias ? `${group.name} (${group.alias})` : group.name;
283
+ const pillWidth = pillLabel.length * LEGEND_PILL_FONT_W + LEGEND_PILL_PAD;
284
+ // Minified pill shows just the group name (no alias)
285
+ const minPillWidth = group.name.length * LEGEND_PILL_FONT_W + LEGEND_PILL_PAD;
280
286
 
281
287
  // Capsule: pad + pill + gap + entries + pad
288
+ const isDefaultValue = group.defaultValue?.toLowerCase();
282
289
  let entriesWidth = 0;
283
290
  for (const entry of group.entries) {
291
+ const entryLabel = isDefaultValue === entry.value.toLowerCase()
292
+ ? `${entry.value} (default)`
293
+ : entry.value;
284
294
  entriesWidth +=
285
295
  LEGEND_DOT_R * 2 +
286
296
  LEGEND_ENTRY_DOT_GAP +
287
- entry.value.length * LEGEND_ENTRY_FONT_W +
297
+ entryLabel.length * LEGEND_ENTRY_FONT_W +
288
298
  LEGEND_ENTRY_TRAIL;
289
299
  }
290
300
  const capsuleWidth =
@@ -292,12 +302,17 @@ function computeLegendGroups(tagGroups: OrgTagGroup[], _showEyeIcons: boolean):
292
302
 
293
303
  groups.push({
294
304
  name: group.name,
295
- entries: group.entries.map((e) => ({ value: e.value, color: e.color })),
305
+ alias: group.alias,
306
+ entries: group.entries.map((e) => ({
307
+ value: e.value,
308
+ color: e.color,
309
+ isDefault: group.defaultValue?.toLowerCase() === e.value.toLowerCase() || undefined,
310
+ })),
296
311
  x: 0,
297
312
  y: 0,
298
313
  width: capsuleWidth,
299
314
  height: LEGEND_HEIGHT,
300
- minifiedWidth: pillWidth,
315
+ minifiedWidth: minPillWidth,
301
316
  minifiedHeight: LEGEND_HEIGHT,
302
317
  });
303
318
  }
@@ -348,20 +363,22 @@ export function layoutOrg(
348
363
  return { nodes: [], edges: [], containers: [], legend: [], width: 0, height: 0 };
349
364
  }
350
365
 
351
- // Layout legend groups horizontally (all minified when no nodes)
352
- let cx = MARGIN;
366
+ // Legend-only mode: stack groups vertically, all expanded
367
+ let cy = MARGIN;
368
+ let maxWidth = 0;
353
369
  for (const g of legendGroups) {
354
- g.x = cx;
355
- g.y = MARGIN;
356
- cx += g.minifiedWidth + LEGEND_GROUP_GAP;
370
+ g.x = MARGIN;
371
+ g.y = cy;
372
+ cy += LEGEND_HEIGHT + LEGEND_GROUP_GAP;
373
+ if (g.width > maxWidth) maxWidth = g.width;
357
374
  }
358
375
  return {
359
376
  nodes: [],
360
377
  edges: [],
361
378
  containers: [],
362
379
  legend: legendGroups,
363
- width: cx - LEGEND_GROUP_GAP + MARGIN,
364
- height: LEGEND_HEIGHT + MARGIN * 2,
380
+ width: maxWidth + MARGIN * 2,
381
+ height: cy - LEGEND_GROUP_GAP + MARGIN,
365
382
  };
366
383
  }
367
384
 
@@ -1090,7 +1107,7 @@ export function layoutOrg(
1090
1107
  let finalWidth = totalWidth;
1091
1108
  let finalHeight = totalHeight;
1092
1109
 
1093
- const legendPosition = parsed.options?.['legend-position'] ?? 'bottom';
1110
+ const legendPosition = parsed.options?.['legend-position'] ?? 'top';
1094
1111
 
1095
1112
  // When a tag group is active, only that group is laid out (full size).
1096
1113
  // When none is active, all groups are laid out minified.
@@ -1133,28 +1150,31 @@ export function layoutOrg(
1133
1150
 
1134
1151
  finalHeight = totalHeight + LEGEND_GAP + LEGEND_HEIGHT;
1135
1152
  } else {
1136
- // Top: horizontal row at top-right
1153
+ // Top: horizontal row above chart content, left-aligned
1154
+ const legendShift = LEGEND_HEIGHT + LEGEND_GROUP_GAP;
1155
+
1156
+ // Push all chart content down
1157
+ for (const n of layoutNodes) n.y += legendShift;
1158
+ for (const c of containers) c.y += legendShift;
1159
+ for (const e of layoutEdges) {
1160
+ for (const p of e.points) p.y += legendShift;
1161
+ }
1162
+
1137
1163
  const totalGroupsWidth =
1138
1164
  visibleGroups.reduce((s, g) => s + effectiveW(g), 0) +
1139
1165
  (visibleGroups.length - 1) * LEGEND_GROUP_GAP;
1140
- const legendStartX = totalWidth - MARGIN + LEGEND_GAP;
1141
- const legendY = MARGIN;
1142
1166
 
1143
- let cx = legendStartX;
1167
+ let cx = MARGIN;
1144
1168
  for (const g of visibleGroups) {
1145
1169
  g.x = cx;
1146
- g.y = legendY;
1170
+ g.y = MARGIN;
1147
1171
  cx += effectiveW(g) + LEGEND_GROUP_GAP;
1148
1172
  }
1149
1173
 
1150
- const legendRight = legendStartX + totalGroupsWidth + MARGIN;
1151
- if (legendRight > finalWidth) {
1152
- finalWidth = legendRight;
1153
- }
1154
-
1155
- const legendBottom = legendY + LEGEND_HEIGHT + MARGIN;
1156
- if (legendBottom > finalHeight) {
1157
- finalHeight = legendBottom;
1174
+ finalHeight += legendShift;
1175
+ const neededWidth = totalGroupsWidth + MARGIN * 2;
1176
+ if (neededWidth > finalWidth) {
1177
+ finalWidth = neededWidth;
1158
1178
  }
1159
1179
  }
1160
1180
  }
@@ -447,27 +447,34 @@ export function renderOrg(
447
447
 
448
448
  // Render legend — kanban-style pills.
449
449
  // Skip in export mode (unless legend-only chart).
450
+ // Legend-only (no nodes): all groups rendered as expanded capsules.
450
451
  // Active group: only that group rendered as capsule (pill + entries).
451
452
  // No active group: all groups rendered as standalone pills.
452
- if (!exportDims || layout.nodes.length === 0) for (const group of layout.legend) {
453
+ const legendOnly = layout.nodes.length === 0;
454
+ if (!exportDims || legendOnly) for (const group of layout.legend) {
453
455
  const isActive =
454
- activeTagGroup != null &&
455
- group.name.toLowerCase() === activeTagGroup.toLowerCase();
456
+ legendOnly ||
457
+ (activeTagGroup != null &&
458
+ group.name.toLowerCase() === activeTagGroup.toLowerCase());
456
459
 
457
- // When a group is active, skip all other groups entirely
458
- if (activeTagGroup != null && !isActive) continue;
460
+ // When a group is active, skip all other groups entirely (not in legend-only mode)
461
+ if (!legendOnly && activeTagGroup != null && !isActive) continue;
459
462
 
460
- const groupBg = mix(palette.surface, palette.bg, isDark ? 35 : 20);
463
+ const groupBg = isDark
464
+ ? mix(palette.surface, palette.bg, 50)
465
+ : mix(palette.surface, palette.bg, 30);
461
466
 
467
+ // Pill label: include alias when expanded (e.g., "Rank (r)")
468
+ const pillLabel = isActive && group.alias ? `${group.name} (${group.alias})` : group.name;
462
469
  const pillWidth =
463
- group.name.length * LEGEND_PILL_FONT_W + LEGEND_PILL_PAD;
470
+ pillLabel.length * LEGEND_PILL_FONT_W + LEGEND_PILL_PAD;
464
471
 
465
472
  const gEl = contentG
466
473
  .append('g')
467
474
  .attr('transform', `translate(${group.x}, ${group.y})`)
468
475
  .attr('class', 'org-legend-group')
469
476
  .attr('data-legend-group', group.name.toLowerCase())
470
- .style('cursor', 'pointer');
477
+ .style('cursor', legendOnly ? 'default' : 'pointer');
471
478
 
472
479
  // Outer capsule background (active only)
473
480
  if (isActive) {
@@ -516,7 +523,7 @@ export function renderOrg(
516
523
  .attr('font-weight', '500')
517
524
  .attr('fill', isActive ? palette.text : palette.textMuted)
518
525
  .attr('text-anchor', 'middle')
519
- .text(group.name);
526
+ .text(pillLabel);
520
527
 
521
528
  // Entries inside capsule (active only)
522
529
  if (isActive) {
@@ -530,15 +537,16 @@ export function renderOrg(
530
537
  .attr('fill', entry.color);
531
538
 
532
539
  const textX = entryX + LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP;
540
+ const entryLabel = entry.isDefault ? `${entry.value} (default)` : entry.value;
533
541
  gEl
534
542
  .append('text')
535
543
  .attr('x', textX)
536
544
  .attr('y', LEGEND_HEIGHT / 2 + LEGEND_ENTRY_FONT_SIZE / 2 - 1)
537
545
  .attr('font-size', LEGEND_ENTRY_FONT_SIZE)
538
546
  .attr('fill', palette.textMuted)
539
- .text(entry.value);
547
+ .text(entryLabel);
540
548
 
541
- entryX = textX + entry.value.length * LEGEND_ENTRY_FONT_W + LEGEND_ENTRY_TRAIL;
549
+ entryX = textX + entryLabel.length * LEGEND_ENTRY_FONT_W + LEGEND_ENTRY_TRAIL;
542
550
  }
543
551
  }
544
552
  }