diff --git a/cypress/integration/rendering/quadrantChart.spec.js b/cypress/integration/rendering/quadrantChart.spec.js index 1be1f7def..83a1455c6 100644 --- a/cypress/integration/rendering/quadrantChart.spec.js +++ b/cypress/integration/rendering/quadrantChart.spec.js @@ -1,4 +1,4 @@ -import { imgSnapshotTest, renderGraph } from '../../helpers/util.ts'; +import { imgSnapshotTest } from '../../helpers/util.ts'; describe('Quadrant Chart', () => { it('should render if only chart type is provided', () => { @@ -226,4 +226,52 @@ describe('Quadrant Chart', () => { ); cy.get('svg'); }); + + it('it should render data points with styles', () => { + imgSnapshotTest( + ` + quadrantChart + title Reach and engagement of campaigns + x-axis Reach --> + y-axis Engagement --> + quadrant-1 We should expand + quadrant-2 Need to promote + quadrant-3 Re-evaluate + quadrant-4 May be improved + Campaign A: [0.3, 0.6] radius: 20 + Campaign B: [0.45, 0.23] color: #ff0000 + Campaign C: [0.57, 0.69] stroke-color: #ff00ff + Campaign D: [0.78, 0.34] stroke-width: 3px + Campaign E: [0.40, 0.34] radius: 20, color: #ff0000 , stroke-color : #ff00ff, stroke-width : 3px + Campaign F: [0.35, 0.78] stroke-width: 3px , color: #ff0000, radius: 20, stroke-color: #ff00ff + Campaign G: [0.22, 0.22] stroke-width: 3px , color: #309708 , radius : 20 , stroke-color: #5060ff + Campaign H: [0.22, 0.44] + `, + {} + ); + cy.get('svg'); + }); + + it('it should render data points with styles + classes', () => { + imgSnapshotTest( + ` + quadrantChart + title Reach and engagement of campaigns + x-axis Reach --> + y-axis Engagement --> + quadrant-1 We should expand + quadrant-2 Need to promote + quadrant-3 Re-evaluate + quadrant-4 May be improved + Campaign A:::class1: [0.3, 0.6] radius: 20 + Campaign B: [0.45, 0.23] color: #ff0000 + Campaign C: [0.57, 0.69] stroke-color: #ff00ff + Campaign D:::class2: [0.78, 0.34] stroke-width: 3px + Campaign E:::class2: [0.40, 0.34] radius: 20, color: #ff0000, stroke-color: #ff00ff, stroke-width: 3px + Campaign F:::class1: [0.35, 0.78] + classDef class1 color: #908342, radius : 10, stroke-color: #310085, stroke-width: 10px + classDef class2 color: #f00fff, radius : 10 + ` + ); + }); }); diff --git a/docs/syntax/quadrantChart.md b/docs/syntax/quadrantChart.md index fdf866779..ba8063845 100644 --- a/docs/syntax/quadrantChart.md +++ b/docs/syntax/quadrantChart.md @@ -168,3 +168,86 @@ quadrantChart quadrant-3 Delegate quadrant-4 Delete ``` + +### Point styling + +Points can either be styled directly or with defined shared classes + +1. Direct styling + +```md +Point A: [0.9, 0.0] radius: 12 +Point B: [0.8, 0.1] color: #ff3300, radius: 10 +Point C: [0.7, 0.2] radius: 25, color: #00ff33, stroke-color: #10f0f0 +Point D: [0.6, 0.3] radius: 15, stroke-color: #00ff0f, stroke-width: 5px ,color: #ff33f0 +``` + +2. Classes styling + +```md +Point A:::class1: [0.9, 0.0] +Point B:::class2: [0.8, 0.1] +Point C:::class3: [0.7, 0.2] +Point D:::class3: [0.7, 0.2] +classDef class1 color: #109060 +classDef class2 color: #908342, radius : 10, stroke-color: #310085, stroke-width: 10px +classDef class3 color: #f00fff, radius : 10 +``` + +#### Available styles: + +| Parameter | Description | +| ------------ | ---------------------------------------------------------------------- | +| color | Fill color of the point | +| radius | Radius of the point | +| stroke-width | Border width of the point | +| stroke-color | Border color of the point (useless when stroke-width is not specified) | + +> **Note** +> Order of preference: +> +> 1. Direct styles +> 2. Class styles +> 3. Theme styles + +## Example on styling + +```mermaid-example +quadrantChart + title Reach and engagement of campaigns + x-axis Low Reach --> High Reach + y-axis Low Engagement --> High Engagement + quadrant-1 We should expand + quadrant-2 Need to promote + quadrant-3 Re-evaluate + quadrant-4 May be improved + Campaign A: [0.9, 0.0] radius: 12 + Campaign B:::class1: [0.8, 0.1] color: #ff3300, radius: 10 + Campaign C: [0.7, 0.2] radius: 25, color: #00ff33, stroke-color: #10f0f0 + Campaign D: [0.6, 0.3] radius: 15, stroke-color: #00ff0f, stroke-width: 5px ,color: #ff33f0 + Campaign E:::class2: [0.5, 0.4] + Campaign F:::class3: [0.4, 0.5] color: #0000ff + classDef class1 color: #109060 + classDef class2 color: #908342, radius : 10, stroke-color: #310085, stroke-width: 10px + classDef class3 color: #f00fff, radius : 10 +``` + +```mermaid +quadrantChart + title Reach and engagement of campaigns + x-axis Low Reach --> High Reach + y-axis Low Engagement --> High Engagement + quadrant-1 We should expand + quadrant-2 Need to promote + quadrant-3 Re-evaluate + quadrant-4 May be improved + Campaign A: [0.9, 0.0] radius: 12 + Campaign B:::class1: [0.8, 0.1] color: #ff3300, radius: 10 + Campaign C: [0.7, 0.2] radius: 25, color: #00ff33, stroke-color: #10f0f0 + Campaign D: [0.6, 0.3] radius: 15, stroke-color: #00ff0f, stroke-width: 5px ,color: #ff33f0 + Campaign E:::class2: [0.5, 0.4] + Campaign F:::class3: [0.4, 0.5] color: #0000ff + classDef class1 color: #109060 + classDef class2 color: #908342, radius : 10, stroke-color: #310085, stroke-width: 10px + classDef class3 color: #f00fff, radius : 10 +``` diff --git a/packages/mermaid/src/diagrams/quadrant-chart/parser/quadrant.jison b/packages/mermaid/src/diagrams/quadrant-chart/parser/quadrant.jison index 255b30a03..85e82b6b5 100644 --- a/packages/mermaid/src/diagrams/quadrant-chart/parser/quadrant.jison +++ b/packages/mermaid/src/diagrams/quadrant-chart/parser/quadrant.jison @@ -11,6 +11,7 @@ %x point_start %x point_x %x point_y +%x class_name %% \%\%(?!\{)[^\n]* /* skip comments */ [^\}]\%\%[^\n]* /* skip comments */ @@ -35,6 +36,7 @@ accDescr\s*"{"\s* { this.begin("acc_descr_multiline");} " "*"quadrant-2"" "* return 'QUADRANT_2'; " "*"quadrant-3"" "* return 'QUADRANT_3'; " "*"quadrant-4"" "* return 'QUADRANT_4'; +"classDef" return 'CLASSDEF'; ["][`] { this.begin("md_string");} [^`"]+ { return "MD_STR";} @@ -43,6 +45,9 @@ accDescr\s*"{"\s* { this.begin("acc_descr_multiline");} ["] this.popState(); [^"]* return "STR"; +\:\:\: {this.begin('class_name')} +^\w+ {this.popState(); return 'class_name';} + \s*\:\s*\[\s* {this.begin("point_start"); return 'point_start';} (1)|(0(.\d+)?) {this.begin('point_x'); return 'point_x';} \s*\]" "* {this.popState();} @@ -75,6 +80,31 @@ accDescr\s*"{"\s* { this.begin("acc_descr_multiline");} %% /* language grammar */ +idStringToken : ALPHA | NUM | NODE_STRING | DOWN | MINUS | DEFAULT | COMMA | COLON | AMP | BRKT | MULT | UNICODE_TEXT; +styleComponent: ALPHA | NUM | NODE_STRING | COLON | UNIT | SPACE | BRKT | STYLE | PCT | MINUS ; + +idString + :idStringToken + {$$=$idStringToken} + | idString idStringToken + {$$=$idString+''+$idStringToken} + ; + +style: styleComponent + |style styleComponent + {$$ = $style + $styleComponent;} + ; + +stylesOpt: style + {$$ = [$style.trim()]} + | stylesOpt COMMA style + {$stylesOpt.push($style.trim());$$ = $stylesOpt;} + ; + +classDefStatement + : CLASSDEF SPACE idString SPACE stylesOpt {$$ = $CLASSDEF;yy.addClass($idString,$stylesOpt);} + ; + start : eol start | SPACE start @@ -92,6 +122,7 @@ line statement : + | classDefStatement {$$=[];} | SPACE statement | axisDetails | quadrantDetails @@ -103,7 +134,10 @@ statement ; points - : text point_start point_x point_y {yy.addPoint($1, $3, $4);} + : text point_start point_x point_y {yy.addPoint($1, "", $3, $4, []);} + | text class_name point_start point_x point_y {yy.addPoint($1, $2, $4, $5, []);} + | text point_start point_x point_y stylesOpt {yy.addPoint($1, "", $3, $4, $stylesOpt);} + | text class_name point_start point_x point_y stylesOpt {yy.addPoint($1, $2, $4, $5, $stylesOpt);} ; axisDetails diff --git a/packages/mermaid/src/diagrams/quadrant-chart/parser/quadrant.jison.spec.ts b/packages/mermaid/src/diagrams/quadrant-chart/parser/quadrant.jison.spec.ts index c09e22228..6ec2ddafb 100644 --- a/packages/mermaid/src/diagrams/quadrant-chart/parser/quadrant.jison.spec.ts +++ b/packages/mermaid/src/diagrams/quadrant-chart/parser/quadrant.jison.spec.ts @@ -212,20 +212,34 @@ describe('Testing quadrantChart jison file', () => { it('should be able to parse points', () => { let str = 'quadrantChart\npoint1: [0.1, 0.4]'; expect(parserFnConstructor(str)).not.toThrow(); - expect(mockDB.addPoint).toHaveBeenCalledWith({ text: 'point1', type: 'text' }, '0.1', '0.4'); + expect(mockDB.addPoint).toHaveBeenCalledWith( + { text: 'point1', type: 'text' }, + '', + '0.1', + '0.4', + [] + ); clearMocks(); str = 'QuadRantChart \n Point1 : [0.1, 0.4] '; expect(parserFnConstructor(str)).not.toThrow(); - expect(mockDB.addPoint).toHaveBeenCalledWith({ text: 'Point1', type: 'text' }, '0.1', '0.4'); + expect(mockDB.addPoint).toHaveBeenCalledWith( + { text: 'Point1', type: 'text' }, + '', + '0.1', + '0.4', + [] + ); clearMocks(); str = 'QuadRantChart \n "Point1 : (* +=[❤": [1, 0] '; expect(parserFnConstructor(str)).not.toThrow(); expect(mockDB.addPoint).toHaveBeenCalledWith( { text: 'Point1 : (* +=[❤', type: 'text' }, + '', '1', - '0' + '0', + [] ); clearMocks(); @@ -264,15 +278,149 @@ describe('Testing quadrantChart jison file', () => { expect(mockDB.setQuadrant4Text).toHaveBeenCalledWith({ text: 'Visionaries', type: 'text' }); expect(mockDB.addPoint).toHaveBeenCalledWith( { text: 'Microsoft', type: 'text' }, + '', '0.75', - '0.75' + '0.75', + [] ); expect(mockDB.addPoint).toHaveBeenCalledWith( { text: 'Salesforce', type: 'text' }, + '', '0.55', - '0.60' + '0.60', + [] + ); + expect(mockDB.addPoint).toHaveBeenCalledWith( + { text: 'IBM', type: 'text' }, + '', + '0.51', + '0.40', + [] + ); + expect(mockDB.addPoint).toHaveBeenCalledWith( + { text: 'Incorta', type: 'text' }, + '', + '0.20', + '0.30', + [] + ); + }); + + it('should be able to parse the whole chart with point styling with all params or some params', () => { + const str = `quadrantChart + title Analytics and Business Intelligence Platforms + x-axis "Completeness of Vision ❤" --> "x-axis-2" + y-axis Ability to Execute --> "y-axis-2" + quadrant-1 Leaders + quadrant-2 Challengers + quadrant-3 Niche + quadrant-4 Visionaries + Microsoft: [0.75, 0.75] radius: 10 + Salesforce: [0.55, 0.60] radius: 10, color: #ff0000 + IBM: [0.51, 0.40] radius: 10, color: #ff0000, stroke-color: #ff00ff + Incorta: [0.20, 0.30] radius: 10 ,color: #ff0000 ,stroke-color: #ff00ff ,stroke-width: 10px`; + + expect(parserFnConstructor(str)).not.toThrow(); + expect(mockDB.setXAxisLeftText).toHaveBeenCalledWith({ + text: 'Completeness of Vision ❤', + type: 'text', + }); + expect(mockDB.setXAxisRightText).toHaveBeenCalledWith({ text: 'x-axis-2', type: 'text' }); + expect(mockDB.setYAxisTopText).toHaveBeenCalledWith({ text: 'y-axis-2', type: 'text' }); + expect(mockDB.setYAxisBottomText).toHaveBeenCalledWith({ + text: 'Ability to Execute', + type: 'text', + }); + expect(mockDB.setQuadrant1Text).toHaveBeenCalledWith({ text: 'Leaders', type: 'text' }); + expect(mockDB.setQuadrant2Text).toHaveBeenCalledWith({ text: 'Challengers', type: 'text' }); + expect(mockDB.setQuadrant3Text).toHaveBeenCalledWith({ text: 'Niche', type: 'text' }); + expect(mockDB.setQuadrant4Text).toHaveBeenCalledWith({ text: 'Visionaries', type: 'text' }); + expect(mockDB.addPoint).toHaveBeenCalledWith( + { text: 'Microsoft', type: 'text' }, + '', + '0.75', + '0.75', + ['radius: 10'] + ); + expect(mockDB.addPoint).toHaveBeenCalledWith( + { text: 'Salesforce', type: 'text' }, + '', + '0.55', + '0.60', + ['radius: 10', 'color: #ff0000'] + ); + expect(mockDB.addPoint).toHaveBeenCalledWith( + { text: 'IBM', type: 'text' }, + '', + '0.51', + '0.40', + ['radius: 10', 'color: #ff0000', 'stroke-color: #ff00ff'] + ); + expect(mockDB.addPoint).toHaveBeenCalledWith( + { text: 'Incorta', type: 'text' }, + '', + '0.20', + '0.30', + ['radius: 10', 'color: #ff0000', 'stroke-color: #ff00ff', 'stroke-width: 10px'] + ); + }); + + it('should be able to parse the whole chart with point styling with params in a random order + class names', () => { + const str = `quadrantChart + title Analytics and Business Intelligence Platforms + x-axis "Completeness of Vision ❤" --> "x-axis-2" + y-axis Ability to Execute --> "y-axis-2" + quadrant-1 Leaders + quadrant-2 Challengers + quadrant-3 Niche + quadrant-4 Visionaries + Microsoft: [0.75, 0.75] stroke-color: #ff00ff ,stroke-width: 10px, color: #ff0000, radius: 10 + Salesforce:::class1: [0.55, 0.60] radius: 10, color: #ff0000 + IBM: [0.51, 0.40] stroke-color: #ff00ff ,stroke-width: 10px + Incorta: [0.20, 0.30] stroke-width: 10px`; + + expect(parserFnConstructor(str)).not.toThrow(); + expect(mockDB.setXAxisLeftText).toHaveBeenCalledWith({ + text: 'Completeness of Vision ❤', + type: 'text', + }); + expect(mockDB.setXAxisRightText).toHaveBeenCalledWith({ text: 'x-axis-2', type: 'text' }); + expect(mockDB.setYAxisTopText).toHaveBeenCalledWith({ text: 'y-axis-2', type: 'text' }); + expect(mockDB.setYAxisBottomText).toHaveBeenCalledWith({ + text: 'Ability to Execute', + type: 'text', + }); + expect(mockDB.setQuadrant1Text).toHaveBeenCalledWith({ text: 'Leaders', type: 'text' }); + expect(mockDB.setQuadrant2Text).toHaveBeenCalledWith({ text: 'Challengers', type: 'text' }); + expect(mockDB.setQuadrant3Text).toHaveBeenCalledWith({ text: 'Niche', type: 'text' }); + expect(mockDB.setQuadrant4Text).toHaveBeenCalledWith({ text: 'Visionaries', type: 'text' }); + expect(mockDB.addPoint).toHaveBeenCalledWith( + { text: 'Microsoft', type: 'text' }, + '', + '0.75', + '0.75', + ['stroke-color: #ff00ff', 'stroke-width: 10px', 'color: #ff0000', 'radius: 10'] + ); + expect(mockDB.addPoint).toHaveBeenCalledWith( + { text: 'Salesforce', type: 'text' }, + 'class1', + '0.55', + '0.60', + ['radius: 10', 'color: #ff0000'] + ); + expect(mockDB.addPoint).toHaveBeenCalledWith( + { text: 'IBM', type: 'text' }, + '', + '0.51', + '0.40', + ['stroke-color: #ff00ff', 'stroke-width: 10px'] + ); + expect(mockDB.addPoint).toHaveBeenCalledWith( + { text: 'Incorta', type: 'text' }, + '', + '0.20', + '0.30', + ['stroke-width: 10px'] ); - expect(mockDB.addPoint).toHaveBeenCalledWith({ text: 'IBM', type: 'text' }, '0.51', '0.40'); - expect(mockDB.addPoint).toHaveBeenCalledWith({ text: 'Incorta', type: 'text' }, '0.20', '0.30'); }); }); diff --git a/packages/mermaid/src/diagrams/quadrant-chart/quadrantBuilder.ts b/packages/mermaid/src/diagrams/quadrant-chart/quadrantBuilder.ts index 9f5e3933a..173b4c078 100644 --- a/packages/mermaid/src/diagrams/quadrant-chart/quadrantBuilder.ts +++ b/packages/mermaid/src/diagrams/quadrant-chart/quadrantBuilder.ts @@ -1,7 +1,7 @@ import { scaleLinear } from 'd3'; -import { log } from '../../logger.js'; import type { BaseDiagramConfig, QuadrantChartConfig } from '../../config.type.js'; import defaultConfig from '../../defaultConfig.js'; +import { log } from '../../logger.js'; import { getThemeVariables } from '../../themes/theme-default.js'; import type { Point } from '../../types.js'; @@ -10,7 +10,15 @@ const defaultThemeVariables = getThemeVariables(); export type TextVerticalPos = 'left' | 'center' | 'right'; export type TextHorizontalPos = 'top' | 'middle' | 'bottom'; -export interface QuadrantPointInputType extends Point { +export interface StylesObject { + className?: string; + radius?: number; + color?: string; + strokeColor?: string; + strokeWidth?: string; +} + +export interface QuadrantPointInputType extends Point, StylesObject { text: string; } @@ -23,7 +31,9 @@ export interface QuadrantTextType extends Point { rotation: number; } -export interface QuadrantPointType extends Point { +export interface QuadrantPointType + extends Point, + Pick { fill: string; radius: number; text: QuadrantTextType; @@ -117,6 +127,7 @@ export class QuadrantBuilder { private config: QuadrantBuilderConfig; private themeConfig: QuadrantBuilderThemeConfig; private data: QuadrantBuilderData; + private classes: Record = {}; constructor() { this.config = this.getDefaultConfig(); @@ -191,6 +202,7 @@ export class QuadrantBuilder { this.config = this.getDefaultConfig(); this.themeConfig = this.getDefaultThemeConfig(); this.data = this.getDefaultData(); + this.classes = {}; log.info('clear called'); } @@ -202,6 +214,10 @@ export class QuadrantBuilder { this.data.points = [...points, ...this.data.points]; } + addClass(className: string, styles: StylesObject) { + this.classes[className] = styles; + } + setConfig(config: Partial) { log.trace('setConfig called with: ', config); this.config = { ...this.config, ...config }; @@ -470,11 +486,15 @@ export class QuadrantBuilder { .range([quadrantHeight + quadrantTop, quadrantTop]); const points: QuadrantPointType[] = this.data.points.map((point) => { + const classStyles = this.classes[point.className as keyof typeof this.classes]; + if (classStyles) { + point = { ...classStyles, ...point }; + } const props: QuadrantPointType = { x: xAxis(point.x), y: yAxis(point.y), - fill: this.themeConfig.quadrantPointFill, - radius: this.config.pointRadius, + fill: point.color || this.themeConfig.quadrantPointFill, + radius: point.radius || this.config.pointRadius, text: { text: point.text, fill: this.themeConfig.quadrantPointTextFill, @@ -485,6 +505,8 @@ export class QuadrantBuilder { fontSize: this.config.pointLabelFontSize, rotation: 0, }, + strokeColor: point.strokeColor || this.themeConfig.quadrantPointFill, + strokeWidth: point.strokeWidth || '0px', }; return props; }); diff --git a/packages/mermaid/src/diagrams/quadrant-chart/quadrantDb.spec.ts b/packages/mermaid/src/diagrams/quadrant-chart/quadrantDb.spec.ts new file mode 100644 index 000000000..2a604304a --- /dev/null +++ b/packages/mermaid/src/diagrams/quadrant-chart/quadrantDb.spec.ts @@ -0,0 +1,50 @@ +import quadrantDb from './quadrantDb.js'; + +describe('quadrant unit tests', () => { + it('should parse the styles array and return a StylesObject', () => { + const styles = ['radius: 10', 'color: #ff0000', 'stroke-color: #ff00ff', 'stroke-width: 10px']; + const result = quadrantDb.parseStyles(styles); + + expect(result).toEqual({ + radius: 10, + color: '#ff0000', + strokeColor: '#ff00ff', + strokeWidth: '10px', + }); + }); + + it('should throw an error for non supported style name', () => { + const styles: string[] = ['test_name: value']; + expect(() => quadrantDb.parseStyles(styles)).toThrowError( + 'style named test_name is not supported.' + ); + }); + + it('should return an empty StylesObject for an empty input array', () => { + const styles: string[] = []; + const result = quadrantDb.parseStyles(styles); + expect(result).toEqual({}); + }); + + it('should throw an error for non supported style value', () => { + let styles: string[] = ['radius: f']; + expect(() => quadrantDb.parseStyles(styles)).toThrowError( + 'value for radius f is invalid, please use a valid number' + ); + + styles = ['color: ffaa']; + expect(() => quadrantDb.parseStyles(styles)).toThrowError( + 'value for color ffaa is invalid, please use a valid hex code' + ); + + styles = ['stroke-color: #f677779']; + expect(() => quadrantDb.parseStyles(styles)).toThrowError( + 'value for stroke-color #f677779 is invalid, please use a valid hex code' + ); + + styles = ['stroke-width: 30']; + expect(() => quadrantDb.parseStyles(styles)).toThrowError( + 'value for stroke-width 30 is invalid, please use a valid number of pixels (eg. 10px)' + ); + }); +}); diff --git a/packages/mermaid/src/diagrams/quadrant-chart/quadrantDb.ts b/packages/mermaid/src/diagrams/quadrant-chart/quadrantDb.ts index e65823355..9e16defa1 100644 --- a/packages/mermaid/src/diagrams/quadrant-chart/quadrantDb.ts +++ b/packages/mermaid/src/diagrams/quadrant-chart/quadrantDb.ts @@ -9,7 +9,14 @@ import { setAccDescription, clear as commonClear, } from '../common/commonDb.js'; +import type { StylesObject } from './quadrantBuilder.js'; import { QuadrantBuilder } from './quadrantBuilder.js'; +import { + validateHexCode, + validateSizeInPixels, + validateNumber, + InvalidStyleError, +} from './utils.js'; const config = getConfig(); @@ -56,8 +63,52 @@ function setYAxisBottomText(textObj: LexTextObj) { quadrantBuilder.setData({ yAxisBottomText: textSanitizer(textObj.text) }); } -function addPoint(textObj: LexTextObj, x: number, y: number) { - quadrantBuilder.addPoints([{ x, y, text: textSanitizer(textObj.text) }]); +function parseStyles(styles: string[]): StylesObject { + const stylesObject: StylesObject = {}; + for (const style of styles) { + const [key, value] = style.trim().split(/\s*:\s*/); + if (key === 'radius') { + if (validateNumber(value)) { + throw new InvalidStyleError(key, value, 'number'); + } + stylesObject.radius = parseInt(value); + } else if (key === 'color') { + if (validateHexCode(value)) { + throw new InvalidStyleError(key, value, 'hex code'); + } + stylesObject.color = value; + } else if (key === 'stroke-color') { + if (validateHexCode(value)) { + throw new InvalidStyleError(key, value, 'hex code'); + } + stylesObject.strokeColor = value; + } else if (key === 'stroke-width') { + if (validateSizeInPixels(value)) { + throw new InvalidStyleError(key, value, 'number of pixels (eg. 10px)'); + } + stylesObject.strokeWidth = value; + } else { + throw new Error(`style named ${key} is not supported.`); + } + } + return stylesObject; +} + +function addPoint(textObj: LexTextObj, className: string, x: number, y: number, styles: string[]) { + const stylesObject = parseStyles(styles); + quadrantBuilder.addPoints([ + { + x, + y, + text: textSanitizer(textObj.text), + className, + ...stylesObject, + }, + ]); +} + +function addClass(className: string, styles: string[]) { + quadrantBuilder.addClass(className, parseStyles(styles)); } function setWidth(width: number) { @@ -111,7 +162,9 @@ export default { setXAxisRightText, setYAxisTopText, setYAxisBottomText, + parseStyles, addPoint, + addClass, getQuadrantData, clear, setAccTitle, diff --git a/packages/mermaid/src/diagrams/quadrant-chart/quadrantRenderer.ts b/packages/mermaid/src/diagrams/quadrant-chart/quadrantRenderer.ts index d272dccd4..c2295da4d 100644 --- a/packages/mermaid/src/diagrams/quadrant-chart/quadrantRenderer.ts +++ b/packages/mermaid/src/diagrams/quadrant-chart/quadrantRenderer.ts @@ -152,7 +152,9 @@ export const draw = (txt: string, id: string, _version: string, diagObj: Diagram .attr('cx', (data: QuadrantPointType) => data.x) .attr('cy', (data: QuadrantPointType) => data.y) .attr('r', (data: QuadrantPointType) => data.radius) - .attr('fill', (data: QuadrantPointType) => data.fill); + .attr('fill', (data: QuadrantPointType) => data.fill) + .attr('stroke', (data: QuadrantPointType) => data.strokeColor) + .attr('stroke-width', (data: QuadrantPointType) => data.strokeWidth); dataPoints .append('text') diff --git a/packages/mermaid/src/diagrams/quadrant-chart/utils.ts b/packages/mermaid/src/diagrams/quadrant-chart/utils.ts new file mode 100644 index 000000000..fca3c8f9a --- /dev/null +++ b/packages/mermaid/src/diagrams/quadrant-chart/utils.ts @@ -0,0 +1,20 @@ +class InvalidStyleError extends Error { + constructor(style: string, value: string, type: string) { + super(`value for ${style} ${value} is invalid, please use a valid ${type}`); + this.name = 'InvalidStyleError'; + } +} + +function validateHexCode(value: string): boolean { + return !/^#?([\dA-Fa-f]{6}|[\dA-Fa-f]{3})$/.test(value); +} + +function validateNumber(value: string): boolean { + return !/^\d+$/.test(value); +} + +function validateSizeInPixels(value: string): boolean { + return !/^\d+px$/.test(value); +} + +export { validateHexCode, validateNumber, validateSizeInPixels, InvalidStyleError }; diff --git a/packages/mermaid/src/docs/syntax/quadrantChart.md b/packages/mermaid/src/docs/syntax/quadrantChart.md index d6793aea6..39bbcafa1 100644 --- a/packages/mermaid/src/docs/syntax/quadrantChart.md +++ b/packages/mermaid/src/docs/syntax/quadrantChart.md @@ -136,3 +136,66 @@ quadrantChart quadrant-3 Delegate quadrant-4 Delete ``` + +### Point styling + +Points can either be styled directly or with defined shared classes + +1. Direct styling + +```md +Point A: [0.9, 0.0] radius: 12 +Point B: [0.8, 0.1] color: #ff3300, radius: 10 +Point C: [0.7, 0.2] radius: 25, color: #00ff33, stroke-color: #10f0f0 +Point D: [0.6, 0.3] radius: 15, stroke-color: #00ff0f, stroke-width: 5px ,color: #ff33f0 +``` + +2. Classes styling + +```md +Point A:::class1: [0.9, 0.0] +Point B:::class2: [0.8, 0.1] +Point C:::class3: [0.7, 0.2] +Point D:::class3: [0.7, 0.2] +classDef class1 color: #109060 +classDef class2 color: #908342, radius : 10, stroke-color: #310085, stroke-width: 10px +classDef class3 color: #f00fff, radius : 10 +``` + +#### Available styles: + +| Parameter | Description | +| ------------ | ---------------------------------------------------------------------- | +| color | Fill color of the point | +| radius | Radius of the point | +| stroke-width | Border width of the point | +| stroke-color | Border color of the point (useless when stroke-width is not specified) | + +```note +Order of preference: +1. Direct styles +2. Class styles +3. Theme styles +``` + +## Example on styling + +```mermaid-example +quadrantChart + title Reach and engagement of campaigns + x-axis Low Reach --> High Reach + y-axis Low Engagement --> High Engagement + quadrant-1 We should expand + quadrant-2 Need to promote + quadrant-3 Re-evaluate + quadrant-4 May be improved + Campaign A: [0.9, 0.0] radius: 12 + Campaign B:::class1: [0.8, 0.1] color: #ff3300, radius: 10 + Campaign C: [0.7, 0.2] radius: 25, color: #00ff33, stroke-color: #10f0f0 + Campaign D: [0.6, 0.3] radius: 15, stroke-color: #00ff0f, stroke-width: 5px ,color: #ff33f0 + Campaign E:::class2: [0.5, 0.4] + Campaign F:::class3: [0.4, 0.5] color: #0000ff + classDef class1 color: #109060 + classDef class2 color: #908342, radius : 10, stroke-color: #310085, stroke-width: 10px + classDef class3 color: #f00fff, radius : 10 +```