mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-01-14 06:43:25 +08:00
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:
commit
3809732e48
@ -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
|
||||
`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@ -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
|
||||
```
|
||||
|
@ -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");}
|
||||
<md_string>[^`"]+ { return "MD_STR";}
|
||||
@ -43,6 +45,9 @@ accDescr\s*"{"\s* { this.begin("acc_descr_multiline");}
|
||||
<string>["] this.popState();
|
||||
<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';}
|
||||
<point_start>(1)|(0(.\d+)?) {this.begin('point_x'); return 'point_x';}
|
||||
<point_start>\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
|
||||
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
@ -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<StylesObject, 'strokeColor' | 'strokeWidth'> {
|
||||
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<string, StylesObject> = {};
|
||||
|
||||
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<QuadrantBuilderConfig>) {
|
||||
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;
|
||||
});
|
||||
|
@ -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)'
|
||||
);
|
||||
});
|
||||
});
|
@ -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,
|
||||
|
@ -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')
|
||||
|
20
packages/mermaid/src/diagrams/quadrant-chart/utils.ts
Normal file
20
packages/mermaid/src/diagrams/quadrant-chart/utils.ts
Normal 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 };
|
@ -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
|
||||
```
|
||||
|
Loading…
x
Reference in New Issue
Block a user