Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .cspell/mermaid-terms.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
Adamiecki
aporetic
arrowend
Bendpoints
bmatrix
Expand Down
22 changes: 18 additions & 4 deletions cypress/integration/rendering/cynefin/cynefin.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ describe('cynefin framework', () => {
chaotic
"Page on-call immediately"

confusion
aporetic
"Unknown failure mode"
`
);
Expand Down Expand Up @@ -104,6 +104,20 @@ describe('cynefin framework', () => {
);
});

it('should render cynefin without outward flow arrows', () => {
imgSnapshotTest(
`cynefin-beta
title No Flow

complex
"Item A"
clear
"Item B"
`,
{ cynefin: { showFlow: false } }
);
});

it('should render cynefin without domain descriptions', () => {
imgSnapshotTest(
`cynefin-beta
Expand Down Expand Up @@ -157,12 +171,12 @@ describe('cynefin framework', () => {
);
});

it('should render cynefin with confusion domain items without overflow', () => {
it('should render cynefin with aporetic domain items without overflow', () => {
imgSnapshotTest(
`cynefin-beta
title Confusion Items
title Aporetic Items

confusion
aporetic
"Unknown A"
"Unknown B"
"Unknown C"
Expand Down
55 changes: 28 additions & 27 deletions docs/syntax/cynefin.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ The Cynefin framework divides the world into five domains, each with its own dec
- **Complicated**: Cause and effect require analysis or expertise. Sense → Analyse → Respond. Apply **good practices**.
- **Complex**: Cause and effect can only be deduced in retrospect. Probe → Sense → Respond. Apply **emergent practices**.
- **Chaotic**: No perceivable cause and effect. Act → Sense → Respond. Apply **novel practices**.
- **Confusion** (or Disorder): You do not know which domain you are in. The goal is to move items out of this state into one of the other four.
- **Aporetic** (the central domain): You do not yet know which domain you are in — a state of productive not-knowing. The goal is to move items out of this state into one of the other four. (Note: "Confused" is a distinct concept that only appears in the liminal/dynamic version of Cynefin, which this diagram does not model.)

The signature visual feature is the wavy, organic boundary between the ordered (Clear, Complicated) and unordered (Complex, Chaotic) halves, and the "cliff" between Clear and Chaotic representing the risk of complacency leading to crisis.
The signature visual features are the phase-shift boundaries between domains — wavy and abrupt where crossing them is a real transition (the central fold, the Complex/Chaotic boundary, and the "cliff" between Clear and Chaotic) — versus the plain Clear/Complicated boundary, which is a gradient rather than a phase shift. Flow radiates outward from the central Aporetic domain.

## Syntax

Expand All @@ -43,7 +43,7 @@ clear
chaotic
"Crisis response"

confusion
aporetic
"Item of unknown domain"

complex --> complicated : "Pattern identified"
Expand All @@ -60,7 +60,7 @@ clear --> chaotic : "Complacency"
| `complicated` | Opens the Complicated domain block |
| `clear` | Opens the Clear domain block |
| `chaotic` | Opens the Chaotic domain block |
| `confusion` | Opens the Confusion / Disorder domain block |
| `aporetic` | Opens the central Aporetic (not-yet-known) domain block |
| `-->` | Declares a transition from one domain to another |

### Items
Expand All @@ -73,7 +73,7 @@ complex
"Run chaos experiment"
```

Keep per-domain item lists short — the quadrants have fixed layout and long lists can visually overflow their boxes. The confusion ellipse caps at three items and shows a `+N more` badge; the four quadrant domains do not clip, so prefer a handful of items each.
Keep per-domain item lists short — the quadrants have fixed layout and long lists can visually overflow their boxes. The aporetic ellipse caps at three items and shows a `+N more` badge; the four quadrant domains do not clip, so prefer a handful of items each.

### Transitions

Expand Down Expand Up @@ -115,7 +115,7 @@ cynefin-beta
chaotic
"Page on-call immediately"

confusion
aporetic
"Unknown failure mode"
```

Expand All @@ -138,7 +138,7 @@ cynefin-beta
chaotic
"Page on-call immediately"

confusion
aporetic
"Unknown failure mode"
```

Expand Down Expand Up @@ -224,6 +224,7 @@ Cynefin diagrams accept the following configuration under the `cynefin` key in t
| `showDomainDescriptions` | boolean | `true` | Show decision model and practice type subtitles per domain |
| `boundaryAmplitude` | number | `8` | Waviness amplitude of domain boundaries in pixels (set to `0` for straight lines) |
| `seed` | number | `0` | Deterministic seed for boundary waviness. `0` (default) hashes the diagram's SVG id so each diagram looks unique. Set any non-zero number to lock the waviness across renders — required for stable visual regression tests. |
| `showFlow` | boolean | `true` | Show flow arrows radiating outward from the central Aporetic domain to each domain (set to `false` to hide them) |

Example:

Expand All @@ -238,29 +239,29 @@ cynefin-beta

Cynefin diagrams use the following theme variables, which can be overridden via `themeVariables.cynefin`:

| Variable | Description |
| ---------------- | ------------------------------------------------ |
| `complexBg` | Background color for the Complex domain |
| `complicatedBg` | Background color for the Complicated domain |
| `clearBg` | Background color for the Clear domain |
| `chaoticBg` | Background color for the Chaotic domain |
| `confusionBg` | Background color for the Confusion center region |
| `boundaryColor` | Color of the wavy domain boundaries |
| `boundaryWidth` | Stroke width of the boundaries |
| `cliffColor` | Color of the Clear/Chaotic cliff |
| `cliffWidth` | Stroke width of the cliff |
| `arrowColor` | Color of transition arrows |
| `arrowWidth` | Stroke width of transition arrows |
| `labelColor` | Color of domain name labels |
| `textColor` | Color of item and subtitle text |
| `domainFontSize` | Font size of domain name labels |
| `itemFontSize` | Font size of item badges and subtitles |
| Variable | Description |
| ---------------- | ----------------------------------------------- |
| `complexBg` | Background color for the Complex domain |
| `complicatedBg` | Background color for the Complicated domain |
| `clearBg` | Background color for the Clear domain |
| `chaoticBg` | Background color for the Chaotic domain |
| `aporeticBg` | Background color for the Aporetic center region |
| `boundaryColor` | Color of the wavy domain boundaries |
| `boundaryWidth` | Stroke width of the boundaries |
| `cliffColor` | Color of the Clear/Chaotic cliff |
| `cliffWidth` | Stroke width of the cliff |
| `arrowColor` | Color of transition arrows |
| `arrowWidth` | Stroke width of transition arrows |
| `labelColor` | Color of domain name labels |
| `textColor` | Color of item and subtitle text |
| `domainFontSize` | Font size of domain name labels |
| `itemFontSize` | Font size of item badges and subtitles |

## Notes

- Domain names are fixed keywords. Only `complex`, `complicated`, `clear`, `chaotic`, and `confusion` are recognized.
- Domains can be declared in any order; their position in the diagram is always the same (Complex top-left, Complicated top-right, Chaotic bottom-left, Clear bottom-right, Confusion center).
- The `confusion` domain has a compact center ellipse. Up to 3 items are shown inside it; if more are provided a `+N more` overflow badge is displayed. In practice, the confusion domain should contain very few items — its purpose is to surface unknowns so they can be moved to one of the four main domains.
- Domain names are fixed keywords. Only `complex`, `complicated`, `clear`, `chaotic`, and `aporetic` are recognized.
- Domains can be declared in any order; their position in the diagram is always the same (Complex top-left, Complicated top-right, Chaotic bottom-left, Clear bottom-right, Aporetic center).
- The `aporetic` domain has a compact center ellipse. Up to 3 items are shown inside it; if more are provided a `+N more` overflow badge is displayed. In practice, the aporetic domain should contain very few items — its purpose is to surface unknowns so they can be moved to one of the four main domains.
- Self-loop transitions (e.g. `complex --> complex`) are silently ignored. Transitions must connect two different domains.
- Handdrawn mode is not currently supported.
- The wavy boundary rendering is deterministic: the same input always produces the same diagram, so diffs are stable across builds.
Expand Down
2 changes: 1 addition & 1 deletion packages/examples/src/examples/cynefin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export default {
chaotic
"Page on-call immediately"

confusion
aporetic
"Unknown failure mode"

complex --> complicated : "Pattern identified"
Expand Down
7 changes: 7 additions & 0 deletions packages/mermaid/src/config.type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2018,6 +2018,13 @@ export interface CynefinDiagramConfig extends BaseDiagramConfig {
*
*/
seed?: number;
/**
* Show flow arrows radiating outward from the central Aporetic domain to each
* of the four domains. This reflects that, in the Cynefin framework, flow moves
* outward from the Aporetic (not-yet-known) centre rather than in from one side.
*
*/
showFlow?: boolean;
}
/**
* Configuration for Railroad (Syntax) Diagrams
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ describe('Cynefin Parsing - Basic', () => {
"C"
chaotic
"D"
confusion
aporetic
"E"
`);
const domains = db.getDomains();
Expand All @@ -39,7 +39,7 @@ describe('Cynefin Parsing - Basic', () => {
expect(domains.has('complicated')).toBe(true);
expect(domains.has('clear')).toBe(true);
expect(domains.has('chaotic')).toBe(true);
expect(domains.has('confusion')).toBe(true);
expect(domains.has('aporetic')).toBe(true);
});

it('should parse empty domains', async () => {
Expand Down Expand Up @@ -179,12 +179,12 @@ describe('Cynefin Parsing - Complex diagram', () => {
"Deployment checklist"
chaotic
"Incident response"
confusion
aporetic
"New initiative"
complex --> complicated : "Pattern emerges"
complicated --> clear : "Best practice found"
chaotic --> complex : "Stabilized"
confusion --> chaotic : "Crisis detected"
aporetic --> chaotic : "Crisis detected"
`);
expect(db.getDiagramTitle()).toBe('Team Practices');
expect(db.getAccTitle()).toBe('Cynefin for team practices');
Expand All @@ -195,7 +195,7 @@ describe('Cynefin Parsing - Complex diagram', () => {
expect(domains.get('complicated')!.items).toHaveLength(2);
expect(domains.get('clear')!.items).toHaveLength(1);
expect(domains.get('chaotic')!.items).toHaveLength(1);
expect(domains.get('confusion')!.items).toHaveLength(1);
expect(domains.get('aporetic')!.items).toHaveLength(1);

const transitions = db.getTransitions();
expect(transitions).toHaveLength(4);
Expand All @@ -205,7 +205,7 @@ describe('Cynefin Parsing - Complex diagram', () => {
label: 'Pattern emerges',
});
expect(transitions[3]).toMatchObject({
from: 'confusion',
from: 'aporetic',
to: 'chaotic',
label: 'Crisis detected',
});
Expand Down
30 changes: 24 additions & 6 deletions packages/mermaid/src/diagrams/cynefin/cynefin.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ import {
resolveSeed,
generateFoldPath,
generateHorizontalBoundary,
generateGradientBoundary,
generateCliffPath,
generateConfusionPath,
generateAporeticPath,
} from './cynefinBoundaries.js';

/** Test helper: build a partial DomainBlock with just the fields the DB reads. */
Expand Down Expand Up @@ -84,7 +85,7 @@ describe('Cynefin Database', () => {
block('complicated'),
block('clear'),
block('chaotic'),
block('confusion'),
block('aporetic'),
]);
expect(db.getDomains().size).toBe(5);
});
Expand All @@ -100,6 +101,10 @@ describe('Cynefin Database', () => {
const config = db.getConfig();
expect(typeof config).toBe('object');
});

it('should default showFlow to true', () => {
expect(db.getConfig().showFlow).toBe(true);
});
});

describe('Cynefin Boundaries', () => {
Expand Down Expand Up @@ -147,15 +152,28 @@ describe('Cynefin Boundaries', () => {
expect(path).toContain('C');
});

it('generateConfusionPath should return valid ellipse path', () => {
const path = generateConfusionPath(400, 300, 50, 40);
it('generateGradientBoundary should return a straight line (no curves)', () => {
const path = generateGradientBoundary(400, 800, 300);
expect(path).toBe('M400,300 L800,300');
expect(path).not.toContain('C');
});

it('generateHorizontalBoundary should respect an explicit x-range and pin its endpoints', () => {
const path = generateHorizontalBoundary(800, 600, 42, 8, 0, 400);
// Pinned endpoints sit exactly on the centre line (cy = 300) at x=0 and x=400.
expect(path).toMatch(/^M0,300 /);
expect(path).toContain('400,300');
});

it('generateAporeticPath should return valid ellipse path', () => {
const path = generateAporeticPath(400, 300, 50, 40);
expect(path).toMatch(/^M/);
expect(path).toContain('A');
expect(path).toMatch(/Z$/);
});

it('generateConfusionPath should use provided center and radii', () => {
const path = generateConfusionPath(400, 300, 50, 40);
it('generateAporeticPath should use provided center and radii', () => {
const path = generateAporeticPath(400, 300, 50, 40);
expect(path).toContain('350'); // cx - rx = 400 - 50
expect(path).toContain('450'); // cx + rx = 400 + 50
expect(path).toContain('300'); // cy
Expand Down
41 changes: 35 additions & 6 deletions packages/mermaid/src/diagrams/cynefin/cynefinBoundaries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,27 +85,40 @@ export function generateFoldPath(

/**
* Generate a horizontal wavy line through the center of the diagram.
*
* Defaults to spanning the full width, but accepts an x-range so a single side
* can be drawn. The Complex/Chaotic boundary (left half) is a genuine phase
* shift and uses this wavy line; the Clear/Complicated boundary (right half) is
* a gradient, not a phase shift, and is drawn separately via
* {@link generateGradientBoundary}. The range endpoints are pinned to the centre
* line so the wavy half meets the gradient half cleanly at the diagram centre.
* @param width - diagram width
* @param height - diagram height
* @param seed - deterministic seed for variation
* @param amplitudeOverride - optional amplitude in pixels; falls back to 1.5% of height
* @param xStart - left edge of the segment (defaults to 0)
* @param xEnd - right edge of the segment (defaults to width)
* @returns SVG path d attribute string
*/
export function generateHorizontalBoundary(
width: number,
height: number,
seed: number,
amplitudeOverride?: number
amplitudeOverride?: number,
xStart = 0,
xEnd = width
): string {
const cy = height / 2;
const amplitude = amplitudeOverride ?? height * 0.015;
const segments = 7;
const segWidth = width / segments;
const segWidth = (xEnd - xStart) / segments;
const points: { x: number; y: number }[] = [];

for (let i = 0; i <= segments; i++) {
const jitter = seededRandom(seed + i * 23) * amplitude * 2 - amplitude;
points.push({ x: i * segWidth, y: cy + jitter });
// Pin the endpoints to the centre line so adjoining segments meet cleanly.
const jitter =
i === 0 || i === segments ? 0 : seededRandom(seed + i * 23) * amplitude * 2 - amplitude;
points.push({ x: xStart + i * segWidth, y: cy + jitter });
}

let d = `M${points[0].x},${points[0].y}`;
Expand All @@ -125,6 +138,22 @@ export function generateHorizontalBoundary(
return d;
}

/**
* Generate a plain straight boundary line between two ordered domains.
*
* Used for the Clear/Complicated boundary, which is a gradient rather than a
* phase shift — unlike the central fold, the Complex/Chaotic boundary, and the
* Clear/Chaotic cliff, crossing it is a smooth, reversible transition, so it is
* drawn as a simple straight line with no waviness.
* @param xStart - left edge of the line
* @param xEnd - right edge of the line
* @param y - vertical position of the line
* @returns SVG path d attribute string
*/
export function generateGradientBoundary(xStart: number, xEnd: number, y: number): string {
return `M${xStart},${y} L${xEnd},${y}`;
}

/**
* Generate the "cliff" path between Clear (bottom-right) and Chaotic (bottom-left).
* This is a thicker, more abrupt boundary near the bottom center.
Expand All @@ -151,14 +180,14 @@ export function generateCliffPath(width: number, height: number): string {
}

/**
* Generate an ellipse SVG path for the confusion/disorder region at the center.
* Generate an ellipse SVG path for the Aporetic region at the center.
* @param cx - center x
* @param cy - center y
* @param rx - horizontal radius
* @param ry - vertical radius
* @returns SVG path d attribute string for an ellipse
*/
export function generateConfusionPath(cx: number, cy: number, rx: number, ry: number): string {
export function generateAporeticPath(cx: number, cy: number, rx: number, ry: number): string {
// Draw an ellipse using two arc commands
return [
`M${cx - rx},${cy}`,
Expand Down
Loading
Loading