aura-glass
Version:
A comprehensive glassmorphism design system for React applications with 142+ production-ready components
375 lines (372 loc) • 14.5 kB
JavaScript
'use client';
import { jsx, jsxs } from 'react/jsx-runtime';
import { cn } from '../../lib/utilsComprehensive.js';
import { useState, useMemo } from 'react';
import '../../primitives/GlassCore.js';
import '../../primitives/glass/GlassAdvanced.js';
import '../../primitives/OptimizedGlassCore.js';
import '../../primitives/glass/OptimizedGlassAdvanced.js';
import '../../primitives/MotionNative.js';
import { MotionFramer } from '../../primitives/motion/MotionFramer.js';
import { CardHeader, CardTitle, CardContent } from '../card/index.js';
import { GlassCard } from '../card/GlassCard.js';
/**
* GlassLineChart component
* A glassmorphism line chart with multiple series support and interactive features
*/
const GlassLineChart = ({
// TODO: Integrate ContrastGuard in chart labels, tooltips, and legends for WCAG AA compliance
title,
series = [],
width = 600,
height = 300,
showGrid = true,
showPoints = true,
showLegend = true,
xAxisLabel,
yAxisLabel,
colors = ["var(--glass-color-primary)", "var(--glass-color-danger)", "var(--glass-color-success)", "var(--glass-color-warning)", "#8b5cf6", "#06b6d4", "#84cc16", "#f97316", "#ec4899", "var(--glass-gray-500)"],
animationDuration = 1000,
showTooltips = true,
formatYValue = value => value.toString(),
formatXValue = value => value.toString(),
className,
loading = false,
...props
}) => {
const [hoveredPoint, setHoveredPoint] = useState(null);
const [hoveredSeriesId, setHoveredSeriesId] = useState(null);
// Chart dimensions with padding
const padding = {
top: 20,
right: 60,
bottom: 60,
left: 60
};
const chartWidth = width - padding.left - padding.right;
const chartHeight = height - padding.top - padding.bottom;
// Process data for chart
const processedData = useMemo(() => {
if (!series || !Array.isArray(series) || series.length === 0) {
return {
scaledSeries: [],
xLabels: [],
yLabels: []
};
}
// Find min/max values across all series
const allPoints = series.flatMap(s => s.data);
const xValues = allPoints.map(p => typeof p.x === "number" ? p.x : 0);
const yValues = allPoints.map(p => p.y);
const xMin = Math.min(...xValues);
const xMax = Math.max(...xValues);
const yMin = Math.min(...yValues);
const yMax = Math.max(...yValues);
// Add some padding to Y axis
const yRange = yMax - yMin;
const yPadding = yRange * 0.1;
const yMinPadded = Math.max(0, yMin - yPadding);
const yMaxPadded = yMax + yPadding;
// Scale functions
const scaleX = x => (x - xMin) / (xMax - xMin) * chartWidth;
const scaleY = y => chartHeight - (y - yMinPadded) / (yMaxPadded - yMinPadded) * chartHeight;
// Process each series
const scaledSeries = series.map((s, seriesIndex) => ({
...s,
color: s.color || colors[seriesIndex % (colors?.length || 0)],
points: s.data?.map((point, pointIndex) => ({
...point,
scaledX: padding.left + scaleX(typeof point.x === "number" ? point.x : pointIndex),
scaledY: padding.top + scaleY(point.y),
originalIndex: pointIndex
}))
}));
// Generate axis labels
const xLabels = scaledSeries[0]?.points.map((point, index) => ({
x: point.scaledX,
y: height - padding.bottom + 20,
label: formatXValue(point.x)
})) || [];
const yLabels = [0, 0.25, 0.5, 0.75, 1].map(ratio => {
const value = yMinPadded + (yMaxPadded - yMinPadded) * ratio;
return {
x: padding.left - 10,
y: padding.top + chartHeight - chartHeight * ratio,
label: formatYValue(value)
};
});
return {
scaledSeries,
xLabels,
yLabels
};
}, [series, width, height, padding, chartWidth, chartHeight, formatYValue, formatXValue, colors]);
// Generate path for line
const generatePath = points => {
if ((points?.length || 0) === 0) return "";
let path = `M ${points[0].scaledX} ${points[0].scaledY}`;
for (let i = 1; i < (points?.length || 0); i++) {
path += ` L ${points[i].scaledX} ${points[i].scaledY}`;
}
return path;
};
// Handle point hover
const handlePointHover = (seriesId, pointIndex, x, y) => {
if (showTooltips) {
setHoveredPoint({
seriesId,
index: pointIndex,
x,
y
});
}
};
const handlePointLeave = () => {
setHoveredPoint(null);
};
// Loading skeleton
if (loading) {
return jsx(GlassCard, {
"data-glass-component": true,
className: cn("glass-p-6", className),
children: jsxs("div", {
className: 'animate-pulse glass-gap-4',
children: [jsx("div", {
className: 'h-6 glass-surface-subtle/20 glass-radius-md w-48'
}), jsx("div", {
className: 'h-64 glass-surface-subtle/10 glass-radius-md'
}), jsxs("div", {
className: "glass-flex glass-justify-center glass-gap-4",
children: [jsx("div", {
className: 'h-4 glass-surface-subtle/20 glass-radius-md w-20'
}), jsx("div", {
className: 'h-4 glass-surface-subtle/20 glass-radius-md w-20'
}), jsx("div", {
className: 'h-4 glass-surface-subtle/20 glass-radius-md w-20'
})]
})]
})
});
}
return jsx(MotionFramer, {
preset: "fadeIn",
className: "glass-w-full",
children: jsxs(GlassCard, {
className: cn("overflow-hidden", className),
...props,
children: [title && jsx(CardHeader, {
children: jsx(CardTitle, {
className: 'text-primary glass-text-lg font-semibold',
children: title
})
}), jsxs(CardContent, {
className: "glass-p-4",
children: [jsxs("div", {
className: 'relative',
children: [jsxs("svg", {
width: width,
height: height,
className: 'overflow-visible',
children: [showGrid && jsxs("g", {
className: 'opacity-20',
children: [processedData.yLabels.map((label, index) => jsx("line", {
x1: padding.left,
y1: label.y,
x2: width - padding.right,
y2: label.y,
stroke: "currentColor",
strokeWidth: "1",
className: 'text-primary/30'
}, `h-grid-${index}`)), processedData.xLabels.map((label, index) => jsx("line", {
x1: label.x,
y1: padding.top,
x2: label.x,
y2: height - padding.bottom,
stroke: "currentColor",
strokeWidth: "1",
className: 'text-primary/30'
}, `v-grid-${index}`))]
}), processedData.scaledSeries.map((s, seriesIndex) => jsxs(MotionFramer, {
preset: "slideUp",
delay: seriesIndex * 100,
className: 'relative',
children: [jsx("path", {
d: generatePath(s.points),
fill: "none",
stroke: s.color,
strokeWidth: s.strokeWidth || 2,
className: 'drop-shadow-sm',
style: {
animation: `drawLine ${animationDuration}ms ease-out ${seriesIndex * 100}ms both`,
opacity: hoveredSeriesId && hoveredSeriesId !== s.id ? 0.35 : 1
}
}), jsx("defs", {
children: jsxs("linearGradient", {
id: `gradient-${s.id}`,
x1: "0%",
y1: "0%",
x2: "0%",
y2: "100%",
children: [jsx("stop", {
offset: "0%",
stopColor: s.color,
stopOpacity: "0.3"
}), jsx("stop", {
offset: "100%",
stopColor: s.color,
stopOpacity: "0"
})]
})
}), showPoints && s.points.map((point, pointIndex) => jsx("circle", {
cx: point.scaledX,
cy: point.scaledY,
r: "4",
fill: s.color,
stroke: "rgba(var(--glass-color-white) / var(--glass-opacity-80))",
strokeWidth: "2",
className: 'cursor-pointer hover:r-6 transition-all duration-200',
onMouseEnter: () => handlePointHover(s.id, pointIndex, point.scaledX, point.scaledY),
onMouseLeave: handlePointLeave,
style: {
animation: `fadeInPoint 300ms ease-out ${animationDuration + seriesIndex * 100 + pointIndex * 50}ms both`,
opacity: hoveredSeriesId && hoveredSeriesId !== s.id ? 0.35 : 1
}
}, `${s.id}-point-${pointIndex}`))]
}, s.id)), hoveredPoint && jsxs("g", {
className: 'pointer-events-none',
children: [jsx("line", {
x1: hoveredPoint.x,
y1: padding.top,
x2: hoveredPoint.x,
y2: height - padding.bottom,
stroke: "white",
strokeOpacity: 0.25,
strokeDasharray: "4 4"
}), jsx("line", {
x1: padding.left,
y1: hoveredPoint.y,
x2: width - padding.right,
y2: hoveredPoint.y,
stroke: "white",
strokeOpacity: 0.25,
strokeDasharray: "4 4"
}), jsx("circle", {
cx: hoveredPoint.x,
cy: hoveredPoint.y,
r: "8",
fill: "none",
stroke: "white",
strokeOpacity: 0.35
})]
}), jsx("line", {
x1: padding.left,
y1: height - padding.bottom,
x2: width - padding.right,
y2: height - padding.bottom,
stroke: "currentColor",
strokeWidth: "1",
className: 'text-primary/50'
}), jsx("line", {
x1: padding.left,
y1: padding.top,
x2: padding.left,
y2: height - padding.bottom,
stroke: "currentColor",
strokeWidth: "1",
className: 'text-primary/50'
}), processedData.xLabels.map((label, index) => jsx("text", {
x: label.x,
y: label.y,
textAnchor: "middle",
className: 'glass-text-xs fill-white/70',
style: {
fontSize: "0.625rem"
},
children: label.label
}, `x-label-${index}`)), processedData.yLabels.map((label, index) => jsx("text", {
x: label.x,
y: label.y + 4,
textAnchor: "end",
className: 'glass-text-xs fill-white/70',
style: {
fontSize: "0.625rem"
},
children: label.label
}, `y-label-${index}`)), xAxisLabel && jsx("text", {
x: width / 2,
y: height - 10,
textAnchor: "middle",
className: 'glass-text-sm fill-white/80 font-medium',
children: xAxisLabel
}), yAxisLabel && jsx("text", {
x: 15,
y: height / 2,
textAnchor: "middle",
transform: `rotate(-90, 15, ${height / 2})`,
className: 'glass-text-sm fill-white/80 font-medium',
children: yAxisLabel
})]
}), hoveredPoint && jsx(MotionFramer, {
preset: "fadeIn",
className: 'absolute z-10',
children: jsx("div", {
className: cn("absolute glass-radius-xl glass-p-3 shadow-xl", "bg-black/70 glass-backdrop-blur-md ring-1 ring-white/10 glass-radial-reveal glass-lift"),
style: {
left: hoveredPoint.x + 10,
top: hoveredPoint.y - 10,
transform: hoveredPoint.x > width / 2 ? "translateX(-100%)" : "none"
},
children: jsxs("div", {
className: 'text-primary glass-text-sm',
children: [jsx("div", {
className: 'font-medium',
children: processedData.scaledSeries.find(s => s.id === hoveredPoint.seriesId)?.name
}), jsx("div", {
className: 'text-primary/80',
children: (() => {
const series = processedData.scaledSeries.find(s => s.id === hoveredPoint.seriesId);
const dataPoint = series?.data?.[hoveredPoint.index];
if (dataPoint) {
return `${formatXValue(dataPoint.x)}: ${formatYValue(dataPoint.y)}`;
}
return "";
})()
})]
})
})
})]
}), showLegend && (processedData.scaledSeries?.length || 0) > 0 && jsx("div", {
className: 'glass-flex glass-flex-wrap glass-justify-center glass-gap-4 mt-6',
role: "list",
"aria-label": "Chart legend",
children: processedData.scaledSeries.map(s => jsxs("div", {
className: cn("flex items-center glass-gap-2 glass-px-2 glass-py-1 glass-radius-md transition-all duration-200 hover:-translate-y-0.5", hoveredSeriesId && hoveredSeriesId !== s.id ? "opacity-50" : "opacity-100"),
role: "listitem",
tabIndex: 0,
"aria-label": `${s.name} data series`,
onMouseEnter: () => setHoveredSeriesId(s.id),
onMouseLeave: () => setHoveredSeriesId(null),
onKeyDown: e => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
setHoveredSeriesId(hoveredSeriesId === s.id ? null : s.id);
}
},
children: [jsx("div", {
className: 'w-3 h-3 glass-radius-full',
style: {
backgroundColor: s.color
},
"aria-hidden": "true"
}), jsx("span", {
className: 'glass-text-sm text-primary/80',
children: s.name
})]
}, s.id))
})]
})]
})
});
};
export { GlassLineChart, GlassLineChart as default };
//# sourceMappingURL=GlassLineChart.js.map