feat(optimize): support state diagrams in the Optimize panel
Extend the Diagram Doctor beyond flowcharts to stateDiagram / stateDiagram-v2.
- New parseStateDiagram: states → nodes, transitions → edges in source order
(including `[*]` pseudo-transitions, so linkStyle indices stay aligned);
`[*]` itself is never a node. Composite states map to groups, notes are
skipped. A parseDiagram() dispatcher feeds analyze/transforms.
- Fixes are tailored per kind (verified empirically against Mermaid 11):
* ELK is flowchart-only (state diagrams reject `layout: elk`), so it's not
offered and the no-ELK score penalty is skipped for state diagrams.
* Spacing uses the `state` config key; direction is set via a `direction`
statement rather than the header.
* State diagrams can't style individual transitions — a `linkStyle` line
renders as a stray node — so the dim-hubs fix and focus mode fade the
busy *states* (e.g. CANCELLED) via classDef/class instead of dimming edges.
Flowcharts keep edge dimming via linkStyle.
- Panel notice now covers flowcharts & state diagrams.
Verified in-browser: state-diagram dim/focus render with zero junk nodes;
flowchart ELK + linkStyle dimming still intact.
This commit is contained in:
17
README.md
17
README.md
@@ -25,14 +25,15 @@ TypeScript** frontend using **CodeMirror 6** and **Mermaid 11**.
|
||||
counts. Network operations shell out to your system `git`, so they reuse your
|
||||
existing credentials (SSH agent, keychain / credential helpers) — no separate
|
||||
login.
|
||||
- **Optimize panel (Diagram Doctor)** — analyse a tangled flowchart and declutter
|
||||
it in one click: a readability score and metrics (hubs, density, cross-group
|
||||
edges), plus source rewrites for the **ELK layered layout**, extra
|
||||
spacing/curved edges, **de-emphasising cross-cutting hub edges** (event bus,
|
||||
audit, cost-style fan-ins), and removing duplicate edges. A non-destructive
|
||||
**focus mode** spotlights any node and its neighbours so you can read a dense
|
||||
graph without changing it. Every rewrite lands in the editor and is reversible
|
||||
with ⌘Z.
|
||||
- **Optimize panel (Diagram Doctor)** — analyse a tangled **flowchart or state
|
||||
diagram** and declutter it in one click: a readability score and metrics
|
||||
(hubs, density, cross-group edges), plus source rewrites for the **ELK layered
|
||||
layout** (flowcharts), extra spacing, a layout-direction toggle,
|
||||
**de-emphasising cross-cutting hubs** (event-bus / audit fan-ins on flowcharts;
|
||||
faded sink states like CANCELLED on state diagrams), and removing duplicate
|
||||
edges. A non-destructive **focus mode** spotlights any node/state and its
|
||||
neighbours so you can read a dense graph without changing it. Every rewrite
|
||||
lands in the editor and is reversible with ⌘Z.
|
||||
- **Export** the current diagram to **SVG** or **PNG** (2× scale), or **copy a
|
||||
PNG straight to the clipboard**. (PNG/clipboard re-render flowcharts with
|
||||
text labels so the bitmap rasterizes cleanly across platforms.)
|
||||
|
||||
@@ -57,10 +57,10 @@
|
||||
<div class="notice faint">Open a diagram to analyse it.</div>
|
||||
{:else if !a.supported}
|
||||
<div class="notice">
|
||||
<div class="muted">Decluttering is built for flowcharts.</div>
|
||||
<div class="muted">Decluttering is built for flowcharts & state diagrams.</div>
|
||||
<p class="faint">
|
||||
This looks like a <code>{a.kind || 'different'}</code> diagram, so only
|
||||
flowchart / graph sources can be analysed here.
|
||||
This looks like a <code>{a.kind || 'different'}</code> diagram, which
|
||||
can't be analysed here yet.
|
||||
</p>
|
||||
</div>
|
||||
{:else if a.metrics && a.graph}
|
||||
|
||||
@@ -44,10 +44,12 @@ export interface SubgraphInfo {
|
||||
title: string;
|
||||
}
|
||||
|
||||
export type DiagramKind = 'flowchart' | 'graph' | 'stateDiagram';
|
||||
|
||||
export interface FlowGraph {
|
||||
kind: 'flowchart' | 'graph';
|
||||
kind: DiagramKind;
|
||||
direction: string;
|
||||
/** 0-based index of the header line (`flowchart TB`). */
|
||||
/** 0-based index of the header line (`flowchart TB` / `stateDiagram-v2`). */
|
||||
headerLine: number;
|
||||
nodes: Map<string, FlowNode>;
|
||||
/** Edges in Mermaid link order (matches `linkStyle` indexing). */
|
||||
@@ -355,6 +357,169 @@ export function parseFlowchart(src: string): FlowGraph | null {
|
||||
};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// State diagrams
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const STATE_ARROW = /--+>/;
|
||||
|
||||
/** Strip a `:::class` suffix and whitespace from a transition endpoint token. */
|
||||
function stateToken(tok: string): string {
|
||||
const t = tok.trim();
|
||||
if (t === '[*]') return '[*]';
|
||||
return t.replace(/:::.*$/, '').trim();
|
||||
}
|
||||
|
||||
/** Parse a state declaration body: `"Label" as ID`, `ID`, or `ID: ...`. */
|
||||
function parseStateDecl(s: string): { id: string; label: string } {
|
||||
const quotedAs = s.match(/^"([^"]*)"\s+as\s+([A-Za-z0-9_]\S*)/);
|
||||
if (quotedAs) return { id: quotedAs[2], label: quotedAs[1] };
|
||||
const idM = s.match(/^([A-Za-z0-9_]\S*)/);
|
||||
return { id: idM ? idM[1] : s.trim(), label: '' };
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse `stateDiagram` / `stateDiagram-v2` source. States become nodes and
|
||||
* transitions become edges in source order — including `[*]` pseudo-state
|
||||
* transitions, which keeps `linkStyle` indices aligned (verified: Mermaid emits
|
||||
* one link per transition, `[*]` included). `[*]` itself is never a node.
|
||||
*/
|
||||
export function parseStateDiagram(src: string): FlowGraph | null {
|
||||
const lines = src.replace(/\r\n/g, '\n').split('\n');
|
||||
const fmEnd = frontmatterLines(src);
|
||||
|
||||
let headerLine = -1;
|
||||
for (let i = fmEnd; i < lines.length; i++) {
|
||||
const t = lines[i].trim();
|
||||
if (!t || t.startsWith('%%')) continue;
|
||||
if (/^stateDiagram(?:-v2)?\b/.test(t)) {
|
||||
headerLine = i;
|
||||
break;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if (headerLine < 0) return null;
|
||||
|
||||
const hasSpacing = /\b(?:nodeSpacing|rankSpacing)\b/.test(src);
|
||||
let direction = 'TB';
|
||||
|
||||
const nodes = new Map<string, FlowNode>();
|
||||
const edges: FlowEdge[] = [];
|
||||
const subgraphs: SubgraphInfo[] = [];
|
||||
const stack: string[] = []; // composite-state nesting
|
||||
let inNote = false;
|
||||
|
||||
const touch = (id: string, label?: string) => {
|
||||
if (id === '[*]' || !id) return;
|
||||
let n = nodes.get(id);
|
||||
if (!n) {
|
||||
n = {
|
||||
id,
|
||||
label: label || id,
|
||||
subgraph: stack.length ? stack[stack.length - 1] : null,
|
||||
inDeg: 0,
|
||||
outDeg: 0,
|
||||
bidirDeg: 0,
|
||||
};
|
||||
nodes.set(id, n);
|
||||
} else if (label && (n.label === n.id || !n.label)) {
|
||||
n.label = label;
|
||||
}
|
||||
};
|
||||
|
||||
for (let i = headerLine + 1; i < lines.length; i++) {
|
||||
const t = lines[i].trim();
|
||||
if (!t || t.startsWith('%%')) continue;
|
||||
|
||||
// Notes (single-line `note … : text`, or a block until `end note`).
|
||||
if (inNote) {
|
||||
if (/^end\s+note\b/.test(t)) inNote = false;
|
||||
continue;
|
||||
}
|
||||
if (/^note\b/.test(t)) {
|
||||
if (!t.includes(':')) inNote = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
const dirM = t.match(/^direction\s+([A-Za-z]{2})\b/);
|
||||
if (dirM) {
|
||||
direction = dirM[1].toUpperCase();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Composite state open: `state Name {` / `state "Label" as Name {`.
|
||||
const compM = t.match(/^state\s+(.+?)\s*\{$/);
|
||||
if (compM) {
|
||||
const ref = parseStateDecl(compM[1]);
|
||||
touch(ref.id, ref.label);
|
||||
subgraphs.push({ id: ref.id, title: ref.label || ref.id });
|
||||
stack.push(ref.id);
|
||||
continue;
|
||||
}
|
||||
if (t === '}') {
|
||||
stack.pop();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (/^(?:classDef|class|style|click|linkStyle)\b/.test(t)) continue;
|
||||
if (t.includes(':::') && !STATE_ARROW.test(t)) continue; // `X:::cls`
|
||||
|
||||
if (STATE_ARROW.test(t)) {
|
||||
const arrow = t.match(STATE_ARROW)![0];
|
||||
const ai = t.indexOf(arrow);
|
||||
const from = stateToken(t.slice(0, ai));
|
||||
let rhs = t.slice(ai + arrow.length).trim();
|
||||
const ci = rhs.indexOf(':');
|
||||
if (ci >= 0) rhs = rhs.slice(0, ci); // drop the transition label
|
||||
const to = stateToken(rhs);
|
||||
touch(from);
|
||||
touch(to);
|
||||
edges.push({ from, to, bidir: false, line: i });
|
||||
continue;
|
||||
}
|
||||
|
||||
// `state "Label" as ID` / `state ID`.
|
||||
const stateDeclM = t.match(/^state\s+(.+)$/);
|
||||
if (stateDeclM) {
|
||||
const ref = parseStateDecl(stateDeclM[1]);
|
||||
touch(ref.id, ref.label);
|
||||
continue;
|
||||
}
|
||||
// `ID : description`.
|
||||
const labelM = t.match(/^([A-Za-z0-9_]\S*)\s*:\s*(.+)$/);
|
||||
if (labelM) {
|
||||
touch(labelM[1], labelM[2].trim());
|
||||
continue;
|
||||
}
|
||||
// Bare state name.
|
||||
const bare = t.match(/^([A-Za-z0-9_]\S*)$/);
|
||||
if (bare) touch(bare[1]);
|
||||
}
|
||||
|
||||
for (const e of edges) {
|
||||
const f = nodes.get(e.from);
|
||||
const tt = nodes.get(e.to);
|
||||
if (f) f.outDeg++;
|
||||
if (tt) tt.inDeg++;
|
||||
}
|
||||
|
||||
return {
|
||||
kind: 'stateDiagram',
|
||||
direction,
|
||||
headerLine,
|
||||
nodes,
|
||||
edges,
|
||||
subgraphs,
|
||||
usesElk: false,
|
||||
hasSpacing,
|
||||
};
|
||||
}
|
||||
|
||||
/** Parse any supported diagram (flowchart/graph or state diagram). */
|
||||
export function parseDiagram(src: string): FlowGraph | null {
|
||||
return parseFlowchart(src) ?? parseStateDiagram(src);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Metrics & scoring
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -416,7 +581,10 @@ export function scoreOf(g: FlowGraph, m: Metrics): number {
|
||||
if (m.edges > 0) {
|
||||
score -= clamp((m.interGroupEdges / m.edges - 0.4) * 50, 0, 20); // crossings
|
||||
}
|
||||
if (!g.usesElk) score -= clamp((m.edges - 25) * 0.5, 0, 16); // dense + no ELK
|
||||
// The ELK penalty only applies where ELK is actually available (flowcharts).
|
||||
if (g.kind !== 'stateDiagram' && !g.usesElk) {
|
||||
score -= clamp((m.edges - 25) * 0.5, 0, 16); // dense + no ELK
|
||||
}
|
||||
score -= clamp(m.duplicateEdges * 3, 0, 12);
|
||||
return Math.round(clamp(score, 0, 100));
|
||||
}
|
||||
@@ -433,17 +601,19 @@ function ratingOf(score: number): Analysis['rating'] {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function analyze(src: string): Analysis {
|
||||
const g = parseFlowchart(src);
|
||||
const g = parseDiagram(src);
|
||||
if (!g) {
|
||||
const head = src.replace(/^---[\s\S]*?---\s*/, '').trim().split(/\s+/)[0] || '';
|
||||
return { supported: false, kind: head, score: 0, rating: 'Clear', fixes: [] };
|
||||
}
|
||||
|
||||
const isFlow = g.kind === 'flowchart' || g.kind === 'graph';
|
||||
const m = computeMetrics(g);
|
||||
const score = scoreOf(g, m);
|
||||
const fixes: Fix[] = [];
|
||||
|
||||
if (!g.usesElk && (m.nodes >= 12 || m.edges >= 20)) {
|
||||
// ELK is a flowchart-only layout engine in Mermaid; state diagrams reject it.
|
||||
if (isFlow && !g.usesElk && (m.nodes >= 12 || m.edges >= 20)) {
|
||||
fixes.push({
|
||||
id: 'elk',
|
||||
title: 'Switch to the ELK layout engine',
|
||||
@@ -458,29 +628,42 @@ export function analyze(src: string): Analysis {
|
||||
if (!g.hasSpacing && (m.density >= 1.3 || m.maxDegree >= HUB_DEGREE)) {
|
||||
fixes.push({
|
||||
id: 'spacing',
|
||||
title: 'Add breathing room & smooth edges',
|
||||
title: 'Add breathing room between nodes',
|
||||
detail:
|
||||
'Increase node/rank spacing and use curved edges so lines are easier ' +
|
||||
'to follow where they bunch up.',
|
||||
'Increase node/rank spacing so transitions are easier to follow where ' +
|
||||
'they bunch up.',
|
||||
severity: 'medium',
|
||||
apply: applySpacing,
|
||||
});
|
||||
}
|
||||
|
||||
if (m.dimHubs.length > 0) {
|
||||
const ids = m.dimHubs.map((n) => n.id);
|
||||
const names = m.dimHubs.slice(0, 4).map((n) => n.label).join(', ');
|
||||
fixes.push({
|
||||
id: 'dim-hubs',
|
||||
title: `De-emphasise ${m.dimHubs.length} cross-cutting hub${
|
||||
m.dimHubs.length > 1 ? 's' : ''
|
||||
}`,
|
||||
detail:
|
||||
`Shared infrastructure (${names}) wires up to everything and dominates ` +
|
||||
'the picture. Fading those edges lets the main flow read clearly while ' +
|
||||
'keeping every connection intact.',
|
||||
severity: m.dimHubs.length >= 3 ? 'high' : 'medium',
|
||||
apply: (src) => dimHubEdges(src, m.dimHubs.map((n) => n.id)),
|
||||
});
|
||||
const plural = m.dimHubs.length > 1;
|
||||
fixes.push(
|
||||
isFlow
|
||||
? {
|
||||
id: 'dim-hubs',
|
||||
title: `De-emphasise ${m.dimHubs.length} cross-cutting hub${plural ? 's' : ''}`,
|
||||
detail:
|
||||
`Shared infrastructure (${names}) wires up to everything and ` +
|
||||
'dominates the picture. Fading those edges lets the main flow read ' +
|
||||
'clearly while keeping every connection intact.',
|
||||
severity: m.dimHubs.length >= 3 ? 'high' : 'medium',
|
||||
apply: (src) => dimHubEdges(src, ids),
|
||||
}
|
||||
: {
|
||||
id: 'dim-hubs',
|
||||
title: `Fade ${m.dimHubs.length} sink state${plural ? 's' : ''}`,
|
||||
detail:
|
||||
`Terminal states (${names}) collect transitions from everywhere. ` +
|
||||
'Greying them out pulls focus onto the main flow. (State diagrams ' +
|
||||
"can't style individual transitions, so the states are faded.)",
|
||||
severity: 'medium',
|
||||
apply: (src) => dimHubStates(src, ids),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (m.duplicateEdges > 0) {
|
||||
@@ -559,7 +742,7 @@ export function mergeInit(src: string, patch: Record<string, any>): string {
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
const g = parseFlowchart(src);
|
||||
const g = parseDiagram(src);
|
||||
const at = g ? g.headerLine : frontmatterLines(src);
|
||||
lines.splice(at, 0, `%%{init: ${JSON.stringify(patch)}}%%`);
|
||||
return lines.join('\n');
|
||||
@@ -570,15 +753,34 @@ export function applyElkLayout(src: string): string {
|
||||
}
|
||||
|
||||
export function applySpacing(src: string): string {
|
||||
return mergeInit(src, {
|
||||
flowchart: { nodeSpacing: 55, rankSpacing: 70, curve: 'basis' },
|
||||
});
|
||||
// Spacing lives under a per-diagram config key; state diagrams also lack a
|
||||
// `curve` option.
|
||||
const patch =
|
||||
parseDiagram(src)?.kind === 'stateDiagram'
|
||||
? { state: { nodeSpacing: 60, rankSpacing: 80 } }
|
||||
: { flowchart: { nodeSpacing: 55, rankSpacing: 70, curve: 'basis' } };
|
||||
return mergeInit(src, patch);
|
||||
}
|
||||
|
||||
export function setDirection(src: string, dir: string): string {
|
||||
const lines = src.replace(/\r\n/g, '\n').split('\n');
|
||||
const g = parseFlowchart(src);
|
||||
const g = parseDiagram(src);
|
||||
if (!g) return src;
|
||||
const lines = src.replace(/\r\n/g, '\n').split('\n');
|
||||
|
||||
if (g.kind === 'stateDiagram') {
|
||||
// State diagrams set direction with a `direction XX` statement, not on the
|
||||
// header. Update an existing one or insert just after the header.
|
||||
const di = lines.findIndex(
|
||||
(l, idx) => idx > g.headerLine && /^\s*direction\s+[A-Za-z]{2}\b/.test(l)
|
||||
);
|
||||
if (di >= 0) {
|
||||
lines[di] = lines[di].replace(/(direction\s+)[A-Za-z]{2}/, `$1${dir}`);
|
||||
} else {
|
||||
lines.splice(g.headerLine + 1, 0, ` direction ${dir}`);
|
||||
}
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
lines[g.headerLine] = lines[g.headerLine].replace(
|
||||
/^(\s*)(flowchart|graph)\b\s*([A-Za-z]{2})?/,
|
||||
`$1$2 ${dir}`
|
||||
@@ -608,7 +810,7 @@ function escapeRe(s: string): string {
|
||||
*/
|
||||
export function dimHubEdges(src: string, hubIds: string[]): string {
|
||||
const base = stripDimBlock(src);
|
||||
const g = parseFlowchart(base);
|
||||
const g = parseDiagram(base);
|
||||
if (!g) return src;
|
||||
const targets = new Set(hubIds);
|
||||
const idx: number[] = [];
|
||||
@@ -620,6 +822,43 @@ export function dimHubEdges(src: string, hubIds: string[]): string {
|
||||
return `${base.replace(/\n+$/, '')}\n\n${DIM_MARK}\n${rule}\n`;
|
||||
}
|
||||
|
||||
const STATE_DIM_MARK = '%% ─ Mermix: faded sink states (re-run Optimize to refresh) ─';
|
||||
|
||||
/** Strip any Mermix-managed state-dim block so transforms stay idempotent. */
|
||||
function stripStateDimBlock(src: string): string {
|
||||
return src
|
||||
.replace(
|
||||
new RegExp(
|
||||
`\\n*${escapeRe(STATE_DIM_MARK)}\\n(?:classDef mmxHubDim[^\\n]*\\n?|class [^\\n]*mmxHubDim[^\\n]*\\n?)+`,
|
||||
'g'
|
||||
),
|
||||
'\n'
|
||||
)
|
||||
.replace(/\n{3,}/g, '\n\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Fade sink states via `classDef`/`class`. State diagrams have no per-transition
|
||||
* styling (and a `linkStyle` line renders as a stray node), so for them we
|
||||
* de-emphasise the busy *states* — e.g. a CANCELLED everything funnels into.
|
||||
*/
|
||||
export function dimHubStates(src: string, ids: string[]): string {
|
||||
const base = stripStateDimBlock(src);
|
||||
const g = parseDiagram(base);
|
||||
if (!g) return src;
|
||||
const valid = ids.filter((id) => g.nodes.has(id));
|
||||
if (valid.length === 0) return base;
|
||||
return (
|
||||
[
|
||||
base.replace(/\n+$/, ''),
|
||||
'',
|
||||
STATE_DIM_MARK,
|
||||
'classDef mmxHubDim fill:#1b1f2a,stroke:#2a2f3a,color:#5b6573',
|
||||
`class ${valid.join(',')} mmxHubDim`,
|
||||
].join('\n') + '\n'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Drop edge statements that repeat an earlier identical one (whitespace and
|
||||
* pipe-label–insensitive). Only whole single-edge lines are removed; lines
|
||||
@@ -649,11 +888,13 @@ export function autoOptimize(src: string): string {
|
||||
const a = analyze(src);
|
||||
if (!a.supported || !a.metrics) return src;
|
||||
let out = src;
|
||||
if (!a.graph!.usesElk) out = applyElkLayout(out);
|
||||
const isFlow = a.graph!.kind === 'flowchart' || a.graph!.kind === 'graph';
|
||||
if (isFlow && !a.graph!.usesElk) out = applyElkLayout(out);
|
||||
if (!a.graph!.hasSpacing) out = applySpacing(out);
|
||||
if (a.metrics.duplicateEdges > 0) out = removeDuplicateEdges(out);
|
||||
if (a.metrics.dimHubs.length > 0) {
|
||||
out = dimHubEdges(out, a.metrics.dimHubs.map((n) => n.id));
|
||||
const ids = a.metrics.dimHubs.map((n) => n.id);
|
||||
out = isFlow ? dimHubEdges(out, ids) : dimHubStates(out, ids);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
@@ -669,8 +910,8 @@ export function autoOptimize(src: string): string {
|
||||
* if the node can't be found.
|
||||
*/
|
||||
export function applyFocus(src: string, nodeId: string): string {
|
||||
const base = stripDimBlock(src);
|
||||
const g = parseFlowchart(base);
|
||||
const base = stripStateDimBlock(stripDimBlock(src));
|
||||
const g = parseDiagram(base);
|
||||
if (!g || !g.nodes.has(nodeId)) return src;
|
||||
|
||||
const neighbours = new Set<string>([nodeId]);
|
||||
@@ -689,15 +930,19 @@ export function applyFocus(src: string, nodeId: string): string {
|
||||
const dimNodes = [...g.nodes.keys()].filter((id) => !neighbours.has(id));
|
||||
|
||||
const parts = [base.replace(/\n+$/, ''), '', `%% ─ Mermix focus: ${nodeId} ─`];
|
||||
parts.push('classDef mmxDim fill:#161922,stroke:#262b36,color:#4a5263;');
|
||||
parts.push('classDef mmxFocus fill:#2c2350,stroke:#a78bfa,color:#fff,stroke-width:2px;');
|
||||
if (dimNodes.length) parts.push(`class ${dimNodes.join(',')} mmxDim;`);
|
||||
parts.push(`class ${nodeId} mmxFocus;`);
|
||||
if (other.length) {
|
||||
parts.push(`linkStyle ${other.join(',')} stroke:#1e222b,stroke-opacity:0.1;`);
|
||||
}
|
||||
if (incident.length) {
|
||||
parts.push(`linkStyle ${incident.join(',')} stroke:#a78bfa,stroke-width:2px;`);
|
||||
parts.push('classDef mmxDim fill:#161922,stroke:#262b36,color:#4a5263');
|
||||
parts.push('classDef mmxFocus fill:#2c2350,stroke:#a78bfa,color:#fff,stroke-width:2px');
|
||||
if (dimNodes.length) parts.push(`class ${dimNodes.join(',')} mmxDim`);
|
||||
parts.push(`class ${nodeId} mmxFocus`);
|
||||
// State diagrams can't style transitions (a `linkStyle` line renders as a
|
||||
// stray node), so fade by state only there; flowcharts also fade the edges.
|
||||
if (g.kind !== 'stateDiagram') {
|
||||
if (other.length) {
|
||||
parts.push(`linkStyle ${other.join(',')} stroke:#1e222b,stroke-opacity:0.1;`);
|
||||
}
|
||||
if (incident.length) {
|
||||
parts.push(`linkStyle ${incident.join(',')} stroke:#a78bfa,stroke-width:2px;`);
|
||||
}
|
||||
}
|
||||
return parts.join('\n') + '\n';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user