UNPKG

aura-glass

Version:

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

457 lines (454 loc) 17.2 kB
'use client'; import { jsx, jsxs, Fragment } 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 { GlassCard, CardHeader, CardTitle, CardContent } from '../card/GlassCard.js'; /** * GlassBarChart component * A glassmorphism bar chart with multiple series support and interactive features */ const GlassBarChart = ({ // TODO: Integrate ContrastGuard in chart labels, tooltips, and legends for WCAG AA compliance title, series = [], width = 600, height = 400, showGrid = true, showDataLabels = false, 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(), orientation = "vertical", barPadding = 0.1, barWidthRatio = 0.8, className, loading = false, ...props }) => { const [hoveredBar, setHoveredBar] = 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 { bars: [], xLabels: [], yLabels: [] }; } // Combine all data points from all series const allDataPoints = []; series.forEach((s, seriesIndex) => { s.data?.forEach((dataPoint, dataIndex) => { allDataPoints.push({ seriesId: s.id, seriesIndex, data: dataPoint, index: dataIndex }); }); }); // Group by X value (for grouped bars) const groupedData = new Map(); allDataPoints.forEach(point => { const key = point.data?.x; if (!groupedData.has(key)) { groupedData.set(key, []); } groupedData.get(key).push(point); }); // Find min/max values const allYValues = allDataPoints.map(p => p.data?.y); const yMin = Math.min(...allYValues); const yMax = Math.max(...allYValues); // 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 scaleY = y => { if (orientation === "vertical") { return chartHeight - (y - yMinPadded) / (yMaxPadded - yMinPadded) * chartHeight; } else { return (y - yMinPadded) / (yMaxPadded - yMinPadded) * chartWidth; } }; // Generate bars const bars = []; const xLabels = []; const numGroups = groupedData.size; const groupWidth = orientation === "vertical" ? chartWidth / numGroups : chartHeight / numGroups; let groupIndex = 0; groupedData.forEach((group, xValue) => { const groupStart = orientation === "vertical" ? padding.left + groupIndex * groupWidth : padding.top + groupIndex * groupWidth; const barsInGroup = group?.length || 0; const barWidth = (groupWidth * barWidthRatio - (barsInGroup - 1) * barPadding) / barsInGroup; group.forEach((point, barIndex) => { const currentSeries = series?.[point.seriesIndex]; const color = point.data?.color || currentSeries.color || colors[point.seriesIndex % (colors?.length || 0)]; if (orientation === "vertical") { const barX = groupStart + barIndex * (barWidth + barPadding) + (groupWidth - groupWidth * barWidthRatio) / 2; const barHeight = chartHeight - scaleY(point.data?.y); const barY = padding.top + scaleY(point.data?.y); bars.push({ seriesId: point.seriesId, seriesIndex: point.seriesIndex, dataIndex: point.index, x: barX, y: barY, width: barWidth, height: barHeight, value: point.data?.y, label: point.data?.label || formatXValue(point.data?.x), color }); } else { // Horizontal bars const barY = groupStart + barIndex * (barWidth + barPadding) + (groupWidth - groupWidth * barWidthRatio) / 2; const barWidthH = scaleY(point.data?.y); const barX = padding.left; bars.push({ seriesId: point.seriesId, seriesIndex: point.seriesIndex, dataIndex: point.index, x: barX, y: barY, width: barWidthH, height: barWidth, value: point.data?.y, label: point.data?.label || formatXValue(point.data?.x), color }); } }); // Add X label if (orientation === "vertical") { xLabels.push({ x: groupStart + groupWidth / 2, y: height - padding.bottom + 20, label: formatXValue(xValue) }); } else { xLabels.push({ x: groupStart + groupWidth / 2, y: padding.left - 10, label: formatXValue(xValue) }); } groupIndex++; }); // Generate Y axis labels const yLabels = [0, 0.25, 0.5, 0.75, 1].map(ratio => { const value = yMinPadded + (yMaxPadded - yMinPadded) * ratio; if (orientation === "vertical") { return { x: padding.left - 10, y: padding.top + chartHeight - chartHeight * ratio + 4, label: formatYValue(value) }; } else { return { x: padding.left + chartWidth * ratio, y: height - padding.bottom + 20, label: formatYValue(value) }; } }); return { bars, xLabels, yLabels }; }, [series, width, height, orientation, barPadding, barWidthRatio, formatYValue, formatXValue, colors]); // Handle bar hover const handleBarHover = (bar, _event) => { if (showTooltips) { const svgX = orientation === "vertical" ? bar.x + bar.width / 2 : bar.x + bar.width; const svgY = orientation === "vertical" ? bar.y : bar.y + bar.height / 2; setHoveredBar({ seriesId: bar.seriesId, dataIndex: bar.dataIndex, x: svgX, y: svgY, value: bar.value }); } }; const handleBarLeave = () => { setHoveredBar(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 glass-flex glass-items-end glass-justify-center glass-gap-2', children: Array.from({ length: 8 }).map((_, i) => jsx("div", { className: "glass-surface-subtle/20 glass-radius-t", style: { width: "20px", height: `${Math.random() * 100}%` } }, i)) }), 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 && jsx("g", { className: 'opacity-20', children: processedData.yLabels.map((label, index) => { if (orientation === "vertical") { return jsx("line", { x1: padding.left, y1: label.y - 4, x2: width - padding.right, y2: label.y - 4, stroke: "currentColor", strokeWidth: "1", className: 'text-primary/30' }, `h-grid-${index}`); } else { return 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.bars.map((bar, index) => jsxs(MotionFramer, { preset: "slideUp", delay: index * 50, className: 'relative', children: [jsx("rect", { x: bar.x, y: bar.y, width: bar.width, height: bar.height, fill: bar.color, className: 'cursor-pointer transition-all duration-200 hover:opacity-80', style: { animation: `barGrow ${animationDuration}ms ease-out ${index * 50}ms both` }, onMouseEnter: e => handleBarHover(bar), onMouseLeave: handleBarLeave, opacity: hoveredSeriesId && hoveredSeriesId !== bar.seriesId ? 0.35 : 1 }), showDataLabels && jsx("text", { x: bar.x + bar.width / 2, y: orientation === "vertical" ? bar.y - 5 : bar.y + bar.height / 2 + 4, textAnchor: "middle", className: 'glass-text-xs fill-white/80 font-medium', style: { fontSize: "0.625rem" }, children: formatYValue(bar.value) })] }, `${bar.seriesId}-${bar.dataIndex}`)), orientation === "vertical" ? 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: height - padding.bottom, x2: padding.left, y2: padding.top, stroke: "currentColor", strokeWidth: "1", className: 'text-primary/50' }), orientation === "vertical" ? jsx("line", { x1: padding.left, y1: padding.top, x2: padding.left, y2: height - padding.bottom, stroke: "currentColor", strokeWidth: "1", className: 'text-primary/50' }) : jsx("line", { x1: padding.left, y1: padding.top, x2: width - padding.right, y2: padding.top, 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, textAnchor: orientation === "vertical" ? "end" : "middle", 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 && orientation === "vertical" && 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 }), yAxisLabel && orientation === "horizontal" && jsx("text", { x: width / 2, y: 15, textAnchor: "middle", className: 'glass-text-sm fill-white/80 font-medium', children: yAxisLabel })] }), hoveredBar && jsxs(Fragment, { children: [jsx("svg", { width: width, height: height, className: 'absolute inset-0 pointer-events-none', children: orientation === "vertical" ? jsx("line", { x1: hoveredBar.x, y1: padding.top, x2: hoveredBar.x, y2: height - padding.bottom, stroke: "white", strokeOpacity: 0.25, strokeDasharray: "4 4" }) : jsx("line", { x1: padding.left, y1: hoveredBar.y, x2: width - padding.right, y2: hoveredBar.y, stroke: "white", strokeOpacity: 0.25, strokeDasharray: "4 4" }) }), 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: hoveredBar.x + 10, top: hoveredBar.y - 10, transform: hoveredBar.x > width / 2 ? "translateX(-100%)" : "none" }, children: jsxs("div", { className: 'text-primary glass-text-sm', children: [jsx("div", { className: 'font-medium', children: series.find(s => s.id === hoveredBar.seriesId)?.name }), jsxs("div", { className: 'text-primary/80', children: ["Value: ", formatYValue(hoveredBar.value)] })] }) }) })] })] }), showLegend && series.length > 0 && jsx("div", { className: 'glass-flex glass-flex-wrap glass-justify-center glass-gap-4 mt-6', role: "list", "aria-label": "Chart legend", children: series.map((s, idx) => 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-md', style: { backgroundColor: s.color || colors[idx % (colors?.length || 0)] }, "aria-hidden": "true" }), jsx("span", { className: 'glass-text-sm text-primary/80', children: s.name })] }, s.id)) })] })] }) }); }; export { GlassBarChart, GlassBarChart as default }; //# sourceMappingURL=GlassBarChart.js.map