aura-glass
Version:
A comprehensive glassmorphism design system for React applications with 142+ production-ready components
557 lines (554 loc) • 21.3 kB
JavaScript
'use client';
import { jsxs, jsx } from 'react/jsx-runtime';
import { useState, useRef, useMemo, useCallback } from 'react';
import { GlassCore } from '../../primitives/GlassCore.js';
import '../../primitives/glass/GlassAdvanced.js';
import '../../primitives/OptimizedGlassCore.js';
import '../../primitives/glass/OptimizedGlassAdvanced.js';
import '../../primitives/MotionNative.js';
import '../../primitives/motion/MotionFramer.js';
import { cn } from '../../lib/utilsComprehensive.js';
import { useAccessibility } from '../../hooks/useAccessibility.js';
// Chart rendering utility functions
const createSVGPath = (points, type = "line") => {
if (points.length === 0) return "";
let path = `M ${points[0].x} ${points[0].y}`;
for (let i = 1; i < points.length; i++) {
path += ` L ${points[i].x} ${points[i].y}`;
}
if (type === "area") {
const firstPoint = points[0];
const lastPoint = points[points.length - 1];
path += ` L ${lastPoint.x} 100 L ${firstPoint.x} 100 Z`;
}
return path;
};
const scaleValue = (value, domain, range) => {
const [domainMin, domainMax] = domain;
const [rangeMin, rangeMax] = range;
return rangeMin + (value - domainMin) / (domainMax - domainMin) * (rangeMax - rangeMin);
};
const generateColors = count => {
const colors = ["var(--glass-color-primary)", "var(--glass-color-danger)", "var(--glass-color-success)", "var(--glass-color-warning)", "#8B5CF6", "#EC4899", "#06B6D4", "#84CC16", "#F97316", "#6366F1"];
if (count <= colors.length) {
return colors.slice(0, count);
}
// Generate additional colors using HSL
const additionalColors = [];
for (let i = colors.length; i < count; i++) {
const hue = i * 137.508 % 360; // Golden angle approximation
additionalColors.push(`hsl(${hue}, 70%, 50%)`);
}
return [...colors, ...additionalColors];
};
const formatValue = (value, type) => {
if (typeof value === "number") {
if (type === "currency") {
return new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD"
}).format(value);
} else if (type === "percentage") {
return `${(value * 100).toFixed(1)}%`;
} else if (value > 1000000) {
return `${(value / 1000000).toFixed(1)}M`;
} else if (value > 1000) {
return `${(value / 1000).toFixed(1)}K`;
}
return value.toLocaleString();
}
if (value instanceof Date) {
return value.toLocaleDateString();
}
return String(value);
};
const GlassAdvancedDataViz = ({
data,
type = "line",
title,
subtitle,
xAxisLabel,
yAxisLabel,
showLegend = true,
showTooltip = true,
showDataLabels = false,
enableZoom = true,
enablePan = true,
enableCrosshair = true,
enableAnimation = true,
enableDrillDown = false,
drillDownLevels = [],
filters = [],
onDataPointClick,
onDrillDown,
onExport,
width = 800,
height = 400,
className
}) => {
const [tooltip, setTooltip] = useState({
visible: false,
x: 0,
y: 0,
content: ""
});
const [selectedSeries, setSelectedSeries] = useState(data.map(s => s.id));
const [zoomLevel, setZoomLevel] = useState({
x: [0, 1],
y: [0, 1]
});
const [panOffset, setPanOffset] = useState({
x: 0,
y: 0
});
const [currentDrillLevel, setCurrentDrillLevel] = useState(0);
const [appliedFilters, setAppliedFilters] = useState(filters);
useState(false);
const svgRef = useRef(null);
const containerRef = useRef(null);
useAccessibility();
// Filter and process data
const processedData = useMemo(() => {
return data.map(series => ({
...series,
data: series.data.filter(point => {
return appliedFilters.every(filter => {
const fieldValue = point.metadata?.[filter.field] ?? point.y;
switch (filter.operator) {
case "equals":
return fieldValue === filter.value;
case "contains":
return String(fieldValue).toLowerCase().includes(String(filter.value).toLowerCase());
case "greaterThan":
return Number(fieldValue) > Number(filter.value);
case "lessThan":
return Number(fieldValue) < Number(filter.value);
case "between":
return Number(fieldValue) >= Number(filter.value[0]) && Number(fieldValue) <= Number(filter.value[1]);
default:
return true;
}
});
})
})).filter(series => selectedSeries.includes(series.id));
}, [data, appliedFilters, selectedSeries]);
// Calculate chart dimensions and scales
const chartDimensions = useMemo(() => {
const margin = {
top: 20,
right: 20,
bottom: 60,
left: 60
};
const chartWidth = width - margin.left - margin.right;
const chartHeight = height - margin.top - margin.bottom;
// Calculate data bounds
const allPoints = processedData.flatMap(s => s.data);
let xMin = Infinity,
xMax = -Infinity;
let yMin = Infinity,
yMax = -Infinity;
allPoints.forEach(point => {
const xVal = typeof point.x === "number" ? point.x : new Date(point.x).getTime();
xMin = Math.min(xMin, xVal);
xMax = Math.max(xMax, xVal);
yMin = Math.min(yMin, point.y);
yMax = Math.max(yMax, point.y);
});
// Add padding
const xPadding = (xMax - xMin) * 0.05;
const yPadding = (yMax - yMin) * 0.1;
xMin -= xPadding;
xMax += xPadding;
yMin -= yPadding;
yMax += yPadding;
// Apply zoom
const zoomedXMin = xMin + (xMax - xMin) * zoomLevel.x[0];
const zoomedXMax = xMin + (xMax - xMin) * zoomLevel.x[1];
const zoomedYMin = yMin + (yMax - yMin) * zoomLevel.y[0];
const zoomedYMax = yMin + (yMax - yMin) * zoomLevel.y[1];
return {
margin,
chartWidth,
chartHeight,
xDomain: [zoomedXMin, zoomedXMax],
yDomain: [zoomedYMin, zoomedYMax]
};
}, [processedData, width, height, zoomLevel]);
// Generate chart colors
const seriesColors = useMemo(() => {
const colors = generateColors(processedData.length);
const colorMap = {};
processedData.forEach((series, index) => {
colorMap[series.id] = series.color || colors[index];
});
return colorMap;
}, [processedData]);
const handleMouseMove = useCallback(event => {
if (!showTooltip) return;
const svg = svgRef.current;
if (!svg) return;
const rect = svg.getBoundingClientRect();
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
// Find nearest data point
let nearestPoint = null;
let nearestSeries = null;
let minDistance = Infinity;
processedData.forEach(series => {
series.data.forEach(point => {
const pointX = scaleValue(typeof point.x === "number" ? point.x : new Date(point.x).getTime(), chartDimensions.xDomain, [chartDimensions.margin.left, chartDimensions.margin.left + chartDimensions.chartWidth]);
const pointY = scaleValue(point.y, chartDimensions.yDomain, [chartDimensions.margin.top + chartDimensions.chartHeight, chartDimensions.margin.top]);
const distance = Math.sqrt(Math.pow(x - pointX, 2) + Math.pow(y - pointY, 2));
if (distance < minDistance && distance < 50) {
// 50px tolerance
minDistance = distance;
nearestPoint = point;
nearestSeries = series;
}
});
});
if (nearestPoint && nearestSeries) {
const s = nearestSeries;
const p = nearestPoint;
setTooltip({
visible: true,
x: event.clientX,
y: event.clientY,
content: `${s.name}: ${formatValue(p.y)} (${formatValue(p.x)})`
});
} else {
setTooltip(prev => ({
...prev,
visible: false
}));
}
}, [showTooltip, processedData, chartDimensions]);
const handleDataPointClick = useCallback((point, series) => {
onDataPointClick?.(point, series);
if (enableDrillDown && currentDrillLevel < drillDownLevels.length - 1) {
const nextLevel = drillDownLevels[currentDrillLevel + 1];
const newFilter = {
field: nextLevel.dataKey,
operator: "equals",
value: point.metadata?.[nextLevel.dataKey] || point.category
};
setAppliedFilters(prev => [...prev, newFilter]);
setCurrentDrillLevel(prev => prev + 1);
onDrillDown?.(nextLevel, [...appliedFilters, newFilter]);
}
}, [onDataPointClick, enableDrillDown, currentDrillLevel, drillDownLevels, appliedFilters, onDrillDown]);
const handleZoom = useCallback((delta, centerX, centerY) => {
if (!enableZoom) return;
const zoomFactor = delta > 0 ? 0.9 : 1.1;
const xRange = zoomLevel.x[1] - zoomLevel.x[0];
const yRange = zoomLevel.y[1] - zoomLevel.y[0];
const newXRange = xRange * zoomFactor;
const newYRange = yRange * zoomFactor;
const xCenter = zoomLevel.x[0] + xRange * centerX;
const yCenter = zoomLevel.y[0] + yRange * centerY;
setZoomLevel({
x: [Math.max(0, xCenter - newXRange / 2), Math.min(1, xCenter + newXRange / 2)],
y: [Math.max(0, yCenter - newYRange / 2), Math.min(1, yCenter + newYRange / 2)]
});
}, [enableZoom, zoomLevel]);
const resetZoom = useCallback(() => {
setZoomLevel({
x: [0, 1],
y: [0, 1]
});
setPanOffset({
x: 0,
y: 0
});
}, []);
const toggleSeries = useCallback(seriesId => {
setSelectedSeries(prev => prev.includes(seriesId) ? prev.filter(id => id !== seriesId) : [...prev, seriesId]);
}, []);
const exportChart = useCallback(format => {
if (format === "csv" || format === "json") {
const exportData = processedData.map(series => ({
series: series.name,
data: series.data.map(point => ({
x: point.x,
y: point.y,
category: point.category,
label: point.label,
...point.metadata
}))
}));
const content = format === "csv" ? convertToCSV(exportData) : JSON.stringify(exportData, null, 2);
const blob = new Blob([content], {
type: format === "csv" ? "text/csv" : "application/json"
});
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `chart-data.${format}`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
onExport?.(format);
}, [processedData, onExport]);
const convertToCSV = data => {
const rows = ["Series,X,Y,Category,Label"];
data.forEach(series => {
series.data.forEach(point => {
rows.push(`${series.series},${point.x},${point.y},${point.category || ""},${point.label || ""}`);
});
});
return rows.join("\n");
};
const renderLineChart = () => {
return processedData.map(series => {
const points = series.data.map(point => ({
x: scaleValue(typeof point.x === "number" ? point.x : new Date(point.x).getTime(), chartDimensions.xDomain, [0, chartDimensions.chartWidth]),
y: scaleValue(point.y, chartDimensions.yDomain, [chartDimensions.chartHeight, 0])
}));
const pathD = createSVGPath(points, series.type === "area" ? "area" : "line");
return jsxs("g", {
"data-glass-component": true,
children: [series.type === "area" && jsx("path", {
d: pathD,
fill: seriesColors[series.id],
fillOpacity: 0.3,
stroke: "none"
}), jsx("path", {
d: pathD,
fill: "none",
stroke: seriesColors[series.id],
strokeWidth: 2,
strokeLinecap: "round",
strokeLinejoin: "round",
className: enableAnimation ? "transition-all duration-300" : ""
}), series.data.map((point, index) => jsx("circle", {
cx: points[index].x,
cy: points[index].y,
r: 4,
fill: seriesColors[series.id],
stroke: "white",
strokeWidth: 2,
className: 'cursor-pointer hover:r-6 transition-all',
onClick: () => handleDataPointClick(point, series)
}, `${point.id}-${index}`))]
}, series.id);
});
};
const renderBarChart = () => {
const barWidth = chartDimensions.chartWidth / (processedData[0]?.data.length || 1) * 0.8;
const barGroupWidth = barWidth / processedData.length;
return processedData.map((series, seriesIndex) => jsx("g", {
children: series.data.map((point, pointIndex) => {
const x = pointIndex * barWidth + seriesIndex * barGroupWidth + barGroupWidth / 2;
const y = scaleValue(point.y, chartDimensions.yDomain, [chartDimensions.chartHeight, 0]);
const height = chartDimensions.chartHeight - y;
return jsx("rect", {
x: x,
y: y,
width: barGroupWidth * 0.8,
height: height,
fill: seriesColors[series.id],
className: 'cursor-pointer hover:opacity-80 transition-all',
onClick: () => handleDataPointClick(point, series)
}, `${point.id}-${pointIndex}`);
})
}, series.id));
};
const renderAxes = () => {
const xTicks = 5;
const yTicks = 5;
return jsxs("g", {
children: [jsx("line", {
x1: 0,
y1: chartDimensions.chartHeight,
x2: chartDimensions.chartWidth,
y2: chartDimensions.chartHeight,
stroke: "var(--glass-gray-200)",
strokeWidth: 1
}), jsx("line", {
x1: 0,
y1: 0,
x2: 0,
y2: chartDimensions.chartHeight,
stroke: "var(--glass-gray-200)",
strokeWidth: 1
}), Array.from({
length: xTicks
}, (_, i) => {
const x = i / (xTicks - 1) * chartDimensions.chartWidth;
const value = chartDimensions.xDomain[0] + (chartDimensions.xDomain[1] - chartDimensions.xDomain[0]) * (i / (xTicks - 1));
return jsxs("g", {
children: [jsx("line", {
x1: x,
y1: chartDimensions.chartHeight,
x2: x,
y2: chartDimensions.chartHeight + 5,
stroke: "var(--glass-gray-400)",
strokeWidth: 1
}), jsx("text", {
x: x,
y: chartDimensions.chartHeight + 20,
textAnchor: "middle",
className: 'glass-text-xs fill-gray-600',
children: formatValue(value)
})]
}, i);
}), Array.from({
length: yTicks
}, (_, i) => {
const y = chartDimensions.chartHeight - i / (yTicks - 1) * chartDimensions.chartHeight;
const value = chartDimensions.yDomain[0] + (chartDimensions.yDomain[1] - chartDimensions.yDomain[0]) * (i / (yTicks - 1));
return jsxs("g", {
children: [jsx("line", {
x1: -5,
y1: y,
x2: 0,
y2: y,
stroke: "var(--glass-gray-400)",
strokeWidth: 1
}), jsx("text", {
x: -10,
y: y + 4,
textAnchor: "end",
className: 'glass-text-xs fill-gray-600',
children: formatValue(value)
})]
}, i);
}), xAxisLabel && jsx("text", {
x: chartDimensions.chartWidth / 2,
y: chartDimensions.chartHeight + 45,
textAnchor: "middle",
className: 'glass-text-sm font-medium fill-gray-700',
children: xAxisLabel
}), yAxisLabel && jsx("text", {
x: -40,
y: chartDimensions.chartHeight / 2,
textAnchor: "middle",
className: 'glass-text-sm font-medium fill-gray-700',
transform: `rotate(-90, -40, ${chartDimensions.chartHeight / 2})`,
children: yAxisLabel
})]
});
};
return jsxs("div", {
ref: containerRef,
className: cn("relative", className),
children: [jsxs(GlassCore, {
className: "glass-p-6",
children: [jsxs("div", {
className: 'glass-flex glass-items-center glass-justify-between mb-6',
children: [jsxs("div", {
children: [title && jsx("h2", {
className: 'glass-text-xl font-semibold glass-text-secondary',
children: title
}), subtitle && jsx("p", {
className: 'glass-text-secondary mt-1',
children: subtitle
})]
}), jsxs("div", {
className: "glass-flex glass-items-center glass-gap-2",
children: [enableZoom && jsx("button", {
onClick: resetZoom,
className: 'glass-px-3 glass-py-1 glass-text-xs glass-surface-subtle glass-text-secondary glass-radius hover:glass-surface-subtle transition-colors glass-focus glass-touch-target glass-contrast-guard',
children: "Reset Zoom"
}), jsxs("div", {
className: 'glass-flex glass-border glass-border-subtle glass-radius overflow-hidden',
children: [jsx("button", {
onClick: () => exportChart("csv"),
className: 'glass-px-3 glass-py-1 glass-text-xs glass-surface-subtle glass-text-secondary hover:glass-surface-subtle glass-border-r glass-border-subtle glass-focus glass-touch-target glass-contrast-guard',
children: "CSV"
}), jsx("button", {
onClick: () => exportChart("json"),
className: 'glass-px-3 glass-py-1 glass-text-xs glass-surface-subtle glass-text-secondary hover:glass-surface-subtle glass-focus glass-touch-target glass-contrast-guard',
children: "JSON"
})]
})]
})]
}), enableDrillDown && appliedFilters.length > 0 && jsxs("div", {
className: 'glass-flex glass-items-center glass-gap-2 mb-4 glass-text-sm glass-text-secondary',
children: [jsx("span", {
children: "\uD83D\uDCCA"
}), jsx("span", {
children: "Filtered by:"
}), appliedFilters.map((filter, index) => jsxs("span", {
className: 'glass-px-2 glass-py-1 glass-surface-subtle text-primary glass-radius glass-flex glass-items-center glass-gap-1',
children: [filter.field, ": ", filter.value, jsx("button", {
onClick: () => setAppliedFilters(prev => prev.filter((_, i) => i !== index)),
className: 'text-primary hover:text-primary ml-1 glass-focus glass-touch-target glass-contrast-guard',
children: "\u2715"
})]
}, index))]
}), jsx("div", {
className: 'relative',
children: jsx("svg", {
ref: svgRef,
width: width,
height: height,
onMouseMove: handleMouseMove,
onMouseLeave: () => setTooltip(prev => ({
...prev,
visible: false
})),
onWheel: e => {
e.preventDefault();
const rect = svgRef.current?.getBoundingClientRect();
if (rect) {
const centerX = (e.clientX - rect.left - chartDimensions.margin.left) / chartDimensions.chartWidth;
const centerY = (e.clientY - rect.top - chartDimensions.margin.top) / chartDimensions.chartHeight;
handleZoom(e.deltaY, centerX, centerY);
}
},
className: "glass-border glass-border-subtle glass-radius",
children: jsxs("g", {
transform: `translate(${chartDimensions.margin.left}, ${chartDimensions.margin.top})`,
children: [renderAxes(), type === "line" || type === "combo" ? renderLineChart() : null, type === "bar" || type === "combo" ? renderBarChart() : null, jsx("defs", {
children: jsx("pattern", {
id: "grid",
width: "40",
height: "40",
patternUnits: "userSpaceOnUse",
children: jsx("path", {
d: "M 40 0 L 0 0 0 40",
fill: "none",
stroke: "var(--glass-gray-100)",
strokeWidth: "1"
})
})
}), jsx("rect", {
width: chartDimensions.chartWidth,
height: chartDimensions.chartHeight,
fill: "url(#grid)",
opacity: "0.5"
})]
})
})
}), showLegend && jsx("div", {
className: 'glass-flex glass-flex-wrap glass-items-center glass-gap-4 mt-4 pt-4 glass-border-t glass-border-subtle',
children: processedData.map(series => jsxs("button", {
onClick: () => toggleSeries(series.id),
className: cn("flex items-center gap-2 px-3 py-1 rounded text-sm transition-opacity glass-focus glass-touch-target glass-contrast-guard", selectedSeries.includes(series.id) ? "opacity-100" : "opacity-50"),
children: [jsx("div", {
className: 'w-3 h-3 glass-radius-full',
style: {
backgroundColor: seriesColors[series.id]
}
}), jsx("span", {
children: series.name
})]
}, series.id))
})]
}), tooltip.visible && jsx("div", {
className: 'fixed z-50 glass-px-3 glass-py-2 glass-surface-subtle text-primary glass-text-sm glass-radius glass-shadow-lg pointer-events-none',
style: {
left: tooltip.x + 10,
top: tooltip.y - 10,
transform: tooltip.x > window.innerWidth - 200 ? "translateX(-100%)" : "none"
},
children: tooltip.content
})]
});
};
export { GlassAdvancedDataViz };
//# sourceMappingURL=GlassAdvancedDataViz.js.map