import { RefProps, bindGlobalStyle, getGlobalStylesId } from 'lupine.web'; import { chartCommonCss, getChartColor, polarToCartesian, BasicChartProps } from './chart-utils'; import { Tooltip } from '../tooltip'; export type RadarChartProps = BasicChartProps & { valueFormatter?: (value: number) => string; }; export const RadarChart = (props: RadarChartProps) => { const globalCssId = getGlobalStylesId(chartCommonCss); bindGlobalStyle(globalCssId, chartCommonCss); const showLegend = props.showLegend !== false; const series = props.data.series; const labels = props.data.labels; if (!series || series.length === 0 || labels.length === 0) { return
No data
; } // Find max value to determine scale let maxVal = 0; series.forEach((s) => { s.data.forEach((val) => { if (val > maxVal) maxVal = val; }); }); const tickCount = 4; const order = Math.floor(Math.log10(maxVal || 1)); const magnitude = Math.pow(10, order); const niceStep = Math.ceil(maxVal / magnitude / tickCount) * magnitude; const niceMax = niceStep * tickCount; // Viewbox and layout const viewBoxSize = 400; const centerX = viewBoxSize / 2; const centerY = viewBoxSize / 2; const maxRadius = (viewBoxSize / 2) * 0.7; // Leave space for labels const angleStep = 360 / labels.length; const formatValue = props.valueFormatter || ((val) => val.toString()); // Render Grid (Polygons) const renderGrid = () => { const gridElements: any[] = []; // Grid webs for (let i = 1; i <= tickCount; i++) { const radius = (i / tickCount) * maxRadius; const points = Array.from({ length: labels.length }, (_, j) => { const angle = j * angleStep; const pos = polarToCartesian(centerX, centerY, radius, angle); return `${pos.x},${pos.y}`; }).join(' '); gridElements.push( ); } // Grid axes for (let j = 0; j < labels.length; j++) { const angle = j * angleStep; const endPos = polarToCartesian(centerX, centerY, maxRadius, angle); gridElements.push( ); // Label const labelPos = polarToCartesian(centerX, centerY, maxRadius + 20, angle); gridElements.push( {labels[j]} ); } return gridElements; }; // Render Data const renderData = () => { const dataElements: any[] = []; series.forEach((s, sIndex) => { const color = s.color || getChartColor(sIndex); const coordinates = s.data.map((val, j) => { const radius = (val / niceMax) * maxRadius; const angle = j * angleStep; return { ...polarToCartesian(centerX, centerY, radius, angle), val, label: labels[j] }; }); const pointsStr = coordinates.map((c) => `${c.x},${c.y}`).join(' '); const seriesGroupElements: any[] = []; // Fill area seriesGroupElements.push( ); // Points and Tooltips coordinates.forEach((c) => { const handleMouseEnter = (e: any) => { Tooltip.show( e,
{c.label}
{s.name}: {formatValue(c.val)}
, { position: 'auto' } ); e.target.setAttribute('r', '6'); }; const handleMouseLeave = (e: any) => { Tooltip.hide(); e.target.setAttribute('r', '4'); }; seriesGroupElements.push( ); }); const handleGroupMouseEnter = (e: any) => { const polygon = e.currentTarget.querySelector('.radar-polygon'); if (polygon) polygon.setAttribute('fill-opacity', '0.6'); }; const handleGroupMouseLeave = (e: any) => { const polygon = e.currentTarget.querySelector('.radar-polygon'); if (polygon) polygon.setAttribute('fill-opacity', '0.15'); }; dataElements.push( {seriesGroupElements} ); }); return dataElements; }; const ref: RefProps = { referToCssId: globalCssId, }; const ratio = props.aspectRatio ?? 16 / 9; const paddingTop = `${(1 / ratio) * 100}%`; const styleStr = `width: ${props.width || '100%'};`; return (
{props.title &&
{props.title}
}
{renderGrid()} {renderData()}
{showLegend && (
{series.map((s, i) => (
{s.name}
))}
)}
); };