mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-08-15 06:19:24 +02:00
📊 Add radar chart
This commit is contained in:
@@ -239,6 +239,22 @@ Code is the heart of every software project. We strive to make it better. Who if
|
|||||||
|
|
||||||
The core of Mermaid is located under `packages/mermaid/src`.
|
The core of Mermaid is located under `packages/mermaid/src`.
|
||||||
|
|
||||||
|
### Building Mermaid Locally
|
||||||
|
|
||||||
|
**Host**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
**Docker**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./run build
|
||||||
|
```
|
||||||
|
|
||||||
|
This will build the Mermaid library and the documentation site.
|
||||||
|
|
||||||
### Running Mermaid Locally
|
### Running Mermaid Locally
|
||||||
|
|
||||||
**Host**
|
**Host**
|
||||||
|
@@ -12,4 +12,4 @@
|
|||||||
|
|
||||||
> `const` **configKeys**: `Set`<`string`>
|
> `const` **configKeys**: `Set`<`string`>
|
||||||
|
|
||||||
Defined in: [packages/mermaid/src/defaultConfig.ts:270](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/defaultConfig.ts#L270)
|
Defined in: [packages/mermaid/src/defaultConfig.ts:274](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/defaultConfig.ts#L274)
|
||||||
|
@@ -105,7 +105,7 @@ You can set this attribute to base the seed on a static string.
|
|||||||
|
|
||||||
> `optional` **dompurifyConfig**: `Config`
|
> `optional` **dompurifyConfig**: `Config`
|
||||||
|
|
||||||
Defined in: [packages/mermaid/src/config.type.ts:202](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L202)
|
Defined in: [packages/mermaid/src/config.type.ts:203](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L203)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -167,7 +167,7 @@ See <https://developer.mozilla.org/en-US/docs/Web/CSS/font-family>
|
|||||||
|
|
||||||
> `optional` **fontSize**: `number`
|
> `optional` **fontSize**: `number`
|
||||||
|
|
||||||
Defined in: [packages/mermaid/src/config.type.ts:204](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L204)
|
Defined in: [packages/mermaid/src/config.type.ts:205](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L205)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -280,7 +280,7 @@ Defines which main look to use for the diagram.
|
|||||||
|
|
||||||
> `optional` **markdownAutoWrap**: `boolean`
|
> `optional` **markdownAutoWrap**: `boolean`
|
||||||
|
|
||||||
Defined in: [packages/mermaid/src/config.type.ts:205](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L205)
|
Defined in: [packages/mermaid/src/config.type.ts:206](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L206)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -336,6 +336,14 @@ Defined in: [packages/mermaid/src/config.type.ts:191](https://github.com/mermaid
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### radar?
|
||||||
|
|
||||||
|
> `optional` **radar**: `RadarDiagramConfig`
|
||||||
|
|
||||||
|
Defined in: [packages/mermaid/src/config.type.ts:202](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L202)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### requirement?
|
### requirement?
|
||||||
|
|
||||||
> `optional` **requirement**: `RequirementDiagramConfig`
|
> `optional` **requirement**: `RequirementDiagramConfig`
|
||||||
@@ -404,7 +412,7 @@ Defined in: [packages/mermaid/src/config.type.ts:188](https://github.com/mermaid
|
|||||||
|
|
||||||
> `optional` **suppressErrorRendering**: `boolean`
|
> `optional` **suppressErrorRendering**: `boolean`
|
||||||
|
|
||||||
Defined in: [packages/mermaid/src/config.type.ts:211](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L211)
|
Defined in: [packages/mermaid/src/config.type.ts:212](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L212)
|
||||||
|
|
||||||
Suppresses inserting 'Syntax error' diagram in the DOM.
|
Suppresses inserting 'Syntax error' diagram in the DOM.
|
||||||
This is useful when you want to control how to handle syntax errors in your application.
|
This is useful when you want to control how to handle syntax errors in your application.
|
||||||
@@ -450,7 +458,7 @@ Defined in: [packages/mermaid/src/config.type.ts:186](https://github.com/mermaid
|
|||||||
|
|
||||||
> `optional` **wrap**: `boolean`
|
> `optional` **wrap**: `boolean`
|
||||||
|
|
||||||
Defined in: [packages/mermaid/src/config.type.ts:203](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L203)
|
Defined in: [packages/mermaid/src/config.type.ts:204](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L204)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
253
docs/syntax/radar.md
Normal file
253
docs/syntax/radar.md
Normal file
@@ -0,0 +1,253 @@
|
|||||||
|
> **Warning**
|
||||||
|
>
|
||||||
|
> ## THIS IS AN AUTOGENERATED FILE. DO NOT EDIT.
|
||||||
|
>
|
||||||
|
> ## Please edit the corresponding file in [/packages/mermaid/src/docs/syntax/radar.md](../../packages/mermaid/src/docs/syntax/radar.md).
|
||||||
|
|
||||||
|
# Radar Diagram (v\<MERMAID_RELEASE_VERSION>+)
|
||||||
|
|
||||||
|
## Introduction
|
||||||
|
|
||||||
|
A radar diagram is a simple way to plot low-dimensional data in a circular format.
|
||||||
|
|
||||||
|
It is also known as a **radar chart**, **spider chart**, **star chart**, **cobweb chart**, **polar chart**, or **Kiviat diagram**.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
This diagram type is particularly useful for developers, data scientists, and engineers who require a clear and concise way to represent data in a circular format.
|
||||||
|
|
||||||
|
It is commonly used to graphically summarize and compare the performance of multiple entities across multiple dimensions.
|
||||||
|
|
||||||
|
## Syntax
|
||||||
|
|
||||||
|
```md
|
||||||
|
radar-beta
|
||||||
|
axis A, B, C, D, E
|
||||||
|
curve c1{1,2,3,4,5}
|
||||||
|
curve c2{5,4,3,2,1}
|
||||||
|
... More Fields ...
|
||||||
|
```
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
```mermaid-example
|
||||||
|
---
|
||||||
|
title: "Grades"
|
||||||
|
---
|
||||||
|
radar-beta
|
||||||
|
axis m["Math"], s["Science"], e["English"]
|
||||||
|
axis h["History"], g["Geography"], a["Art"]
|
||||||
|
curve a["Alice"]{85, 90, 80, 70, 75, 90}
|
||||||
|
curve b["Bob"]{70, 75, 85, 80, 90, 85}
|
||||||
|
|
||||||
|
max 100
|
||||||
|
min 0
|
||||||
|
```
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
---
|
||||||
|
title: "Grades"
|
||||||
|
---
|
||||||
|
radar-beta
|
||||||
|
axis m["Math"], s["Science"], e["English"]
|
||||||
|
axis h["History"], g["Geography"], a["Art"]
|
||||||
|
curve a["Alice"]{85, 90, 80, 70, 75, 90}
|
||||||
|
curve b["Bob"]{70, 75, 85, 80, 90, 85}
|
||||||
|
|
||||||
|
max 100
|
||||||
|
min 0
|
||||||
|
```
|
||||||
|
|
||||||
|
```mermaid-example
|
||||||
|
radar-beta
|
||||||
|
title Restaurant Comparison
|
||||||
|
axis food["Food Quality"], service["Service"], price["Price"]
|
||||||
|
axis ambiance["Ambiance"],
|
||||||
|
|
||||||
|
curve a["Restaurant A"]{4, 3, 2, 4}
|
||||||
|
curve b["Restaurant B"]{3, 4, 3, 3}
|
||||||
|
curve c["Restaurant C"]{2, 3, 4, 2}
|
||||||
|
curve d["Restaurant D"]{2, 2, 4, 3}
|
||||||
|
|
||||||
|
graticule polygon
|
||||||
|
max 5
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
radar-beta
|
||||||
|
title Restaurant Comparison
|
||||||
|
axis food["Food Quality"], service["Service"], price["Price"]
|
||||||
|
axis ambiance["Ambiance"],
|
||||||
|
|
||||||
|
curve a["Restaurant A"]{4, 3, 2, 4}
|
||||||
|
curve b["Restaurant B"]{3, 4, 3, 3}
|
||||||
|
curve c["Restaurant C"]{2, 3, 4, 2}
|
||||||
|
curve d["Restaurant D"]{2, 2, 4, 3}
|
||||||
|
|
||||||
|
graticule polygon
|
||||||
|
max 5
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## Details of Syntax
|
||||||
|
|
||||||
|
### Title
|
||||||
|
|
||||||
|
`title`: The title is an optional field that allows to render a title at the top of the radar diagram.
|
||||||
|
|
||||||
|
```
|
||||||
|
radar-beta
|
||||||
|
title Title of the Radar Diagram
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
### Axis
|
||||||
|
|
||||||
|
`axis`: The axis keyword is used to define the axes of the radar diagram.
|
||||||
|
|
||||||
|
Each axis is represented by an ID and an optional label.
|
||||||
|
|
||||||
|
Multiple axes can be defined in a single line.
|
||||||
|
|
||||||
|
```
|
||||||
|
radar-beta
|
||||||
|
axis id1["Label1"]
|
||||||
|
axis id2["Label2"], id3["Label3"]
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
### Curve
|
||||||
|
|
||||||
|
`curve`: The curve keyword is used to define the data points for a curve in the radar diagram.
|
||||||
|
|
||||||
|
Each curve is represented by an ID, an optional label, and a list of values.
|
||||||
|
|
||||||
|
Values can be defined by a list of numbers or a list of key-value pairs. If key-value pairs are used, the key represents the axis ID and the value represents the data point. Else, the data points are assumed to be in the order of the axes defined.
|
||||||
|
|
||||||
|
Multiple curves can be defined in a single line.
|
||||||
|
|
||||||
|
```
|
||||||
|
radar-beta
|
||||||
|
axis axis1, axis2, axis3
|
||||||
|
curve id1["Label1"]{1, 2, 3}
|
||||||
|
curve id2["Label2"]{4, 5, 6}, id3{7, 8, 9}
|
||||||
|
curve id4{ axis3: 30, axis1: 20, axis2: 10 }
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
### Options
|
||||||
|
|
||||||
|
- `showLegend`: The showLegend keyword is used to show or hide the legend in the radar diagram. The legend is shown by default.
|
||||||
|
- `max`: The maximum value for the radar diagram. This is used to scale the radar diagram. If not provided, the maximum value is calculated from the data points.
|
||||||
|
- `min`: The minimum value for the radar diagram. This is used to scale the radar diagram. If not provided, the minimum value is `0`.
|
||||||
|
- `graticule`: The graticule keyword is used to define the type of graticule to be rendered in the radar diagram. The graticule can be `circle` or `polygon`. If not provided, the default graticule is `circle`.
|
||||||
|
- `ticks`: The ticks keyword is used to define the number of ticks on the graticule. It is the number of concentric circles or polygons drawn to indicate the scale of the radar diagram. If not provided, the default number of ticks is `5`.
|
||||||
|
|
||||||
|
```
|
||||||
|
radar-beta
|
||||||
|
...
|
||||||
|
showLegend true
|
||||||
|
max 100
|
||||||
|
min 0
|
||||||
|
graticule circle
|
||||||
|
ticks 5
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Please refer to the [configuration](/config/schema-docs/config-defs-radar-diagram-config.html) guide for details.
|
||||||
|
|
||||||
|
| Parameter | Description | Default Value |
|
||||||
|
| --------------- | ---------------------------------------- | ------------- |
|
||||||
|
| width | Width of the radar diagram | `600` |
|
||||||
|
| height | Height of the radar diagram | `600` |
|
||||||
|
| marginTop | Top margin of the radar diagram | `50` |
|
||||||
|
| marginBottom | Bottom margin of the radar diagram | `50` |
|
||||||
|
| marginLeft | Left margin of the radar diagram | `50` |
|
||||||
|
| marginRight | Right margin of the radar diagram | `50` |
|
||||||
|
| axisScaleFactor | Scale factor for the axis | `1` |
|
||||||
|
| axisLabelFactor | Factor to adjust the axis label position | `1.05` |
|
||||||
|
| curveTension | Tension for the rounded curves | `0.17` |
|
||||||
|
|
||||||
|
## Theme Variables
|
||||||
|
|
||||||
|
### Global Theme Variables
|
||||||
|
|
||||||
|
> **Note**
|
||||||
|
> The default values for these variables depend on the theme used. To override the default values, set the desired values in the themeVariables section of the configuration:
|
||||||
|
> %%{init: {"themeVariables": {"cScale0": "#FF0000", "cScale1": "#00FF00"}} }%%
|
||||||
|
|
||||||
|
Radar charts support the color scales `cScale${i}` where `i` is a number from `0` to the theme's maximum number of colors in its color scale. Usually, the maximum number of colors is `12`.
|
||||||
|
|
||||||
|
| Property | Description |
|
||||||
|
| ---------- | ------------------------------ |
|
||||||
|
| fontSize | Font size of the title |
|
||||||
|
| titleColor | Color of the title |
|
||||||
|
| cScale${i} | Color scale for the i-th curve |
|
||||||
|
|
||||||
|
### Radar Style Options
|
||||||
|
|
||||||
|
> **Note**
|
||||||
|
> Specific variables for radar resides inside the `radar` key. To set the radar style options, use this syntax.
|
||||||
|
> %%{init: {"themeVariables": {"radar": {"axisColor": "#FF0000"}} } }%%
|
||||||
|
|
||||||
|
| Property | Description | Default Value |
|
||||||
|
| -------------------- | ---------------------------- | ------------- |
|
||||||
|
| axisColor | Color of the axis lines | `black` |
|
||||||
|
| axisStrokeWidth | Width of the axis lines | `1` |
|
||||||
|
| axisLabelFontSize | Font size of the axis labels | `12px` |
|
||||||
|
| curveOpacity | Opacity of the curves | `0.7` |
|
||||||
|
| curveStrokeWidth | Width of the curves | `2` |
|
||||||
|
| graticuleColor | Color of the graticule | `black` |
|
||||||
|
| graticuleOpacity | Opacity of the graticule | `0.5` |
|
||||||
|
| graticuleStrokeWidth | Width of the graticule | `1` |
|
||||||
|
| legendBoxSize | Size of the legend box | `10` |
|
||||||
|
| legendFontSize | Font size of the legend | `14px` |
|
||||||
|
|
||||||
|
## Example on config and theme
|
||||||
|
|
||||||
|
```mermaid-example
|
||||||
|
---
|
||||||
|
config:
|
||||||
|
radar:
|
||||||
|
axisScaleFactor: 0.25
|
||||||
|
curveTension: 0.1
|
||||||
|
theme: base
|
||||||
|
themeVariables:
|
||||||
|
cScale0: "#FF0000"
|
||||||
|
cScale1: "#00FF00"
|
||||||
|
cScale2: "#0000FF"
|
||||||
|
radar:
|
||||||
|
curveOpacity: 0
|
||||||
|
---
|
||||||
|
radar-beta
|
||||||
|
axis A, B, C, D, E
|
||||||
|
curve c1{1,2,3,4,5}
|
||||||
|
curve c2{5,4,3,2,1}
|
||||||
|
curve c3{3,3,3,3,3}
|
||||||
|
```
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
---
|
||||||
|
config:
|
||||||
|
radar:
|
||||||
|
axisScaleFactor: 0.25
|
||||||
|
curveTension: 0.1
|
||||||
|
theme: base
|
||||||
|
themeVariables:
|
||||||
|
cScale0: "#FF0000"
|
||||||
|
cScale1: "#00FF00"
|
||||||
|
cScale2: "#0000FF"
|
||||||
|
radar:
|
||||||
|
curveOpacity: 0
|
||||||
|
---
|
||||||
|
radar-beta
|
||||||
|
axis A, B, C, D, E
|
||||||
|
curve c1{1,2,3,4,5}
|
||||||
|
curve c2{5,4,3,2,1}
|
||||||
|
curve c3{3,3,3,3,3}
|
||||||
|
```
|
||||||
|
|
||||||
|
<!--- cspell:ignore Kiviat --->
|
@@ -199,6 +199,7 @@ export interface MermaidConfig {
|
|||||||
sankey?: SankeyDiagramConfig;
|
sankey?: SankeyDiagramConfig;
|
||||||
packet?: PacketDiagramConfig;
|
packet?: PacketDiagramConfig;
|
||||||
block?: BlockDiagramConfig;
|
block?: BlockDiagramConfig;
|
||||||
|
radar?: RadarDiagramConfig;
|
||||||
dompurifyConfig?: DOMPurifyConfiguration;
|
dompurifyConfig?: DOMPurifyConfiguration;
|
||||||
wrap?: boolean;
|
wrap?: boolean;
|
||||||
fontSize?: number;
|
fontSize?: number;
|
||||||
@@ -1526,6 +1527,50 @@ export interface PacketDiagramConfig extends BaseDiagramConfig {
|
|||||||
export interface BlockDiagramConfig extends BaseDiagramConfig {
|
export interface BlockDiagramConfig extends BaseDiagramConfig {
|
||||||
padding?: number;
|
padding?: number;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* The object containing configurations specific for radar diagrams.
|
||||||
|
*
|
||||||
|
* This interface was referenced by `MermaidConfig`'s JSON-Schema
|
||||||
|
* via the `definition` "RadarDiagramConfig".
|
||||||
|
*/
|
||||||
|
export interface RadarDiagramConfig extends BaseDiagramConfig {
|
||||||
|
/**
|
||||||
|
* The size of the radar diagram.
|
||||||
|
*/
|
||||||
|
width?: number;
|
||||||
|
/**
|
||||||
|
* The size of the radar diagram.
|
||||||
|
*/
|
||||||
|
height?: number;
|
||||||
|
/**
|
||||||
|
* The margin from the top of the radar diagram.
|
||||||
|
*/
|
||||||
|
marginTop?: number;
|
||||||
|
/**
|
||||||
|
* The margin from the right of the radar diagram.
|
||||||
|
*/
|
||||||
|
marginRight?: number;
|
||||||
|
/**
|
||||||
|
* The margin from the bottom of the radar diagram.
|
||||||
|
*/
|
||||||
|
marginBottom?: number;
|
||||||
|
/**
|
||||||
|
* The margin from the left of the radar diagram.
|
||||||
|
*/
|
||||||
|
marginLeft?: number;
|
||||||
|
/**
|
||||||
|
* The scale factor of the axis.
|
||||||
|
*/
|
||||||
|
axisScaleFactor?: number;
|
||||||
|
/**
|
||||||
|
* The scale factor of the axis label.
|
||||||
|
*/
|
||||||
|
axisLabelFactor?: number;
|
||||||
|
/**
|
||||||
|
* The tension factor for the Catmull-Rom spline conversion to cubic Bézier curves.
|
||||||
|
*/
|
||||||
|
curveTension?: number;
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* This interface was referenced by `MermaidConfig`'s JSON-Schema
|
* This interface was referenced by `MermaidConfig`'s JSON-Schema
|
||||||
* via the `definition` "FontConfig".
|
* via the `definition` "FontConfig".
|
||||||
|
@@ -255,8 +255,12 @@ const config: RequiredDeep<MermaidConfig> = {
|
|||||||
packet: {
|
packet: {
|
||||||
...defaultConfigJson.packet,
|
...defaultConfigJson.packet,
|
||||||
},
|
},
|
||||||
|
radar: {
|
||||||
|
...defaultConfigJson.radar,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const keyify = (obj: any, prefix = ''): string[] =>
|
const keyify = (obj: any, prefix = ''): string[] =>
|
||||||
Object.keys(obj).reduce((res: string[], el): string[] => {
|
Object.keys(obj).reduce((res: string[], el): string[] => {
|
||||||
if (Array.isArray(obj[el])) {
|
if (Array.isArray(obj[el])) {
|
||||||
|
@@ -22,6 +22,7 @@ import mindmap from '../diagrams/mindmap/detector.js';
|
|||||||
import kanban from '../diagrams/kanban/detector.js';
|
import kanban from '../diagrams/kanban/detector.js';
|
||||||
import sankey from '../diagrams/sankey/sankeyDetector.js';
|
import sankey from '../diagrams/sankey/sankeyDetector.js';
|
||||||
import { packet } from '../diagrams/packet/detector.js';
|
import { packet } from '../diagrams/packet/detector.js';
|
||||||
|
import { radar } from '../diagrams/radar/detector.js';
|
||||||
import block from '../diagrams/block/blockDetector.js';
|
import block from '../diagrams/block/blockDetector.js';
|
||||||
import architecture from '../diagrams/architecture/architectureDetector.js';
|
import architecture from '../diagrams/architecture/architectureDetector.js';
|
||||||
import { registerLazyLoadedDiagrams } from './detectType.js';
|
import { registerLazyLoadedDiagrams } from './detectType.js';
|
||||||
@@ -94,6 +95,7 @@ export const addDiagrams = () => {
|
|||||||
packet,
|
packet,
|
||||||
xychart,
|
xychart,
|
||||||
block,
|
block,
|
||||||
architecture
|
architecture,
|
||||||
|
radar
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
128
packages/mermaid/src/diagrams/radar/db.ts
Normal file
128
packages/mermaid/src/diagrams/radar/db.ts
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
import { getConfig as commonGetConfig } from '../../config.js';
|
||||||
|
import type { RadarDiagramConfig } from '../../config.type.js';
|
||||||
|
import DEFAULT_CONFIG from '../../defaultConfig.js';
|
||||||
|
import { cleanAndMerge } from '../../utils.js';
|
||||||
|
import {
|
||||||
|
clear as commonClear,
|
||||||
|
getAccDescription,
|
||||||
|
getAccTitle,
|
||||||
|
getDiagramTitle,
|
||||||
|
setAccDescription,
|
||||||
|
setAccTitle,
|
||||||
|
setDiagramTitle,
|
||||||
|
} from '../common/commonDb.js';
|
||||||
|
import type {
|
||||||
|
Axis,
|
||||||
|
Curve,
|
||||||
|
Option,
|
||||||
|
Entry,
|
||||||
|
} from '../../../../parser/dist/src/language/generated/ast.js';
|
||||||
|
import type { RadarAxis, RadarCurve, RadarOptions, RadarDB, RadarData } from './types.js';
|
||||||
|
|
||||||
|
const defaultOptions: RadarOptions = {
|
||||||
|
showLegend: true,
|
||||||
|
ticks: 5,
|
||||||
|
max: null,
|
||||||
|
min: 0,
|
||||||
|
graticule: 'circle',
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultRadarData: RadarData = {
|
||||||
|
axes: [],
|
||||||
|
curves: [],
|
||||||
|
options: defaultOptions,
|
||||||
|
};
|
||||||
|
|
||||||
|
let data: RadarData = structuredClone(defaultRadarData);
|
||||||
|
|
||||||
|
const DEFAULT_RADAR_CONFIG: Required<RadarDiagramConfig> = DEFAULT_CONFIG.radar;
|
||||||
|
|
||||||
|
const getConfig = (): Required<RadarDiagramConfig> => {
|
||||||
|
const config = cleanAndMerge({
|
||||||
|
...DEFAULT_RADAR_CONFIG,
|
||||||
|
...commonGetConfig().radar,
|
||||||
|
});
|
||||||
|
return config;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAxes = (): RadarAxis[] => data.axes;
|
||||||
|
const getCurves = (): RadarCurve[] => data.curves;
|
||||||
|
const getOptions = (): RadarOptions => data.options;
|
||||||
|
|
||||||
|
const setAxes = (axes: Axis[]) => {
|
||||||
|
data.axes = axes.map((axis) => {
|
||||||
|
return {
|
||||||
|
name: axis.name,
|
||||||
|
label: axis.label ?? axis.name,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const setCurves = (curves: Curve[]) => {
|
||||||
|
data.curves = curves.map((curve) => {
|
||||||
|
return {
|
||||||
|
name: curve.name,
|
||||||
|
label: curve.label ?? curve.name,
|
||||||
|
entries: computeCurveEntries(curve.entries),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const computeCurveEntries = (entries: Entry[]): number[] => {
|
||||||
|
// If entries have axis reference, we must order them according to the axes
|
||||||
|
if (entries[0].axis == undefined) {
|
||||||
|
return entries.map((entry) => entry.value);
|
||||||
|
}
|
||||||
|
const axes = getAxes();
|
||||||
|
if (axes.length === 0) {
|
||||||
|
throw new Error('Axes must be populated before curves for reference entries');
|
||||||
|
}
|
||||||
|
return axes.map((axis) => {
|
||||||
|
const entry = entries.find((entry) => entry.axis?.$refText === axis.name);
|
||||||
|
if (entry === undefined) {
|
||||||
|
throw new Error('Missing entry for axis ' + axis.label);
|
||||||
|
}
|
||||||
|
return entry.value;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const setOptions = (options: Option[]) => {
|
||||||
|
// Create a map from option names to option objects for quick lookup
|
||||||
|
const optionMap = options.reduce(
|
||||||
|
(acc, option) => {
|
||||||
|
acc[option.name] = option;
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{} as Record<string, Option>
|
||||||
|
);
|
||||||
|
|
||||||
|
data.options = {
|
||||||
|
showLegend: (optionMap.showLegend?.value as boolean) ?? defaultOptions.showLegend,
|
||||||
|
ticks: (optionMap.ticks?.value as number) ?? defaultOptions.ticks,
|
||||||
|
max: (optionMap.max?.value as number) ?? defaultOptions.max,
|
||||||
|
min: (optionMap.min?.value as number) ?? defaultOptions.min,
|
||||||
|
graticule: (optionMap.graticule?.value as 'circle' | 'polygon') ?? defaultOptions.graticule,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const clear = () => {
|
||||||
|
commonClear();
|
||||||
|
data = structuredClone(defaultRadarData);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const db: RadarDB = {
|
||||||
|
getAxes,
|
||||||
|
getCurves,
|
||||||
|
getOptions,
|
||||||
|
setAxes,
|
||||||
|
setCurves,
|
||||||
|
setOptions,
|
||||||
|
getConfig,
|
||||||
|
clear,
|
||||||
|
setAccTitle,
|
||||||
|
getAccTitle,
|
||||||
|
setDiagramTitle,
|
||||||
|
getDiagramTitle,
|
||||||
|
getAccDescription,
|
||||||
|
setAccDescription,
|
||||||
|
};
|
22
packages/mermaid/src/diagrams/radar/detector.ts
Normal file
22
packages/mermaid/src/diagrams/radar/detector.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import type {
|
||||||
|
DiagramDetector,
|
||||||
|
DiagramLoader,
|
||||||
|
ExternalDiagramDefinition,
|
||||||
|
} from '../../diagram-api/types.js';
|
||||||
|
|
||||||
|
const id = 'radar';
|
||||||
|
|
||||||
|
const detector: DiagramDetector = (txt) => {
|
||||||
|
return /^\s*radar-beta/.test(txt);
|
||||||
|
};
|
||||||
|
|
||||||
|
const loader: DiagramLoader = async () => {
|
||||||
|
const { diagram } = await import('./diagram.js');
|
||||||
|
return { id, diagram };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const radar: ExternalDiagramDefinition = {
|
||||||
|
id,
|
||||||
|
detector,
|
||||||
|
loader,
|
||||||
|
};
|
12
packages/mermaid/src/diagrams/radar/diagram.ts
Normal file
12
packages/mermaid/src/diagrams/radar/diagram.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import type { DiagramDefinition } from '../../diagram-api/types.js';
|
||||||
|
import { db } from './db.js';
|
||||||
|
import { parser } from './parser.js';
|
||||||
|
import { renderer } from './renderer.js';
|
||||||
|
import { styles } from './styles.js';
|
||||||
|
|
||||||
|
export const diagram: DiagramDefinition = {
|
||||||
|
parser,
|
||||||
|
db,
|
||||||
|
renderer,
|
||||||
|
styles,
|
||||||
|
};
|
23
packages/mermaid/src/diagrams/radar/parser.ts
Normal file
23
packages/mermaid/src/diagrams/radar/parser.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import type { Radar } from '@mermaid-js/parser';
|
||||||
|
import { parse } from '@mermaid-js/parser';
|
||||||
|
import type { ParserDefinition } from '../../diagram-api/types.js';
|
||||||
|
import { log } from '../../logger.js';
|
||||||
|
import { populateCommonDb } from '../common/populateCommonDb.js';
|
||||||
|
import { db } from './db.js';
|
||||||
|
|
||||||
|
const populate = (ast: Radar) => {
|
||||||
|
populateCommonDb(ast, db);
|
||||||
|
const { axes, curves, options } = ast;
|
||||||
|
// Here we can add specific logic between the AST and the DB
|
||||||
|
db.setAxes(axes);
|
||||||
|
db.setCurves(curves);
|
||||||
|
db.setOptions(options);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const parser: ParserDefinition = {
|
||||||
|
parse: async (input: string): Promise<void> => {
|
||||||
|
const ast: Radar = await parse('radar', input);
|
||||||
|
log.debug(ast);
|
||||||
|
populate(ast);
|
||||||
|
},
|
||||||
|
};
|
167
packages/mermaid/src/diagrams/radar/radar.spec.ts
Normal file
167
packages/mermaid/src/diagrams/radar/radar.spec.ts
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
import { it, describe, expect } from 'vitest';
|
||||||
|
import { db } from './db.js';
|
||||||
|
import { parser } from './parser.js';
|
||||||
|
|
||||||
|
const {
|
||||||
|
clear,
|
||||||
|
getDiagramTitle,
|
||||||
|
getAccTitle,
|
||||||
|
getAccDescription,
|
||||||
|
getAxes,
|
||||||
|
getCurves,
|
||||||
|
getOptions,
|
||||||
|
getConfig,
|
||||||
|
} = db;
|
||||||
|
|
||||||
|
describe('radar diagrams', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
clear();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle a simple radar definition', async () => {
|
||||||
|
const str = `radar-beta
|
||||||
|
axis A,B,C
|
||||||
|
curve mycurve{1,2,3}`;
|
||||||
|
await expect(parser.parse(str)).resolves.not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle diagram with data and title', async () => {
|
||||||
|
const str = `radar-beta
|
||||||
|
title Radar diagram
|
||||||
|
accTitle: Radar accTitle
|
||||||
|
accDescr: Radar accDescription
|
||||||
|
axis A["Axis A"], B["Axis B"] ,C["Axis C"]
|
||||||
|
curve mycurve["My Curve"]{1,2,3}
|
||||||
|
`;
|
||||||
|
await expect(parser.parse(str)).resolves.not.toThrow();
|
||||||
|
expect(getDiagramTitle()).toMatchInlineSnapshot('"Radar diagram"');
|
||||||
|
expect(getAccTitle()).toMatchInlineSnapshot('"Radar accTitle"');
|
||||||
|
expect(getAccDescription()).toMatchInlineSnapshot('"Radar accDescription"');
|
||||||
|
expect(getAxes()).toMatchInlineSnapshot(`
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"label": "Axis A",
|
||||||
|
"name": "A",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Axis B",
|
||||||
|
"name": "B",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Axis C",
|
||||||
|
"name": "C",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
`);
|
||||||
|
expect(getCurves()).toMatchInlineSnapshot(`
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"entries": [
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
3,
|
||||||
|
],
|
||||||
|
"label": "My Curve",
|
||||||
|
"name": "mycurve",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
`);
|
||||||
|
expect(getOptions()).toMatchInlineSnapshot(`
|
||||||
|
{
|
||||||
|
"graticule": "circle",
|
||||||
|
"max": null,
|
||||||
|
"min": 0,
|
||||||
|
"showLegend": true,
|
||||||
|
"ticks": 5,
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle a radar diagram with options', async () => {
|
||||||
|
const str = `radar-beta
|
||||||
|
ticks 10
|
||||||
|
showLegend false
|
||||||
|
graticule polygon
|
||||||
|
min 1
|
||||||
|
max 10
|
||||||
|
`;
|
||||||
|
await expect(parser.parse(str)).resolves.not.toThrow();
|
||||||
|
expect(getOptions()).toMatchInlineSnapshot(`
|
||||||
|
{
|
||||||
|
"graticule": "polygon",
|
||||||
|
"max": 10,
|
||||||
|
"min": 1,
|
||||||
|
"showLegend": false,
|
||||||
|
"ticks": 10,
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle curve with detailed data in any order', async () => {
|
||||||
|
const str = `radar-beta
|
||||||
|
axis A,B,C
|
||||||
|
curve mycurve{ C: 3, A: 1, B: 2 }`;
|
||||||
|
await expect(parser.parse(str)).resolves.not.toThrow();
|
||||||
|
expect(getCurves()).toMatchInlineSnapshot(`
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"entries": [
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
3,
|
||||||
|
],
|
||||||
|
"label": "mycurve",
|
||||||
|
"name": "mycurve",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle radar diagram with comments', async () => {
|
||||||
|
const str = `radar-beta
|
||||||
|
%% This is a comment
|
||||||
|
axis A,B,C
|
||||||
|
%% This is another comment
|
||||||
|
curve mycurve{1,2,3}
|
||||||
|
`;
|
||||||
|
await expect(parser.parse(str)).resolves.not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle radar diagram with config override', async () => {
|
||||||
|
const str = `
|
||||||
|
%%{init: {'radar': {'marginTop': 80, 'axisLabelFactor': 1.25}}}%%
|
||||||
|
radar-beta
|
||||||
|
axis A,B,C
|
||||||
|
curve mycurve{1,2,3}
|
||||||
|
`;
|
||||||
|
await expect(parser.parse(str)).resolves.not.toThrow();
|
||||||
|
|
||||||
|
// TODO: ✨ Fix this test
|
||||||
|
// expect(getConfig().marginTop).toBe(80);
|
||||||
|
// expect(getConfig().axisLabelFactor).toBe(1.25);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse radar diagram with theme override', async () => {
|
||||||
|
const str = `
|
||||||
|
%%{init: { "theme": "base", "themeVariables": {'fontSize': 80, 'cScale0': '#123456' }}}%%
|
||||||
|
radar-beta:
|
||||||
|
axis A,B,C
|
||||||
|
curve mycurve{1,2,3}
|
||||||
|
`;
|
||||||
|
await expect(parser.parse(str)).resolves.not.toThrow();
|
||||||
|
|
||||||
|
// TODO: ✨ Add tests for theme override
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle radar diagram with radar style override', async () => {
|
||||||
|
const str = `
|
||||||
|
%%{init: { "theme": "base", "themeVariables": {'fontSize': 10, 'radar': { 'axisColor': '#FF0000' }}}}%%
|
||||||
|
radar-beta
|
||||||
|
axis A,B,C
|
||||||
|
curve mycurve{1,2,3}
|
||||||
|
`;
|
||||||
|
await expect(parser.parse(str)).resolves.not.toThrow();
|
||||||
|
|
||||||
|
// TODO: ✨ Add tests for style override
|
||||||
|
});
|
||||||
|
});
|
226
packages/mermaid/src/diagrams/radar/renderer.ts
Normal file
226
packages/mermaid/src/diagrams/radar/renderer.ts
Normal file
@@ -0,0 +1,226 @@
|
|||||||
|
import type { Diagram } from '../../Diagram.js';
|
||||||
|
import type { RadarDiagramConfig } from '../../config.type.js';
|
||||||
|
import type { DiagramRenderer, DrawDefinition, SVG, SVGGroup } from '../../diagram-api/types.js';
|
||||||
|
import { selectSvgElement } from '../../rendering-util/selectSvgElement.js';
|
||||||
|
import type { RadarDB, RadarAxis, RadarCurve } from './types.js';
|
||||||
|
|
||||||
|
const draw: DrawDefinition = (_text, id, _version, diagram: Diagram) => {
|
||||||
|
const db = diagram.db as RadarDB;
|
||||||
|
const axes = db.getAxes();
|
||||||
|
const curves = db.getCurves();
|
||||||
|
const options = db.getOptions();
|
||||||
|
const config = db.getConfig();
|
||||||
|
const title = db.getDiagramTitle();
|
||||||
|
|
||||||
|
const svg: SVG = selectSvgElement(id);
|
||||||
|
|
||||||
|
// 🖼️ Draw the main frame
|
||||||
|
const g = drawFrame(svg, config);
|
||||||
|
|
||||||
|
// The maximum value for the radar chart is the 'max' option if it exists,
|
||||||
|
// otherwise it is the maximum value of the curves
|
||||||
|
const maxValue: number =
|
||||||
|
options.max ?? Math.max(...curves.map((curve) => Math.max(...curve.entries)));
|
||||||
|
const minValue: number = options.min;
|
||||||
|
const radius = Math.min(config.width, config.height) / 2;
|
||||||
|
|
||||||
|
// 🕸️ Draw graticule
|
||||||
|
drawGraticule(g, axes, radius, options.ticks, options.graticule);
|
||||||
|
|
||||||
|
// 🪓 Draw the axes
|
||||||
|
drawAxes(g, axes, radius, config);
|
||||||
|
|
||||||
|
// 📊 Draw the curves
|
||||||
|
drawCurves(g, axes, curves, minValue, maxValue, options.graticule, config);
|
||||||
|
|
||||||
|
// 🏷 Draw Legend
|
||||||
|
drawLegend(g, curves, options.showLegend, config);
|
||||||
|
|
||||||
|
// 🏷 Draw Title
|
||||||
|
g.append('text')
|
||||||
|
.attr('class', 'radarTitle')
|
||||||
|
.text(title)
|
||||||
|
.attr('x', 0)
|
||||||
|
.attr('y', -config.height / 2 - config.marginTop);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Returns a g element to center the radar chart
|
||||||
|
// it is of type SVGElement
|
||||||
|
const drawFrame = (svg: SVG, config: Required<RadarDiagramConfig>): SVGGroup => {
|
||||||
|
const totalWidth = config.width + config.marginLeft + config.marginRight;
|
||||||
|
const totalHeight = config.height + config.marginTop + config.marginBottom;
|
||||||
|
const center = {
|
||||||
|
x: config.marginLeft + config.width / 2,
|
||||||
|
y: config.marginTop + config.height / 2,
|
||||||
|
};
|
||||||
|
// Initialize the SVG
|
||||||
|
svg
|
||||||
|
.attr('viewbox', `0 0 ${totalWidth} ${totalHeight}`)
|
||||||
|
.attr('width', totalWidth)
|
||||||
|
.attr('height', totalHeight);
|
||||||
|
// g element to center the radar chart
|
||||||
|
return svg.append('g').attr('transform', `translate(${center.x}, ${center.y})`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const drawGraticule = (
|
||||||
|
g: SVGGroup,
|
||||||
|
axes: RadarAxis[],
|
||||||
|
radius: number,
|
||||||
|
ticks: number,
|
||||||
|
graticule: string
|
||||||
|
) => {
|
||||||
|
if (graticule === 'circle') {
|
||||||
|
// Draw a circle for each tick
|
||||||
|
for (let i = 0; i < ticks; i++) {
|
||||||
|
const r = (radius * (i + 1)) / ticks;
|
||||||
|
g.append('circle').attr('r', r).attr('class', 'radarGraticule');
|
||||||
|
}
|
||||||
|
} else if (graticule === 'polygon') {
|
||||||
|
// Draw a polygon
|
||||||
|
const numAxes = axes.length;
|
||||||
|
for (let i = 0; i < ticks; i++) {
|
||||||
|
const r = (radius * (i + 1)) / ticks;
|
||||||
|
const points = axes
|
||||||
|
.map((_, j) => {
|
||||||
|
const angle = (2 * j * Math.PI) / numAxes - Math.PI / 2;
|
||||||
|
const x = r * Math.cos(angle);
|
||||||
|
const y = r * Math.sin(angle);
|
||||||
|
return `${x},${y}`;
|
||||||
|
})
|
||||||
|
.join(' ');
|
||||||
|
g.append('polygon').attr('points', points).attr('class', 'radarGraticule');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const drawAxes = (
|
||||||
|
g: SVGGroup,
|
||||||
|
axes: RadarAxis[],
|
||||||
|
radius: number,
|
||||||
|
config: Required<RadarDiagramConfig>
|
||||||
|
) => {
|
||||||
|
const numAxes = axes.length;
|
||||||
|
|
||||||
|
for (let i = 0; i < numAxes; i++) {
|
||||||
|
const label = axes[i].label;
|
||||||
|
const angle = (2 * i * Math.PI) / numAxes - Math.PI / 2;
|
||||||
|
g.append('line')
|
||||||
|
.attr('x1', 0)
|
||||||
|
.attr('y1', 0)
|
||||||
|
.attr('x2', radius * config.axisScaleFactor * Math.cos(angle))
|
||||||
|
.attr('y2', radius * config.axisScaleFactor * Math.sin(angle))
|
||||||
|
.attr('class', 'radarAxisLine');
|
||||||
|
g.append('text')
|
||||||
|
.text(label)
|
||||||
|
.attr('x', radius * config.axisLabelFactor * Math.cos(angle))
|
||||||
|
.attr('y', radius * config.axisLabelFactor * Math.sin(angle))
|
||||||
|
.attr('class', 'radarAxisLabel');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const renderer: DiagramRenderer = { draw };
|
||||||
|
function drawCurves(
|
||||||
|
g: SVGGroup,
|
||||||
|
axes: RadarAxis[],
|
||||||
|
curves: RadarCurve[],
|
||||||
|
minValue: number,
|
||||||
|
maxValue: number,
|
||||||
|
graticule: string,
|
||||||
|
config: Required<RadarDiagramConfig>
|
||||||
|
) {
|
||||||
|
const numAxes = axes.length;
|
||||||
|
const radius = Math.min(config.width, config.height) / 2;
|
||||||
|
|
||||||
|
curves.forEach((curve, index) => {
|
||||||
|
if (curve.entries.length !== numAxes) {
|
||||||
|
// Skip curves that do not have an entry for each axis.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Compute points for the curve.
|
||||||
|
const points = curve.entries.map((entry, i) => {
|
||||||
|
const angle = (2 * Math.PI * i) / numAxes - Math.PI / 2;
|
||||||
|
const r = relativeRadius(entry, minValue, maxValue, radius);
|
||||||
|
const x = r * Math.cos(angle);
|
||||||
|
const y = r * Math.sin(angle);
|
||||||
|
return { x, y };
|
||||||
|
});
|
||||||
|
|
||||||
|
if (graticule === 'circle') {
|
||||||
|
// Draw a closed curve through the points.
|
||||||
|
g.append('path')
|
||||||
|
.attr('d', closedRoundCurve(points, config.curveTension))
|
||||||
|
.attr('class', `radarCurve-${index}`);
|
||||||
|
} else if (graticule === 'polygon') {
|
||||||
|
// Draw a polygon for each curve.
|
||||||
|
g.append('polygon')
|
||||||
|
.attr('points', points.map((p) => `${p.x},${p.y}`).join(' '))
|
||||||
|
.attr('class', `radarCurve-${index}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function relativeRadius(value: number, minValue: number, maxValue: number, radius: number): number {
|
||||||
|
const clippedValue = Math.min(Math.max(value, minValue), maxValue);
|
||||||
|
return (radius * (clippedValue - minValue)) / (maxValue - minValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
function closedRoundCurve(points: { x: number; y: number }[], tension: number): string {
|
||||||
|
// Catmull-Rom spline helper function
|
||||||
|
const numPoints = points.length;
|
||||||
|
let d = `M${points[0].x},${points[0].y}`;
|
||||||
|
// For each segment from point i to point (i+1) mod n, compute control points.
|
||||||
|
for (let i = 0; i < numPoints; i++) {
|
||||||
|
const p0 = points[(i - 1 + numPoints) % numPoints];
|
||||||
|
const p1 = points[i];
|
||||||
|
const p2 = points[(i + 1) % numPoints];
|
||||||
|
const p3 = points[(i + 2) % numPoints];
|
||||||
|
// Calculate the control points for the cubic Bezier segment
|
||||||
|
const cp1 = {
|
||||||
|
x: p1.x + (p2.x - p0.x) * tension,
|
||||||
|
y: p1.y + (p2.y - p0.y) * tension,
|
||||||
|
};
|
||||||
|
const cp2 = {
|
||||||
|
x: p2.x - (p3.x - p1.x) * tension,
|
||||||
|
y: p2.y - (p3.y - p1.y) * tension,
|
||||||
|
};
|
||||||
|
d += ` C${cp1.x},${cp1.y} ${cp2.x},${cp2.y} ${p2.x},${p2.y}`;
|
||||||
|
}
|
||||||
|
return `${d} Z`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawLegend(
|
||||||
|
g: SVGGroup,
|
||||||
|
curves: RadarCurve[],
|
||||||
|
showLegend: boolean,
|
||||||
|
config: Required<RadarDiagramConfig>
|
||||||
|
) {
|
||||||
|
if (!showLegend) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a legend group and position it in the top-right corner of the chart.
|
||||||
|
const legendX = ((config.width / 2 + config.marginRight) * 3) / 4;
|
||||||
|
const legendY = (-(config.height / 2 + config.marginTop) * 3) / 4;
|
||||||
|
const lineHeight = 20;
|
||||||
|
|
||||||
|
curves.forEach((curve, index) => {
|
||||||
|
const itemGroup = g
|
||||||
|
.append('g')
|
||||||
|
.attr('transform', `translate(${legendX}, ${legendY + index * lineHeight})`);
|
||||||
|
|
||||||
|
// Draw a square marker for this curve.
|
||||||
|
itemGroup
|
||||||
|
.append('rect')
|
||||||
|
.attr('width', 12)
|
||||||
|
.attr('height', 12)
|
||||||
|
.attr('class', `radarLegendBox-${index}`);
|
||||||
|
|
||||||
|
// Draw the label text next to the marker.
|
||||||
|
itemGroup
|
||||||
|
.append('text')
|
||||||
|
.attr('x', 16)
|
||||||
|
.attr('y', 0)
|
||||||
|
.attr('class', 'radarLegendText')
|
||||||
|
.text(curve.label);
|
||||||
|
});
|
||||||
|
}
|
71
packages/mermaid/src/diagrams/radar/styles.ts
Normal file
71
packages/mermaid/src/diagrams/radar/styles.ts
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import type { DiagramStylesProvider } from '../../diagram-api/types.js';
|
||||||
|
import { cleanAndMerge } from '../../utils.js';
|
||||||
|
import type { RadarStyleOptions } from './types.js';
|
||||||
|
import { getThemeVariables } from '../../themes/theme-default.js';
|
||||||
|
import { getConfig as getConfigAPI } from '../../config.js';
|
||||||
|
|
||||||
|
const genIndexStyles = (
|
||||||
|
themeVariables: ReturnType<typeof getThemeVariables>,
|
||||||
|
radarOptions: RadarStyleOptions
|
||||||
|
) => {
|
||||||
|
let sections = '';
|
||||||
|
for (let i = 0; i < themeVariables.THEME_COLOR_LIMIT; i++) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const indexColor = (themeVariables as any)[`cScale${i}`];
|
||||||
|
sections += `
|
||||||
|
.radarCurve-${i} {
|
||||||
|
color: ${indexColor};
|
||||||
|
fill: ${indexColor};
|
||||||
|
fill-opacity: ${radarOptions.curveOpacity};
|
||||||
|
stroke: ${indexColor};
|
||||||
|
stroke-width: ${radarOptions.curveStrokeWidth};
|
||||||
|
}
|
||||||
|
.radarLegendBox-${i} {
|
||||||
|
fill: ${indexColor};
|
||||||
|
fill-opacity: ${radarOptions.curveOpacity};
|
||||||
|
stroke: ${indexColor};
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
return sections;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const styles: DiagramStylesProvider = ({ radar }: { radar?: RadarStyleOptions } = {}) => {
|
||||||
|
const defaultThemeVariables = getThemeVariables();
|
||||||
|
const currentConfig = getConfigAPI();
|
||||||
|
|
||||||
|
const themeVariables = cleanAndMerge(defaultThemeVariables, currentConfig.themeVariables);
|
||||||
|
const radarOptions: RadarStyleOptions = cleanAndMerge(themeVariables.radar, radar);
|
||||||
|
return `
|
||||||
|
.radarTitle {
|
||||||
|
font-size: ${themeVariables.fontSize};
|
||||||
|
text-color: ${themeVariables.titleColor};
|
||||||
|
dominant-baseline: hanging;
|
||||||
|
text-anchor: middle;
|
||||||
|
}
|
||||||
|
.radarAxisLine {
|
||||||
|
stroke: ${radarOptions.axisColor};
|
||||||
|
stroke-width: ${radarOptions.axisStrokeWidth};
|
||||||
|
}
|
||||||
|
.radarAxisLabel {
|
||||||
|
dominant-baseline: middle;
|
||||||
|
text-anchor: middle;
|
||||||
|
font-size: ${radarOptions.axisLabelFontSize}px;
|
||||||
|
color: ${radarOptions.axisColor};
|
||||||
|
}
|
||||||
|
.radarGraticule {
|
||||||
|
fill: ${radarOptions.graticuleColor};
|
||||||
|
fill-opacity: ${radarOptions.graticuleOpacity};
|
||||||
|
stroke: ${radarOptions.graticuleColor};
|
||||||
|
stroke-width: ${radarOptions.graticuleStrokeWidth};
|
||||||
|
}
|
||||||
|
.radarLegendText {
|
||||||
|
text-anchor: start;
|
||||||
|
font-size: ${radarOptions.legendFontSize}px;
|
||||||
|
dominant-baseline: hanging;
|
||||||
|
}
|
||||||
|
${genIndexStyles(themeVariables, radarOptions)}
|
||||||
|
`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default styles;
|
47
packages/mermaid/src/diagrams/radar/types.ts
Normal file
47
packages/mermaid/src/diagrams/radar/types.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import type { Axis, Curve, Option } from '../../../../parser/dist/src/language/generated/ast.js';
|
||||||
|
import type { RadarDiagramConfig } from '../../config.type.js';
|
||||||
|
import type { DiagramDBBase } from '../../diagram-api/types.js';
|
||||||
|
|
||||||
|
export interface RadarAxis {
|
||||||
|
name: string;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
export interface RadarCurve {
|
||||||
|
name: string;
|
||||||
|
entries: number[];
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
export interface RadarOptions {
|
||||||
|
showLegend: boolean;
|
||||||
|
ticks: number;
|
||||||
|
max: number | null;
|
||||||
|
min: number;
|
||||||
|
graticule: 'circle' | 'polygon';
|
||||||
|
}
|
||||||
|
export interface RadarDB extends DiagramDBBase<RadarDiagramConfig> {
|
||||||
|
getAxes: () => RadarAxis[];
|
||||||
|
getCurves: () => RadarCurve[];
|
||||||
|
getOptions: () => RadarOptions;
|
||||||
|
setAxes: (axes: Axis[]) => void;
|
||||||
|
setCurves: (curves: Curve[]) => void;
|
||||||
|
setOptions: (options: Option[]) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RadarStyleOptions {
|
||||||
|
axisColor: string;
|
||||||
|
axisStrokeWidth: number;
|
||||||
|
axisLabelFontSize: number;
|
||||||
|
curveOpacity: number;
|
||||||
|
curveStrokeWidth: number;
|
||||||
|
graticuleColor: string;
|
||||||
|
graticuleOpacity: number;
|
||||||
|
graticuleStrokeWidth: number;
|
||||||
|
legendBoxSize: number;
|
||||||
|
legendFontSize: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RadarData {
|
||||||
|
axes: RadarAxis[];
|
||||||
|
curves: RadarCurve[];
|
||||||
|
options: RadarOptions;
|
||||||
|
}
|
@@ -30,6 +30,7 @@ vi.mock('./diagrams/packet/renderer.js');
|
|||||||
vi.mock('./diagrams/xychart/xychartRenderer.js');
|
vi.mock('./diagrams/xychart/xychartRenderer.js');
|
||||||
vi.mock('./diagrams/requirement/requirementRenderer.js');
|
vi.mock('./diagrams/requirement/requirementRenderer.js');
|
||||||
vi.mock('./diagrams/sequence/sequenceRenderer.js');
|
vi.mock('./diagrams/sequence/sequenceRenderer.js');
|
||||||
|
vi.mock('./diagrams/radar/renderer.js');
|
||||||
|
|
||||||
// -------------------------------------
|
// -------------------------------------
|
||||||
|
|
||||||
@@ -797,6 +798,7 @@ graph TD;A--x|text including URL space|B;`)
|
|||||||
{ textDiagramType: 'requirementDiagram', expectedType: 'requirement' },
|
{ textDiagramType: 'requirementDiagram', expectedType: 'requirement' },
|
||||||
{ textDiagramType: 'sequenceDiagram', expectedType: 'sequence' },
|
{ textDiagramType: 'sequenceDiagram', expectedType: 'sequence' },
|
||||||
{ textDiagramType: 'stateDiagram-v2', expectedType: 'stateDiagram' },
|
{ textDiagramType: 'stateDiagram-v2', expectedType: 'stateDiagram' },
|
||||||
|
{ textDiagramType: 'radar-beta', expectedType: 'radar' },
|
||||||
];
|
];
|
||||||
|
|
||||||
describe('accessibility', () => {
|
describe('accessibility', () => {
|
||||||
|
@@ -55,6 +55,7 @@ required:
|
|||||||
- packet
|
- packet
|
||||||
- block
|
- block
|
||||||
- look
|
- look
|
||||||
|
- radar
|
||||||
properties:
|
properties:
|
||||||
theme:
|
theme:
|
||||||
description: |
|
description: |
|
||||||
@@ -292,6 +293,8 @@ properties:
|
|||||||
$ref: '#/$defs/PacketDiagramConfig'
|
$ref: '#/$defs/PacketDiagramConfig'
|
||||||
block:
|
block:
|
||||||
$ref: '#/$defs/BlockDiagramConfig'
|
$ref: '#/$defs/BlockDiagramConfig'
|
||||||
|
radar:
|
||||||
|
$ref: '#/$defs/RadarDiagramConfig'
|
||||||
dompurifyConfig:
|
dompurifyConfig:
|
||||||
title: DOM Purify Configuration
|
title: DOM Purify Configuration
|
||||||
description: Configuration options to pass to the `dompurify` library.
|
description: Configuration options to pass to the `dompurify` library.
|
||||||
@@ -2208,6 +2211,60 @@ $defs: # JSON Schema definition (maybe we should move these to a separate file)
|
|||||||
minimum: 0
|
minimum: 0
|
||||||
default: 8
|
default: 8
|
||||||
|
|
||||||
|
RadarDiagramConfig:
|
||||||
|
title: Radar Diagram Config
|
||||||
|
allOf: [{ $ref: '#/$defs/BaseDiagramConfig' }]
|
||||||
|
description: The object containing configurations specific for radar diagrams.
|
||||||
|
type: object
|
||||||
|
unevaluatedProperties: false
|
||||||
|
properties:
|
||||||
|
width:
|
||||||
|
description: The size of the radar diagram.
|
||||||
|
type: number
|
||||||
|
minimum: 1
|
||||||
|
default: 600
|
||||||
|
height:
|
||||||
|
description: The size of the radar diagram.
|
||||||
|
type: number
|
||||||
|
minimum: 1
|
||||||
|
default: 600
|
||||||
|
marginTop:
|
||||||
|
description: The margin from the top of the radar diagram.
|
||||||
|
type: number
|
||||||
|
minimum: 0
|
||||||
|
default: 50
|
||||||
|
marginRight:
|
||||||
|
description: The margin from the right of the radar diagram.
|
||||||
|
type: number
|
||||||
|
minimum: 0
|
||||||
|
default: 50
|
||||||
|
marginBottom:
|
||||||
|
description: The margin from the bottom of the radar diagram.
|
||||||
|
type: number
|
||||||
|
minimum: 0
|
||||||
|
default: 50
|
||||||
|
marginLeft:
|
||||||
|
description: The margin from the left of the radar diagram.
|
||||||
|
type: number
|
||||||
|
minimum: 0
|
||||||
|
default: 50
|
||||||
|
axisScaleFactor:
|
||||||
|
description: The scale factor of the axis.
|
||||||
|
type: number
|
||||||
|
minimum: 0
|
||||||
|
default: 1
|
||||||
|
axisLabelFactor:
|
||||||
|
description: The scale factor of the axis label.
|
||||||
|
type: number
|
||||||
|
minimum: 0
|
||||||
|
default: 1.05
|
||||||
|
curveTension:
|
||||||
|
description: The tension factor for the Catmull-Rom spline conversion to cubic Bézier curves.
|
||||||
|
type: number
|
||||||
|
minimum: 0
|
||||||
|
maximum: 1
|
||||||
|
default: 0.17
|
||||||
|
|
||||||
FontCalculator:
|
FontCalculator:
|
||||||
title: Font Calculator
|
title: Font Calculator
|
||||||
description: |
|
description: |
|
||||||
|
@@ -29,6 +29,7 @@ import timeline from './diagrams/timeline/styles.js';
|
|||||||
import mindmap from './diagrams/mindmap/styles.js';
|
import mindmap from './diagrams/mindmap/styles.js';
|
||||||
import packet from './diagrams/packet/styles.js';
|
import packet from './diagrams/packet/styles.js';
|
||||||
import block from './diagrams/block/styles.js';
|
import block from './diagrams/block/styles.js';
|
||||||
|
import radar from './diagrams/radar/styles.js';
|
||||||
import themes from './themes/index.js';
|
import themes from './themes/index.js';
|
||||||
|
|
||||||
function checkValidStylisCSSStyleSheet(stylisString: string) {
|
function checkValidStylisCSSStyleSheet(stylisString: string) {
|
||||||
@@ -99,6 +100,7 @@ describe('styles', () => {
|
|||||||
block,
|
block,
|
||||||
timeline,
|
timeline,
|
||||||
packet,
|
packet,
|
||||||
|
radar,
|
||||||
})) {
|
})) {
|
||||||
test(`should return a valid style for diagram ${diagramId} and theme ${themeId}`, async () => {
|
test(`should return a valid style for diagram ${diagramId} and theme ${themeId}`, async () => {
|
||||||
const { default: getStyles, addStylesForDiagram } = await import('./styles.js');
|
const { default: getStyles, addStylesForDiagram } = await import('./styles.js');
|
||||||
|
Reference in New Issue
Block a user