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 (
);
};
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) => (
))}
)}
);
};