UNPKG

aura-glass

Version:

A comprehensive glassmorphism design system for React applications with 142+ production-ready components

375 lines (372 loc) 14.5 kB
'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