diff --git a/__mocks__/pieRenderer.js b/__mocks__/pieRenderer.js deleted file mode 100644 index 317c69901d..0000000000 --- a/__mocks__/pieRenderer.js +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Mocked pie (picChart) diagram renderer - */ - -import { vi } from 'vitest'; - -export const draw = vi.fn().mockImplementation(() => { - return ''; -}); - -export default { - draw, -}; diff --git a/__mocks__/pieRenderer.ts b/__mocks__/pieRenderer.ts new file mode 100644 index 0000000000..439800f8c5 --- /dev/null +++ b/__mocks__/pieRenderer.ts @@ -0,0 +1,8 @@ +/** + * Mocked pie (picChart) diagram renderer + */ +import { vi } from 'vitest'; + +const draw = vi.fn().mockImplementation(() => ''); + +export const renderer = { draw }; diff --git a/cypress/integration/rendering/pie.spec.js b/cypress/integration/rendering/pie.spec.ts similarity index 54% rename from cypress/integration/rendering/pie.spec.js rename to cypress/integration/rendering/pie.spec.ts index 01b2484868..269efafb26 100644 --- a/cypress/integration/rendering/pie.spec.js +++ b/cypress/integration/rendering/pie.spec.ts @@ -1,89 +1,85 @@ import { imgSnapshotTest, renderGraph } from '../../helpers/util.ts'; -describe('Pie Chart', () => { +describe('pie chart', () => { it('should render a simple pie diagram', () => { imgSnapshotTest( + `pie title Sports in Sweden + "Bandy": 40 + "Ice-Hockey": 80 + "Football": 90 ` - pie title Sports in Sweden - "Bandy" : 40 - "Ice-Hockey" : 80 - "Football" : 90 - `, - {} ); - cy.get('svg'); }); + it('should render a simple pie diagram with long labels', () => { imgSnapshotTest( + `pie title NETFLIX + "Time spent looking for movie": 90 + "Time spent watching it": 10 ` - pie title NETFLIX - "Time spent looking for movie" : 90 - "Time spent watching it" : 10 - `, - {} ); - cy.get('svg'); }); + it('should render a simple pie diagram with capital letters for labels', () => { imgSnapshotTest( + `pie title What Voldemort doesn't have? + "FRIENDS": 2 + "FAMILY": 3 + "NOSE": 45 ` - pie title What Voldemort doesn't have? - "FRIENDS" : 2 - "FAMILY" : 3 - "NOSE" : 45 - `, - {} ); - cy.get('svg'); }); + it('should render a pie diagram when useMaxWidth is true (default)', () => { renderGraph( - ` - pie title Sports in Sweden - "Bandy" : 40 - "Ice-Hockey" : 80 - "Football" : 90 + `pie title Sports in Sweden + "Bandy": 40 + "Ice-Hockey": 80 + "Football": 90 `, { pie: { useMaxWidth: true } } ); cy.get('svg').should((svg) => { expect(svg).to.have.attr('width', '100%'); - // expect(svg).to.have.attr('height'); - // const height = parseFloat(svg.attr('height')); - // expect(height).to.eq(450); const style = svg.attr('style'); expect(style).to.match(/^max-width: [\d.]+px;$/); const maxWidthValue = parseFloat(style.match(/[\d.]+/g).join('')); expect(maxWidthValue).to.eq(984); }); }); + it('should render a pie diagram when useMaxWidth is false', () => { renderGraph( - ` - pie title Sports in Sweden - "Bandy" : 40 - "Ice-Hockey" : 80 - "Football" : 90 + `pie title Sports in Sweden + "Bandy": 40 + "Ice-Hockey": 80 + "Football": 90 `, { pie: { useMaxWidth: false } } ); cy.get('svg').should((svg) => { - // const height = parseFloat(svg.attr('height')); const width = parseFloat(svg.attr('width')); - // expect(height).to.eq(450); expect(width).to.eq(984); expect(svg).to.not.have.attr('style'); }); }); - it('should render a pie diagram when textPosition is set', () => { + + it('should render a pie diagram when textPosition is setted', () => { imgSnapshotTest( - ` - pie - "Dogs": 50 - "Cats": 25 - `, + `pie + "Dogs": 50 + "Cats": 25 + `, { logLevel: 1, pie: { textPosition: 0.9 } } ); - cy.get('svg'); + }); + + it('should render a pie diagram with showData', () => { + imgSnapshotTest( + `pie showData + "Dogs": 50 + "Cats": 25 + ` + ); }); }); diff --git a/demos/pie.html b/demos/pie.html index 031d14a853..3c315ab63e 100644 --- a/demos/pie.html +++ b/demos/pie.html @@ -7,7 +7,6 @@ @@ -17,37 +16,32 @@

Pie chart demos

       pie title Pets adopted by volunteers
-      accTitle: simple pie char demo
-      accDescr: pie chart with 3 sections: dogs, cats, rats. Most are dogs.
-    "Dogs" : 386
-    "Cats" : 85
-    "Rats" : 15
+        accTitle: simple pie char demo
+        accDescr: pie chart with 3 sections: dogs, cats, rats. Most are dogs.
+        "Dogs": 386
+        "Cats": 85
+        "Rats": 15
     

-    %%{init: {"pie": {"textPosition": 0.9}, "themeVariables": {"pieOuterStrokeWidth": "5px"}} }%%
-    pie
-      title Key elements in Product X
+      %%{init: {"pie": {"textPosition": 0.9}, "themeVariables": {"pieOuterStrokeWidth": "5px"}}}%%
+      pie
+        title Key elements in Product X
         accTitle: Key elements in Product X
-      accDescr: This is a pie chart showing the key elements in Product X.
-      "Calcium" : 42.96
-      "Potassium" : 50.05
-      "Magnesium" : 10.01
-      "Iron" :  5
+        accDescr: This is a pie chart showing the key elements in Product X.
+        "Calcium": 42.96
+        "Potassium": 50.05
+        "Magnesium": 10.01
+        "Iron": 5
     
diff --git a/docs/config/setup/modules/defaultConfig.md b/docs/config/setup/modules/defaultConfig.md index a55ec18085..2d23977fed 100644 --- a/docs/config/setup/modules/defaultConfig.md +++ b/docs/config/setup/modules/defaultConfig.md @@ -14,13 +14,13 @@ #### Defined in -[defaultConfig.ts:266](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/defaultConfig.ts#L266) +[defaultConfig.ts:268](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/defaultConfig.ts#L268) --- ### default -• `Const` **default**: `Partial`<`MermaidConfig`> +• `Const` **default**: `RequiredDeep`<`MermaidConfig`> Default mermaid configuration options. @@ -30,4 +30,4 @@ Non-JSON JS default values are listed in this file, e.g. functions, or #### Defined in -[defaultConfig.ts:16](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/defaultConfig.ts#L16) +[defaultConfig.ts:18](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/defaultConfig.ts#L18) diff --git a/packages/mermaid/package.json b/packages/mermaid/package.json index a32bcb257a..d9eb8701f7 100644 --- a/packages/mermaid/package.json +++ b/packages/mermaid/package.json @@ -84,7 +84,9 @@ "@types/cytoscape": "^3.19.9", "@types/d3": "^7.4.0", "@types/d3-sankey": "^0.12.1", + "@types/d3-scale": "^4.0.3", "@types/d3-selection": "^3.0.5", + "@types/d3-shape": "^3.1.1", "@types/dompurify": "^3.0.2", "@types/jsdom": "^21.1.1", "@types/lodash-es": "^4.17.7", @@ -113,6 +115,7 @@ "remark-gfm": "^3.0.1", "rimraf": "^5.0.0", "start-server-and-test": "^2.0.0", + "type-fest": "^4.1.0", "typedoc": "^0.24.5", "typedoc-plugin-markdown": "^3.15.2", "typescript": "^5.0.4", diff --git a/packages/mermaid/src/defaultConfig.ts b/packages/mermaid/src/defaultConfig.ts index 62b361cff4..0b232116da 100644 --- a/packages/mermaid/src/defaultConfig.ts +++ b/packages/mermaid/src/defaultConfig.ts @@ -1,5 +1,7 @@ +import type { RequiredDeep } from 'type-fest'; + import theme from './themes/index.js'; -import { type MermaidConfig } from './config.type.js'; +import type { MermaidConfig } from './config.type.js'; // Uses our custom Vite jsonSchemaPlugin to load only the default values from // our JSON Schema @@ -13,7 +15,7 @@ import defaultConfigJson from './schemas/config.schema.yaml?only-defaults=true'; * Non-JSON JS default values are listed in this file, e.g. functions, or * `undefined` (explicitly set so that `configKeys` finds them). */ -const config: Partial = { +const config: RequiredDeep = { ...defaultConfigJson, // Set, even though they're `undefined` so that `configKeys` finds these keys // TODO: Should we replace these with `null` so that they can go in the JSON Schema? @@ -232,7 +234,7 @@ const config: Partial = { }, pie: { ...defaultConfigJson.pie, - useWidth: undefined, + useWidth: 984, }, requirement: { ...defaultConfigJson.requirement, diff --git a/packages/mermaid/src/diagram-api/diagram-orchestration.ts b/packages/mermaid/src/diagram-api/diagram-orchestration.ts index 9c03e27f31..80665cfa2e 100644 --- a/packages/mermaid/src/diagram-api/diagram-orchestration.ts +++ b/packages/mermaid/src/diagram-api/diagram-orchestration.ts @@ -5,7 +5,7 @@ import er from '../diagrams/er/erDetector.js'; import git from '../diagrams/git/gitGraphDetector.js'; import gantt from '../diagrams/gantt/ganttDetector.js'; import { info } from '../diagrams/info/infoDetector.js'; -import pie from '../diagrams/pie/pieDetector.js'; +import { pie } from '../diagrams/pie/pieDetector.js'; import quadrantChart from '../diagrams/quadrant-chart/quadrantDetector.js'; import requirement from '../diagrams/requirement/requirementDetector.js'; import sequence from '../diagrams/sequence/sequenceDetector.js'; diff --git a/packages/mermaid/src/diagram-api/types.ts b/packages/mermaid/src/diagram-api/types.ts index 100b92e871..4d30fe97dd 100644 --- a/packages/mermaid/src/diagram-api/types.ts +++ b/packages/mermaid/src/diagram-api/types.ts @@ -1,5 +1,5 @@ import { Diagram } from '../Diagram.js'; -import type { MermaidConfig } from '../config.type.js'; +import type { BaseDiagramConfig, MermaidConfig } from '../config.type.js'; import type * as d3 from 'd3'; export interface InjectUtils { @@ -16,11 +16,19 @@ export interface InjectUtils { * Generic Diagram DB that may apply to any diagram type. */ export interface DiagramDB { + // config + getConfig?: () => BaseDiagramConfig | undefined; + + // db clear?: () => void; setDiagramTitle?: (title: string) => void; - setDisplayMode?: (title: string) => void; + getDiagramTitle?: () => string; + setAccTitle?: (title: string) => void; getAccTitle?: () => string; + setAccDescription?: (describetion: string) => void; getAccDescription?: () => string; + + setDisplayMode?: (title: string) => void; bindFunctions?: (element: Element) => void; } diff --git a/packages/mermaid/src/diagrams/pie/amonts.csv b/packages/mermaid/src/diagrams/pie/amonts.csv deleted file mode 100644 index 25cf919ddb..0000000000 --- a/packages/mermaid/src/diagrams/pie/amonts.csv +++ /dev/null @@ -1,10 +0,0 @@ -name,amounts -Foo, 33 -Rishab, 12 -Alexis, 41 -Tom, 16 -Courtney, 59 -Christina, 38 -Jack, 21 -Mickey, 25 -Paul, 30 diff --git a/packages/mermaid/src/diagrams/pie/parser/pie.spec.js b/packages/mermaid/src/diagrams/pie/parser/pie.spec.js deleted file mode 100644 index 5e5c0b4f51..0000000000 --- a/packages/mermaid/src/diagrams/pie/parser/pie.spec.js +++ /dev/null @@ -1,132 +0,0 @@ -import pieDb from '../pieDb.js'; -import pie from './pie.jison'; -import { setConfig } from '../../../config.js'; - -setConfig({ - securityLevel: 'strict', -}); - -describe('when parsing pie', function () { - beforeEach(function () { - pie.parser.yy = pieDb; - pie.parser.yy.clear(); - }); - it('should handle very simple pie', function () { - const res = pie.parser.parse(`pie -"ash" : 100 -`); - const sections = pieDb.getSections(); - const section1 = sections['ash']; - expect(section1).toBe(100); - }); - it('should handle simple pie', function () { - const res = pie.parser.parse(`pie -"ash" : 60 -"bat" : 40 -`); - const sections = pieDb.getSections(); - const section1 = sections['ash']; - expect(section1).toBe(60); - }); - it('should handle simple pie with comments', function () { - const res = pie.parser.parse(`pie - %% comments -"ash" : 60 -"bat" : 40 -`); - const sections = pieDb.getSections(); - const section1 = sections['ash']; - expect(section1).toBe(60); - }); - - it('should handle simple pie with a directive', function () { - const res = pie.parser.parse(`%%{init: {'logLevel':0}}%% -pie -"ash" : 60 -"bat" : 40 -`); - const sections = pieDb.getSections(); - const section1 = sections['ash']; - expect(section1).toBe(60); - }); - - it('should handle simple pie with a title', function () { - const res = pie.parser.parse(`pie title a 60/40 pie -"ash" : 60 -"bat" : 40 -`); - const sections = pieDb.getSections(); - const title = pieDb.getDiagramTitle(); - const section1 = sections['ash']; - expect(section1).toBe(60); - expect(title).toBe('a 60/40 pie'); - }); - - it('should handle simple pie without an acc description (accDescr)', function () { - const res = pie.parser.parse(`pie title a neat chart -"ash" : 60 -"bat" : 40 -`); - - const sections = pieDb.getSections(); - const title = pieDb.getDiagramTitle(); - const description = pieDb.getAccDescription(); - const section1 = sections['ash']; - expect(section1).toBe(60); - expect(title).toBe('a neat chart'); - expect(description).toBe(''); - }); - - it('should handle simple pie with an acc description (accDescr)', function () { - const res = pie.parser.parse(`pie title a neat chart - accDescr: a neat description -"ash" : 60 -"bat" : 40 -`); - - const sections = pieDb.getSections(); - const title = pieDb.getDiagramTitle(); - const description = pieDb.getAccDescription(); - const section1 = sections['ash']; - expect(section1).toBe(60); - expect(title).toBe('a neat chart'); - expect(description).toBe('a neat description'); - }); - it('should handle simple pie with a multiline acc description (accDescr)', function () { - const res = pie.parser.parse(`pie title a neat chart - accDescr { - a neat description - on multiple lines - } -"ash" : 60 -"bat" : 40 -`); - - const sections = pieDb.getSections(); - const title = pieDb.getDiagramTitle(); - const description = pieDb.getAccDescription(); - const section1 = sections['ash']; - expect(section1).toBe(60); - expect(title).toBe('a neat chart'); - expect(description).toBe('a neat description\non multiple lines'); - }); - - it('should handle simple pie with positive decimal', function () { - const res = pie.parser.parse(`pie -"ash" : 60.67 -"bat" : 40 -`); - const sections = pieDb.getSections(); - const section1 = sections['ash']; - expect(section1).toBe(60.67); - }); - - it('should handle simple pie with negative decimal', function () { - expect(() => { - pie.parser.parse(`pie -"ash" : 60.67 -"bat" : 40..12 -`); - }).toThrowError(); - }); -}); diff --git a/packages/mermaid/src/diagrams/pie/pie.spec.ts b/packages/mermaid/src/diagrams/pie/pie.spec.ts new file mode 100644 index 0000000000..7c8e0809a3 --- /dev/null +++ b/packages/mermaid/src/diagrams/pie/pie.spec.ts @@ -0,0 +1,180 @@ +// @ts-ignore: JISON doesn't support types +import { parser } from './parser/pie.jison'; +import { DEFAULT_PIE_DB, db } from './pieDb.js'; +import { setConfig } from '../../config.js'; + +setConfig({ + securityLevel: 'strict', +}); + +describe('pie', () => { + beforeAll(() => { + parser.yy = db; + }); + + beforeEach(() => { + parser.yy.clear(); + }); + + describe('parse', () => { + it('should handle very simple pie', () => { + parser.parse(`pie + "ash": 100 + `); + + const sections = db.getSections(); + expect(sections['ash']).toBe(100); + }); + + it('should handle simple pie', () => { + parser.parse(`pie + "ash" : 60 + "bat" : 40 + `); + + const sections = db.getSections(); + expect(sections['ash']).toBe(60); + expect(sections['bat']).toBe(40); + }); + + it('should handle simple pie with showData', () => { + parser.parse(`pie showData + "ash" : 60 + "bat" : 40 + `); + + expect(db.getShowData()).toBeTruthy(); + + const sections = db.getSections(); + expect(sections['ash']).toBe(60); + expect(sections['bat']).toBe(40); + }); + + it('should handle simple pie with comments', () => { + parser.parse(`pie + %% comments + "ash" : 60 + "bat" : 40 + `); + + const sections = db.getSections(); + expect(sections['ash']).toBe(60); + expect(sections['bat']).toBe(40); + }); + + it('should handle simple pie with a directive', () => { + parser.parse(`%%{init: {'logLevel':0}}%% + pie + "ash" : 60 + "bat" : 40 + `); + const sections = db.getSections(); + expect(sections['ash']).toBe(60); + expect(sections['bat']).toBe(40); + }); + + it('should handle simple pie with a title', () => { + parser.parse(`pie title a 60/40 pie + "ash" : 60 + "bat" : 40 + `); + + expect(db.getDiagramTitle()).toBe('a 60/40 pie'); + + const sections = db.getSections(); + expect(sections['ash']).toBe(60); + expect(sections['bat']).toBe(40); + }); + + it('should handle simple pie with an acc title (accTitle)', () => { + parser.parse(`pie title a neat chart + accTitle: a neat acc title + "ash" : 60 + "bat" : 40 + `); + + expect(db.getDiagramTitle()).toBe('a neat chart'); + + expect(db.getAccTitle()).toBe('a neat acc title'); + + const sections = db.getSections(); + expect(sections['ash']).toBe(60); + expect(sections['bat']).toBe(40); + }); + + it('should handle simple pie with an acc description (accDescr)', () => { + parser.parse(`pie title a neat chart + accDescr: a neat description + "ash" : 60 + "bat" : 40 + `); + + expect(db.getDiagramTitle()).toBe('a neat chart'); + + expect(db.getAccDescription()).toBe('a neat description'); + + const sections = db.getSections(); + expect(sections['ash']).toBe(60); + expect(sections['bat']).toBe(40); + }); + + it('should handle simple pie with a multiline acc description (accDescr)', () => { + parser.parse(`pie title a neat chart + accDescr { + a neat description + on multiple lines + } + "ash" : 60 + "bat" : 40 + `); + + expect(db.getDiagramTitle()).toBe('a neat chart'); + + expect(db.getAccDescription()).toBe('a neat description\non multiple lines'); + + const sections = db.getSections(); + expect(sections['ash']).toBe(60); + expect(sections['bat']).toBe(40); + }); + + it('should handle simple pie with positive decimal', () => { + parser.parse(`pie + "ash" : 60.67 + "bat" : 40 + `); + + const sections = db.getSections(); + expect(sections['ash']).toBe(60.67); + expect(sections['bat']).toBe(40); + }); + + it('should handle simple pie with negative decimal', () => { + expect(() => { + parser.parse(`pie + "ash" : -60.67 + "bat" : 40.12 + `); + }).toThrowError(); + }); + }); + + describe('config', () => { + it.todo('setConfig', () => { + // db.setConfig({ useWidth: 850, useMaxWidth: undefined }); + + const config = db.getConfig(); + expect(config.useWidth).toBe(850); + expect(config.useMaxWidth).toBeTruthy(); + }); + + it('getConfig', () => { + expect(db.getConfig()).toStrictEqual(DEFAULT_PIE_DB.config); + }); + + it.todo('resetConfig', () => { + // db.setConfig({ textPosition: 0 }); + // db.resetConfig(); + expect(db.getConfig().textPosition).toStrictEqual(DEFAULT_PIE_DB.config.textPosition); + }); + }); +}); diff --git a/packages/mermaid/src/diagrams/pie/pieDb.js b/packages/mermaid/src/diagrams/pie/pieDb.js deleted file mode 100644 index 2c86752c66..0000000000 --- a/packages/mermaid/src/diagrams/pie/pieDb.js +++ /dev/null @@ -1,69 +0,0 @@ -import { log } from '../../logger.js'; -import mermaidAPI from '../../mermaidAPI.js'; -import * as configApi from '../../config.js'; -import common from '../common/common.js'; -import { - setAccTitle, - getAccTitle, - setDiagramTitle, - getDiagramTitle, - getAccDescription, - setAccDescription, - clear as commonClear, -} from '../../commonDb.js'; - -let sections = {}; -let showData = false; - -export const parseDirective = function (statement, context, type) { - mermaidAPI.parseDirective(this, statement, context, type); -}; - -const addSection = function (id, value) { - id = common.sanitizeText(id, configApi.getConfig()); - if (sections[id] === undefined) { - sections[id] = value; - log.debug('Added new section :', id); - } -}; -const getSections = () => sections; - -const setShowData = function (toggle) { - showData = toggle; -}; - -const getShowData = function () { - return showData; -}; - -const cleanupValue = function (value) { - if (value.substring(0, 1) === ':') { - value = value.substring(1).trim(); - return Number(value.trim()); - } else { - return Number(value.trim()); - } -}; - -const clear = function () { - sections = {}; - showData = false; - commonClear(); -}; - -export default { - parseDirective, - getConfig: () => configApi.getConfig().pie, - addSection, - getSections, - cleanupValue, - clear, - setAccTitle, - getAccTitle, - setDiagramTitle, - getDiagramTitle, - setShowData, - getShowData, - getAccDescription, - setAccDescription, -}; diff --git a/packages/mermaid/src/diagrams/pie/pieDb.ts b/packages/mermaid/src/diagrams/pie/pieDb.ts new file mode 100644 index 0000000000..dbe50f08a0 --- /dev/null +++ b/packages/mermaid/src/diagrams/pie/pieDb.ts @@ -0,0 +1,84 @@ +import { log } from '../../logger.js'; +import { parseDirective as _parseDirective } from '../../directiveUtils.js'; +import { getConfig as commonGetConfig } from '../../config.js'; +import { sanitizeText } from '../common/common.js'; +import { + setAccTitle, + getAccTitle, + setDiagramTitle, + getDiagramTitle, + getAccDescription, + setAccDescription, + clear as commonClear, +} from '../../commonDb.js'; +import type { ParseDirectiveDefinition } from '../../diagram-api/types.js'; +import type { PieFields, PieDB, Sections } from './pieTypes.js'; +import type { RequiredDeep } from 'type-fest'; +import type { PieDiagramConfig } from '../../config.type.js'; +import DEFAULT_CONFIG from '../../defaultConfig.js'; + +export const DEFAULT_PIE_CONFIG: Required = DEFAULT_CONFIG.pie; + +export const DEFAULT_PIE_DB: RequiredDeep = { + sections: {}, + showData: false, + config: DEFAULT_PIE_CONFIG, +} as const; + +let sections: Sections = DEFAULT_PIE_DB.sections; +let showData: boolean = DEFAULT_PIE_DB.showData; +const config: Required = structuredClone(DEFAULT_PIE_CONFIG); + +const getConfig = (): Required => structuredClone(config); + +const parseDirective: ParseDirectiveDefinition = (statement, context, type) => { + _parseDirective(this, statement, context, type); +}; + +const clear = (): void => { + sections = structuredClone(DEFAULT_PIE_DB.sections); + showData = DEFAULT_PIE_DB.showData; + commonClear(); +}; + +const addSection = (label: string, value: number): void => { + label = sanitizeText(label, commonGetConfig()); + if (sections[label] === undefined) { + sections[label] = value; + log.debug(`added new section: ${label}, with value: ${value}`); + } +}; + +const getSections = (): Sections => sections; + +const cleanupValue = (value: string): number => { + if (value.substring(0, 1) === ':') { + value = value.substring(1).trim(); + } + return Number(value.trim()); +}; + +const setShowData = (toggle: boolean): void => { + showData = toggle; +}; + +const getShowData = (): boolean => showData; + +export const db: PieDB = { + getConfig, + + parseDirective, + clear, + setDiagramTitle, + getDiagramTitle, + setAccTitle, + getAccTitle, + setAccDescription, + getAccDescription, + + addSection, + getSections, + cleanupValue, + setShowData, + getShowData, +}; diff --git a/packages/mermaid/src/diagrams/pie/pieDetector.ts b/packages/mermaid/src/diagrams/pie/pieDetector.ts index 93eded52cd..f5acd1aa03 100644 --- a/packages/mermaid/src/diagrams/pie/pieDetector.ts +++ b/packages/mermaid/src/diagrams/pie/pieDetector.ts @@ -15,10 +15,8 @@ const loader: DiagramLoader = async () => { return { id, diagram }; }; -const plugin: ExternalDiagramDefinition = { +export const pie: ExternalDiagramDefinition = { id, detector, loader, }; - -export default plugin; diff --git a/packages/mermaid/src/diagrams/pie/pieDiagram.ts b/packages/mermaid/src/diagrams/pie/pieDiagram.ts index 21756dd4e8..f0aa19b419 100644 --- a/packages/mermaid/src/diagrams/pie/pieDiagram.ts +++ b/packages/mermaid/src/diagrams/pie/pieDiagram.ts @@ -1,9 +1,9 @@ -import { DiagramDefinition } from '../../diagram-api/types.js'; +import type { DiagramDefinition } from '../../diagram-api/types.js'; // @ts-ignore: JISON doesn't support types import parser from './parser/pie.jison'; -import db from './pieDb.js'; -import styles from './styles.js'; -import renderer from './pieRenderer.js'; +import { db } from './pieDb.js'; +import styles from './pieStyles.js'; +import { renderer } from './pieRenderer.js'; export const diagram: DiagramDefinition = { parser, diff --git a/packages/mermaid/src/diagrams/pie/pieRenderer.js b/packages/mermaid/src/diagrams/pie/pieRenderer.js deleted file mode 100644 index d4242c087a..0000000000 --- a/packages/mermaid/src/diagrams/pie/pieRenderer.js +++ /dev/null @@ -1,204 +0,0 @@ -/** Created by AshishJ on 11-09-2019. */ -import { select, scaleOrdinal, pie as d3pie, arc } from 'd3'; -import { log } from '../../logger.js'; -import { configureSvgSize } from '../../setupGraphViewbox.js'; -import * as configApi from '../../config.js'; -import { parseFontSize } from '../../utils.js'; - -let conf = configApi.getConfig(); - -/** - * Draws a Pie Chart with the data given in text. - * - * @param text - * @param id - */ -let width; -const height = 450; -export const draw = (txt, id, _version, diagObj) => { - try { - conf = configApi.getConfig(); - log.debug('Rendering info diagram\n' + txt); - - const securityLevel = configApi.getConfig().securityLevel; - // Handle root and Document for when rendering in sandbox mode - let sandboxElement; - if (securityLevel === 'sandbox') { - sandboxElement = select('#i' + id); - } - const root = - securityLevel === 'sandbox' - ? select(sandboxElement.nodes()[0].contentDocument.body) - : select('body'); - const doc = securityLevel === 'sandbox' ? sandboxElement.nodes()[0].contentDocument : document; - - // Parse the Pie Chart definition - const elem = doc.getElementById(id); - width = elem.parentElement.offsetWidth; - - if (width === undefined) { - width = 1200; - } - - if (conf.useWidth !== undefined) { - width = conf.useWidth; - } - if (conf.pie.useWidth !== undefined) { - width = conf.pie.useWidth; - } - - const diagram = root.select('#' + id); - configureSvgSize(diagram, height, width, conf.pie.useMaxWidth); - - // Set viewBox - elem.setAttribute('viewBox', '0 0 ' + width + ' ' + height); - - // Fetch the default direction, use TD if none was found - var margin = 40; - var legendRectSize = 18; - var legendSpacing = 4; - - var radius = Math.min(width, height) / 2 - margin; - - var svg = diagram - .append('g') - .attr('transform', 'translate(' + width / 2 + ',' + height / 2 + ')'); - - var data = diagObj.db.getSections(); - var sum = 0; - Object.keys(data).forEach(function (key) { - sum += data[key]; - }); - - const themeVariables = conf.themeVariables; - var myGeneratedColors = [ - themeVariables.pie1, - themeVariables.pie2, - themeVariables.pie3, - themeVariables.pie4, - themeVariables.pie5, - themeVariables.pie6, - themeVariables.pie7, - themeVariables.pie8, - themeVariables.pie9, - themeVariables.pie10, - themeVariables.pie11, - themeVariables.pie12, - ]; - - const textPosition = conf.pie?.textPosition ?? 0.75; - let [outerStrokeWidth] = parseFontSize(themeVariables.pieOuterStrokeWidth); - outerStrokeWidth ??= 2; - - // Set the color scale - var color = scaleOrdinal().range(myGeneratedColors); - - // Compute the position of each group on the pie: - var pieData = Object.entries(data).map(function (el, idx) { - return { - order: idx, - name: el[0], - value: el[1], - }; - }); - var pie = d3pie() - .value(function (d) { - return d.value; - }) - .sort(function (a, b) { - // Sort slices in clockwise direction - return a.order - b.order; - }); - var dataReady = pie(pieData); - - // Shape helper to build arcs: - var arcGenerator = arc().innerRadius(0).outerRadius(radius); - var labelArcGenerator = arc() - .innerRadius(radius * textPosition) - .outerRadius(radius * textPosition); - - svg - .append('circle') - .attr('cx', 0) - .attr('cy', 0) - .attr('r', radius + outerStrokeWidth / 2) - .attr('class', 'pieOuterCircle'); - - // Build the pie chart: each part of the pie is a path that we build using the arc function. - svg - .selectAll('mySlices') - .data(dataReady) - .enter() - .append('path') - .attr('d', arcGenerator) - .attr('fill', function (d) { - return color(d.data.name); - }) - .attr('class', 'pieCircle'); - - // Now add the percentage. - // Use the centroid method to get the best coordinates. - svg - .selectAll('mySlices') - .data(dataReady) - .enter() - .append('text') - .text(function (d) { - return ((d.data.value / sum) * 100).toFixed(0) + '%'; - }) - .attr('transform', function (d) { - return 'translate(' + labelArcGenerator.centroid(d) + ')'; - }) - .style('text-anchor', 'middle') - .attr('class', 'slice'); - - svg - .append('text') - .text(diagObj.db.getDiagramTitle()) - .attr('x', 0) - .attr('y', -(height - 50) / 2) - .attr('class', 'pieTitleText'); - - // Add the legends/annotations for each section - var legend = svg - .selectAll('.legend') - .data(color.domain()) - .enter() - .append('g') - .attr('class', 'legend') - .attr('transform', function (d, i) { - const height = legendRectSize + legendSpacing; - const offset = (height * color.domain().length) / 2; - const horizontal = 12 * legendRectSize; - const vertical = i * height - offset; - return 'translate(' + horizontal + ',' + vertical + ')'; - }); - - legend - .append('rect') - .attr('width', legendRectSize) - .attr('height', legendRectSize) - .style('fill', color) - .style('stroke', color); - - legend - .data(dataReady) - .append('text') - .attr('x', legendRectSize + legendSpacing) - .attr('y', legendRectSize - legendSpacing) - .text(function (d) { - if (diagObj.db.getShowData() || conf.showData || conf.pie.showData) { - return d.data.name + ' [' + d.data.value + ']'; - } else { - return d.data.name; - } - }); - } catch (e) { - log.error('Error while rendering info diagram'); - log.error(e); - } -}; - -export default { - draw, -}; diff --git a/packages/mermaid/src/diagrams/pie/pieRenderer.ts b/packages/mermaid/src/diagrams/pie/pieRenderer.ts new file mode 100644 index 0000000000..57dd4002bd --- /dev/null +++ b/packages/mermaid/src/diagrams/pie/pieRenderer.ts @@ -0,0 +1,179 @@ +import d3, { scaleOrdinal, pie as d3pie, arc } from 'd3'; + +import { log } from '../../logger.js'; +import { configureSvgSize } from '../../setupGraphViewbox.js'; +import { getConfig } from '../../config.js'; +import { cleanAndMerge, parseFontSize } from '../../utils.js'; +import type { DrawDefinition, Group, SVG } from '../../diagram-api/types.js'; +import type { D3Sections, PieDB, Sections } from './pieTypes.js'; +import type { MermaidConfig, PieDiagramConfig } from '../../config.type.js'; +import { selectSvgElement } from '../../rendering-util/selectSvgElement.js'; + +const createPieArcs = (sections: Sections): d3.PieArcDatum[] => { + // Compute the position of each group on the pie: + const pieData: D3Sections[] = Object.entries(sections).map( + (element: [string, number]): D3Sections => { + return { + label: element[0], + value: element[1], + }; + } + ); + const pie: d3.Pie = d3pie().value( + (d3Section: D3Sections): number => d3Section.value + ); + return pie(pieData); +}; + +/** + * Draws a Pie Chart with the data given in text. + * + * @param text - pie chart code + * @param id - diagram id + * @param _version - MermaidJS version from package.json. + * @param diagObj - A standard diagram containing the DB and the text and type etc of the diagram. + */ +export const draw: DrawDefinition = (text, id, _version, diagObj) => { + log.debug('rendering pie chart\n' + text); + + const db = diagObj.db as PieDB; + const globalConfig: MermaidConfig = getConfig(); + const pieConfig: Required = cleanAndMerge(db.getConfig(), globalConfig.pie); + + const height = 450; + // TODO: remove document width + const width: number = + document.getElementById(id)?.parentElement?.offsetWidth ?? pieConfig.useWidth; + const svg: SVG = selectSvgElement(id); + // Set viewBox + svg.attr('viewBox', `0 0 ${width} ${height}`); + configureSvgSize(svg, height, width, pieConfig.useMaxWidth); + + const MARGIN = 40; + const LEGEND_RECT_SIZE = 18; + const LEGEND_SPACING = 4; + + const group: Group = svg.append('g'); + group.attr('transform', 'translate(' + width / 2 + ',' + height / 2 + ')'); + + const { themeVariables } = globalConfig; + let [outerStrokeWidth] = parseFontSize(themeVariables.pieOuterStrokeWidth); + outerStrokeWidth ??= 2; + + const textPosition: number = pieConfig.textPosition; + const radius: number = Math.min(width, height) / 2 - MARGIN; + // Shape helper to build arcs: + const arcGenerator: d3.Arc> = arc< + d3.PieArcDatum + >() + .innerRadius(0) + .outerRadius(radius); + const labelArcGenerator: d3.Arc> = arc< + d3.PieArcDatum + >() + .innerRadius(radius * textPosition) + .outerRadius(radius * textPosition); + + group + .append('circle') + .attr('cx', 0) + .attr('cy', 0) + .attr('r', radius + outerStrokeWidth / 2) + .attr('class', 'pieOuterCircle'); + + const sections: Sections = db.getSections(); + const arcs: d3.PieArcDatum[] = createPieArcs(sections); + + const myGeneratedColors = [ + themeVariables.pie1, + themeVariables.pie2, + themeVariables.pie3, + themeVariables.pie4, + themeVariables.pie5, + themeVariables.pie6, + themeVariables.pie7, + themeVariables.pie8, + themeVariables.pie9, + themeVariables.pie10, + themeVariables.pie11, + themeVariables.pie12, + ]; + // Set the color scale + const color: d3.ScaleOrdinal = scaleOrdinal(myGeneratedColors); + + // Build the pie chart: each part of the pie is a path that we build using the arc function. + group + .selectAll('mySlices') + .data(arcs) + .enter() + .append('path') + .attr('d', arcGenerator) + .attr('fill', (datum: d3.PieArcDatum) => { + return color(datum.data.label); + }) + .attr('class', 'pieCircle'); + + let sum = 0; + Object.keys(sections).forEach((key: string): void => { + sum += sections[key]; + }); + // Now add the percentage. + // Use the centroid method to get the best coordinates. + group + .selectAll('mySlices') + .data(arcs) + .enter() + .append('text') + .text((datum: d3.PieArcDatum): string => { + return ((datum.data.value / sum) * 100).toFixed(0) + '%'; + }) + .attr('transform', (datum: d3.PieArcDatum): string => { + return 'translate(' + labelArcGenerator.centroid(datum) + ')'; + }) + .style('text-anchor', 'middle') + .attr('class', 'slice'); + + group + .append('text') + .text(db.getDiagramTitle()) + .attr('x', 0) + .attr('y', -(height - 50) / 2) + .attr('class', 'pieTitleText'); + + // Add the legends/annotations for each section + const legend = group + .selectAll('.legend') + .data(color.domain()) + .enter() + .append('g') + .attr('class', 'legend') + .attr('transform', (_datum, index: number): string => { + const height = LEGEND_RECT_SIZE + LEGEND_SPACING; + const offset = (height * color.domain().length) / 2; + const horizontal = 12 * LEGEND_RECT_SIZE; + const vertical = index * height - offset; + return 'translate(' + horizontal + ',' + vertical + ')'; + }); + + legend + .append('rect') + .attr('width', LEGEND_RECT_SIZE) + .attr('height', LEGEND_RECT_SIZE) + .style('fill', color) + .style('stroke', color); + + legend + .data(arcs) + .append('text') + .attr('x', LEGEND_RECT_SIZE + LEGEND_SPACING) + .attr('y', LEGEND_RECT_SIZE - LEGEND_SPACING) + .text((datum: d3.PieArcDatum): string => { + const { label, value } = datum.data; + if (db.getShowData()) { + return `${label} [${value}]`; + } + return label; + }); +}; + +export const renderer = { draw }; diff --git a/packages/mermaid/src/diagrams/pie/styles.js b/packages/mermaid/src/diagrams/pie/pieStyles.ts similarity index 79% rename from packages/mermaid/src/diagrams/pie/styles.js rename to packages/mermaid/src/diagrams/pie/pieStyles.ts index 6f0f600061..39a7f21d54 100644 --- a/packages/mermaid/src/diagrams/pie/styles.js +++ b/packages/mermaid/src/diagrams/pie/pieStyles.ts @@ -1,4 +1,7 @@ -const getStyles = (options) => +import type { DiagramStylesProvider } from '../../diagram-api/types.js'; +import type { PieStyleOptions } from './pieTypes.js'; + +const getStyles: DiagramStylesProvider = (options: PieStyleOptions) => ` .pieCircle{ stroke: ${options.pieStrokeColor}; diff --git a/packages/mermaid/src/diagrams/pie/pieTypes.ts b/packages/mermaid/src/diagrams/pie/pieTypes.ts new file mode 100644 index 0000000000..67fb1dca25 --- /dev/null +++ b/packages/mermaid/src/diagrams/pie/pieTypes.ts @@ -0,0 +1,64 @@ +import type { PieDiagramConfig } from '../../config.type.js'; +import type { DiagramDB, ParseDirectiveDefinition } from '../../diagram-api/types.js'; + +export interface PieFields { + sections: Sections; + showData: boolean; + config: PieDiagramConfig; +} + +export interface PieStyleOptions { + fontFamily: string; + pie1: string; + pie2: string; + pie3: string; + pie4: string; + pie5: string; + pie6: string; + pie7: string; + pie8: string; + pie9: string; + pie10: string; + pie11: string; + pie12: string; + pieTitleTextSize: string; + pieTitleTextColor: string; + pieSectionTextSize: string; + pieSectionTextColor: string; + pieLegendTextSize: string; + pieLegendTextColor: string; + pieStrokeColor: string; + pieStrokeWidth: string; + pieOuterStrokeWidth: string; + pieOuterStrokeColor: string; + pieOpacity: string; +} + +export type Sections = Record; + +export interface D3Sections { + label: string; + value: number; +} + +export interface PieDB extends DiagramDB { + // config + getConfig: () => Required; + + // common db + parseDirective: ParseDirectiveDefinition; + clear: () => void; + setDiagramTitle: (title: string) => void; + getDiagramTitle: () => string; + setAccTitle: (title: string) => void; + getAccTitle: () => string; + setAccDescription: (describetion: string) => void; + getAccDescription: () => string; + + // diagram db + addSection: (label: string, value: number) => void; + getSections: () => Sections; + cleanupValue: (value: string) => number; + setShowData: (toggle: boolean) => void; + getShowData: () => boolean; +} diff --git a/packages/mermaid/src/styles.spec.ts b/packages/mermaid/src/styles.spec.ts index 9353416410..420ee9757b 100644 --- a/packages/mermaid/src/styles.spec.ts +++ b/packages/mermaid/src/styles.spec.ts @@ -21,7 +21,7 @@ import flowchartElk from './diagrams/flowchart/elk/styles.js'; import er from './diagrams/er/styles.js'; import git from './diagrams/git/styles.js'; import gantt from './diagrams/gantt/styles.js'; -import pie from './diagrams/pie/styles.js'; +import pie from './diagrams/pie/pieStyles.js'; import requirement from './diagrams/requirement/styles.js'; import sequence from './diagrams/sequence/styles.js'; import state from './diagrams/state/styles.js'; diff --git a/packages/mermaid/src/utils.spec.js b/packages/mermaid/src/utils.spec.ts similarity index 79% rename from packages/mermaid/src/utils.spec.js rename to packages/mermaid/src/utils.spec.ts index ae3234cb91..271dc588c6 100644 --- a/packages/mermaid/src/utils.spec.js +++ b/packages/mermaid/src/utils.spec.ts @@ -1,5 +1,5 @@ import { vi } from 'vitest'; -import utils from './utils.js'; +import utils, { cleanAndMerge } from './utils.js'; import assignWithDepth from './assignWithDepth.js'; import { detectType } from './diagram-api/detectType.js'; import { addDiagrams } from './diagram-api/diagram-orchestration.js'; @@ -10,51 +10,51 @@ addDiagrams(); describe('when assignWithDepth: should merge objects within objects', function () { it('should handle simple, depth:1 types (identity)', function () { - let config_0 = { foo: 'bar', bar: 0 }; - let config_1 = { foo: 'bar', bar: 0 }; - let result = assignWithDepth(config_0, config_1); + const config_0 = { foo: 'bar', bar: 0 }; + const config_1 = { foo: 'bar', bar: 0 }; + const result = assignWithDepth(config_0, config_1); expect(result).toEqual(config_1); }); it('should handle simple, depth:1 types (dst: undefined)', function () { - let config_0 = undefined; - let config_1 = { foo: 'bar', bar: 0 }; - let result = assignWithDepth(config_0, config_1); + const config_0 = undefined; + const config_1 = { foo: 'bar', bar: 0 }; + const result = assignWithDepth(config_0, config_1); expect(result).toEqual(config_1); }); it('should handle simple, depth:1 types (src: undefined)', function () { - let config_0 = { foo: 'bar', bar: 0 }; - let config_1 = undefined; - let result = assignWithDepth(config_0, config_1); + const config_0 = { foo: 'bar', bar: 0 }; + const config_1 = undefined; + const result = assignWithDepth(config_0, config_1); expect(result).toEqual(config_0); }); it('should handle simple, depth:1 types (merge)', function () { - let config_0 = { foo: 'bar', bar: 0 }; - let config_1 = { foo: 'foo' }; - let result = assignWithDepth(config_0, config_1); + const config_0 = { foo: 'bar', bar: 0 }; + const config_1 = { foo: 'foo' }; + const result = assignWithDepth(config_0, config_1); expect(result).toEqual({ foo: 'foo', bar: 0 }); }); it('should handle depth:2 types (dst: orphan)', function () { - let config_0 = { foo: 'bar', bar: { foo: 'bar' } }; - let config_1 = { foo: 'bar' }; - let result = assignWithDepth(config_0, config_1); + const config_0 = { foo: 'bar', bar: { foo: 'bar' } }; + const config_1 = { foo: 'bar' }; + const result = assignWithDepth(config_0, config_1); expect(result).toEqual(config_0); }); it('should handle depth:2 types (dst: object, src: simple type)', function () { - let config_0 = { foo: 'bar', bar: { foo: 'bar' } }; - let config_1 = { foo: 'foo', bar: 'should NOT clobber' }; - let result = assignWithDepth(config_0, config_1); + const config_0 = { foo: 'bar', bar: { foo: 'bar' } }; + const config_1 = { foo: 'foo', bar: 'should NOT clobber' }; + const result = assignWithDepth(config_0, config_1); expect(result).toEqual({ foo: 'foo', bar: { foo: 'bar' } }); }); it('should handle depth:2 types (src: orphan)', function () { - let config_0 = { foo: 'bar' }; - let config_1 = { foo: 'bar', bar: { foo: 'bar' } }; - let result = assignWithDepth(config_0, config_1); + const config_0 = { foo: 'bar' }; + const config_1 = { foo: 'bar', bar: { foo: 'bar' } }; + const result = assignWithDepth(config_0, config_1); expect(result).toEqual(config_1); }); it('should handle depth:2 types (merge)', function () { - let config_0 = { foo: 'bar', bar: { foo: 'bar' }, boofar: 1 }; - let config_1 = { foo: 'foo', bar: { bar: 0 }, foobar: 'foobar' }; - let result = assignWithDepth(config_0, config_1); + const config_0 = { foo: 'bar', bar: { foo: 'bar' }, boofar: 1 }; + const config_1 = { foo: 'foo', bar: { bar: 0 }, foobar: 'foobar' }; + const result = assignWithDepth(config_0, config_1); expect(result).toEqual({ foo: 'foo', bar: { foo: 'bar', bar: 0 }, @@ -63,17 +63,17 @@ describe('when assignWithDepth: should merge objects within objects', function ( }); }); it('should handle depth:3 types (merge with clobber because assignWithDepth::depth == 2)', function () { - let config_0 = { + const config_0 = { foo: 'bar', bar: { foo: 'bar', bar: { foo: { message: 'this', willbe: 'clobbered' } } }, boofar: 1, }; - let config_1 = { + const config_1 = { foo: 'foo', bar: { foo: 'foo', bar: { foo: { message: 'clobbered other foo' } } }, foobar: 'foobar', }; - let result = assignWithDepth(config_0, config_1); + const result = assignWithDepth(config_0, config_1); expect(result).toEqual({ foo: 'foo', bar: { foo: 'foo', bar: { foo: { message: 'clobbered other foo' } } }, @@ -82,7 +82,7 @@ describe('when assignWithDepth: should merge objects within objects', function ( }); }); it('should handle depth:3 types (merge with clobber because assignWithDepth::depth == 1)', function () { - let config_0 = { + const config_0 = { foo: 'bar', bar: { foo: 'bar', @@ -90,12 +90,12 @@ describe('when assignWithDepth: should merge objects within objects', function ( }, boofar: 1, }; - let config_1 = { + const config_1 = { foo: 'foo', bar: { foo: 'foo', bar: { foo: { message: 'this' } } }, foobar: 'foobar', }; - let result = assignWithDepth(config_0, config_1, { depth: 1 }); + const result = assignWithDepth(config_0, config_1, { depth: 1 }); expect(result).toEqual({ foo: 'foo', bar: { foo: 'foo', bar: { foo: { message: 'this' } } }, @@ -104,17 +104,17 @@ describe('when assignWithDepth: should merge objects within objects', function ( }); }); it('should handle depth:3 types (merge with no clobber because assignWithDepth::depth == 3)', function () { - let config_0 = { + const config_0 = { foo: 'bar', bar: { foo: 'bar', bar: { foo: { message: '', willbe: 'present' } } }, boofar: 1, }; - let config_1 = { + const config_1 = { foo: 'foo', bar: { foo: 'foo', bar: { foo: { message: 'this' } } }, foobar: 'foobar', }; - let result = assignWithDepth(config_0, config_1, { depth: 3 }); + const result = assignWithDepth(config_0, config_1, { depth: 3 }); expect(result).toEqual({ foo: 'foo', bar: { foo: 'foo', bar: { foo: { message: 'this', willbe: 'present' } } }, @@ -125,8 +125,8 @@ describe('when assignWithDepth: should merge objects within objects', function ( }); describe('when memoizing', function () { it('should return the same value', function () { - const fib = memoize( - function (n, x, canary) { + const fib: any = memoize( + function (n: number, x: string, canary: { flag: boolean }) { canary.flag = true; if (n < 2) { return 1; @@ -260,7 +260,7 @@ describe('when formatting urls', function () { it('should handle links', function () { const url = 'https://mermaid-js.github.io/mermaid/#/'; - let config = { securityLevel: 'loose' }; + const config = { securityLevel: 'loose' }; let result = utils.formatUrl(url, config); expect(result).toEqual(url); @@ -271,7 +271,7 @@ describe('when formatting urls', function () { it('should handle anchors', function () { const url = '#interaction'; - let config = { securityLevel: 'loose' }; + const config = { securityLevel: 'loose' }; let result = utils.formatUrl(url, config); expect(result).toEqual(url); @@ -282,7 +282,7 @@ describe('when formatting urls', function () { it('should handle mailto', function () { const url = 'mailto:user@user.user'; - let config = { securityLevel: 'loose' }; + const config = { securityLevel: 'loose' }; let result = utils.formatUrl(url, config); expect(result).toEqual(url); @@ -293,7 +293,7 @@ describe('when formatting urls', function () { it('should handle other protocols', function () { const url = 'notes://do-your-thing/id'; - let config = { securityLevel: 'loose' }; + const config = { securityLevel: 'loose' }; let result = utils.formatUrl(url, config); expect(result).toEqual(url); @@ -304,7 +304,7 @@ describe('when formatting urls', function () { it('should handle scripts', function () { const url = 'javascript:alert("test")'; - let config = { securityLevel: 'loose' }; + const config = { securityLevel: 'loose' }; let result = utils.formatUrl(url, config); expect(result).toEqual(url); @@ -425,6 +425,42 @@ describe('when parsing font sizes', function () { }); it('handles unparseable input', function () { + // @ts-expect-error Explicitly testing unparsable input expect(utils.parseFontSize({ fontSize: 14 })).toEqual([undefined, undefined]); }); }); + +describe('cleanAndMerge', () => { + test('should merge objects', () => { + expect(cleanAndMerge({ a: 1, b: 2 }, { b: 3 })).toEqual({ a: 1, b: 3 }); + expect(cleanAndMerge({ a: 1 }, { a: 2 })).toEqual({ a: 2 }); + }); + + test('should remove undefined values', () => { + expect(cleanAndMerge({ a: 1, b: 2 }, { b: undefined })).toEqual({ a: 1, b: 2 }); + expect(cleanAndMerge({ a: 1, b: 2 }, { a: 2, b: undefined })).toEqual({ a: 2, b: 2 }); + expect(cleanAndMerge({ a: 1, b: { c: 2 } }, { a: 2, b: undefined })).toEqual({ + a: 2, + b: { c: 2 }, + }); + // @ts-expect-error Explicitly testing different type + expect(cleanAndMerge({ a: 1, b: { c: 2 } }, { a: 2, b: { c: undefined } })).toEqual({ + a: 2, + b: { c: 2 }, + }); + }); + + test('should create deep copies of object', () => { + const input: { a: number; b?: number } = { a: 1 }; + const output = cleanAndMerge(input, { b: 2 }); + expect(output).toEqual({ a: 1, b: 2 }); + output.b = 3; + expect(input).toEqual({ a: 1 }); + + const inputDeep = { a: { b: 1 } }; + const outputDeep = cleanAndMerge(inputDeep, { a: { b: 2 } }); + expect(outputDeep).toEqual({ a: { b: 2 } }); + outputDeep.a.b = 3; + expect(inputDeep).toEqual({ a: { b: 1 } }); + }); +}); diff --git a/packages/mermaid/src/utils.ts b/packages/mermaid/src/utils.ts index e48b49fcda..937f3f8f84 100644 --- a/packages/mermaid/src/utils.ts +++ b/packages/mermaid/src/utils.ts @@ -31,6 +31,7 @@ import { detectType } from './diagram-api/detectType.js'; import assignWithDepth from './assignWithDepth.js'; import { MermaidConfig } from './config.type.js'; import memoize from 'lodash-es/memoize.js'; +import merge from 'lodash-es/merge.js'; export const ZERO_WIDTH_SPACE = '\u200b'; @@ -802,7 +803,7 @@ export const calculateTextDimensions: ( ); export const initIdGenerator = class iterator { - constructor(deterministic, seed) { + constructor(deterministic, seed?: any) { this.deterministic = deterministic; // TODO: Seed is only used for length? this.seed = seed; @@ -994,12 +995,17 @@ export const parseFontSize = (fontSize: string | number | undefined): [number?, } }; +export function cleanAndMerge(defaultData: T, data?: Partial): T { + return merge({}, defaultData, data); +} + export default { assignWithDepth, wrapLabel, calculateTextHeight, calculateTextWidth, calculateTextDimensions, + cleanAndMerge, detectInit, detectDirective, isSubstringInArray, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f6aad72eaa..d30b49a73d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -193,7 +193,7 @@ importers: dependencies: '@braintree/sanitize-url': specifier: ^6.0.1 - version: 6.0.1 + version: 6.0.2 '@types/d3-scale': specifier: ^4.0.3 version: 4.0.3 @@ -267,6 +267,9 @@ importers: '@types/d3-selection': specifier: ^3.0.5 version: 3.0.5 + '@types/d3-shape': + specifier: ^3.1.1 + version: 3.1.1 '@types/dompurify': specifier: ^3.0.2 version: 3.0.2 @@ -351,6 +354,9 @@ importers: start-server-and-test: specifier: ^2.0.0 version: 2.0.0 + type-fest: + specifier: ^4.1.0 + version: 4.1.0 typedoc: specifier: ^0.24.5 version: 0.24.5(typescript@5.0.4) @@ -377,7 +383,7 @@ importers: dependencies: '@braintree/sanitize-url': specifier: ^6.0.1 - version: 6.0.1 + version: 6.0.2 cytoscape: specifier: ^3.23.0 version: 3.23.0 @@ -2323,8 +2329,8 @@ packages: resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} dev: true - /@braintree/sanitize-url@6.0.1: - resolution: {integrity: sha512-zr9Qs9KFQiEvMWdZesjcmRJlUck5NR+eKGS1uyKk+oYTWwlYrsoPEi6VmG6/TzBD1hKCGEimrhTgGS6hvn/xIQ==} + /@braintree/sanitize-url@6.0.2: + resolution: {integrity: sha512-Tbsj02wXCbqGmzdnXNk0SOF19ChhRU70BsroIi4Pm6Ehp56in6vch94mfbdQ17DozxkL3BAVjbZ4Qc1a0HFRAg==} dev: false /@colors/colors@1.5.0: @@ -3692,7 +3698,7 @@ packages: '@types/node': 18.16.0 ansi-escapes: 4.3.2 chalk: 4.1.2 - ci-info: 3.6.2 + ci-info: 3.8.0 exit: 0.1.2 graceful-fs: 4.2.10 jest-changed-files: 29.5.0 @@ -4357,8 +4363,8 @@ packages: '@types/d3-path': 1.0.9 dev: true - /@types/d3-shape@3.1.0: - resolution: {integrity: sha512-jYIYxFFA9vrJ8Hd4Se83YI6XF+gzDL1aC5DCsldai4XYYiVNdhtpGbA/GM6iyQ8ayhSp3a148LY34hy7A4TxZA==} + /@types/d3-shape@3.1.1: + resolution: {integrity: sha512-6Uh86YFF7LGg4PQkuO2oG6EMBRLuW9cbavUW46zkIO5kuS2PfTqo2o9SkgtQzguBHbLgNnU90UNsITpsX1My+A==} dependencies: '@types/d3-path': 3.0.0 dev: true @@ -4414,7 +4420,7 @@ packages: '@types/d3-scale': 4.0.3 '@types/d3-scale-chromatic': 3.0.0 '@types/d3-selection': 3.0.5 - '@types/d3-shape': 3.1.0 + '@types/d3-shape': 3.1.1 '@types/d3-time': 3.0.0 '@types/d3-time-format': 4.0.0 '@types/d3-timer': 3.0.0 @@ -6668,11 +6674,6 @@ packages: engines: {node: '>=6.0'} dev: true - /ci-info@3.6.2: - resolution: {integrity: sha512-lVZdhvbEudris15CLytp2u6Y0p5EKfztae9Fqa189MfNmln9F33XuH69v5fvNfiRN5/0eAUz2yJL3mo+nhaRKg==} - engines: {node: '>=8'} - dev: true - /ci-info@3.8.0: resolution: {integrity: sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==} engines: {node: '>=8'} @@ -14792,6 +14793,11 @@ packages: engines: {node: '>=10'} dev: true + /type-fest@4.1.0: + resolution: {integrity: sha512-VJGJVepayd8OWavP+rgXt4i3bfLk+tSomTV7r4mca2XD/oTCWnkJlNkpXavkxdmtU2aKdAmFGeHvoQutOVHCZg==} + engines: {node: '>=16'} + dev: true + /type-is@1.6.18: resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} engines: {node: '>= 0.6'}