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 @@

Journey

  • Mindmap

  • +
  • +

    Ishikawa

    +
  • Pie

  • 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' }]