UNPKG

aura-glass

Version:

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

298 lines (295 loc) 12.3 kB
'use client'; import { jsx, jsxs } from 'react/jsx-runtime'; import { cn } from '../../lib/utilsComprehensive.js'; import { forwardRef, 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'; /** * GlassPieChart component * A glassmorphism pie/donut chart with interactive segments and smooth animations */ const GlassPieChart = /*#__PURE__*/forwardRef(function GlassPieChartComponent({ // TODO: Integrate ContrastGuard in chart labels, tooltips, and legends for WCAG AA compliance title, data = [], size = 300, innerRadius = 0, showLegend = true, legendPosition = "right", showLabels = false, showPercentages = true, colors, animationDuration = 1000, showTooltips = true, formatValue, formatPercentage, loading = false, className, ...props }, ref) { const defaultColors = ["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)"]; const actualColors = colors || defaultColors; const actualAnimationDuration = animationDuration; const actualShowTooltips = showTooltips; const actualFormatValue = formatValue || (value => value.toString()); const actualFormatPercentage = formatPercentage || (percentage => `${Math.round(percentage)}%`); const actualLoading = loading; const [hoveredSegment, setHoveredSegment] = useState(null); const [hoveredLegendIndex, setHoveredLegendIndex] = useState(null); const centerX = size / 2; const centerY = size / 2; const radius = (size - 40) / 2; // Padding around the chart const labelRadius = radius + 30; // Distance for labels // Process data for chart const processedData = useMemo(() => { if (!data || !Array.isArray(data) || data?.length === 0) { return { segments: [], total: 0 }; } const total = data?.reduce((sum, item) => sum + item?.value, 0); let currentAngle = -Math.PI / 2; // Start from top const segments = data?.map((item, index) => { const percentage = item?.value / total * 100; const angle = item?.value / total * 2 * Math.PI; const startAngle = currentAngle; const endAngle = currentAngle + angle; // Calculate path for segment const x1 = centerX + Math.cos(startAngle) * radius; const y1 = centerY + Math.sin(startAngle) * radius; const x2 = centerX + Math.cos(endAngle) * radius; const y2 = centerY + Math.sin(endAngle) * radius; const largeArcFlag = angle > Math.PI ? 1 : 0; let path = ""; if (innerRadius > 0) { // Donut chart const innerX1 = centerX + Math.cos(startAngle) * innerRadius; const innerY1 = centerY + Math.sin(startAngle) * innerRadius; const innerX2 = centerX + Math.cos(endAngle) * innerRadius; const innerY2 = centerY + Math.sin(endAngle) * innerRadius; path = `M ${x1} ${y1} A ${radius} ${radius} 0 ${largeArcFlag} 1 ${x2} ${y2} L ${innerX2} ${innerY2} A ${innerRadius} ${innerRadius} 0 ${largeArcFlag} 0 ${innerX1} ${innerY1} Z`; } else { // Pie chart path = `M ${centerX} ${centerY} L ${x1} ${y1} A ${radius} ${radius} 0 ${largeArcFlag} 1 ${x2} ${y2} Z`; } // Calculate label position const labelAngle = startAngle + angle / 2; const labelX = centerX + Math.cos(labelAngle) * labelRadius; const labelY = centerY + Math.sin(labelAngle) * labelRadius; currentAngle = endAngle; return { ...item, path, percentage, startAngle, endAngle, labelX, labelY, color: item?.color || actualColors[index % (actualColors?.length || 0)] }; }); return { segments, total }; }, [data, centerX, centerY, radius, innerRadius, labelRadius, actualColors]); // Handle segment hover const handleSegmentHover = (segment, event) => { if (actualShowTooltips) { const rect = event.currentTarget.getBoundingClientRect(); setHoveredSegment({ index: processedData.segments.indexOf(segment), x: rect.left + rect.width / 2, y: rect.top, label: segment.label, value: segment.value, percentage: segment.percentage }); } }; const handleSegmentLeave = () => { setHoveredSegment(null); }; // Calculate legend layout const legendItems = processedData.segments.map((segment, index) => ({ ...segment, index })); // Loading skeleton if (actualLoading) { 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: "glass-flex glass-items-center glass-justify-center", children: jsx("div", { className: 'w-64 h-64 glass-surface-subtle/10 glass-radius-full' }) }), showLegend && 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, { ref: ref, 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 }) }), jsx(CardContent, { className: "glass-p-4", children: jsxs("div", { className: cn("flex", legendPosition === "right" ? "flex-row" : "flex-col", "items-center glass-gap-6"), children: [jsxs("div", { className: 'relative glass-flex-shrink-0', children: [jsxs("svg", { width: size, height: size, className: 'overflow-visible', children: [processedData.segments.map((segment, index) => jsxs(MotionFramer, { preset: "scaleIn", delay: index * 100, className: 'relative', children: [jsx("path", { d: segment.path, fill: segment.color, stroke: "rgba(var(--glass-color-white) / var(--glass-opacity-20))", strokeWidth: "1", className: 'cursor-pointer transition-all duration-200 hover:opacity-80', onMouseEnter: e => handleSegmentHover(segment, e), onMouseLeave: handleSegmentLeave, style: { animation: `pieSegment ${actualAnimationDuration}ms ease-out ${index * 100}ms both`, opacity: hoveredLegendIndex !== null && hoveredLegendIndex !== index ? 0.35 : 1 } }), showLabels && jsxs("text", { x: segment.labelX, y: segment.labelY, textAnchor: segment.labelX > centerX ? "start" : "end", className: 'glass-text-xs fill-white/80 font-medium', style: { fontSize: "var(--typography-caption-size)" }, children: [segment.label, showPercentages && jsx("tspan", { x: segment.labelX, dy: "14", className: 'glass-text-xs fill-white/60', children: actualFormatPercentage(segment.percentage) })] })] }, `${segment.label}-${index}`)), innerRadius > 0 && jsxs("g", { children: [jsx("circle", { cx: centerX, cy: centerY, r: innerRadius, fill: "var(--glass-bg-default)", stroke: "rgba(var(--glass-color-white) / var(--glass-opacity-20))", strokeWidth: "1" }), jsx("text", { x: centerX, y: centerY - 5, textAnchor: "middle", className: 'glass-text-sm fill-white/80 font-medium', children: "Total" }), jsx("text", { x: centerX, y: centerY + 15, textAnchor: "middle", className: 'glass-text-lg fill-white font-semibold', children: actualFormatValue(processedData.total) })] })] }), hoveredSegment && 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: hoveredSegment.x + 10, top: hoveredSegment.y - 10, transform: hoveredSegment.x > size / 2 ? "translateX(-100%)" : "none" }, children: jsxs("div", { className: 'text-primary glass-text-sm', children: [jsx("div", { className: 'font-medium', children: hoveredSegment.label }), jsxs("div", { className: 'text-primary/80', children: [actualFormatValue(hoveredSegment.value), " (", actualFormatPercentage(hoveredSegment.percentage), ")"] })] }) }) })] }), showLegend && (legendItems?.length || 0) > 0 && jsx("div", { className: cn("flex flex-wrap glass-gap-3", legendPosition === "right" ? "flex-col" : "justify-center"), children: legendItems.map(item => jsxs(MotionFramer, { preset: "slideUp", delay: item?.index * 50, 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", hoveredLegendIndex !== null && hoveredLegendIndex !== item?.index ? "opacity-50" : "opacity-100"), onMouseEnter: () => setHoveredLegendIndex(item?.index), onMouseLeave: () => setHoveredLegendIndex(null), children: [jsx("div", { className: 'w-3 h-3 glass-radius-md', style: { backgroundColor: item?.color } }), jsxs("div", { className: 'glass-text-sm text-primary/80', children: [jsx("span", { className: 'font-medium', children: item?.label }), jsxs("span", { className: 'glass-ml-2 text-primary/60', children: [actualFormatValue(item?.value), " (", actualFormatPercentage(item?.percentage), ")"] })] })] }, item?.label)) })] }) })] }) }); }); GlassPieChart.displayName = "GlassPieChart"; const GlassDonutChart = /*#__PURE__*/forwardRef(({ size = 300, innerRadiusRatio = 0.6, ...props }, ref) => { const innerRadius = (size - 40) / 2 * innerRadiusRatio; return jsx(GlassPieChart, { ref: ref, ...props, size: size, innerRadius: innerRadius }); }); GlassDonutChart.displayName = "GlassDonutChart"; export { GlassDonutChart, GlassPieChart, GlassPieChart as default }; //# sourceMappingURL=GlassPieChart.js.map