diff --git a/.build/jsonSchema.ts b/.build/jsonSchema.ts
index 21355e8fbc4..c1ff5f481de 100644
--- a/.build/jsonSchema.ts
+++ b/.build/jsonSchema.ts
@@ -19,6 +19,7 @@ const MERMAID_CONFIG_DIAGRAM_KEYS = [
'xyChart',
'requirement',
'mindmap',
+ 'ishikawa',
'kanban',
'timeline',
'gitGraph',
diff --git a/.changeset/odd-crabs-bake.md b/.changeset/odd-crabs-bake.md
new file mode 100644
index 00000000000..78cee324b95
--- /dev/null
+++ b/.changeset/odd-crabs-bake.md
@@ -0,0 +1,6 @@
+---
+'@mermaid-js/examples': minor
+'mermaid': minor
+---
+
+feat: Add Ishikawa diagram (ishikawa-beta)
diff --git a/cypress/integration/rendering/ishikawa.spec.ts b/cypress/integration/rendering/ishikawa.spec.ts
new file mode 100644
index 00000000000..7d97f8e2d22
--- /dev/null
+++ b/cypress/integration/rendering/ishikawa.spec.ts
@@ -0,0 +1,216 @@
+import { imgSnapshotTest, renderGraph } from '../../helpers/util';
+
+describe('Ishikawa diagram', () => {
+ it('1: should render a simple ishikawa diagram', () => {
+ imgSnapshotTest(
+ `ishikawa-beta
+ Blurry Photo
+ Process
+ Out of focus
+ User
+ Shaky hands
+ `
+ );
+ });
+
+ it('2: should render with many causes on both sides', () => {
+ imgSnapshotTest(
+ `ishikawa-beta
+ Manufacturing Defect
+ Machine
+ Worn tooling
+ Calibration
+ Method
+ Missing step
+ Material
+ Contamination
+ Wrong grade
+ Manpower
+ Insufficient training
+ `
+ );
+ });
+
+ it('3: should render with deeply nested causes', () => {
+ imgSnapshotTest(
+ `ishikawa-beta
+ Server Outage
+ Hardware
+ Disk
+ Bad sectors
+ Full capacity
+ Memory
+ Leak detected
+ Software
+ Bug
+ Race condition
+ `
+ );
+ });
+
+ it('4: should render with a single cause', () => {
+ imgSnapshotTest(
+ `ishikawa-beta
+ Problem
+ Cause A
+ `
+ );
+ });
+
+ it('5: should render with no children (root only)', () => {
+ imgSnapshotTest(
+ `ishikawa-beta
+ Problem
+ `
+ );
+ });
+
+ it('6: should render with handDrawn look', () => {
+ imgSnapshotTest(
+ `ishikawa-beta
+ Blurry Photo
+ Process
+ Out of focus
+ User
+ Shaky hands
+ `,
+ { look: 'handDrawn' }
+ );
+ });
+
+ it('7: should render with forest theme', () => {
+ imgSnapshotTest(
+ `ishikawa-beta
+ Blurry Photo
+ Process
+ Out of focus
+ User
+ Shaky hands
+ `,
+ { theme: 'forest' }
+ );
+ });
+
+ it('8: should render with dark theme', () => {
+ imgSnapshotTest(
+ `ishikawa-beta
+ Blurry Photo
+ Process
+ Out of focus
+ User
+ Shaky hands
+ `,
+ { theme: 'dark' }
+ );
+ });
+
+ it('9: should render with custom diagramPadding', () => {
+ imgSnapshotTest(
+ `ishikawa-beta
+ Blurry Photo
+ Process
+ Out of focus
+ User
+ Shaky hands
+ `,
+ { ishikawa: { diagramPadding: 50 } }
+ );
+ });
+
+ it('10: should render when useMaxWidth is true', () => {
+ renderGraph(
+ `ishikawa-beta
+ Blurry Photo
+ Process
+ Out of focus
+ User
+ Shaky hands
+ `,
+ { ishikawa: { useMaxWidth: true } }
+ );
+ cy.get('svg').should((svg) => {
+ expect(svg).to.have.attr('width', '100%');
+ const style = svg.attr('style');
+ expect(style).to.match(/^max-width: [\d.]+px;$/);
+ });
+ });
+
+ it('11: should render when useMaxWidth is false', () => {
+ renderGraph(
+ `ishikawa-beta
+ Blurry Photo
+ Process
+ Out of focus
+ User
+ Shaky hands
+ `,
+ { ishikawa: { useMaxWidth: false } }
+ );
+ cy.get('svg').should((svg) => {
+ const width = parseFloat(svg.attr('width')!);
+ expect(width).to.be.greaterThan(0);
+ const height = parseFloat(svg.attr('height')!);
+ expect(height).to.be.greaterThan(0);
+ expect(svg).to.not.have.attr('style');
+ });
+ });
+
+ it('12: should render a very deep nested diagram', () => {
+ imgSnapshotTest(
+ `ishikawa-beta
+ Very deep
+ Cause 1
+ 1-1
+ 1-2
+ 1-3
+ 1-3-1
+ 1-3-2
+ 1-3-3
+ 1-3-4
+ Cause 2
+ 2-1
+ 2-1-1
+ 2-1-2
+ 2-2
+ 2-2-1
+ 2-2-2
+ 2-2-2-1
+ 2-2-2-1-1
+ 2-2-2-1-2
+ 2-2-2-1-2-1
+ 2-2-2-1
+ 2-3
+ 2-3-1
+ Cause 3
+ 3-1
+ 3-1-1
+ 3-1-1-1
+ 3-1-1-1-1
+ 3-1-1-1-1-1
+ 3-1-1-1-1-1-1
+ Cause 4
+ 4-1
+ 4-1-1
+ 4-1-2
+ 4-1-3
+ 4-1-4
+ 4-1-5
+ 4-1-6
+ 4-1-7
+ 4-1-8
+ 4-2
+ 4-2-1
+ 4-2-2
+ Cause 5
+ 5-1
+ Cause 6
+ 6-1
+ 6-2
+ 6-3
+ 6-4
+ 6-5
+ 6-6
+ `
+ );
+ });
+});
diff --git a/demos/index.html b/demos/index.html
index f9c3e34e93f..c5a062cd94b 100644
--- a/demos/index.html
+++ b/demos/index.html
@@ -55,6 +55,9 @@
+
+
+
diff --git a/demos/ishikawa.html b/demos/ishikawa.html
new file mode 100644
index 00000000000..cd16a06b135
--- /dev/null
+++ b/demos/ishikawa.html
@@ -0,0 +1,155 @@
+
+
+
+
+
+ Ishikawa Mermaid Quick Test Page
+
+
+
+
+
+ Ishikawa diagram demo
+
+ishikawa-beta
+ Blurry Photo
+ Process
+ Out of focus
+ Shutter speed too slow
+ Protective film not removed
+ Beautification filter applied
+ User
+ Shaky hands
+ Equipment
+ LENS
+ Inappropriate lens
+ Damaged lens
+ Dirty lens
+ SENSOR
+ Damaged sensor
+ Dirty sensor
+ Environment
+ Subject moved too quickly
+ Too dark
+
+
+
+ishikawa-beta
+ Problem Only
+
+
+
+---
+config:
+ ishikawa:
+ diagramPadding: 100
+---
+ishikawa-beta
+ Very deep
+ Cause 1
+ 1-1
+ 1-2
+ 1-3
+ 1-3-1
+ 1-3-2
+ 1-3-3
+ 1-3-4
+ Cause 2
+ 2-1
+ 2-1-1
+ 2-1-2
+ 2-2
+ 2-2-1
+ 2-2-2
+ 2-2-2-1
+ 2-2-2-1-1
+ 2-2-2-1-2
+ 2-2-2-1-2-1
+ 2-2-2-1
+ 2-3
+ 2-3-1
+ Cause 3
+ 3-1
+ 3-1-1
+ 3-1-1-1
+ 3-1-1-1-1
+ 3-1-1-1-1-1
+ 3-1-1-1-1-1-1
+ Cause 4
+ 4-1
+ 4-1-1
+ 4-1-2
+ 4-1-3
+ 4-1-4
+ 4-1-5
+ 4-1-6
+ 4-1-7
+ 4-1-8
+ 4-2
+ 4-2-1
+ 4-2-2
+ Cause 5
+ 5-1
+ Cause 6
+ 6-1
+ 6-2
+ 6-3
+ 6-4
+ 6-5
+ 6-6
+
+
+ Hand-drawn mode
+
+%%{init: { 'look': 'handDrawn', 'handDrawnSeed': 1 } }%%
+ishikawa-beta
+ Blurry Photo
+ Process
+ Out of focus
+ Shutter speed too slow
+ User
+ Shaky hands
+ Equipment
+ LENS
+ Dirty lens
+ SENSOR
+ Damaged sensor
+ Environment
+ Too dark
+
+
+ Forest theme
+
+%%{init: { 'theme': 'forest' } }%%
+ishikawa-beta
+ Low Test Coverage
+ People
+ Lack of training
+ Insufficient reviewers
+ Process
+ No CI pipeline
+ Skipping code review
+ Tools
+ Outdated test framework
+ Flaky test runner
+ Environment
+ Tight deadlines
+ Unclear requirements
+
+
+
+
+
diff --git a/docs/config/setup/defaultConfig/variables/configKeys.md b/docs/config/setup/defaultConfig/variables/configKeys.md
index f5a461b477e..2e7258bb854 100644
--- a/docs/config/setup/defaultConfig/variables/configKeys.md
+++ b/docs/config/setup/defaultConfig/variables/configKeys.md
@@ -12,4 +12,4 @@
> `const` **configKeys**: `Set`<`string`>
-Defined in: [packages/mermaid/src/defaultConfig.ts:295](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/defaultConfig.ts#L295)
+Defined in: [packages/mermaid/src/defaultConfig.ts:298](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/defaultConfig.ts#L298)
diff --git a/docs/config/setup/mermaid/interfaces/MermaidConfig.md b/docs/config/setup/mermaid/interfaces/MermaidConfig.md
index 4e10da89e61..8d1fea23e59 100644
--- a/docs/config/setup/mermaid/interfaces/MermaidConfig.md
+++ b/docs/config/setup/mermaid/interfaces/MermaidConfig.md
@@ -45,7 +45,7 @@ This matters if you are using base tag settings.
> `optional` **block**: `BlockDiagramConfig`
-Defined in: [packages/mermaid/src/config.type.ts:218](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L218)
+Defined in: [packages/mermaid/src/config.type.ts:219](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L219)
---
@@ -53,7 +53,7 @@ Defined in: [packages/mermaid/src/config.type.ts:218](https://github.com/mermaid
> `optional` **c4**: `C4DiagramConfig`
-Defined in: [packages/mermaid/src/config.type.ts:215](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L215)
+Defined in: [packages/mermaid/src/config.type.ts:216](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L216)
---
@@ -105,7 +105,7 @@ You can set this attribute to base the seed on a static string.
> `optional` **dompurifyConfig**: `Config`
-Defined in: [packages/mermaid/src/config.type.ts:221](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L221)
+Defined in: [packages/mermaid/src/config.type.ts:222](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L222)
---
@@ -179,7 +179,7 @@ See
> `optional` **fontSize**: `number`
-Defined in: [packages/mermaid/src/config.type.ts:223](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L223)
+Defined in: [packages/mermaid/src/config.type.ts:224](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L224)
---
@@ -207,7 +207,7 @@ Defined in: [packages/mermaid/src/config.type.ts:201](https://github.com/mermaid
> `optional` **gitGraph**: `GitGraphDiagramConfig`
-Defined in: [packages/mermaid/src/config.type.ts:214](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L214)
+Defined in: [packages/mermaid/src/config.type.ts:215](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L215)
---
@@ -234,6 +234,14 @@ over any diagram-specific settings.
---
+### ishikawa?
+
+> `optional` **ishikawa**: `IshikawaDiagramConfig`
+
+Defined in: [packages/mermaid/src/config.type.ts:213](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L213)
+
+---
+
### journey?
> `optional` **journey**: `JourneyDiagramConfig`
@@ -246,7 +254,7 @@ Defined in: [packages/mermaid/src/config.type.ts:202](https://github.com/mermaid
> `optional` **kanban**: `KanbanDiagramConfig`
-Defined in: [packages/mermaid/src/config.type.ts:213](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L213)
+Defined in: [packages/mermaid/src/config.type.ts:214](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L214)
---
@@ -297,7 +305,7 @@ Defines which main look to use for the diagram.
> `optional` **markdownAutoWrap**: `boolean`
-Defined in: [packages/mermaid/src/config.type.ts:224](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L224)
+Defined in: [packages/mermaid/src/config.type.ts:225](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L225)
---
@@ -333,7 +341,7 @@ Defined in: [packages/mermaid/src/config.type.ts:212](https://github.com/mermaid
> `optional` **packet**: `PacketDiagramConfig`
-Defined in: [packages/mermaid/src/config.type.ts:217](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L217)
+Defined in: [packages/mermaid/src/config.type.ts:218](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L218)
---
@@ -357,7 +365,7 @@ Defined in: [packages/mermaid/src/config.type.ts:208](https://github.com/mermaid
> `optional` **radar**: `RadarDiagramConfig`
-Defined in: [packages/mermaid/src/config.type.ts:219](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L219)
+Defined in: [packages/mermaid/src/config.type.ts:220](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L220)
---
@@ -373,7 +381,7 @@ Defined in: [packages/mermaid/src/config.type.ts:210](https://github.com/mermaid
> `optional` **sankey**: `SankeyDiagramConfig`
-Defined in: [packages/mermaid/src/config.type.ts:216](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L216)
+Defined in: [packages/mermaid/src/config.type.ts:217](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L217)
---
@@ -429,7 +437,7 @@ Defined in: [packages/mermaid/src/config.type.ts:205](https://github.com/mermaid
> `optional` **suppressErrorRendering**: `boolean`
-Defined in: [packages/mermaid/src/config.type.ts:230](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L230)
+Defined in: [packages/mermaid/src/config.type.ts:231](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L231)
Suppresses inserting 'Syntax error' diagram in the DOM.
This is useful when you want to control how to handle syntax errors in your application.
@@ -475,7 +483,7 @@ Defined in: [packages/mermaid/src/config.type.ts:203](https://github.com/mermaid
> `optional` **venn**: `VennDiagramConfig`
-Defined in: [packages/mermaid/src/config.type.ts:220](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L220)
+Defined in: [packages/mermaid/src/config.type.ts:221](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L221)
---
@@ -483,7 +491,7 @@ Defined in: [packages/mermaid/src/config.type.ts:220](https://github.com/mermaid
> `optional` **wrap**: `boolean`
-Defined in: [packages/mermaid/src/config.type.ts:222](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L222)
+Defined in: [packages/mermaid/src/config.type.ts:223](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L223)
---
diff --git a/docs/syntax/ishikawa.md b/docs/syntax/ishikawa.md
new file mode 100644
index 00000000000..778d73d3525
--- /dev/null
+++ b/docs/syntax/ishikawa.md
@@ -0,0 +1,66 @@
+> **Warning**
+>
+> ## THIS IS AN AUTOGENERATED FILE. DO NOT EDIT.
+>
+> ## Please edit the corresponding file in [/packages/mermaid/src/docs/syntax/ishikawa.md](../../packages/mermaid/src/docs/syntax/ishikawa.md).
+
+# Ishikawa diagram (v\+)
+
+Ishikawa diagrams are used to represent causes of a specific event (or a problem).
+They are also known as fishbone diagrams, herringbone diagrams or cause-and-effect diagrams.
+The diagram resembles a fish skeleton, with the main problem at the head and the causes branching off from the spine.
+
+> **Warning**
+> This is a new diagram type in Mermaid. Its syntax may evolve in future versions.
+
+## Syntax
+
+```mermaid-example
+ishikawa-beta
+ Blurry Photo
+ Process
+ Out of focus
+ Shutter speed too slow
+ Protective film not removed
+ Beautification filter applied
+ User
+ Shaky hands
+ Equipment
+ LENS
+ Inappropriate lens
+ Damaged lens
+ Dirty lens
+ SENSOR
+ Damaged sensor
+ Dirty sensor
+ Environment
+ Subject moved too quickly
+ Too dark
+```
+
+```mermaid
+ishikawa-beta
+ Blurry Photo
+ Process
+ Out of focus
+ Shutter speed too slow
+ Protective film not removed
+ Beautification filter applied
+ User
+ Shaky hands
+ Equipment
+ LENS
+ Inappropriate lens
+ Damaged lens
+ Dirty lens
+ SENSOR
+ Damaged sensor
+ Dirty sensor
+ Environment
+ Subject moved too quickly
+ Too dark
+```
+
+- The first line is the event (problem) of the diagram.
+- Subsequent lines are causes of the event.
+- "Fishbone" structure is indicated by indentation.
diff --git a/packages/examples/src/examples/ishikawa.ts b/packages/examples/src/examples/ishikawa.ts
new file mode 100644
index 00000000000..696d18279e5
--- /dev/null
+++ b/packages/examples/src/examples/ishikawa.ts
@@ -0,0 +1,35 @@
+import type { DiagramMetadata } from '../types.js';
+
+export default {
+ id: 'ishikawa',
+ name: 'Ishikawa Diagram',
+ description: 'Visualize problem and causes in fishbone',
+ examples: [
+ {
+ title: 'Ishikawa Diagram',
+ isDefault: true,
+ code: `
+ishikawa-beta
+ Blurry Photo
+ Process
+ Out of focus
+ Shutter speed too slow
+ Protective film not removed
+ Beautification filter applied
+ User
+ Shaky hands
+ Equipment
+ LENS
+ Inappropriate lens
+ Damaged lens
+ Dirty lens
+ SENSOR
+ Damaged sensor
+ Dirty sensor
+ Environment
+ Subject moved too quickly
+ Too dark
+`,
+ },
+ ],
+} satisfies DiagramMetadata;
diff --git a/packages/examples/src/index.ts b/packages/examples/src/index.ts
index 77661cf0420..720d2d6da3b 100644
--- a/packages/examples/src/index.ts
+++ b/packages/examples/src/index.ts
@@ -1,6 +1,7 @@
import type { DiagramMetadata } from './types.js';
import flowChart from './examples/flowchart.js';
import c4 from './examples/c4.js';
+import ishikawaDiagram from './examples/ishikawa.js';
import kanban from './examples/kanban.js';
import classDiagram from './examples/class.js';
import sequenceDiagram from './examples/sequence.js';
@@ -26,6 +27,7 @@ import vennDiagram from './examples/venn.js';
export const diagramData: DiagramMetadata[] = [
flowChart,
c4,
+ ishikawaDiagram,
kanban,
classDiagram,
sequenceDiagram,
diff --git a/packages/mermaid/src/config.type.ts b/packages/mermaid/src/config.type.ts
index a808b2b9901..14fc18969c4 100644
--- a/packages/mermaid/src/config.type.ts
+++ b/packages/mermaid/src/config.type.ts
@@ -210,6 +210,7 @@ export interface MermaidConfig {
requirement?: RequirementDiagramConfig;
architecture?: ArchitectureDiagramConfig;
mindmap?: MindmapDiagramConfig;
+ ishikawa?: IshikawaDiagramConfig;
kanban?: KanbanDiagramConfig;
gitGraph?: GitGraphDiagramConfig;
c4?: C4DiagramConfig;
@@ -1093,6 +1094,21 @@ export interface MindmapDiagramConfig extends BaseDiagramConfig {
*/
layoutAlgorithm?: string;
}
+/**
+ * The object containing configurations specific for ishikawa diagrams
+ *
+ * This interface was referenced by `MermaidConfig`'s JSON-Schema
+ * via the `definition` "IshikawaDiagramConfig".
+ */
+export interface IshikawaDiagramConfig extends BaseDiagramConfig {
+ /**
+ * The amount of padding around the diagram as a whole so that embedded
+ * diagrams have margins, expressed in pixels.
+ *
+ */
+ diagramPadding?: number;
+ useMaxWidth?: boolean;
+}
/**
* The object containing configurations specific for kanban diagrams
*
diff --git a/packages/mermaid/src/defaultConfig.ts b/packages/mermaid/src/defaultConfig.ts
index 8bd99c39f40..49731b311ab 100644
--- a/packages/mermaid/src/defaultConfig.ts
+++ b/packages/mermaid/src/defaultConfig.ts
@@ -264,6 +264,9 @@ const config: RequiredDeep = {
radar: {
...defaultConfigJson.radar,
},
+ ishikawa: {
+ ...defaultConfigJson.ishikawa,
+ },
treemap: {
useMaxWidth: true,
padding: 10,
diff --git a/packages/mermaid/src/diagram-api/diagram-orchestration.ts b/packages/mermaid/src/diagram-api/diagram-orchestration.ts
index b6a913002c4..e365a6227ce 100644
--- a/packages/mermaid/src/diagram-api/diagram-orchestration.ts
+++ b/packages/mermaid/src/diagram-api/diagram-orchestration.ts
@@ -25,6 +25,7 @@ import { packet } from '../diagrams/packet/detector.js';
import { radar } from '../diagrams/radar/detector.js';
import block from '../diagrams/block/blockDetector.js';
import architecture from '../diagrams/architecture/architectureDetector.js';
+import { ishikawa } from '../diagrams/ishikawa/ishikawaDetector.js';
import venn from '../diagrams/venn/vennDetector.js';
import { registerLazyLoadedDiagrams } from './detectType.js';
import { registerDiagram } from './diagramAPI.js';
@@ -102,6 +103,7 @@ export const addDiagrams = () => {
xychart,
block,
radar,
+ ishikawa,
treemap,
venn
);
diff --git a/packages/mermaid/src/diagrams/ishikawa/ishikawa.spec.ts b/packages/mermaid/src/diagrams/ishikawa/ishikawa.spec.ts
new file mode 100644
index 00000000000..6167ca4fc38
--- /dev/null
+++ b/packages/mermaid/src/diagrams/ishikawa/ishikawa.spec.ts
@@ -0,0 +1,48 @@
+// @ts-expect-error No types available for JISON
+import { parser as ishikawa } from './parser/ishikawa.jison';
+import { IshikawaDB } from './ishikawaDb.js';
+import { setLogLevel } from '../../diagram-api/diagramAPI.js';
+
+describe('when parsing an Ishikawa diagram', function () {
+ beforeEach(function () {
+ ishikawa.yy = new IshikawaDB();
+ ishikawa.yy.clear();
+ setLogLevel('trace');
+ });
+
+ it('should parse a basic Ishikawa hierarchy', function () {
+ const str = `ishikawa-beta
+ Blurry Photo
+ Process
+ Out of focus
+ User
+ Shaky hands
+`;
+
+ ishikawa.parse(str);
+ const root = ishikawa.yy.getRoot();
+ expect(root?.text).toEqual('Blurry Photo');
+ expect(root?.children.length).toEqual(2);
+ expect(root?.children[0].text).toEqual('Process');
+ expect(root?.children[0].children[0].text).toEqual('Out of focus');
+ expect(root?.children[1].text).toEqual('User');
+ expect(root?.children[1].children[0].text).toEqual('Shaky hands');
+ });
+
+ it('should support an unindented root with nested causes', function () {
+ const str = `ishikawa-beta
+Problem
+Cause A
+ Subcause A1
+Cause B
+`;
+
+ ishikawa.parse(str);
+ const root = ishikawa.yy.getRoot();
+ expect(root?.text).toEqual('Problem');
+ expect(root?.children.length).toEqual(2);
+ expect(root?.children[0].text).toEqual('Cause A');
+ expect(root?.children[0].children[0].text).toEqual('Subcause A1');
+ expect(root?.children[1].text).toEqual('Cause B');
+ });
+});
diff --git a/packages/mermaid/src/diagrams/ishikawa/ishikawaDb.ts b/packages/mermaid/src/diagrams/ishikawa/ishikawaDb.ts
new file mode 100644
index 00000000000..a66611db138
--- /dev/null
+++ b/packages/mermaid/src/diagrams/ishikawa/ishikawaDb.ts
@@ -0,0 +1,92 @@
+import { getConfig } from '../../diagram-api/diagramAPI.js';
+import type { DiagramDB } from '../../diagram-api/types.js';
+import common from '../common/common.js';
+import {
+ clear as commonClear,
+ getAccDescription,
+ getAccTitle,
+ getDiagramTitle,
+ setAccDescription,
+ setAccTitle,
+ setDiagramTitle,
+} from '../common/commonDb.js';
+import type { IshikawaNode } from './ishikawaTypes.js';
+
+interface StackEntry {
+ level: number;
+ node: IshikawaNode;
+}
+
+export class IshikawaDB implements DiagramDB {
+ private root?: IshikawaNode;
+ private stack: StackEntry[] = [];
+ private baseLevel?: number;
+
+ constructor() {
+ this.clear = this.clear.bind(this);
+ this.addNode = this.addNode.bind(this);
+ this.getRoot = this.getRoot.bind(this);
+ }
+
+ clear(): void {
+ this.root = undefined;
+ this.stack = [];
+ this.baseLevel = undefined;
+ commonClear();
+ }
+
+ getRoot(): IshikawaNode | undefined {
+ return this.root;
+ }
+
+ addNode(rawLevel: number, text: string): void {
+ const label = common.sanitizeText(text, getConfig());
+
+ if (!this.root) {
+ this.baseLevel = rawLevel;
+ this.root = { text: label, children: [] };
+ this.stack = [{ level: 0, node: this.root }];
+ setDiagramTitle(label);
+ return;
+ }
+
+ let level = rawLevel - (this.baseLevel ?? 0);
+ if (level <= 0) {
+ level = 1;
+ }
+
+ // Pop stack until the top has a strictly lower level (= parent)
+ while (this.stack.length > 1 && this.stack[this.stack.length - 1].level >= level) {
+ this.stack.pop();
+ }
+
+ const parent = this.stack[this.stack.length - 1].node;
+ const node: IshikawaNode = { text: label, children: [] };
+ parent.children.push(node);
+ this.stack.push({ level, node });
+ }
+
+ getAccTitle(): string {
+ return getAccTitle();
+ }
+
+ setAccTitle(title: string): void {
+ setAccTitle(title);
+ }
+
+ getAccDescription(): string {
+ return getAccDescription();
+ }
+
+ setAccDescription(description: string): void {
+ setAccDescription(description);
+ }
+
+ getDiagramTitle(): string {
+ return getDiagramTitle();
+ }
+
+ setDiagramTitle(title: string): void {
+ setDiagramTitle(title);
+ }
+}
diff --git a/packages/mermaid/src/diagrams/ishikawa/ishikawaDetector.ts b/packages/mermaid/src/diagrams/ishikawa/ishikawaDetector.ts
new file mode 100644
index 00000000000..266fa469830
--- /dev/null
+++ b/packages/mermaid/src/diagrams/ishikawa/ishikawaDetector.ts
@@ -0,0 +1,22 @@
+import type {
+ DiagramDetector,
+ DiagramLoader,
+ ExternalDiagramDefinition,
+} from '../../diagram-api/types.js';
+
+const id = 'ishikawa';
+
+const detector: DiagramDetector = (txt) => {
+ return /^\s*ishikawa(-beta)?\b/i.test(txt);
+};
+
+const loader: DiagramLoader = async () => {
+ const { diagram } = await import('./ishikawaDiagram.js');
+ return { id, diagram };
+};
+
+export const ishikawa: ExternalDiagramDefinition = {
+ id,
+ detector,
+ loader,
+};
diff --git a/packages/mermaid/src/diagrams/ishikawa/ishikawaDiagram.ts b/packages/mermaid/src/diagrams/ishikawa/ishikawaDiagram.ts
new file mode 100644
index 00000000000..b4705cf5ca0
--- /dev/null
+++ b/packages/mermaid/src/diagrams/ishikawa/ishikawaDiagram.ts
@@ -0,0 +1,15 @@
+import type { DiagramDefinition } from '../../diagram-api/types.js';
+// @ts-ignore: JISON doesn't support types
+import parser from './parser/ishikawa.jison';
+import { IshikawaDB } from './ishikawaDb.js';
+import { renderer } from './ishikawaRenderer.js';
+import styles from './ishikawaStyles.js';
+
+export const diagram: DiagramDefinition = {
+ parser,
+ get db() {
+ return new IshikawaDB();
+ },
+ renderer,
+ styles,
+};
diff --git a/packages/mermaid/src/diagrams/ishikawa/ishikawaRenderer.ts b/packages/mermaid/src/diagrams/ishikawa/ishikawaRenderer.ts
new file mode 100644
index 00000000000..7f3e0d29598
--- /dev/null
+++ b/packages/mermaid/src/diagrams/ishikawa/ishikawaRenderer.ts
@@ -0,0 +1,513 @@
+// cspell:ignore lerp
+import type { Diagram } from '../../Diagram.js';
+import { getConfig } from '../../diagram-api/diagramAPI.js';
+import type { DiagramRenderer, DrawDefinition, SVG, SVGGroup } from '../../diagram-api/types.js';
+import { selectSvgElement } from '../../rendering-util/selectSvgElement.js';
+import { configureSvgSize } from '../../setupGraphViewbox.js';
+import { parseFontSize } from '../../utils.js';
+import type { IshikawaDB } from './ishikawaDb.js';
+import type { IshikawaNode } from './ishikawaTypes.js';
+import rough from 'roughjs';
+
+interface RoughContext {
+ roughSvg: ReturnType;
+ seed: number;
+ lineColor: string;
+ fillColor: string;
+}
+
+const FONT_SIZE_DEFAULT = 14;
+const SPINE_BASE_LENGTH = 250;
+const BONE_STUB = 30;
+const BONE_BASE = 60;
+const BONE_PER_CHILD = 5;
+const ANGLE = (82 * Math.PI) / 180;
+const COS_A = Math.cos(ANGLE);
+const SIN_A = Math.sin(ANGLE);
+
+const applyPaddedViewBox = (svgEl: SVG, pad: number, maxW: boolean) => {
+ const bbox = svgEl.node()!.getBBox();
+ const w = bbox.width + pad * 2;
+ const h = bbox.height + pad * 2;
+ configureSvgSize(svgEl, h, w, maxW);
+ svgEl.attr('viewBox', `${bbox.x - pad} ${bbox.y - pad} ${w} ${h}`);
+};
+
+const draw: DrawDefinition = (_text, id, _version, diagram: Diagram) => {
+ const db = diagram.db as IshikawaDB;
+ const root = db.getRoot();
+ if (!root) {
+ return;
+ }
+
+ const drawConfig = getConfig();
+ const { look, handDrawnSeed, themeVariables } = drawConfig;
+ const fontSize = parseFontSize(drawConfig.fontSize)[0] ?? FONT_SIZE_DEFAULT;
+ const isHandDrawn = look === 'handDrawn';
+
+ const causes = root.children ?? [];
+ const padding = drawConfig.ishikawa?.diagramPadding ?? 20;
+ const useMaxWidth = drawConfig.ishikawa?.useMaxWidth ?? false;
+ const svg = selectSvgElement(id);
+ const g = svg.append('g').attr('class', 'ishikawa');
+
+ const roughSvg = isHandDrawn ? rough.svg(svg.node()!) : undefined;
+ const roughContext: RoughContext | undefined = roughSvg
+ ? {
+ roughSvg,
+ seed: handDrawnSeed ?? 0,
+ lineColor: themeVariables?.lineColor ?? '#333',
+ fillColor: themeVariables?.mainBkg ?? '#fff',
+ }
+ : undefined;
+
+ const markerId = `ishikawa-arrow-${id}`;
+ if (!isHandDrawn) {
+ g.append('defs')
+ .append('marker')
+ .attr('id', markerId)
+ .attr('viewBox', '0 0 10 10')
+ .attr('refX', 0)
+ .attr('refY', 5)
+ .attr('markerWidth', 6)
+ .attr('markerHeight', 6)
+ .attr('orient', 'auto')
+ .append('path')
+ .attr('d', 'M 10 0 L 0 5 L 10 10 Z')
+ .attr('class', 'ishikawa-arrow');
+ }
+
+ let spineX = 0;
+ let spineY = SPINE_BASE_LENGTH;
+
+ // For handDrawn, defer spine drawing until coordinates are final
+ const spineLine = isHandDrawn
+ ? undefined
+ : drawLine(g, spineX, spineY, spineX, spineY, 'ishikawa-spine');
+ drawHead(g, spineX, spineY, root.text, fontSize, roughContext);
+
+ if (!causes.length) {
+ if (isHandDrawn) {
+ drawLine(g, spineX, spineY, spineX, spineY, 'ishikawa-spine', roughContext);
+ }
+ applyPaddedViewBox(svg, padding, useMaxWidth);
+ return;
+ }
+
+ spineX -= 20;
+
+ const upperCauses = causes.filter((_, i) => i % 2 === 0);
+ const lowerCauses = causes.filter((_, i) => i % 2 === 1);
+
+ const upperStats = sideStats(upperCauses);
+ const lowerStats = sideStats(lowerCauses);
+ const descendantTotal = upperStats.total + lowerStats.total;
+
+ let upperLen = SPINE_BASE_LENGTH;
+ let lowerLen = SPINE_BASE_LENGTH;
+ if (descendantTotal > 0) {
+ const pool = SPINE_BASE_LENGTH * 2;
+ const minLen = SPINE_BASE_LENGTH * 0.3;
+ upperLen = Math.max(minLen, pool * (upperStats.total / descendantTotal));
+ lowerLen = Math.max(minLen, pool * (lowerStats.total / descendantTotal));
+ }
+
+ const minSpacing = fontSize * 2;
+ upperLen = Math.max(upperLen, upperStats.max * minSpacing);
+ lowerLen = Math.max(lowerLen, lowerStats.max * minSpacing);
+
+ spineY = Math.max(upperLen, SPINE_BASE_LENGTH);
+ if (spineLine) {
+ spineLine.attr('y1', spineY).attr('y2', spineY);
+ }
+ g.select('.ishikawa-head-group').attr('transform', `translate(0,${spineY})`);
+
+ const pairCount = Math.ceil(causes.length / 2);
+ for (let p = 0; p < pairCount; p++) {
+ const pg = g.append('g').attr('class', 'ishikawa-pair');
+ for (const [cause, dir, len] of [
+ [causes[p * 2], -1, upperLen] as const,
+ [causes[p * 2 + 1], 1, lowerLen] as const,
+ ]) {
+ if (cause) {
+ drawBranch(pg, cause, spineX, spineY, dir, len, fontSize, roughContext);
+ }
+ }
+ spineX = pg
+ .selectAll('text')
+ .nodes()
+ .reduce((left, n) => Math.min(left, (n as SVGGraphicsElement).getBBox().x), Infinity);
+ }
+
+ if (isHandDrawn) {
+ drawLine(g, spineX, spineY, 0, spineY, 'ishikawa-spine', roughContext);
+ } else {
+ spineLine!.attr('x1', spineX);
+ const markerUrl = `url(#${markerId})`;
+ g.selectAll('line.ishikawa-branch, line.ishikawa-sub-branch').attr('marker-start', markerUrl);
+ }
+ applyPaddedViewBox(svg, padding, useMaxWidth);
+};
+
+const sideStats = (nodes: IshikawaNode[]) => {
+ const countDescendants = (node: IshikawaNode): number =>
+ node.children.reduce((sum, child) => sum + 1 + countDescendants(child), 0);
+
+ return nodes.reduce(
+ (stats, node) => {
+ const descendants = countDescendants(node);
+ stats.total += descendants;
+ stats.max = Math.max(stats.max, descendants);
+ return stats;
+ },
+ { total: 0, max: 0 }
+ );
+};
+
+const drawHead = (
+ svg: SVGGroup,
+ x: number,
+ y: number,
+ label: string,
+ fontSize: number,
+ roughContext?: RoughContext
+): void => {
+ const maxChars = Math.max(6, Math.floor(110 / (fontSize * 0.6)));
+ const headGroup = svg
+ .append('g')
+ .attr('class', 'ishikawa-head-group')
+ .attr('transform', `translate(${x},${y})`);
+
+ const textEl = drawMultilineText(
+ headGroup,
+ wrapText(label, maxChars),
+ 0,
+ 0,
+ 'ishikawa-head-label',
+ 'start',
+ fontSize
+ );
+ const tb = textEl.node()!.getBBox();
+ const w = Math.max(60, tb.width + 6);
+ const h = Math.max(40, tb.height * 2 + 40);
+
+ const headPath = `M 0 ${-h / 2} L 0 ${h / 2} Q ${w * 2.4} 0 0 ${-h / 2} Z`;
+ if (roughContext) {
+ const roughNode = roughContext.roughSvg.path(headPath, {
+ roughness: 1.5,
+ seed: roughContext.seed,
+ fill: roughContext.fillColor,
+ fillStyle: 'hachure',
+ fillWeight: 2.5,
+ hachureGap: 5,
+ stroke: roughContext.lineColor,
+ strokeWidth: 2,
+ });
+ headGroup.insert(() => roughNode, ':first-child').attr('class', 'ishikawa-head');
+ } else {
+ headGroup.insert('path', ':first-child').attr('class', 'ishikawa-head').attr('d', headPath);
+ }
+ textEl.attr('transform', `translate(${(w - tb.width) / 2 - tb.x + 3},${-tb.y - tb.height / 2})`);
+};
+
+interface LabelEntry {
+ text: string;
+ depth: number;
+ parentIndex: number;
+ childCount: number;
+}
+
+interface BoneInfo {
+ x0: number;
+ y0: number;
+ x1: number;
+ y1: number;
+ childCount: number;
+ childrenDrawn: number;
+}
+
+// Flatten children so we can assign Y positions without recursion when drawing.
+// Even depths are placed in pre-order (close to the spine), odd depths in post-order
+// to keep diagonal bones within their parent wedge.
+const flattenTree = (children: IshikawaNode[], direction: 1 | -1) => {
+ const entries: LabelEntry[] = [];
+ const yOrder: number[] = [];
+ const walk = (nodes: IshikawaNode[], pid: number, depth: number) => {
+ const ordered = direction === -1 ? [...nodes].reverse() : nodes;
+ for (const child of ordered) {
+ const idx = entries.length;
+ const gc = child.children ?? [];
+ entries.push({
+ depth,
+ text: wrapText(child.text, 15),
+ parentIndex: pid,
+ childCount: gc.length,
+ });
+ if (depth % 2 === 0) {
+ // Even-depth: pre-order (closer to spine)
+ yOrder.push(idx);
+ if (gc.length) {
+ walk(gc, idx, depth + 1);
+ }
+ } else {
+ // odd-depth: post-order (within parent diagonal)
+ if (gc.length) {
+ walk(gc, idx, depth + 1);
+ }
+ yOrder.push(idx);
+ }
+ }
+ };
+ walk(children, -1, 2);
+ return { entries, yOrder };
+};
+
+const drawCauseLabel = (
+ svg: SVGGroup,
+ text: string,
+ x: number,
+ y: number,
+ direction: 1 | -1,
+ fontSize: number,
+ roughContext?: RoughContext
+) => {
+ const lg = svg.append('g').attr('class', 'ishikawa-label-group');
+ const lt = drawMultilineText(
+ lg,
+ text,
+ x,
+ y + 11 * direction,
+ 'ishikawa-label cause',
+ 'middle',
+ fontSize
+ );
+ const tb = lt.node()!.getBBox();
+ if (roughContext) {
+ const roughNode = roughContext.roughSvg.rectangle(
+ tb.x - 20,
+ tb.y - 2,
+ tb.width + 40,
+ tb.height + 4,
+ {
+ roughness: 1.5,
+ seed: roughContext.seed,
+ fill: roughContext.fillColor,
+ fillStyle: 'hachure',
+ fillWeight: 2.5,
+ hachureGap: 5,
+ stroke: roughContext.lineColor,
+ strokeWidth: 2,
+ }
+ );
+ lg.insert(() => roughNode, ':first-child').attr('class', 'ishikawa-label-box');
+ } else {
+ lg.insert('rect', ':first-child')
+ .attr('class', 'ishikawa-label-box')
+ .attr('x', tb.x - 20)
+ .attr('y', tb.y - 2)
+ .attr('width', tb.width + 40)
+ .attr('height', tb.height + 4);
+ }
+};
+
+// Emulate arrow marker since rough.js does not support marker on line
+const drawArrowMarker = (
+ g: SVGGroup,
+ x: number,
+ y: number,
+ dx: number,
+ dy: number,
+ roughContext: RoughContext
+) => {
+ const len = Math.sqrt(dx * dx + dy * dy);
+ if (len === 0) {
+ return;
+ }
+ const ux = dx / len;
+ const uy = dy / len;
+ const s = 6;
+ const px = -uy * s;
+ const py = ux * s;
+ const tipX = x;
+ const tipY = y;
+ const d = `M ${tipX} ${tipY} L ${tipX - ux * s * 2 + px} ${tipY - uy * s * 2 + py} L ${tipX - ux * s * 2 - px} ${tipY - uy * s * 2 - py} Z`;
+ const roughNode = roughContext.roughSvg.path(d, {
+ roughness: 1,
+ seed: roughContext.seed,
+ fill: roughContext.lineColor,
+ fillStyle: 'solid',
+ stroke: roughContext.lineColor,
+ strokeWidth: 1,
+ });
+ g.append(() => roughNode);
+};
+
+const drawBranch = (
+ svg: SVGGroup,
+ node: IshikawaNode,
+ startX: number,
+ startY: number,
+ direction: 1 | -1,
+ length: number,
+ fontSize: number,
+ roughContext?: RoughContext
+): void => {
+ const children = node.children ?? [];
+ const lineLen = length * (children.length ? 1 : 0.2);
+ const dx = -COS_A * lineLen;
+ const dy = SIN_A * lineLen * direction;
+ const endX = startX + dx;
+ const endY = startY + dy;
+
+ drawLine(svg, startX, startY, endX, endY, 'ishikawa-branch', roughContext);
+ if (roughContext) {
+ drawArrowMarker(svg, startX, startY, startX - endX, startY - endY, roughContext);
+ }
+ drawCauseLabel(svg, node.text, endX, endY, direction, fontSize, roughContext);
+
+ if (!children.length) {
+ return;
+ }
+
+ const { entries, yOrder } = flattenTree(children, direction);
+ const entryCount = entries.length;
+ const ys = new Array(entryCount);
+ for (const [slot, entryIdx] of yOrder.entries()) {
+ ys[entryIdx] = startY + dy * ((slot + 1) / (entryCount + 1));
+ }
+
+ const bones = new Map();
+ bones.set(-1, {
+ x0: startX,
+ y0: startY,
+ x1: endX,
+ y1: endY,
+ childCount: children.length,
+ childrenDrawn: 0,
+ });
+
+ const diagonalX = -COS_A;
+ const diagonalY = SIN_A * direction;
+ const oddLabel = direction < 0 ? 'ishikawa-label up' : 'ishikawa-label down';
+
+ for (const [i, e] of entries.entries()) {
+ const y = ys[i];
+ const par = bones.get(e.parentIndex)!;
+ const grp = svg.append('g').attr('class', 'ishikawa-sub-group');
+
+ let bx0 = 0;
+ let by0 = 0;
+ let bx1 = 0;
+
+ if (e.depth % 2 === 0) {
+ // Horizontal bone: attach to parent's diagonal at the target Y, extend left
+ const dyP = par.y1 - par.y0;
+ bx0 = lerp(par.x0, par.x1, dyP ? (y - par.y0) / dyP : 0.5);
+ by0 = y;
+ bx1 = bx0 - (e.childCount > 0 ? BONE_BASE + e.childCount * BONE_PER_CHILD : BONE_STUB);
+ drawLine(grp, bx0, y, bx1, y, 'ishikawa-sub-branch', roughContext);
+ if (roughContext) {
+ drawArrowMarker(grp, bx0, y, 1, 0, roughContext);
+ }
+ drawMultilineText(grp, e.text, bx1, y, 'ishikawa-label align', 'end', fontSize);
+ } else {
+ // Diagonal bone: start from evenly-spaced point on parent's horizontal, angle toward target Y
+ const k = par.childrenDrawn++;
+ bx0 = lerp(par.x0, par.x1, (par.childCount - k) / (par.childCount + 1));
+ by0 = par.y0;
+ bx1 = bx0 + diagonalX * ((y - by0) / diagonalY);
+ drawLine(grp, bx0, by0, bx1, y, 'ishikawa-sub-branch', roughContext);
+ if (roughContext) {
+ drawArrowMarker(grp, bx0, by0, bx0 - bx1, by0 - y, roughContext);
+ }
+ drawMultilineText(grp, e.text, bx1, y, oddLabel, 'end', fontSize);
+ }
+
+ if (e.childCount > 0) {
+ bones.set(i, {
+ x0: bx0,
+ y0: by0,
+ x1: bx1,
+ y1: y,
+ childCount: e.childCount,
+ childrenDrawn: 0,
+ });
+ }
+ }
+};
+
+const splitLines = (text: string): string[] => text.split(/
|\n/);
+
+const wrapText = (text: string, maxChars: number): string => {
+ if (text.length <= maxChars) {
+ return text;
+ }
+ const lines: string[] = [];
+ for (const word of text.split(/\s+/)) {
+ const last = lines.length - 1;
+ if (last >= 0 && lines[last].length + 1 + word.length <= maxChars) {
+ lines[last] += ' ' + word;
+ } else {
+ lines.push(word);
+ }
+ }
+ return lines.join('\n');
+};
+
+const drawMultilineText = (
+ g: SVGGroup,
+ text: string,
+ x: number,
+ y: number,
+ cls: string,
+ anchor: 'middle' | 'start' | 'end',
+ fontSize: number
+) => {
+ const lines = splitLines(text);
+ const lh = fontSize * 1.05;
+ const el = g
+ .append('text')
+ .attr('class', cls)
+ .attr('text-anchor', anchor)
+ .attr('x', x)
+ .attr('y', y - ((lines.length - 1) * lh) / 2);
+ for (const [i, line] of lines.entries()) {
+ el.append('tspan')
+ .attr('x', x)
+ .attr('dy', i === 0 ? 0 : lh)
+ .text(line);
+ }
+ return el;
+};
+
+const lerp = (a: number, b: number, t: number) => a + (b - a) * t;
+
+const drawLine = (
+ g: SVGGroup,
+ x1: number,
+ y1: number,
+ x2: number,
+ y2: number,
+ cls: string,
+ roughContext?: RoughContext
+) => {
+ if (roughContext) {
+ const roughNode = roughContext.roughSvg.line(x1, y1, x2, y2, {
+ roughness: 1.5,
+ seed: roughContext.seed,
+ stroke: roughContext.lineColor,
+ strokeWidth: 2,
+ });
+ g.append(() => roughNode).attr('class', cls);
+ return undefined;
+ }
+ return g
+ .append('line')
+ .attr('class', cls)
+ .attr('x1', x1)
+ .attr('y1', y1)
+ .attr('x2', x2)
+ .attr('y2', y2);
+};
+
+export const renderer: DiagramRenderer = { draw };
diff --git a/packages/mermaid/src/diagrams/ishikawa/ishikawaStyles.ts b/packages/mermaid/src/diagrams/ishikawa/ishikawaStyles.ts
new file mode 100644
index 00000000000..0c5a7630bfc
--- /dev/null
+++ b/packages/mermaid/src/diagrams/ishikawa/ishikawaStyles.ts
@@ -0,0 +1,68 @@
+import type { DiagramStylesProvider } from '../../diagram-api/types.js';
+
+const getStyles: DiagramStylesProvider = (options) => `
+.ishikawa .ishikawa-spine,
+.ishikawa .ishikawa-branch,
+.ishikawa .ishikawa-sub-branch {
+ stroke: ${options.lineColor};
+ stroke-width: 2;
+ fill: none;
+}
+
+.ishikawa .ishikawa-sub-branch {
+ stroke-width: 1;
+}
+
+.ishikawa .ishikawa-arrow {
+ fill: ${options.lineColor};
+}
+
+.ishikawa .ishikawa-head {
+ fill: ${options.mainBkg};
+ stroke: ${options.lineColor};
+ stroke-width: 2;
+}
+
+.ishikawa .ishikawa-label-box {
+ fill: ${options.mainBkg};
+ stroke: ${options.lineColor};
+ stroke-width: 2;
+}
+
+.ishikawa text {
+ font-family: ${options.fontFamily};
+ font-size: ${options.fontSize};
+ fill: ${options.textColor};
+}
+
+.ishikawa .ishikawa-head-label {
+ font-weight: 600;
+ text-anchor: middle;
+ dominant-baseline: middle;
+ font-size: 14px;
+}
+
+.ishikawa .ishikawa-label {
+ text-anchor: end;
+}
+
+.ishikawa .ishikawa-label.cause {
+ text-anchor: middle;
+ dominant-baseline: middle;
+}
+
+.ishikawa .ishikawa-label.align {
+ text-anchor: end;
+ dominant-baseline: middle;
+}
+
+.ishikawa .ishikawa-label.up {
+ dominant-baseline: baseline;
+}
+
+.ishikawa .ishikawa-label.down {
+ dominant-baseline: hanging;
+}
+`;
+
+export default getStyles;
diff --git a/packages/mermaid/src/diagrams/ishikawa/ishikawaTypes.ts b/packages/mermaid/src/diagrams/ishikawa/ishikawaTypes.ts
new file mode 100644
index 00000000000..46b9f3406a9
--- /dev/null
+++ b/packages/mermaid/src/diagrams/ishikawa/ishikawaTypes.ts
@@ -0,0 +1,4 @@
+export interface IshikawaNode {
+ text: string;
+ children: IshikawaNode[];
+}
diff --git a/packages/mermaid/src/diagrams/ishikawa/parser/ishikawa.jison b/packages/mermaid/src/diagrams/ishikawa/parser/ishikawa.jison
new file mode 100644
index 00000000000..2e88e131a75
--- /dev/null
+++ b/packages/mermaid/src/diagrams/ishikawa/parser/ishikawa.jison
@@ -0,0 +1,56 @@
+%lex
+%options case-insensitive
+
+%%
+
+\s*\%\%.* { return 'SPACELINE'; }
+"ishikawa-beta" { return 'ISHIKAWA'; }
+"ishikawa" { return 'ISHIKAWA'; }
+[\s]+[\n] { return 'SPACELINE'; }
+[\n]+ { return 'NL'; }
+[\s]+ { return 'SPACELIST'; }
+[^\n]+ { return 'TEXT'; }
+<> return 'EOF';
+
+/lex
+
+%start start
+
+%%
+
+start
+ : ishikawa
+ | spaceLines ishikawa
+ ;
+
+spaceLines
+ : SPACELINE
+ | spaceLines SPACELINE
+ | spaceLines NL
+ ;
+
+ishikawa
+ : ISHIKAWA document { return yy; }
+ | ISHIKAWA NL document { return yy; }
+ ;
+
+stop
+ : NL
+ | EOF
+ | SPACELINE
+ | stop NL
+ | stop EOF
+ ;
+
+document
+ : document statement stop
+ | statement stop
+ ;
+
+statement
+ : SPACELIST TEXT { yy.addNode($1.length, $2.trim()); }
+ | TEXT { yy.addNode(0, $1.trim()); }
+ | SPACELINE
+ | SPACELIST
+ ;
+%%
diff --git a/packages/mermaid/src/docs/syntax/ishikawa.md b/packages/mermaid/src/docs/syntax/ishikawa.md
new file mode 100644
index 00000000000..0cd1eafc7a3
--- /dev/null
+++ b/packages/mermaid/src/docs/syntax/ishikawa.md
@@ -0,0 +1,38 @@
+# Ishikawa diagram (v+)
+
+Ishikawa diagrams are used to represent causes of a specific event (or a problem).
+They are also known as fishbone diagrams, herringbone diagrams or cause-and-effect diagrams.
+The diagram resembles a fish skeleton, with the main problem at the head and the causes branching off from the spine.
+
+```warning
+This is a new diagram type in Mermaid. Its syntax may evolve in future versions.
+```
+
+## Syntax
+
+```mermaid
+ishikawa-beta
+ Blurry Photo
+ Process
+ Out of focus
+ Shutter speed too slow
+ Protective film not removed
+ Beautification filter applied
+ User
+ Shaky hands
+ Equipment
+ LENS
+ Inappropriate lens
+ Damaged lens
+ Dirty lens
+ SENSOR
+ Damaged sensor
+ Dirty sensor
+ Environment
+ Subject moved too quickly
+ Too dark
+```
+
+- The first line is the event (problem) of the diagram.
+- Subsequent lines are causes of the event.
+- "Fishbone" structure is indicated by indentation.
diff --git a/packages/mermaid/src/schemas/config.schema.yaml b/packages/mermaid/src/schemas/config.schema.yaml
index cba4c497f57..8858fc980f9 100644
--- a/packages/mermaid/src/schemas/config.schema.yaml
+++ b/packages/mermaid/src/schemas/config.schema.yaml
@@ -301,6 +301,8 @@ properties:
$ref: '#/$defs/ArchitectureDiagramConfig'
mindmap:
$ref: '#/$defs/MindmapDiagramConfig'
+ ishikawa:
+ $ref: '#/$defs/IshikawaDiagramConfig'
kanban:
$ref: '#/$defs/KanbanDiagramConfig'
gitGraph:
@@ -997,6 +999,23 @@ $defs: # JSON Schema definition (maybe we should move these to a separate file)
type: string
default: 'cose-bilkent'
+ IshikawaDiagramConfig:
+ title: Ishikawa Diagram Config
+ allOf: [{ $ref: '#/$defs/BaseDiagramConfig' }]
+ description: The object containing configurations specific for ishikawa diagrams
+ type: object
+ properties:
+ diagramPadding:
+ description: |
+ The amount of padding around the diagram as a whole so that embedded
+ diagrams have margins, expressed in pixels.
+ type: integer
+ default: 20
+ minimum: 0
+ useMaxWidth:
+ type: boolean
+ default: false
+
KanbanDiagramConfig:
title: Kanban Diagram Config
allOf: [{ $ref: '#/$defs/BaseDiagramConfig' }]