📊 Add radar chart

This commit is contained in:
Thomas Di Cizerone
2025-03-16 18:13:50 +01:00
parent d80cc38bb2
commit 1fb91d14c9
18 changed files with 1092 additions and 7 deletions

View File

@@ -199,6 +199,7 @@ export interface MermaidConfig {
sankey?: SankeyDiagramConfig;
packet?: PacketDiagramConfig;
block?: BlockDiagramConfig;
radar?: RadarDiagramConfig;
dompurifyConfig?: DOMPurifyConfiguration;
wrap?: boolean;
fontSize?: number;
@@ -1526,6 +1527,50 @@ export interface PacketDiagramConfig extends BaseDiagramConfig {
export interface BlockDiagramConfig extends BaseDiagramConfig {
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
* via the `definition` "FontConfig".

View File

@@ -255,8 +255,12 @@ const config: RequiredDeep<MermaidConfig> = {
packet: {
...defaultConfigJson.packet,
},
radar: {
...defaultConfigJson.radar,
},
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const keyify = (obj: any, prefix = ''): string[] =>
Object.keys(obj).reduce((res: string[], el): string[] => {
if (Array.isArray(obj[el])) {

View File

@@ -22,6 +22,7 @@ import mindmap from '../diagrams/mindmap/detector.js';
import kanban from '../diagrams/kanban/detector.js';
import sankey from '../diagrams/sankey/sankeyDetector.js';
import { packet } from '../diagrams/packet/detector.js';
import { radar } from '../diagrams/radar/detector.js';
import block from '../diagrams/block/blockDetector.js';
import architecture from '../diagrams/architecture/architectureDetector.js';
import { registerLazyLoadedDiagrams } from './detectType.js';
@@ -94,6 +95,7 @@ export const addDiagrams = () => {
packet,
xychart,
block,
architecture
architecture,
radar
);
};

View 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,
};

View 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,
};

View 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,
};

View 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);
},
};

View 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
});
});

View 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);
});
}

View 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;

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

View File

@@ -30,6 +30,7 @@ vi.mock('./diagrams/packet/renderer.js');
vi.mock('./diagrams/xychart/xychartRenderer.js');
vi.mock('./diagrams/requirement/requirementRenderer.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: 'sequenceDiagram', expectedType: 'sequence' },
{ textDiagramType: 'stateDiagram-v2', expectedType: 'stateDiagram' },
{ textDiagramType: 'radar-beta', expectedType: 'radar' },
];
describe('accessibility', () => {

View File

@@ -55,6 +55,7 @@ required:
- packet
- block
- look
- radar
properties:
theme:
description: |
@@ -292,6 +293,8 @@ properties:
$ref: '#/$defs/PacketDiagramConfig'
block:
$ref: '#/$defs/BlockDiagramConfig'
radar:
$ref: '#/$defs/RadarDiagramConfig'
dompurifyConfig:
title: DOM Purify Configuration
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
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:
title: Font Calculator
description: |

View File

@@ -29,6 +29,7 @@ import timeline from './diagrams/timeline/styles.js';
import mindmap from './diagrams/mindmap/styles.js';
import packet from './diagrams/packet/styles.js';
import block from './diagrams/block/styles.js';
import radar from './diagrams/radar/styles.js';
import themes from './themes/index.js';
function checkValidStylisCSSStyleSheet(stylisString: string) {
@@ -99,6 +100,7 @@ describe('styles', () => {
block,
timeline,
packet,
radar,
})) {
test(`should return a valid style for diagram ${diagramId} and theme ${themeId}`, async () => {
const { default: getStyles, addStylesForDiagram } = await import('./styles.js');