Merge pull request #5173 from ilyes-ced/feature/add-point-styling-quadrant-to-charts

feat: Add point styling for quadrant chart
This commit is contained in:
Sidharth Vinod 2024-04-16 09:17:07 +05:30 committed by GitHub
commit 3809732e48
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 540 additions and 17 deletions

View File

@ -1,4 +1,4 @@
import { imgSnapshotTest, renderGraph } from '../../helpers/util.ts'; import { imgSnapshotTest } from '../../helpers/util.ts';
describe('Quadrant Chart', () => { describe('Quadrant Chart', () => {
it('should render if only chart type is provided', () => { it('should render if only chart type is provided', () => {
@ -226,4 +226,52 @@ describe('Quadrant Chart', () => {
); );
cy.get('svg'); 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
`
);
});
}); });

View File

@ -168,3 +168,86 @@ quadrantChart
quadrant-3 Delegate quadrant-3 Delegate
quadrant-4 Delete 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
```

View File

@ -11,6 +11,7 @@
%x point_start %x point_start
%x point_x %x point_x
%x point_y %x point_y
%x class_name
%% %%
\%\%(?!\{)[^\n]* /* skip comments */ \%\%(?!\{)[^\n]* /* skip comments */
[^\}]\%\%[^\n]* /* skip comments */ [^\}]\%\%[^\n]* /* skip comments */
@ -35,6 +36,7 @@ accDescr\s*"{"\s* { this.begin("acc_descr_multiline");}
" "*"quadrant-2"" "* return 'QUADRANT_2'; " "*"quadrant-2"" "* return 'QUADRANT_2';
" "*"quadrant-3"" "* return 'QUADRANT_3'; " "*"quadrant-3"" "* return 'QUADRANT_3';
" "*"quadrant-4"" "* return 'QUADRANT_4'; " "*"quadrant-4"" "* return 'QUADRANT_4';
"classDef" return 'CLASSDEF';
["][`] { this.begin("md_string");} ["][`] { this.begin("md_string");}
<md_string>[^`"]+ { return "MD_STR";} <md_string>[^`"]+ { return "MD_STR";}
@ -43,6 +45,9 @@ accDescr\s*"{"\s* { this.begin("acc_descr_multiline");}
<string>["] this.popState(); <string>["] this.popState();
<string>[^"]* return "STR"; <string>[^"]* return "STR";
\:\:\: {this.begin('class_name')}
<class_name>^\w+ {this.popState(); return 'class_name';}
\s*\:\s*\[\s* {this.begin("point_start"); return 'point_start';} \s*\:\s*\[\s* {this.begin("point_start"); return 'point_start';}
<point_start>(1)|(0(.\d+)?) {this.begin('point_x'); return 'point_x';} <point_start>(1)|(0(.\d+)?) {this.begin('point_x'); return 'point_x';}
<point_start>\s*\]" "* {this.popState();} <point_start>\s*\]" "* {this.popState();}
@ -75,6 +80,31 @@ accDescr\s*"{"\s* { this.begin("acc_descr_multiline");}
%% /* language grammar */ %% /* 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 start
: eol start : eol start
| SPACE start | SPACE start
@ -92,6 +122,7 @@ line
statement statement
: :
| classDefStatement {$$=[];}
| SPACE statement | SPACE statement
| axisDetails | axisDetails
| quadrantDetails | quadrantDetails
@ -103,7 +134,10 @@ statement
; ;
points 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 axisDetails

View File

@ -212,20 +212,34 @@ describe('Testing quadrantChart jison file', () => {
it('should be able to parse points', () => { it('should be able to parse points', () => {
let str = 'quadrantChart\npoint1: [0.1, 0.4]'; let str = 'quadrantChart\npoint1: [0.1, 0.4]';
expect(parserFnConstructor(str)).not.toThrow(); 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(); clearMocks();
str = 'QuadRantChart \n Point1 : [0.1, 0.4] '; str = 'QuadRantChart \n Point1 : [0.1, 0.4] ';
expect(parserFnConstructor(str)).not.toThrow(); 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(); clearMocks();
str = 'QuadRantChart \n "Point1 : (* +=[❤": [1, 0] '; str = 'QuadRantChart \n "Point1 : (* +=[❤": [1, 0] ';
expect(parserFnConstructor(str)).not.toThrow(); expect(parserFnConstructor(str)).not.toThrow();
expect(mockDB.addPoint).toHaveBeenCalledWith( expect(mockDB.addPoint).toHaveBeenCalledWith(
{ text: 'Point1 : (* +=[❤', type: 'text' }, { text: 'Point1 : (* +=[❤', type: 'text' },
'',
'1', '1',
'0' '0',
[]
); );
clearMocks(); clearMocks();
@ -264,15 +278,149 @@ describe('Testing quadrantChart jison file', () => {
expect(mockDB.setQuadrant4Text).toHaveBeenCalledWith({ text: 'Visionaries', type: 'text' }); expect(mockDB.setQuadrant4Text).toHaveBeenCalledWith({ text: 'Visionaries', type: 'text' });
expect(mockDB.addPoint).toHaveBeenCalledWith( expect(mockDB.addPoint).toHaveBeenCalledWith(
{ text: 'Microsoft', type: 'text' }, { text: 'Microsoft', type: 'text' },
'',
'0.75', '0.75',
'0.75' '0.75',
[]
); );
expect(mockDB.addPoint).toHaveBeenCalledWith( expect(mockDB.addPoint).toHaveBeenCalledWith(
{ text: 'Salesforce', type: 'text' }, { text: 'Salesforce', type: 'text' },
'',
'0.55', '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');
}); });
}); });

View File

@ -1,7 +1,7 @@
import { scaleLinear } from 'd3'; import { scaleLinear } from 'd3';
import { log } from '../../logger.js';
import type { BaseDiagramConfig, QuadrantChartConfig } from '../../config.type.js'; import type { BaseDiagramConfig, QuadrantChartConfig } from '../../config.type.js';
import defaultConfig from '../../defaultConfig.js'; import defaultConfig from '../../defaultConfig.js';
import { log } from '../../logger.js';
import { getThemeVariables } from '../../themes/theme-default.js'; import { getThemeVariables } from '../../themes/theme-default.js';
import type { Point } from '../../types.js'; import type { Point } from '../../types.js';
@ -10,7 +10,15 @@ const defaultThemeVariables = getThemeVariables();
export type TextVerticalPos = 'left' | 'center' | 'right'; export type TextVerticalPos = 'left' | 'center' | 'right';
export type TextHorizontalPos = 'top' | 'middle' | 'bottom'; 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; text: string;
} }
@ -23,7 +31,9 @@ export interface QuadrantTextType extends Point {
rotation: number; rotation: number;
} }
export interface QuadrantPointType extends Point { export interface QuadrantPointType
extends Point,
Pick<StylesObject, 'strokeColor' | 'strokeWidth'> {
fill: string; fill: string;
radius: number; radius: number;
text: QuadrantTextType; text: QuadrantTextType;
@ -117,6 +127,7 @@ export class QuadrantBuilder {
private config: QuadrantBuilderConfig; private config: QuadrantBuilderConfig;
private themeConfig: QuadrantBuilderThemeConfig; private themeConfig: QuadrantBuilderThemeConfig;
private data: QuadrantBuilderData; private data: QuadrantBuilderData;
private classes: Record<string, StylesObject> = {};
constructor() { constructor() {
this.config = this.getDefaultConfig(); this.config = this.getDefaultConfig();
@ -191,6 +202,7 @@ export class QuadrantBuilder {
this.config = this.getDefaultConfig(); this.config = this.getDefaultConfig();
this.themeConfig = this.getDefaultThemeConfig(); this.themeConfig = this.getDefaultThemeConfig();
this.data = this.getDefaultData(); this.data = this.getDefaultData();
this.classes = {};
log.info('clear called'); log.info('clear called');
} }
@ -202,6 +214,10 @@ export class QuadrantBuilder {
this.data.points = [...points, ...this.data.points]; this.data.points = [...points, ...this.data.points];
} }
addClass(className: string, styles: StylesObject) {
this.classes[className] = styles;
}
setConfig(config: Partial<QuadrantBuilderConfig>) { setConfig(config: Partial<QuadrantBuilderConfig>) {
log.trace('setConfig called with: ', config); log.trace('setConfig called with: ', config);
this.config = { ...this.config, ...config }; this.config = { ...this.config, ...config };
@ -470,11 +486,15 @@ export class QuadrantBuilder {
.range([quadrantHeight + quadrantTop, quadrantTop]); .range([quadrantHeight + quadrantTop, quadrantTop]);
const points: QuadrantPointType[] = this.data.points.map((point) => { 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 = { const props: QuadrantPointType = {
x: xAxis(point.x), x: xAxis(point.x),
y: yAxis(point.y), y: yAxis(point.y),
fill: this.themeConfig.quadrantPointFill, fill: point.color || this.themeConfig.quadrantPointFill,
radius: this.config.pointRadius, radius: point.radius || this.config.pointRadius,
text: { text: {
text: point.text, text: point.text,
fill: this.themeConfig.quadrantPointTextFill, fill: this.themeConfig.quadrantPointTextFill,
@ -485,6 +505,8 @@ export class QuadrantBuilder {
fontSize: this.config.pointLabelFontSize, fontSize: this.config.pointLabelFontSize,
rotation: 0, rotation: 0,
}, },
strokeColor: point.strokeColor || this.themeConfig.quadrantPointFill,
strokeWidth: point.strokeWidth || '0px',
}; };
return props; return props;
}); });

View File

@ -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)'
);
});
});

View File

@ -9,7 +9,14 @@ import {
setAccDescription, setAccDescription,
clear as commonClear, clear as commonClear,
} from '../common/commonDb.js'; } from '../common/commonDb.js';
import type { StylesObject } from './quadrantBuilder.js';
import { QuadrantBuilder } from './quadrantBuilder.js'; import { QuadrantBuilder } from './quadrantBuilder.js';
import {
validateHexCode,
validateSizeInPixels,
validateNumber,
InvalidStyleError,
} from './utils.js';
const config = getConfig(); const config = getConfig();
@ -56,8 +63,52 @@ function setYAxisBottomText(textObj: LexTextObj) {
quadrantBuilder.setData({ yAxisBottomText: textSanitizer(textObj.text) }); quadrantBuilder.setData({ yAxisBottomText: textSanitizer(textObj.text) });
} }
function addPoint(textObj: LexTextObj, x: number, y: number) { function parseStyles(styles: string[]): StylesObject {
quadrantBuilder.addPoints([{ x, y, text: textSanitizer(textObj.text) }]); 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) { function setWidth(width: number) {
@ -111,7 +162,9 @@ export default {
setXAxisRightText, setXAxisRightText,
setYAxisTopText, setYAxisTopText,
setYAxisBottomText, setYAxisBottomText,
parseStyles,
addPoint, addPoint,
addClass,
getQuadrantData, getQuadrantData,
clear, clear,
setAccTitle, setAccTitle,

View File

@ -152,7 +152,9 @@ export const draw = (txt: string, id: string, _version: string, diagObj: Diagram
.attr('cx', (data: QuadrantPointType) => data.x) .attr('cx', (data: QuadrantPointType) => data.x)
.attr('cy', (data: QuadrantPointType) => data.y) .attr('cy', (data: QuadrantPointType) => data.y)
.attr('r', (data: QuadrantPointType) => data.radius) .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 dataPoints
.append('text') .append('text')

View File

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

View File

@ -136,3 +136,66 @@ quadrantChart
quadrant-3 Delegate quadrant-3 Delegate
quadrant-4 Delete 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
```