@diagrammo/dgmo 0.31.0 → 0.32.1

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.
Files changed (72) hide show
  1. package/.cursorrules +4 -1
  2. package/.github/copilot-instructions.md +4 -1
  3. package/.windsurfrules +4 -1
  4. package/SKILL.md +4 -1
  5. package/dist/advanced.cjs +1297 -358
  6. package/dist/advanced.d.cts +117 -15
  7. package/dist/advanced.d.ts +117 -15
  8. package/dist/advanced.js +1291 -358
  9. package/dist/auto.cjs +1087 -316
  10. package/dist/auto.js +98 -98
  11. package/dist/auto.mjs +1087 -316
  12. package/dist/cli.cjs +140 -140
  13. package/dist/index.cjs +1090 -397
  14. package/dist/index.js +1090 -397
  15. package/docs/ai-integration.md +4 -1
  16. package/docs/language-reference.md +282 -27
  17. package/gallery/fixtures/boxes-and-lines.dgmo +2 -2
  18. package/gallery/fixtures/c4-full.dgmo +4 -5
  19. package/gallery/fixtures/c4.dgmo +2 -3
  20. package/package.json +7 -1
  21. package/src/advanced.ts +7 -0
  22. package/src/boxes-and-lines/focus.ts +257 -0
  23. package/src/boxes-and-lines/layout-search.ts +131 -65
  24. package/src/boxes-and-lines/layout.ts +7 -1
  25. package/src/boxes-and-lines/parser.ts +19 -4
  26. package/src/boxes-and-lines/renderer.ts +54 -3
  27. package/src/c4/parser.ts +8 -7
  28. package/src/chart-type-registry.ts +129 -4
  29. package/src/chart-types.ts +4 -4
  30. package/src/chart.ts +18 -1
  31. package/src/colors.ts +225 -2
  32. package/src/cycle/parser.ts +2 -7
  33. package/src/d3.ts +67 -54
  34. package/src/diagnostics.ts +17 -0
  35. package/src/dimensions.ts +9 -13
  36. package/src/echarts.ts +42 -14
  37. package/src/er/parser.ts +6 -1
  38. package/src/gantt/parser.ts +44 -7
  39. package/src/graph/flowchart-parser.ts +77 -3
  40. package/src/graph/state-renderer.ts +2 -2
  41. package/src/infra/parser.ts +80 -0
  42. package/src/journey-map/parser.ts +8 -7
  43. package/src/kanban/parser.ts +8 -7
  44. package/src/map/context-labels.ts +134 -27
  45. package/src/map/geo.ts +10 -2
  46. package/src/map/layout.ts +259 -4
  47. package/src/map/parser.ts +2 -0
  48. package/src/map/renderer.ts +22 -11
  49. package/src/map/resolver.ts +68 -19
  50. package/src/mindmap/parser.ts +15 -7
  51. package/src/mindmap/renderer.ts +50 -12
  52. package/src/org/parser.ts +8 -7
  53. package/src/org/renderer.ts +22 -7
  54. package/src/palettes/color-utils.ts +12 -2
  55. package/src/palettes/index.ts +1 -0
  56. package/src/pert/renderer.ts +2 -2
  57. package/src/pyramid/parser.ts +2 -7
  58. package/src/quadrant/renderer.ts +2 -2
  59. package/src/raci/parser.ts +2 -7
  60. package/src/raci/renderer.ts +4 -4
  61. package/src/ring/parser.ts +2 -7
  62. package/src/sequence/parser.ts +18 -7
  63. package/src/sequence/renderer.ts +4 -4
  64. package/src/sitemap/parser.ts +8 -7
  65. package/src/sitemap/renderer.ts +2 -2
  66. package/src/tech-radar/parser.ts +2 -7
  67. package/src/timeline/renderer.ts +15 -5
  68. package/src/utils/parsing.ts +13 -1
  69. package/src/utils/scaling.ts +38 -81
  70. package/src/utils/tag-groups.ts +38 -0
  71. package/src/visualizations/parse.ts +6 -1
  72. package/src/wireframe/parser.ts +6 -1
@@ -199,16 +199,22 @@ export async function layoutBoxesAndLines(
199
199
  /** Previous node positions (label → {x,y}) for layout stability —
200
200
  * minimizes node drift on edit/collapse. */
201
201
  previousPositions?: ReadonlyMap<string, { x: number; y: number }>;
202
+ /** Progress hook (interactive path). When set, the search yields between
203
+ * candidates so the UI can paint a "trying X of Y" indicator. */
204
+ onProgress?: (done: number, total: number, phase: string) => void;
202
205
  }
203
206
  ): Promise<BLLayoutResult> {
204
207
  const { layoutBoxesAndLinesSearch } = await import('./layout-search');
205
- const searched = layoutBoxesAndLinesSearch(parsed, collapseInfo, {
208
+ const searched = await layoutBoxesAndLinesSearch(parsed, collapseInfo, {
206
209
  ...(layoutOptions?.hideDescriptions !== undefined && {
207
210
  hideDescriptions: layoutOptions.hideDescriptions,
208
211
  }),
209
212
  ...(layoutOptions?.previousPositions !== undefined && {
210
213
  previousPositions: layoutOptions.previousPositions,
211
214
  }),
215
+ ...(layoutOptions?.onProgress !== undefined && {
216
+ onProgress: layoutOptions.onProgress,
217
+ }),
212
218
  });
213
219
  // Engine-agnostic post-processing: fan parallel edges, then float notes
214
220
  // (and shift the canvas to fit them).
@@ -42,6 +42,11 @@ import type { PaletteColors } from '../palettes';
42
42
 
43
43
  const MAX_GROUP_DEPTH = 2;
44
44
 
45
+ // §1.4/§1.10 legacy-pipe detection — module-scope so they aren't re-created per
46
+ // line. `|` inside a directed (`->`) or undirected (`~>`) arrow label is valid.
47
+ const ARROW_LABEL_PIPE_DIRECTED_RE = /-\S*\|\S*->/;
48
+ const ARROW_LABEL_PIPE_UNDIRECTED_RE = /~\S*\|\S*~>/;
49
+
45
50
  /** Boxes-and-lines requires explicit first line — no heuristic detection. */
46
51
  export function looksLikeBoxesAndLines(_content: string): boolean {
47
52
  return false;
@@ -247,8 +252,8 @@ export function parseBoxesAndLines(
247
252
  // regions.
248
253
  if (
249
254
  trimmed.includes('|') &&
250
- !/-\S*\|\S*->/.test(trimmed) &&
251
- !/~\S*\|\S*~>/.test(trimmed)
255
+ !ARROW_LABEL_PIPE_DIRECTED_RE.test(trimmed) &&
256
+ !ARROW_LABEL_PIPE_UNDIRECTED_RE.test(trimmed)
252
257
  ) {
253
258
  result.diagnostics.push(
254
259
  makeDgmoError(
@@ -404,7 +409,12 @@ export function parseBoxesAndLines(
404
409
  if (tagBlockMatch.inlineValues) {
405
410
  for (const rawVal of tagBlockMatch.inlineValues) {
406
411
  const { text: cleanVal, isDefault } = stripDefaultModifier(rawVal);
407
- const { label, color } = extractColor(cleanVal);
412
+ const { label, color } = extractColor(
413
+ cleanVal,
414
+ palette,
415
+ result.diagnostics,
416
+ lineNum
417
+ );
408
418
  newTagGroup.entries.push({
409
419
  value: label,
410
420
  color: color ?? AUTO_TAG_COLOR_SENTINEL,
@@ -424,7 +434,12 @@ export function parseBoxesAndLines(
424
434
  // Tag group entries (indented under tag heading)
425
435
  if (currentTagGroup && !contentStarted && indent > 0) {
426
436
  const { text: cleanEntry, isDefault } = stripDefaultModifier(trimmed);
427
- const { label, color } = extractColor(cleanEntry);
437
+ const { label, color } = extractColor(
438
+ cleanEntry,
439
+ palette,
440
+ result.diagnostics,
441
+ lineNum
442
+ );
428
443
  currentTagGroup.entries.push({
429
444
  value: label,
430
445
  color: color ?? AUTO_TAG_COLOR_SENTINEL,
@@ -30,6 +30,7 @@ import {
30
30
  relativeLuminance,
31
31
  shapeFill,
32
32
  valueRampColor,
33
+ themeBaseBg,
33
34
  } from '../palettes/color-utils';
34
35
  import { resolveColor } from '../colors';
35
36
  import { resolveTagColor } from '../utils/tag-groups';
@@ -438,6 +439,11 @@ interface BLRenderOptions {
438
439
  /** When 'app', the description toggle is hosted by the app overlay strip
439
440
  * (inline gear suppressed, controls row + anchor reserved). */
440
441
  controlsHost?: 'app' | 'inline';
442
+ /** Explicit value-ramp domain override. When provided, the choropleth ramp
443
+ * uses these endpoints instead of computing min/max from `parsed.nodes`.
444
+ * Focus mode passes the GLOBAL (pre-filter) domain so neighbor colours stay
445
+ * stable when only a subset is rendered (Decision 20 / FM1). */
446
+ rampDomain?: { min: number; max: number };
441
447
  }
442
448
 
443
449
  export function renderBoxesAndLines(
@@ -459,6 +465,7 @@ export function renderBoxesAndLines(
459
465
  onToggleControlsExpand,
460
466
  exportMode = false,
461
467
  controlsHost,
468
+ rampDomain,
462
469
  } = options ?? {};
463
470
  d3Selection.select(container).selectAll(':not([data-d3-tooltip])').remove();
464
471
 
@@ -492,8 +499,10 @@ export function renderBoxesAndLines(
492
499
  // Anchor the low end at the lowest value (not 0) to maximise within-diagram
493
500
  // dynamic range; mirrors the map's region-metric ramp. Equal-value data
494
501
  // (rampMin === rampMax) falls back to t = 1 in fillForValue below.
495
- const rampMin = hasRamp ? Math.min(...nodeValues) : 0;
496
- const rampMax = Math.max(...nodeValues);
502
+ // A caller-supplied domain (focus mode) wins so colours don't shift when a
503
+ // subset is rendered; otherwise derive from the nodes on screen.
504
+ const rampMin = rampDomain?.min ?? (hasRamp ? Math.min(...nodeValues) : 0);
505
+ const rampMax = rampDomain?.max ?? Math.max(...nodeValues);
497
506
  // Default hue = palette.primary (NOT red like the map — boxes have no water to
498
507
  // stand out against, and red reads as alarm on a neutral metric). A trailing
499
508
  // color on `box-metric` overrides.
@@ -751,7 +760,7 @@ export function renderBoxesAndLines(
751
760
 
752
761
  if (group.collapsed) {
753
762
  // Collapsed: solid rounded rect matching node style + 6px collapse bar
754
- const fillColor = isDark ? palette.surface : palette.bg;
763
+ const fillColor = themeBaseBg(palette, isDark);
755
764
  const strokeColor = palette.border;
756
765
 
757
766
  groupG
@@ -1446,6 +1455,48 @@ export function renderBoxesAndLines(
1446
1455
  });
1447
1456
  legendG.selectAll('[data-legend-group]').classed('bl-legend-group', true);
1448
1457
  }
1458
+
1459
+ // ── Focus mode: one reusable hover-reveal icon (interactive only) ──
1460
+ // A single hidden icon the app repositions over the hovered box/group and
1461
+ // stamps `data-focus-id`/`data-focus-kind` on (Decision 22 / ADR-4) — NOT one
1462
+ // per node (~4k elements on a large graph). Appended to the SVG root so the
1463
+ // app positions it in root (screen-mapped) coordinates, counter-scaled to a
1464
+ // constant size regardless of fit. Excluded from export like org's icon.
1465
+ if (!exportDims && !exportMode) {
1466
+ const iconSize = 14;
1467
+ const focusG = svg
1468
+ .append('g')
1469
+ .attr('class', 'bl-focus-icon')
1470
+ .attr('data-export-ignore', 'true')
1471
+ .style('display', 'none')
1472
+ .style('pointer-events', 'auto')
1473
+ .style('cursor', 'pointer');
1474
+ // Hit area
1475
+ focusG
1476
+ .append('rect')
1477
+ .attr('x', -3)
1478
+ .attr('y', -3)
1479
+ .attr('width', iconSize + 6)
1480
+ .attr('height', iconSize + 6)
1481
+ .attr('fill', 'transparent');
1482
+ // Scope/target icon: outer circle + inner dot (mirrors org-focus-icon)
1483
+ const cx = iconSize / 2;
1484
+ const cy = iconSize / 2;
1485
+ focusG
1486
+ .append('circle')
1487
+ .attr('cx', cx)
1488
+ .attr('cy', cy)
1489
+ .attr('r', iconSize / 2 - 1)
1490
+ .attr('fill', palette.bg)
1491
+ .attr('stroke', palette.textMuted)
1492
+ .attr('stroke-width', 1.5);
1493
+ focusG
1494
+ .append('circle')
1495
+ .attr('cx', cx)
1496
+ .attr('cy', cy)
1497
+ .attr('r', 2)
1498
+ .attr('fill', palette.textMuted);
1499
+ }
1449
1500
  }
1450
1501
 
1451
1502
  // ── Export helper ──────────────────────────────────────────
package/src/c4/parser.ts CHANGED
@@ -8,6 +8,7 @@ import {
8
8
  descriptionBareRemovedMessage,
9
9
  formatDgmoError,
10
10
  makeDgmoError,
11
+ makeFail,
11
12
  METADATA_DIAGNOSTIC_CODES,
12
13
  pipeOperatorRemovedMessage,
13
14
  suggest,
@@ -296,12 +297,7 @@ export function parseC4(content: string, palette?: PaletteColors): ParsedC4 {
296
297
  result.error = formatDgmoError(diag);
297
298
  };
298
299
 
299
- const fail = (line: number, message: string): ParsedC4 => {
300
- const diag = makeDgmoError(line, message);
301
- result.diagnostics.push(diag);
302
- result.error = formatDgmoError(diag);
303
- return result;
304
- };
300
+ const fail = makeFail(result);
305
301
 
306
302
  if (!content?.trim()) {
307
303
  return fail(0, 'No content provided');
@@ -455,7 +451,12 @@ export function parseC4(content: string, palette?: PaletteColors): ParsedC4 {
455
451
  const indent = measureIndent(line);
456
452
  if (indent > 0) {
457
453
  const { text: cleanEntry, isDefault } = stripDefaultModifier(trimmed);
458
- const { label, color } = extractColor(cleanEntry, palette);
454
+ const { label, color } = extractColor(
455
+ cleanEntry,
456
+ palette,
457
+ result.diagnostics,
458
+ lineNumber
459
+ );
459
460
  // Bare value (no explicit color) → keep it; the post-parse
460
461
  // finalize pass assigns a deterministic palette color.
461
462
  if (isDefault) {
@@ -87,6 +87,10 @@ export interface ChartTypeDescriptor {
87
87
  readonly category: RenderCategory;
88
88
  readonly parse: ParseFn;
89
89
  readonly measure?: (content: string) => ContentCounts;
90
+ readonly minDims?: (counts: ContentCounts) => {
91
+ width: number;
92
+ height: number;
93
+ };
90
94
  }
91
95
 
92
96
  // ============================================================
@@ -210,6 +214,92 @@ function measureInfra(content: string): ContentCounts {
210
214
  return { nodes: parsed.nodes.length };
211
215
  }
212
216
 
217
+ // ============================================================
218
+ // minDims() implementations — relocated verbatim from computeMinDimensions() in
219
+ // utils/scaling.ts so the registry owns per-type minimum-dimension formulas
220
+ // alongside measure(). Each maps ContentCounts → {width,height}. Types without a
221
+ // minDims fall back to {300,200} (the old switch `default`) via the
222
+ // REGISTRY_BY_ID lookup in dimensions.ts.
223
+ // ============================================================
224
+
225
+ function minDimsSequence(c: ContentCounts): { width: number; height: number } {
226
+ return {
227
+ width: Math.max((c.participants ?? 2) * 80, 320),
228
+ height: Math.max((c.messages ?? 1) * 20 + 120, 200),
229
+ };
230
+ }
231
+ function minDimsRaci(c: ContentCounts): { width: number; height: number } {
232
+ return {
233
+ width: Math.max((c.roles ?? 2) * 50 + 180, 300),
234
+ height: Math.max((c.tasks ?? 1) * 28 + 80, 200),
235
+ };
236
+ }
237
+ function minDimsMindmap(c: ContentCounts): { width: number; height: number } {
238
+ return {
239
+ width: Math.max((c.nodes ?? 3) * 30, 300),
240
+ height: Math.max((c.depth ?? 2) * 60, 200),
241
+ };
242
+ }
243
+ function minDimsTechRadar(): { width: number; height: number } {
244
+ return { width: 360, height: 400 };
245
+ }
246
+ function minDimsHeatmap(c: ContentCounts): { width: number; height: number } {
247
+ return {
248
+ width: Math.max((c.columns ?? 3) * 40, 300),
249
+ height: Math.max((c.rows ?? 3) * 30 + 60, 200),
250
+ };
251
+ }
252
+ function minDimsArc(c: ContentCounts): { width: number; height: number } {
253
+ return {
254
+ width: 300,
255
+ height: Math.max((c.nodes ?? 3) * 20 + 120, 200),
256
+ };
257
+ }
258
+ function minDimsOrg(c: ContentCounts): { width: number; height: number } {
259
+ return {
260
+ width: Math.max((c.nodes ?? 3) * 60, 300),
261
+ height: Math.max((c.depth ?? 2) * 80, 200),
262
+ };
263
+ }
264
+ function minDimsGantt(c: ContentCounts): { width: number; height: number } {
265
+ return {
266
+ width: 400,
267
+ height: Math.max((c.tasks ?? 3) * 24 + 80, 200),
268
+ };
269
+ }
270
+ function minDimsKanban(c: ContentCounts): { width: number; height: number } {
271
+ return {
272
+ width: Math.max((c.columns ?? 3) * 120, 360),
273
+ height: 300,
274
+ };
275
+ }
276
+ // er + class share this formula.
277
+ function minDimsEntities(c: ContentCounts): { width: number; height: number } {
278
+ return {
279
+ width: Math.max((c.nodes ?? 2) * 140, 300),
280
+ height: Math.max((c.nodes ?? 2) * 80, 200),
281
+ };
282
+ }
283
+ // flowchart + state share this formula.
284
+ function minDimsGraph(c: ContentCounts): { width: number; height: number } {
285
+ return {
286
+ width: Math.max((c.nodes ?? 3) * 60, 300),
287
+ height: Math.max((c.nodes ?? 3) * 50, 200),
288
+ };
289
+ }
290
+ function minDimsPert(c: ContentCounts): { width: number; height: number } {
291
+ return {
292
+ width: Math.max((c.tasks ?? 3) * 80, 340),
293
+ height: Math.max((c.tasks ?? 3) * 40 + 80, 200),
294
+ };
295
+ }
296
+ function minDimsInfra(c: ContentCounts): { width: number; height: number } {
297
+ return {
298
+ width: Math.max((c.nodes ?? 3) * 80, 300),
299
+ height: Math.max((c.nodes ?? 3) * 60, 200),
300
+ };
301
+ }
302
+
213
303
  // ============================================================
214
304
  // THE REGISTRY — ordered to match the previous chartTypeParsers grouping
215
305
  // (structured diagrams, standard ECharts, extended ECharts, D3 visualizations,
@@ -224,32 +314,49 @@ export const CHART_TYPE_REGISTRY: readonly ChartTypeDescriptor[] = [
224
314
  category: 'diagram',
225
315
  parse: parseSequenceDgmo,
226
316
  measure: measureSequence,
317
+ minDims: minDimsSequence,
227
318
  },
228
319
  {
229
320
  id: 'flowchart',
230
321
  category: 'diagram',
231
322
  parse: parseFlowchart,
232
323
  measure: measureFlowchart,
324
+ minDims: minDimsGraph,
233
325
  },
234
326
  {
235
327
  id: 'class',
236
328
  category: 'diagram',
237
329
  parse: parseClassDiagram,
238
330
  measure: measureClass,
331
+ minDims: minDimsEntities,
332
+ },
333
+ {
334
+ id: 'er',
335
+ category: 'diagram',
336
+ parse: parseERDiagram,
337
+ measure: measureER,
338
+ minDims: minDimsEntities,
239
339
  },
240
- { id: 'er', category: 'diagram', parse: parseERDiagram, measure: measureER },
241
340
  {
242
341
  id: 'state',
243
342
  category: 'diagram',
244
343
  parse: parseState,
245
344
  measure: measureStateGraph,
345
+ minDims: minDimsGraph,
346
+ },
347
+ {
348
+ id: 'org',
349
+ category: 'diagram',
350
+ parse: parseOrg,
351
+ measure: measureOrg,
352
+ minDims: minDimsOrg,
246
353
  },
247
- { id: 'org', category: 'diagram', parse: parseOrg, measure: measureOrg },
248
354
  {
249
355
  id: 'kanban',
250
356
  category: 'diagram',
251
357
  parse: parseKanban,
252
358
  measure: measureKanban,
359
+ minDims: minDimsKanban,
253
360
  },
254
361
  { id: 'c4', category: 'diagram', parse: parseC4 },
255
362
  { id: 'sitemap', category: 'diagram', parse: parseSitemap },
@@ -258,24 +365,39 @@ export const CHART_TYPE_REGISTRY: readonly ChartTypeDescriptor[] = [
258
365
  category: 'diagram',
259
366
  parse: parseInfra,
260
367
  measure: measureInfra,
368
+ minDims: minDimsInfra,
261
369
  },
262
370
  {
263
371
  id: 'gantt',
264
372
  category: 'diagram',
265
373
  parse: parseGantt,
266
374
  measure: measureGantt,
375
+ minDims: minDimsGantt,
376
+ },
377
+ {
378
+ id: 'pert',
379
+ category: 'diagram',
380
+ parse: parsePert,
381
+ measure: measurePert,
382
+ minDims: minDimsPert,
267
383
  },
268
- { id: 'pert', category: 'diagram', parse: parsePert, measure: measurePert },
269
384
  { id: 'boxes-and-lines', category: 'diagram', parse: parseBoxesAndLines },
270
385
  {
271
386
  id: 'mindmap',
272
387
  category: 'diagram',
273
388
  parse: parseMindmap,
274
389
  measure: measureMindmap,
390
+ minDims: minDimsMindmap,
275
391
  },
276
392
  { id: 'wireframe', category: 'diagram', parse: parseWireframe },
277
393
  { id: 'journey-map', category: 'diagram', parse: parseJourneyMap },
278
- { id: 'raci', category: 'diagram', parse: parseRaci, measure: measureRaci },
394
+ {
395
+ id: 'raci',
396
+ category: 'diagram',
397
+ parse: parseRaci,
398
+ measure: measureRaci,
399
+ minDims: minDimsRaci,
400
+ },
279
401
  { id: 'rasci', category: 'diagram', parse: parseRaci, measure: measureRaci },
280
402
  { id: 'daci', category: 'diagram', parse: parseRaci, measure: measureRaci },
281
403
 
@@ -300,6 +422,7 @@ export const CHART_TYPE_REGISTRY: readonly ChartTypeDescriptor[] = [
300
422
  category: 'data-chart',
301
423
  parse: parseHeatmap,
302
424
  measure: measureHeatmap,
425
+ minDims: minDimsHeatmap,
303
426
  },
304
427
  { id: 'funnel', category: 'data-chart', parse: parseFunnel },
305
428
 
@@ -311,6 +434,7 @@ export const CHART_TYPE_REGISTRY: readonly ChartTypeDescriptor[] = [
311
434
  category: 'visualization',
312
435
  parse: parseArc,
313
436
  measure: measureArc,
437
+ minDims: minDimsArc,
314
438
  },
315
439
  { id: 'timeline', category: 'visualization', parse: parseTimeline },
316
440
  { id: 'venn', category: 'visualization', parse: parseVenn },
@@ -322,6 +446,7 @@ export const CHART_TYPE_REGISTRY: readonly ChartTypeDescriptor[] = [
322
446
  category: 'visualization',
323
447
  parse: parseTechRadar,
324
448
  measure: measureTechRadar,
449
+ minDims: minDimsTechRadar,
325
450
  },
326
451
  { id: 'cycle', category: 'visualization', parse: parseCycle },
327
452
  { id: 'pyramid', category: 'visualization', parse: parsePyramid },
@@ -46,7 +46,7 @@ export const chartTypes: readonly ChartTypeMeta[] = [
46
46
  },
47
47
  {
48
48
  id: 'sequence',
49
- description: 'Message / interaction flows',
49
+ description: 'Message request and response interaction flows',
50
50
  fallback: true,
51
51
  },
52
52
  {
@@ -125,7 +125,7 @@ export const chartTypes: readonly ChartTypeMeta[] = [
125
125
  {
126
126
  id: 'map',
127
127
  description:
128
- 'Geographic map: a value or count per country, state, or region (choropleth); points of interest; routes. Use when categories are real-world places.',
128
+ 'Geographic concept map: highlight/score regions, drop points of interest, connect with routes or edges',
129
129
  },
130
130
 
131
131
  // ── Tier 3 — Specialized analytical charts ────────────────
@@ -143,7 +143,7 @@ export const chartTypes: readonly ChartTypeMeta[] = [
143
143
  },
144
144
  {
145
145
  id: 'slope',
146
- description: 'Change for multiple things between exactly two periods',
146
+ description: 'Change between 2 time periods',
147
147
  },
148
148
  {
149
149
  id: 'sankey',
@@ -173,7 +173,7 @@ export const chartTypes: readonly ChartTypeMeta[] = [
173
173
  // ── Tier 4 — General-purpose data charts ──────────────────
174
174
  {
175
175
  id: 'bar',
176
- description: 'Categorical comparisons',
176
+ description: 'Categorical comparisons for 3 - 5 figures',
177
177
  fallback: true,
178
178
  },
179
179
  {
package/src/chart.ts CHANGED
@@ -405,7 +405,9 @@ export function parseChart(
405
405
  if (dataValues) {
406
406
  const { label: rawLabel, color: pointColor } = extractColor(
407
407
  dataValues.label,
408
- palette
408
+ palette,
409
+ result.diagnostics,
410
+ lineNumber
409
411
  );
410
412
  const [first, ...rest] = dataValues.values;
411
413
  result.data.push({
@@ -485,6 +487,21 @@ export function parseChart(
485
487
  );
486
488
  }
487
489
 
490
+ // Plain "bar" renders a single series per row — extra series are dropped
491
+ // silently by the renderer. Surface that instead of losing data quietly so
492
+ // the author switches to a multi-series type. (multi-line/line parse to
493
+ // type "line" and DO render every series, so they are unaffected.)
494
+ if (
495
+ !result.error &&
496
+ result.type === 'bar' &&
497
+ (result.seriesNames?.length ?? 0) > 1
498
+ ) {
499
+ warn(
500
+ result.seriesLineNumber ?? 1,
501
+ `Plain "bar" shows only the first series ("${result.seriesNames![0]}"); the other ${result.seriesNames!.length - 1} are dropped at render. Use "bar-stacked" for stacked bars or "multi-line" to plot every series.`
502
+ );
503
+ }
504
+
488
505
  if (!result.error && result.seriesNames) {
489
506
  const expectedCount = result.seriesNames.length;
490
507
  for (const dp of result.data) {