aura-glass
Version:
A comprehensive glassmorphism design system for React applications with 142+ production-ready components
298 lines (295 loc) • 12.3 kB
JavaScript
'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