import { bindGlobalStyle, getGlobalStylesId, HtmlVar } from 'lupine.components'; import { chartCommonCss, getChartColor, BasicChartProps } from './chart-utils'; import { Tooltip } from '../tooltip'; export type LineChartProps = BasicChartProps & { yAxisFormatter?: (value: number) => string; curved?: boolean; // Whether the line should be smooth }; export const LineChart = (props: LineChartProps) => { 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 for Y axis scale let maxVal = 0; series.forEach((s) => { s.data.forEach((val) => { if (val > maxVal) maxVal = val; }); }); const tickCount = 5; 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; const yTicks = Array.from({ length: tickCount + 1 }, (_, i) => parseFloat((i * niceStep).toPrecision(12))); const renderChart = (viewBoxWidth: number, viewBoxHeight: number) => { // Layout parameters const padding = { top: 20, right: 80, bottom: 40, left: 60 }; const chartWidth = viewBoxWidth - padding.left - padding.right; const chartHeight = viewBoxHeight - padding.top - padding.bottom; // Calculate points considering a half step padding on left and right for line const pointStep = chartWidth / Math.max(1, labels.length); const dataLeftPadding = pointStep / 2; const formatY = props.yAxisFormatter || ((val) => val.toString()); // Render Y Axis const renderYAxis = () => { return yTicks.map((val) => { const y = padding.top + chartHeight - (val / niceMax) * chartHeight; return ( {formatY(val)} ); }); }; // Render X Axis const renderXAxis = () => { return labels.map((label, index) => { const x = padding.left + dataLeftPadding + index * pointStep; const y = viewBoxHeight - padding.bottom + 25; return ( {label} ); }); }; // Render Lines & Points const renderLinesAndPoints = () => { const lines: any[] = []; const points: any[] = []; series.forEach((s, sIndex) => { const color = s.color || getChartColor(sIndex); const coordinates = s.data.map((val, lIndex) => { const x = padding.left + dataLeftPadding + lIndex * pointStep; const y = padding.top + chartHeight - (val / niceMax) * chartHeight; return { x, y, val, label: labels[lIndex] }; }); // Draw Path let d = ''; if (props.curved && coordinates.length > 2) { // Simple smoothing d = `M ${coordinates[0].x} ${coordinates[0].y} `; for (let i = 0; i < coordinates.length - 1; i++) { const current = coordinates[i]; const next = coordinates[i + 1]; const mx = (current.x + next.x) / 2; d += `C ${mx} ${current.y}, ${mx} ${next.y}, ${next.x} ${next.y} `; } } else { d = 'M ' + coordinates.map((c) => `${c.x} ${c.y}`).join(' L '); } lines.push(); // Draw interactive points overlay coordinates.forEach((c) => { const handleMouseEnter = (e: any) => { Tooltip.show( e,
{c.label}
{s.name}: {formatY(c.val)}
, { position: 'auto' } ); e.target.setAttribute('r', '6'); }; const handleMouseLeave = (e: any) => { Tooltip.hide(); e.target.setAttribute('r', '4'); }; points.push( ); }); }); return { lines, points }; }; const { lines, points } = renderLinesAndPoints(); return ( {/* Base Axis line */} {renderYAxis()} {renderXAxis()} {lines} {points} ); }; const chartVar = new HtmlVar(renderChart(1000, 300)); const ref = { globalCssId, onLoad: async (el: Element) => { const ro = new ResizeObserver((entries) => { const { width, height } = entries[0].contentRect; if (width > 50 && height > 50) { chartVar.value = renderChart(width, height); } }); ro.observe(el); (el as any)._ro = ro; }, onUnload: async (el: Element) => { if ((el as any)._ro) { (el as any)._ro.disconnect(); } }, }; const ratio = props.aspectRatio ?? 16 / 9; const paddingTop = `${(1 / ratio) * 100}%`; const styleStr = `width: ${props.width || '100%'};`; return (
{props.title &&
{props.title}
}
{chartVar.node}
{showLegend && (
{series.map((s, i) => (
{s.name}
))}
)}
); };