Skip to content
Open
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
18 changes: 18 additions & 0 deletions .changeset/wardley-autoplace-labels.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
'mermaid': minor
---

feat(wardley): opt-in automatic label placement (`autoPlaceLabels`)

New opt-in `autoPlaceLabels` config flag for the `wardley-beta` diagram. When
enabled, component, anchor, link and annotation labels are automatically
repositioned to avoid overlapping each other, node markers, pipeline boxes,
the chart boundary and link lines. Labels moved far from their node get a thin
leader line; a collision-free manual `label [x, y]` is kept exactly as
authored, and pipeline child labels prefer to sit underneath their node.

Off by default — existing maps render unchanged. Enable it via config:

```js
mermaid.initialize({ 'wardley-beta': { autoPlaceLabels: true } });
```
24 changes: 24 additions & 0 deletions .changeset/wardley-ecosystem-and-attitudes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
'mermaid': minor
---

feat(wardley): add `(ecosystem)` source strategy and pioneers/settlers/townplanners attitude zones

Extends the `wardley-beta` diagram to match more of the OWM syntax:

- `(ecosystem)` decorator on a component, drawn as concentric circles with a diagonal-hatch ring (matches OWM's ecosystem-play symbol).
- Attitude zones using OWM's canonical 4-coordinate form `keyword [v1, m1, v2, m2]` for the two opposing corners. Renders as a translucent labelled rectangle in the OWM colour palette:
- `pioneers` — light blue
- `settlers` — medium blue
- `townplanners` — purple
- `explorers` is accepted as an alias for `pioneers`, `villagers` as an alias for `settlers`.

Example:

```
wardley-beta
component Developer Platform [0.78, 0.10] (ecosystem)
pioneers [0.95, 0.05, 0.55, 0.30]
settlers [0.95, 0.35, 0.55, 0.65]
townplanners [0.95, 0.70, 0.55, 0.95]
```
1 change: 1 addition & 0 deletions .cspell/mermaid-terms.txt
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ sentencepiece
siebling
statediagram
substate
townplanners
unfixable
Unmodelled
Viewbox
Expand Down
116 changes: 116 additions & 0 deletions cypress/integration/rendering/wardley/wardley.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -268,4 +268,120 @@ note "Voting members: Adobe, Amazon, Apple, Google, Meta, Microsoft, Netflix, Sa
{}
);
});

it('should render component sourcing strategies including market and ecosystem', () => {
imgSnapshotTest(
`
wardley-beta
title Sourcing Strategies
size [1100, 800]

component Custom Built [0.80, 0.20] (build)
component Off The Shelf [0.65, 0.45] (buy)
component Outsourced Service [0.50, 0.65] (outsource)
component Marketplace [0.35, 0.80] (market)
component Platform [0.20, 0.92] (ecosystem)
`,
{}
);
});

it('should render pioneers/settlers/townplanners attitude zones', () => {
imgSnapshotTest(
`
wardley-beta
title Pioneers, Settlers, Town Planners
size [1100, 800]

pioneers [0.95, 0.05, 0.55, 0.30]
settlers [0.95, 0.35, 0.55, 0.65]
townplanners [0.95, 0.70, 0.55, 0.95]

component Custom Research [0.80, 0.15]
component Product [0.55, 0.50]
component Commodity Service [0.30, 0.85]
`,
{}
);
});

it('should render dense map labels without overlap when autoPlaceLabels is enabled', () => {
// Seven long-named components packed into a tiny coordinate box. Without
// auto-placement their default NE-offset labels collide into an unreadable
// blob, so this map genuinely exercises the placement algorithm.
imgSnapshotTest(
`
wardley-beta
title Overlapping Label Stress Test
size [1100, 800]

component Customer Service Portal [0.55, 0.62]
component Customer Support Desk [0.58, 0.60]
component Customer Records Store [0.52, 0.64]
component Customer Data Platform [0.56, 0.585]
component Account Management API [0.53, 0.59]
component Billing And Invoicing [0.57, 0.635]
component Identity Provider [0.545, 0.61]

Customer Service Portal -> Customer Data Platform
Customer Support Desk -> Customer Records Store
Account Management API -> Billing And Invoicing
Identity Provider -> Customer Data Platform
`,
{ 'wardley-beta': { autoPlaceLabels: true } }
);
});

it('should keep collision-free manual labels when autoPlaceLabels is enabled', () => {
// Three cases: `Kept Manual Label` is isolated with a manual label that
// lands in clear space and has no link touching it -> kept untouched.
// `Colliding Manual` has a manual label dropped onto a node cluster ->
// re-placed. The remaining components are untuned -> auto-placed.
imgSnapshotTest(
`
wardley-beta
title Manual Label Mix
size [1100, 800]

component Kept Manual Label [0.25, 0.30] label [20, -18]
component Colliding Manual [0.55, 0.60] label [-90, 2]
component Crowded Node A [0.52, 0.62]
component Crowded Node B [0.56, 0.585]
component Crowded Node C [0.53, 0.59]
component Untuned Component [0.78, 0.40]

Colliding Manual -> Crowded Node B
Crowded Node A -> Untuned Component
`,
{ 'wardley-beta': { autoPlaceLabels: true } }
);
});

it('should place pipeline child labels underneath when autoPlaceLabels is enabled', () => {
// Pipeline child components have no manual `label [x,y]`, so they are
// auto-placed; their preferred direction is straight down.
imgSnapshotTest(
`
wardley-beta
title Pipeline Autoplace
size [1100, 800]

component Kettle [0.57, 0.45]
component Power [0.10, 0.70]

Kettle -> Power

pipeline Kettle {
component Campfire Kettle [0.30]
component Electric Kettle [0.52]
component Smart Kettle [0.74]
}

Campfire Kettle -> Kettle
Electric Kettle -> Kettle
Smart Kettle -> Kettle
`,
{ 'wardley-beta': { autoPlaceLabels: true } }
);
});
});
91 changes: 69 additions & 22 deletions docs/syntax/wardley.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
Wardley Maps position components along two axes:

- **Visibility** (Y-axis): How visible/valuable a component is to users (0.0 = infrastructure, 1.0 = user-facing)
- **Evolution** (X-axis): How evolved/mature a component is (0.0 = genesis/novel, 1.0 = commodity/utility)
- **Evolution** (X-axis): How evolved a component is (0.0 = genesis/novel, 1.0 = commodity/utility)

This dual positioning enables strategic analysis of:

Expand Down Expand Up @@ -94,7 +94,7 @@ size [width, height]

### Coordinate System

**IMPORTANT**: Wardley Maps use the OnlineWardleyMaps (OWM) format: `[visibility, evolution]`
**IMPORTANT**: Wardley Maps use the OWM format: `[visibility, evolution]`

- **First value (Visibility)**: 0.0-1.0 (bottom to top) - Y-axis position
- **Second value (Evolution)**: 0.0-1.0 (left to right) - X-axis position
Expand Down Expand Up @@ -237,41 +237,88 @@ Legacy System -> New Platform

Indicate build/buy/outsource decisions:

- `(build)` - Triangle symbol
- `(buy)` - Diamond symbol
- `(outsource)` - Square symbol
- `(market)` - Circle symbol
- `(build)` - Light grey overlay circle with black border (in-house build)
- `(buy)` - Light grey overlay circle (off-the-shelf purchase)
- `(outsource)` - Dark grey overlay circle (outsourced delivery)
- `(market)` - Outline circle with three small connected circles in a triangle (commodity market)
- `(ecosystem)` - Concentric circles with a diagonal-hatch ring (ecosystem play)

```mermaid-example
wardley-beta
title Sourcing Strategy

anchor Customer [0.80, 0.95]
component Custom App [0.45, 0.85] (build)
component Off-the-shelf Tool [0.85, 0.65] (buy)
component Managed Service [0.60, 0.40] (outsource)
component Cloud Platform [0.95, 0.25] (market)
anchor Customer [0.97, 0.43]
component Custom App [0.71, 0.35] (build)
component "Off-the-shelf Tool" [0.85, 0.65] (buy)
component Managed Service [0.58, 0.60] (outsource)
component Cloud Platform [0.04, 0.84] (market)
component ML Service [0.10, 0.50] (market)
component Developer Marketplace [0.26, 0.85] (ecosystem)

Customer -> Custom App
Custom App -> Off-the-shelf Tool
Custom App -> "Off-the-shelf Tool"
Custom App -> Managed Service
Off-the-shelf Tool -> Cloud Platform
Custom App -> ML Service
"Off-the-shelf Tool" -> Cloud Platform
Managed Service -> Cloud Platform
Custom App -> Developer Marketplace
```

```mermaid
wardley-beta
title Sourcing Strategy

anchor Customer [0.80, 0.95]
component Custom App [0.45, 0.85] (build)
component Off-the-shelf Tool [0.85, 0.65] (buy)
component Managed Service [0.60, 0.40] (outsource)
component Cloud Platform [0.95, 0.25] (market)
anchor Customer [0.97, 0.43]
component Custom App [0.71, 0.35] (build)
component "Off-the-shelf Tool" [0.85, 0.65] (buy)
component Managed Service [0.58, 0.60] (outsource)
component Cloud Platform [0.04, 0.84] (market)
component ML Service [0.10, 0.50] (market)
component Developer Marketplace [0.26, 0.85] (ecosystem)

Customer -> Custom App
Custom App -> Off-the-shelf Tool
Custom App -> "Off-the-shelf Tool"
Custom App -> Managed Service
Off-the-shelf Tool -> Cloud Platform
Custom App -> ML Service
"Off-the-shelf Tool" -> Cloud Platform
Managed Service -> Cloud Platform
Custom App -> Developer Marketplace
```

### Attitude Zones

Highlight rectangular regions of the map with the **Pioneers / Settlers / Town Planners** framing. Each zone is defined by two opposing corners (top-left and bottom-right) inside a single bracket: `[visibility1, evolution1, visibility2, evolution2]`.

- `pioneers` - Light blue zone, typically over the genesis / custom-built area
- `settlers` - Medium blue zone, typically over the product / rental area
- `townplanners` - Purple zone, typically over the commodity / utility area
- `explorers` - Alias for `pioneers`
- `villagers` - Alias for `settlers`

```mermaid-example
wardley-beta
title Pioneers, Settlers, Town Planners

pioneers [0.95, 0.05, 0.55, 0.30]
settlers [0.95, 0.35, 0.55, 0.65]
townplanners [0.95, 0.70, 0.55, 0.95]

component Custom Research [0.80, 0.15]
component Product [0.55, 0.50]
component Commodity Service [0.30, 0.85]
```

```mermaid
wardley-beta
title Pioneers, Settlers, Town Planners

pioneers [0.95, 0.05, 0.55, 0.30]
settlers [0.95, 0.35, 0.55, 0.65]
townplanners [0.95, 0.70, 0.55, 0.95]

component Custom Research [0.80, 0.15]
component Product [0.55, 0.50]
component Commodity Service [0.30, 0.85]
```

### Links and Dependencies
Expand Down Expand Up @@ -725,9 +772,9 @@ Wardley Maps support Mermaid's theme system. Use standard Mermaid configuration
## Resources

- [Wardley Mapping Book](https://medium.com/wardleymaps) by Simon Wardley
- [OnlineWardleyMaps](https://onlinewardleymaps.com/) - Interactive mapping tool
- [Wardley Maps Community](https://community.wardleymaps.com/)
- [Create Wardley Maps](https://create.wardleymaps.ai/) - Interactive mapping tool
- [Learn Wardley Mapping](https://learnwardleymapping.com/)
- [Wardley Maps Community](https://community.wardleymaps.com/)

## Syntax Summary

Expand Down
8 changes: 8 additions & 0 deletions packages/mermaid/src/config.type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1815,6 +1815,14 @@ export interface WardleyDiagramConfig extends BaseDiagramConfig {
* Whether to display a background grid.
*/
showGrid?: boolean;
/**
* When `true`, component, anchor, link, and annotation labels are
* automatically repositioned to avoid overlapping each other, node
* markers, the chart boundary, and link lines. Manual `label [x, y]`
* offsets are ignored while this is enabled.
*
*/
autoPlaceLabels?: boolean;
}
/**
* This interface was referenced by `MermaidConfig`'s JSON-Schema
Expand Down
6 changes: 6 additions & 0 deletions packages/mermaid/src/diagrams/wardley/styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,12 @@ export const styles: DiagramStylesProvider = ({
.wardley-notes text {
fill: ${w.axisTextColor};
}
.wardley-leader-line {
stroke: ${w.annotationStroke};
stroke-width: 1px;
stroke-opacity: 0.6;
fill: none;
}
`;
};

Expand Down
33 changes: 33 additions & 0 deletions packages/mermaid/src/diagrams/wardley/wardleyBuilder.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,36 @@ describe('WardleyBuilder.resolveNodeId', () => {
expect(builder.resolveNodeId('Beta')).toBe('Beta');
});
});

describe('WardleyBuilder.attitudes', () => {
let builder: WardleyBuilder;

beforeEach(() => {
builder = new WardleyBuilder();
builder.addNode({ id: 'A', label: 'A', x: 0, y: 0 });
});

it('stores attitudes in build output', () => {
builder.addAttitude({ kind: 'pioneers', x1: 10, y1: 90, x2: 30, y2: 70 });
builder.addAttitude({ kind: 'settlers', x1: 40, y1: 70, x2: 60, y2: 50 });
builder.addAttitude({ kind: 'townplanners', x1: 70, y1: 50, x2: 95, y2: 30 });

const result = builder.build();
expect(result.attitudes).toHaveLength(3);
expect(result.attitudes.map((a) => a.kind)).toEqual(['pioneers', 'settlers', 'townplanners']);
expect(result.attitudes[0]).toEqual({
kind: 'pioneers',
x1: 10,
y1: 90,
x2: 30,
y2: 70,
});
});

it('clears attitudes on reset', () => {
builder.addAttitude({ kind: 'pioneers', x1: 10, y1: 90, x2: 30, y2: 70 });
builder.clear();
builder.addNode({ id: 'A', label: 'A', x: 0, y: 0 });
expect(builder.build().attitudes).toEqual([]);
});
});
Loading
Loading