@diagrammo/dgmo 0.25.0 → 0.25.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diagrammo/dgmo",
3
- "version": "0.25.0",
3
+ "version": "0.25.1",
4
4
  "description": "DGMO diagram markup language — parser, renderer, and color system",
5
5
  "license": "MIT",
6
6
  "repository": {
package/src/advanced.ts CHANGED
@@ -402,14 +402,6 @@ export {
402
402
  renderPertForExport,
403
403
  renderPertAnalysisBlock,
404
404
  measurePertAnalysisBlock,
405
- highlightPertCriticalPath,
406
- highlightPertSet,
407
- pertLegendEntries,
408
- pertLegendBlockWidth,
409
- PERT_LEGEND_PILL_HEIGHT,
410
- renderLegendBlock as renderPertLegendBlock,
411
- resetPertCriticalPath,
412
- resetPertHighlight,
413
405
  } from './pert/renderer';
414
406
  export type { PertRenderOptions } from './pert/renderer';
415
407
  export type {
package/src/completion.ts CHANGED
@@ -534,6 +534,7 @@ export const COMPLETION_REGISTRY = new Map<string, DirectiveSpec>([
534
534
  description: 'Which tag group leads when several are present',
535
535
  },
536
536
  caption: { description: 'Caption line (data-source attribution)' },
537
+ 'no-title': { description: 'Suppress the title banner' },
537
538
  'no-legend': { description: 'Suppress the legend' },
538
539
  'no-coastline': {
539
540
  description: 'Turn off coastal water-lines (on by default)',
@@ -552,6 +553,10 @@ export const COMPLETION_REGISTRY = new Map<string, DirectiveSpec>([
552
553
  description:
553
554
  'Force plain green-land reference dress (regions are auto-coloured by default)',
554
555
  },
556
+ 'no-cluster-pois': {
557
+ description:
558
+ 'Always fan out coincident POI markers instead of collapsing them into a count badge',
559
+ },
555
560
  }),
556
561
  ],
557
562
  ]);
package/src/d3.ts CHANGED
@@ -8609,6 +8609,14 @@ export async function renderForExport(
8609
8609
  const { renderSequenceDiagram } = await import('./sequence/renderer');
8610
8610
  const seqParsed = parseSequenceDgmo(content);
8611
8611
  if (seqParsed.error || seqParsed.participants.length === 0) return '';
8612
+ // Apply interactive view state from share links (read from unified viewState).
8613
+ // Sequences key both sections and groups by source line number; `cg` is the
8614
+ // shared string[] field, so coerce its entries back to numbers.
8615
+ const collapsedSections = viewState?.cs ? new Set(viewState.cs) : undefined;
8616
+ const collapsedGroups = viewState?.cg
8617
+ ? new Set(viewState.cg.map(Number).filter((n) => Number.isFinite(n)))
8618
+ : undefined;
8619
+ const seqActiveTagGroup = viewState?.tag ?? options?.tagGroup;
8612
8620
  renderSequenceDiagram(
8613
8621
  container,
8614
8622
  seqParsed,
@@ -8617,9 +8625,11 @@ export async function renderForExport(
8617
8625
  undefined,
8618
8626
  {
8619
8627
  exportWidth: EXPORT_WIDTH,
8620
- ...(options?.tagGroup !== undefined && {
8621
- activeTagGroup: options.tagGroup,
8628
+ ...(seqActiveTagGroup !== undefined && {
8629
+ activeTagGroup: seqActiveTagGroup,
8622
8630
  }),
8631
+ ...(collapsedSections !== undefined && { collapsedSections }),
8632
+ ...(collapsedGroups !== undefined && { collapsedGroups }),
8623
8633
  }
8624
8634
  );
8625
8635
  } else if (parsed.type === 'wordcloud') {
@@ -166,6 +166,7 @@ export const DIRECTIVE_KEYWORDS = new Set([
166
166
  'no-region-labels',
167
167
  'no-poi-labels',
168
168
  'no-colorize',
169
+ 'no-cluster-pois',
169
170
  'poi',
170
171
  'route',
171
172
  // Data charts
package/src/map/layout.ts CHANGED
@@ -1027,18 +1027,22 @@ export function layoutMap(
1027
1027
 
1028
1028
  // -- Colorize: content-inferred distinct political fills (§24B) --
1029
1029
  // Colorize is the DEFAULT dress for any map that is NOT colouring regions by
1030
- // data. The ONLY two things that turn it off: (1) a data dimension exists on a
1030
+ // data. The things that turn it off: (1) a data dimension exists on a
1031
1031
  // region (any `value:` or tag group) — data owns the saturation, so the basemap
1032
- // recedes to the gray choropleth/categorical dress; or (2) the `no-colorize`
1033
- // opt-out. Everything elsebare `map`, POI/route-only maps, named regions
1034
- // without data gets distinct political pastels (markers/routes draw on top).
1035
- // Data EXISTENCE (not which dimension is *active*) is the discriminator, so a
1036
- // tag map viewed with `active-tag none` still keeps its neutral data dress; and
1037
- // the live-preview `California` `California value: 92` edit transitions
1038
- // colorized choropleth cleanly.
1032
+ // recedes to the gray choropleth/categorical dress; (2) any region carries a
1033
+ // direct trailing color (`Japan red`) that's explicit authoring intent, so
1034
+ // auto-political-tinting would only fight the hand-picked colours; or (3) the
1035
+ // `no-colorize` opt-out. Everything else bare `map`, POI/route-only maps,
1036
+ // named regions without data or direct colours gets distinct political
1037
+ // pastels (markers/routes draw on top). Data EXISTENCE (not which dimension is
1038
+ // *active*) is the discriminator, so a tag map viewed with `active-tag none`
1039
+ // still keeps its neutral data dress; and the live-preview `California` →
1040
+ // `California value: 92` edit transitions colorized → choropleth cleanly.
1041
+ const hasDirectColor = resolved.regions.some((r) => r.color !== undefined);
1039
1042
  const colorizeActive =
1040
1043
  resolved.directives.noColorize !== true &&
1041
1044
  !hasRamp &&
1045
+ !hasDirectColor &&
1042
1046
  resolved.tagGroups.length === 0;
1043
1047
  // Hue per ISO over ONE UNIFIED graph spanning every drawn topology, so no two
1044
1048
  // bordering regions share a hue — INCLUDING across the international seam. The
@@ -1201,9 +1205,12 @@ export function layoutMap(
1201
1205
  // the foreground). A POI-less choropleth needs no reserve — the land fills to
1202
1206
  // the top and the title simply overlays it, so neighbour land (e.g. Canada)
1203
1207
  // isn't cut short by a band of empty water above it.
1208
+ // `no-title` suppresses the banner entirely — drop it from layout so the title
1209
+ // reserves no top band and the renderer's `if (layout.title)` skips it.
1210
+ const shownTitle = resolved.directives.noTitle ? null : resolved.title;
1204
1211
  const TITLE_GAP = 16;
1205
1212
  let topPad = FIT_PAD;
1206
- if (resolved.title && resolved.pois.length > 0) {
1213
+ if (shownTitle && resolved.pois.length > 0) {
1207
1214
  const bannerBottom =
1208
1215
  (resolved.subtitle ? TITLE_Y + TITLE_FONT_SIZE : TITLE_Y) +
1209
1216
  TITLE_FONT_SIZE / 2;
@@ -1217,7 +1224,7 @@ export function layoutMap(
1217
1224
  const legendBand = mapLegendBand(legend, {
1218
1225
  width,
1219
1226
  mode: opts.legendMode ?? 'preview',
1220
- hasTitle: Boolean(resolved.title),
1227
+ hasTitle: Boolean(shownTitle),
1221
1228
  hasSubtitle: Boolean(resolved.subtitle),
1222
1229
  });
1223
1230
  if (legendBand > topPad) topPad = legendBand;
@@ -2932,7 +2939,7 @@ export function layoutMap(
2932
2939
  width,
2933
2940
  height,
2934
2941
  background: water,
2935
- title: resolved.title,
2942
+ title: shownTitle,
2936
2943
  ...(resolved.subtitle !== undefined && { subtitle: resolved.subtitle }),
2937
2944
  ...(resolved.caption !== undefined && { caption: resolved.caption }),
2938
2945
  regions,
package/src/map/parser.ts CHANGED
@@ -46,8 +46,10 @@ const HUB_RE = /^(->|~>)\s+(.+)$/;
46
46
  const LEG_ARROW_RE = /^(-[^>]*?->|->|~[^>]*?~>|~>|--)\s+(.+)$/;
47
47
  const AT_RE = /(^|[\s,])at\s*:/i; // the removed `at:` coord form (§24B.9)
48
48
 
49
- // Final 13 (§24B.2): 6 irreducible-intent directives + 7 `no-*` cosmetic
50
- // opt-outs. Every cosmetic is on by default; its `no-*` flag is the only switch.
49
+ // The 14 map-specific directives (§24B.2): 6 irreducible-intent + 8 `no-*`
50
+ // cosmetic opt-outs (every cosmetic on by default; its `no-*` flag is the only
51
+ // switch). Plus `no-title` — the universal banner-suppression flag (§1), wired
52
+ // in here so the map parser recognizes it rather than mis-parsing it as a region.
51
53
  const DIRECTIVE_SET: ReadonlySet<string> = new Set([
52
54
  'region-metric',
53
55
  'poi-metric',
@@ -55,6 +57,7 @@ const DIRECTIVE_SET: ReadonlySet<string> = new Set([
55
57
  'locale',
56
58
  'active-tag',
57
59
  'caption',
60
+ 'no-title',
58
61
  'no-legend',
59
62
  'no-coastline',
60
63
  'no-relief',
@@ -62,6 +65,7 @@ const DIRECTIVE_SET: ReadonlySet<string> = new Set([
62
65
  'no-region-labels',
63
66
  'no-poi-labels',
64
67
  'no-colorize',
68
+ 'no-cluster-pois',
65
69
  ]);
66
70
 
67
71
  /** True when the first non-blank/non-comment line declares `map`. */
@@ -313,6 +317,9 @@ export function parseMap(content: string): ParsedMap {
313
317
  break;
314
318
  // ── Cosmetic `no-*` opt-outs: bare flags, idempotent (mirror `no-legend`,
315
319
  // no dup warning); each defaults the feature ON when absent. ──
320
+ case 'no-title':
321
+ d.noTitle = true;
322
+ break;
316
323
  case 'no-legend':
317
324
  d.noLegend = true;
318
325
  break;
@@ -334,6 +341,9 @@ export function parseMap(content: string): ParsedMap {
334
341
  case 'no-colorize':
335
342
  d.noColorize = true;
336
343
  break;
344
+ case 'no-cluster-pois':
345
+ d.noClusterPois = true;
346
+ break;
337
347
  }
338
348
  }
339
349
 
@@ -694,11 +694,16 @@ export function renderMap(
694
694
  // toggles only their opacity. Drawn VISIBLE so export + the no-JS default show
695
695
  // the expanded fan. An invisible hit-area circle (interactive only) owns all
696
696
  // pointer interaction so hover/click drives the spiderfy controller robustly.
697
+ // `no-cluster-pois` (or any export) keeps the fan permanently expanded: draw
698
+ // the legs/hub/members visible but emit NO hit-area + NO badge, so there is
699
+ // nothing for the app's spiderfy controller to collapse — the map reads the
700
+ // same on screen as on paper.
701
+ const clusterUi = !exportDims && !resolved.directives.noClusterPois;
697
702
  const gSpider = svg.append('g').attr('class', 'dgmo-map-spider');
698
703
  for (const cl of layout.clusters) {
699
704
  // Pointer hit-area — bottom of the stack so member dots still take their own
700
705
  // clicks (line-jump); clicks on the empty centre fall through to here.
701
- if (!exportDims) {
706
+ if (clusterUi) {
702
707
  gSpider
703
708
  .append('circle')
704
709
  .attr('cx', cl.cx)
@@ -863,7 +868,7 @@ export function renderMap(
863
868
  // export keeps the expanded fan (every label visible), so no badge there. A
864
869
  // neutral dot ringed with the bare member count, emitted hidden; the app shows
865
870
  // it at rest and hides it (revealing the spider) on click. ──
866
- if (!exportDims && layout.clusters.length) {
871
+ if (clusterUi && layout.clusters.length) {
867
872
  const gBadge = svg.append('g').attr('class', 'dgmo-map-cluster-badges');
868
873
  for (const cl of layout.clusters) {
869
874
  // Decorative: the hit-area (drawn under the dots) owns hover + click; the
@@ -889,6 +894,24 @@ export function renderMap(
889
894
  .attr('fill', 'none')
890
895
  .attr('stroke', palette.textMuted)
891
896
  .attr('stroke-width', 1);
897
+ // Directional colour beads: one small dot threaded on the outer ring per
898
+ // member, placed at the ANGLE of that member's spider leg (centroid → its
899
+ // expanded position) and filled with the member's own marker colour. So the
900
+ // collapsed badge previews WHAT is stacked here and roughly WHERE each item
901
+ // will fan out, before the spider opens. A bg-coloured halo keeps adjacent
902
+ // beads + the ring line legible.
903
+ const beadR = R + 2.5;
904
+ for (const leg of cl.legs) {
905
+ const a = Math.atan2(leg.y2 - cl.cy, leg.x2 - cl.cx);
906
+ g.append('circle')
907
+ .attr('class', 'dgmo-map-cluster-bead')
908
+ .attr('cx', cl.cx + beadR * Math.cos(a))
909
+ .attr('cy', cl.cy + beadR * Math.sin(a))
910
+ .attr('r', 1.8)
911
+ .attr('fill', leg.color)
912
+ .attr('stroke', palette.bg)
913
+ .attr('stroke-width', 0.5);
914
+ }
892
915
  // Bare count (RQ1).
893
916
  emitText(
894
917
  g,
package/src/map/types.ts CHANGED
@@ -33,6 +33,9 @@ export interface MapDirectives {
33
33
  locale?: string;
34
34
  activeTag?: string;
35
35
  caption?: string;
36
+ /** `no-title` — suppress the title banner (the subtitle/caption, if any, still
37
+ * render). Mirrors the `no-title` directive across the other chart types. */
38
+ noTitle?: boolean;
36
39
  /** `no-legend` — suppress the legend (default-on). */
37
40
  noLegend?: boolean;
38
41
  /** `no-coastline` — suppress the faint nautical-chart water-lines along
@@ -52,6 +55,12 @@ export interface MapDirectives {
52
55
  * are referenced (regions are auto-coloured by default; §24B colorize). A
53
56
  * no-op under data — the basemap is already gray there. */
54
57
  noColorize?: boolean;
58
+ /** `no-cluster-pois` — never collapse coincident POI markers into a count badge
59
+ * (clustering/spiderfy is default-on in the interactive preview). With this set
60
+ * the markers always render fanned out with their legs — the same as a static
61
+ * export — so a dense map reads the same on screen as on paper. No-op for
62
+ * export (already always expanded). */
63
+ noClusterPois?: boolean;
55
64
  }
56
65
 
57
66
  /** A region-fill: a subdivision name with an optional score and/or tag values