UNPKG

r8r

Version:

A modern, zero-dependency radar (spider) chart for visual comparisons in React

1,050 lines (1,042 loc) 69 kB
'use strict'; var jsxRuntime = require('react/jsx-runtime'); var react = require('react'); /****************************************************************************** Copyright (c) Microsoft Corporation. Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ***************************************************************************** */ var __assign = function() { __assign = Object.assign || function __assign(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; } return t; }; return __assign.apply(this, arguments); }; function __spreadArray(to, from, pack) { if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { if (ar || !(i in from)) { if (!ar) ar = Array.prototype.slice.call(from, 0, i); ar[i] = from[i]; } } return to.concat(ar || Array.prototype.slice.call(from)); } typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) { var e = new Error(message); return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e; }; // Predefined themes var themes = { light: { backgroundColor: '#ffffff', gridColor: '#dbe4ef', textColor: '#374151', legendBackgroundColor: '#f9fafb', legendBorderColor: '#e5e7eb', }, dark: { backgroundColor: '#1f2937', gridColor: '#6b6d6f', textColor: '#f9fafb', legendBackgroundColor: '#111827', legendBorderColor: '#374151', }, unicorn: { backgroundColor: 'linear-gradient(142deg, rgba(255, 240, 217, 1) 0%, rgba(255, 196, 233, 1) 100%)', gridColor: '#F598D3', textColor: '#581c87', legendBackgroundColor: 'linear-gradient(142deg, rgba(50, 16, 64, 1) 0%, rgba(84, 14, 135, 1) 100%)', legendBorderColor: '#F598D3', legendTextColor: '#ffffffdd', }, retro: { backgroundColor: '#f5f1e8', gridColor: '#8b7355', textColor: '#4a3c2a', legendBackgroundColor: '#5d4e37', legendBorderColor: '#8b7355', legendTextColor: '#f5f1e8', }, }; // Default colors for datasets var defaultColors = [ '#3b82f6', '#ef4444', '#10b981', '#f59e0b', '#8b5cf6', '#06b6d4', '#84cc16', '#f97316', '#ec4899', '#6b7280', // gray ]; // Unicorn theme colors var unicornColors = [ '#8531EB', '#EC4899', '#f59e0b', '#10b981', '#3b82f6', '#f97316', '#06b6d4', '#84cc16', '#ef4444', '#8b5cf6', // violet ]; // Retro theme colors (70s Colorado inspired - vibrant) var retroColors = [ '#ff6b35', '#f7931e', '#e6b800', '#d62828', '#f77f00', '#d4a017', '#e76f51', '#2a9d8f', '#264653', '#b8860b', // darker warm gold ]; // Helper function to process colors and gradients var processColorOrGradient = function (colorOrGradient, fallback, chartCenter, polygonBounds) { if (!colorOrGradient) { return { fill: fallback }; } if (typeof colorOrGradient === 'string') { return { fill: colorOrGradient }; } if (colorOrGradient.type === 'gradient') { var gradientId = "gradient-".concat(Math.random().toString(36).substr(2, 9)); var gradientDef = void 0; if (chartCenter && polygonBounds) { // Calculate relative position of chart center within polygon bounds var polygonWidth = polygonBounds.maxX - polygonBounds.minX; var polygonHeight = polygonBounds.maxY - polygonBounds.minY; ((chartCenter.x - polygonBounds.minX) / polygonWidth) * 100; ((chartCenter.y - polygonBounds.minY) / polygonHeight) * 100; // Calculate gradient direction from chart center to polygon center var polygonCenterX = (polygonBounds.minX + polygonBounds.maxX) / 2; var polygonCenterY = (polygonBounds.minY + polygonBounds.maxY) / 2; var angle = Math.atan2(polygonCenterY - chartCenter.y, polygonCenterX - chartCenter.x) * 180 / Math.PI; // Convert angle to SVG gradient coordinates var userAngle = colorOrGradient.angle || 0; var totalAngle = angle + userAngle; var radians = (totalAngle * Math.PI) / 180; var x1 = 0.5 - Math.cos(radians) * 0.5; var y1 = 0.5 - Math.sin(radians) * 0.5; var x2 = 0.5 + Math.cos(radians) * 0.5; var y2 = 0.5 + Math.sin(radians) * 0.5; gradientDef = (jsxRuntime.jsx("defs", { children: jsxRuntime.jsxs("linearGradient", __assign({ id: gradientId, x1: x1, y1: y1, x2: x2, y2: y2 }, { children: [jsxRuntime.jsx("stop", { offset: "0%", stopColor: colorOrGradient.from }), jsxRuntime.jsx("stop", { offset: "100%", stopColor: colorOrGradient.to })] })) })); } else { // Fallback to center of polygon with default angle var angle = colorOrGradient.angle || 0; var radians = (angle * Math.PI) / 180; var x1 = 0.5 - Math.cos(radians) * 0.5; var y1 = 0.5 - Math.sin(radians) * 0.5; var x2 = 0.5 + Math.cos(radians) * 0.5; var y2 = 0.5 + Math.sin(radians) * 0.5; gradientDef = (jsxRuntime.jsx("defs", { children: jsxRuntime.jsxs("linearGradient", __assign({ id: gradientId, x1: x1, y1: y1, x2: x2, y2: y2 }, { children: [jsxRuntime.jsx("stop", { offset: "0%", stopColor: colorOrGradient.from }), jsxRuntime.jsx("stop", { offset: "100%", stopColor: colorOrGradient.to })] })) })); } return { fill: "url(#".concat(gradientId, ")"), gradientId: gradientId, gradientDef: gradientDef }; } return { fill: fallback }; }; var R8R = function (_a) { var _b; var data = _a.data, chart = _a.chart, _c = _a.width, width = _c === void 0 ? 400 : _c, _d = _a.theme, theme = _d === void 0 ? 'light' : _d, backgroundColor = _a.backgroundColor, gridColor = _a.gridColor, textColor = _a.textColor, legendBackgroundColor = _a.legendBackgroundColor, legendTextColor = _a.legendTextColor, legendBorderColor = _a.legendBorderColor, _e = _a.colors, colors = _e === void 0 ? theme === 'unicorn' ? unicornColors : theme === 'retro' ? retroColors : defaultColors : _e, _f = _a.showGrid, showGrid = _f === void 0 ? true : _f, _g = _a.showLabels, showLabels = _g === void 0 ? true : _g, _h = _a.showLegend, showLegend = _h === void 0 ? true : _h, _j = _a.legendTitle, legendTitle = _j === void 0 ? '' : _j, _k = _a.showBorder, showBorder = _k === void 0 ? true : _k, _l = _a.animationDuration, animationDuration = _l === void 0 ? 200 : _l, _m = _a.dataBorderRadius, dataBorderRadius = _m === void 0 ? 0 : _m, _o = _a.footnotes, footnotes = _o === void 0 ? [] : _o, _p = _a.placeholder, placeholder = _p === void 0 ? 'Footnotes: Click to explore' : _p, initialActiveFootnote = _a.activeFootnote, _q = _a.className, className = _q === void 0 ? '' : _q, _r = _a.style, style = _r === void 0 ? {} : _r, onChartChange = _a.onChartChange; var _s = react.useState(false), isAnimating = _s[0], setIsAnimating = _s[1]; var _t = react.useState(function () { // Initialize dataset states based on data var states = new Map(); data.forEach(function (dataset, index) { states.set(index, { status: dataset.status || 'active' }); }); return states; }), datasetStates = _t[0], setDatasetStates = _t[1]; var _u = react.useState(null); _u[0]; var setHoveredDataset = _u[1]; var _v = react.useState(false); _v[0]; var setIsMobile = _v[1]; var _w = react.useState(0), containerWidth = _w[0], setContainerWidth = _w[1]; var _x = react.useState(data), localData = _x[0], setLocalData = _x[1]; var _y = react.useState(colors), localColors = _y[0], setLocalColors = _y[1]; var _z = react.useState(null), highlightedAxis = _z[0], setHighlightedAxis = _z[1]; var _0 = react.useState(null), axisLongPressTimer = _0[0], setAxisLongPressTimer = _0[1]; var _1 = react.useState(false), isAxisAnimating = _1[0], setIsAxisAnimating = _1[1]; var _2 = react.useState(chart), internalChartState = _2[0], setInternalChartState = _2[1]; var _3 = react.useState(initialActiveFootnote !== null && initialActiveFootnote !== void 0 ? initialActiveFootnote : null), activeFootnote = _3[0], setActiveFootnote = _3[1]; react.useRef(null); var containerRef = react.useRef(null); // Get container width react.useEffect(function () { var _a; var updateContainerWidth = function () { if (!containerRef.current) return; // Try to find the Storybook main container first var storybookMain = document.querySelector('body.sb-show-main'); var observerElement = null; if (storybookMain) { // Use Storybook's main container observerElement = storybookMain; console.log('Using Storybook main container for width calculation'); } else { // Fallback to parent element observerElement = containerRef.current.parentElement; console.log('Using parent element for width calculation'); } if (observerElement) { var observerWidth = observerElement.getBoundingClientRect().width; // Set chart width to minimum of observer element width and width prop // If observer element is smaller than width prop, subtract 20px for padding var availableWidth = observerWidth < width ? observerWidth - 20 : Math.min(observerWidth, width); setContainerWidth(availableWidth); } else { // Fallback to viewport width console.log('No observer element found, using viewport width:', window.innerWidth); setContainerWidth(Math.min(window.innerWidth, width)); } }; // Initial update updateContainerWidth(); // Set up ResizeObserver var resizeObserver = null; // Try to find the Storybook main container first var storybookMain = document.querySelector('body.sb-show-main'); var observerElement = null; if (storybookMain) { // Use Storybook's main container observerElement = storybookMain; console.log('Setting up ResizeObserver for Storybook main container'); } else { // Fallback to parent element observerElement = ((_a = containerRef.current) === null || _a === void 0 ? void 0 : _a.parentElement) || null; console.log('Setting up ResizeObserver for parent element'); } if (observerElement) { resizeObserver = new ResizeObserver(function (entries) { console.log('ResizeObserver triggered with', entries.length, 'entries'); updateContainerWidth(); }); // Observe the determined element resizeObserver.observe(observerElement); } // Window resize listener as backup var handleResize = function () { console.log('Window resize detected'); updateContainerWidth(); }; window.addEventListener('resize', handleResize); return function () { window.removeEventListener('resize', handleResize); if (resizeObserver) { console.log('Disconnecting ResizeObserver'); resizeObserver.disconnect(); } }; }, [width]); // Mobile detection - now based on whether we should stack legend react.useEffect(function () { var checkMobile = function () { // Use the observed container width directly var availableWidth = containerWidth || window.innerWidth; var actualWidth = availableWidth >= width ? width : availableWidth; var shouldStackLegend = actualWidth < 450; setIsMobile(shouldStackLegend); }; checkMobile(); }, [width, containerWidth]); // Merge theme with custom overrides var currentTheme = react.useMemo(function () { var baseTheme = themes[theme]; return { backgroundColor: backgroundColor || baseTheme.backgroundColor, gridColor: gridColor || baseTheme.gridColor, textColor: textColor || baseTheme.textColor, legendBackgroundColor: legendBackgroundColor || baseTheme.legendBackgroundColor, legendTextColor: legendTextColor || baseTheme.legendTextColor || baseTheme.textColor, legendBorderColor: legendBorderColor || baseTheme.legendBorderColor, }; }, [theme, backgroundColor, gridColor, textColor, legendBackgroundColor, legendTextColor, legendBorderColor]); // Validate data react.useEffect(function () { if (!data || data.length === 0) { console.warn('R8R: data is required and must not be empty'); } if (!chart || chart.length === 0) { console.warn('R8R: chart is required and must not be empty'); } if (chart.length > 10) { console.warn('R8R: Maximum 10 dimensions supported'); } }, [data, chart]); // Update dataset states when data changes react.useEffect(function () { var newStates = new Map(); var currentData = localData; currentData.forEach(function (dataset, index) { var existingState = datasetStates.get(index); newStates.set(index, { status: dataset.status || (existingState === null || existingState === void 0 ? void 0 : existingState.status) || 'active' }); }); setDatasetStates(newStates); }, [data, localData]); // Sync localData with data prop when it changes react.useEffect(function () { setLocalData(data); }, [data]); // Sync localColors with colors prop when it changes react.useEffect(function () { setLocalColors(colors); }, [colors]); // Sync internal chart state with chart prop when it changes react.useEffect(function () { setInternalChartState(chart); }, [chart]); // Calculate chart dimensions and positioning var chartConfig = react.useMemo(function () { // Use the observed container width directly var availableWidth = containerWidth || window.innerWidth; // Determine actual width: use width prop if container is larger, otherwise use container width var actualWidth = availableWidth >= width ? width : availableWidth; // Determine if we should stack legend above chart (when component is narrow) var shouldStackLegend = actualWidth < 450; // Calculate legend dimensions var legendWidth = showLegend && !shouldStackLegend ? 160 : 0; var legendHeight = showLegend && shouldStackLegend ? 80 : 0; // Chart dimensions - subtract 40px for legend padding when legend is to the left var chartWidth = actualWidth - legendWidth - (showLegend && !shouldStackLegend ? 40 : 0); var chartHeight = chartWidth; // Make chart square // Center and radius var centerX = chartWidth / 2; var centerY = chartHeight / 2; var radius = Math.min(centerX, centerY) * 0.7; var maxValue = Math.max.apply(Math, chart.map(function (d) { return d.maxValue || 100; })); // Dynamic height calculation var totalHeight = chartHeight + legendHeight; return { centerX: centerX, centerY: centerY, radius: radius, maxValue: maxValue, legendWidth: legendWidth, legendHeight: legendHeight, chartWidth: chartWidth, chartHeight: chartHeight, actualWidth: actualWidth, totalHeight: totalHeight, shouldStackLegend: shouldStackLegend, }; }, [width, chart, showLegend, containerWidth]); // Calculate polygon points for a dataset var calculatePoints = function (dataset, color) { var centerX = chartConfig.centerX, centerY = chartConfig.centerY, radius = chartConfig.radius, maxValue = chartConfig.maxValue; var angleStep = (2 * Math.PI) / chart.length; return chart.map(function (point, index) { var angle = index * angleStep - Math.PI / 2; // Start from top var value = dataset.values[point.label] || 0; var normalizedValue = value / (point.maxValue || maxValue); var pointRadius = radius * normalizedValue; return { x: centerX + pointRadius * Math.cos(angle), y: centerY + pointRadius * Math.sin(angle), label: point.label, value: value, maxValue: point.maxValue || maxValue, angle: angle, }; }); }; // Process dataset colors and gradients var processedDatasetColors = react.useMemo(function () { var centerX = chartConfig.centerX, centerY = chartConfig.centerY; var chartCenter = { x: centerX, y: centerY }; var currentData = localData; var currentColors = localColors; return currentData.map(function (dataset, index) { var colorOrGradient = dataset.color || currentColors[index % currentColors.length]; return processColorOrGradient(colorOrGradient, '#3b82f6', chartCenter); }); }, [localData, localColors, chartConfig]); // Generate all dataset points var allDatasetPoints = react.useMemo(function () { var currentData = localData; return currentData.map(function (dataset, index) { return ({ dataset: dataset, points: calculatePoints(dataset, processedDatasetColors[index].fill), color: processedDatasetColors[index], index: index, }); }); }, [localData, chart, processedDatasetColors, chartConfig]); // Generate grid circles var gridCircles = react.useMemo(function () { if (!showGrid) return []; var centerX = chartConfig.centerX, centerY = chartConfig.centerY, radius = chartConfig.radius; var circles = []; var levels = 5; for (var i = 1; i <= levels; i++) { var circleRadius = (radius * i) / levels; circles.push({ cx: centerX, cy: centerY, r: circleRadius, }); } return circles; }, [showGrid, chartConfig]); // Generate axis lines var axisLines = react.useMemo(function () { var centerX = chartConfig.centerX, centerY = chartConfig.centerY, radius = chartConfig.radius; var lines = []; var angleStep = (2 * Math.PI) / chart.length; for (var i = 0; i < chart.length; i++) { var angle = i * angleStep - Math.PI / 2; var endX = centerX + radius * Math.cos(angle); var endY = centerY + radius * Math.sin(angle); lines.push({ x1: centerX, y1: centerY, x2: endX, y2: endY, label: chart[i].label, angle: angle, index: i, }); } return lines; }, [chart, chartConfig]); // Generate intersection lines and labels for highlighted axes var intersectionElements = react.useMemo(function () { var _a, _b; // Show intersection elements when hovering OR when any chart element is highlighted var shouldShowElements = highlightedAxis !== null || internalChartState.some(function (dataPoint) { var _a; return (_a = dataPoint.highlighted) !== null && _a !== void 0 ? _a : false; }); if (!shouldShowElements) return { lines: [], labels: [], axisOverlays: [] }; var centerX = chartConfig.centerX, centerY = chartConfig.centerY, radius = chartConfig.radius; var allLines = []; var allLabels = []; var allAxisOverlays = []; // Get all axes that should show elements (hovered, highlighted, or footnote-highlighted) var axesToShow = new Set(); // Add hovered axis if any (but only if it's not already highlighted) if (highlightedAxis !== null && !((_b = (_a = internalChartState[highlightedAxis]) === null || _a === void 0 ? void 0 : _a.highlighted) !== null && _b !== void 0 ? _b : false)) { axesToShow.add(highlightedAxis); } // Add all highlighted axes from chart configuration internalChartState.forEach(function (dataPoint, index) { var _a; if ((_a = dataPoint.highlighted) !== null && _a !== void 0 ? _a : false) { axesToShow.add(index); } }); // Generate elements for each axis axesToShow.forEach(function (axisIndex) { var _a; var highlightedLine = axisLines[axisIndex]; if (!highlightedLine) return; var lines = []; var labels = []; var levels = 6; // 0%, 20%, 40%, 60%, 80%, 100% for (var i = 0; i < levels; i++) { var percentage = i / (levels - 1); // 0, 0.2, 0.4, 0.6, 0.8, 1 var pointRadius = radius * percentage; // Calculate intersection point on the highlighted axis var intersectionX = centerX + pointRadius * Math.cos(highlightedLine.angle); var intersectionY = centerY + pointRadius * Math.sin(highlightedLine.angle); // Calculate perpendicular line (intersecting line) - only to the right side var perpAngle = highlightedLine.angle + Math.PI / 2; var lineLength = 10; // Half the original length // Only draw line to the right side of the axis var lineStartX = intersectionX; var lineStartY = intersectionY; var lineEndX = intersectionX + lineLength * Math.cos(perpAngle); var lineEndY = intersectionY + lineLength * Math.sin(perpAngle); lines.push({ x1: lineStartX, y1: lineStartY, x2: lineEndX, y2: lineEndY, }); // Calculate label position (aligned with the dash, away from center) var labelDistance = 15; // More space between dash and label var labelAngle = perpAngle; // Same angle as the dash var labelX = intersectionX + labelDistance * Math.cos(labelAngle); var labelY = intersectionY + labelDistance * Math.sin(labelAngle); // Calculate the actual value based on the chart's maxValue var maxValue = ((_a = internalChartState[axisIndex]) === null || _a === void 0 ? void 0 : _a.maxValue) || chartConfig.maxValue; var actualValue = Math.round(percentage * maxValue); // Determine if label is below center and needs 180-degree rotation var isBelowCenter = highlightedLine.y2 > chartConfig.centerY; var finalAngle = isBelowCenter ? labelAngle + Math.PI : labelAngle; labels.push({ x: labelX, y: labelY, text: actualValue.toString(), angle: finalAngle, isBelowCenter: isBelowCenter, }); } // Create axis overlay line var axisOverlay = { x1: highlightedLine.x1, y1: highlightedLine.y1, x2: highlightedLine.x2, y2: highlightedLine.y2, }; allLines.push.apply(allLines, lines); allLabels.push.apply(allLabels, labels); allAxisOverlays.push(axisOverlay); }); return { lines: allLines, labels: allLabels, axisOverlays: allAxisOverlays }; }, [highlightedAxis, axisLines, chartConfig, internalChartState]); // Create polygon path with optional border radius var createPolygonPath = function (points, radius) { if (radius === void 0) { radius = 0; } if (points.length === 0) return ''; if (radius === 0) { // No border radius - return simple polygon return "M ".concat(points.map(function (p) { return "".concat(p.x, " ").concat(p.y); }).join(' L '), " Z"); } // Only apply complex logic for 4+ points and positive radius var shouldUseComplexLogic = points.length >= 4 && radius > 0; // Create rounded polygon path using quadratic Bézier curves var path = []; var numPoints = points.length; for (var i = 0; i < numPoints; i++) { var current = points[i]; var next = points[(i + 1) % numPoints]; // Calculate midpoint of the current line segment var midX = (current.x + next.x) / 2; var midY = (current.y + next.y) / 2; // Calculate vector from current to next point var vx = next.x - current.x; var vy = next.y - current.y; var len = Math.sqrt(vx * vx + vy * vy); if (len === 0) { // Degenerate case - just move to point if (i === 0) { path.push("M ".concat(current.x, " ").concat(current.y)); } else { path.push("L ".concat(current.x, " ").concat(current.y)); } continue; } // Normalize vector var nx = vx / len; var ny = vy / len; // Calculate perpendicular vector (outward from polygon center) var perpX = -ny; var perpY = nx; var pushDistance = Math.min(Math.abs(radius), len * 0.3) * -Math.sign(radius); if (shouldUseComplexLogic) { // Get adjacent points A and D var prev = points[(i - 1 + numPoints) % numPoints]; var nextNext = points[(i + 2) % numPoints]; // Calculate center line of AC (prev to next) var acMidX = (prev.x + next.x) / 2; var acMidY = (prev.y + next.y) / 2; var acDirX = next.x - prev.x; var acDirY = next.y - prev.y; var acLength = Math.sqrt(Math.pow(acDirX, 2) + Math.pow(acDirY, 2)); // Calculate center line of BD (current to nextNext) var bdMidX = (current.x + nextNext.x) / 2; var bdMidY = (current.y + nextNext.y) / 2; var bdDirX = nextNext.x - current.x; var bdDirY = nextNext.y - current.y; var bdLength = Math.sqrt(Math.pow(bdDirX, 2) + Math.pow(bdDirY, 2)); if (acLength > 0 && bdLength > 0) { var acNormalizedDirX = acDirX / acLength; var acNormalizedDirY = acDirY / acLength; var bdNormalizedDirX = bdDirX / bdLength; var bdNormalizedDirY = bdDirY / bdLength; // Calculate distance from B to center line of AC var bToAC = Math.abs((current.x - acMidX) * acNormalizedDirY - (current.y - acMidY) * acNormalizedDirX); // Calculate distance from C to center line of BD var cToBD = Math.abs((next.x - bdMidX) * bdNormalizedDirY - (next.y - bdMidY) * bdNormalizedDirX); // Set max bulge to half of the smallest distance var maxBulge = Math.min(bToAC, cToBD) / 2; pushDistance = Math.min(Math.abs(pushDistance), maxBulge) * Math.sign(pushDistance); } } else { // Use the previous adaptive scaling logic for negative radius or < 4 points var centerX = chartConfig.centerX, centerY = chartConfig.centerY; var currentDistance = Math.sqrt(Math.pow((current.x - centerX), 2) + Math.pow((current.y - centerY), 2)); var nextDistance = Math.sqrt(Math.pow((next.x - centerX), 2) + Math.pow((next.y - centerY), 2)); var maxDistanceFromCenter = Math.max(currentDistance, nextDistance); var maxRadius = chartConfig.radius; var centerProximity = 1 - (maxDistanceFromCenter / maxRadius); var adaptiveScale = Math.max(0.3, 0.3 + centerProximity * 0.7); pushDistance *= adaptiveScale; } var controlX = midX + perpX * pushDistance; var controlY = midY + perpY * pushDistance; if (i === 0) { path.push("M ".concat(current.x, " ").concat(current.y)); } // Use quadratic Bézier curve to the next point path.push("Q ".concat(controlX, " ").concat(controlY, " ").concat(next.x, " ").concat(next.y)); } path.push('Z'); return path.join(' '); }; // Toggle dataset status through 3-way cycle: inactive -> active -> highlighted var toggleDataset = function (index) { // Handle legend click when footnote is active if (activeFootnote !== null) { // Clear the active footnote but preserve current state setActiveFootnote(null); // Keep current chart state (don't reset it) // Keep current dataset states, just toggle the clicked dataset var newStates_1 = new Map(datasetStates); var currentState_1 = newStates_1.get(index); if (currentState_1) { // Toggle the clicked dataset: inactive -> active -> highlighted -> inactive var newStatus_1; switch (currentState_1.status) { case 'inactive': newStatus_1 = 'active'; break; case 'active': newStatus_1 = 'highlighted'; break; case 'highlighted': newStatus_1 = 'inactive'; break; default: newStatus_1 = 'active'; } newStates_1.set(index, { status: newStatus_1 }); setDatasetStates(newStates_1); } return; } var newStates = new Map(datasetStates); var currentState = newStates.get(index); if (!currentState) return; // Cycle through: inactive -> active -> highlighted -> inactive var newStatus; switch (currentState.status) { case 'inactive': newStatus = 'active'; break; case 'active': newStatus = 'highlighted'; break; case 'highlighted': newStatus = 'inactive'; break; default: newStatus = 'active'; } newStates.set(index, { status: newStatus }); setDatasetStates(newStates); }; // Long press state management var _4 = react.useState(null), longPressTimer = _4[0], setLongPressTimer = _4[1]; var _5 = react.useState(null), longPressedDataset = _5[0], setLongPressedDataset = _5[1]; // Polygon stacking order management var _6 = react.useState([]), polygonOrder = _6[0], setPolygonOrder = _6[1]; var handleMouseDown = function (index) { // Start long press timer for potential future features var timer = setTimeout(function () { setLongPressedDataset(index); }, 500); // 500ms long press delay setLongPressTimer(timer); }; var handleMouseUp = function (index) { // Clear long press timer if (longPressTimer) { clearTimeout(longPressTimer); setLongPressTimer(null); } // If this was a long press, don't remove highlight immediately if (longPressedDataset === index) { return; } }; var handleMouseEnter = function (index) { setHoveredDataset(index); }; var handleMouseLeave = function (index) { // Clear long press timer if (longPressTimer) { clearTimeout(longPressTimer); setLongPressTimer(null); } setHoveredDataset(null); }; var handleClick = function (index) { // Clear long press state if it was set if (longPressedDataset === index) { setLongPressedDataset(null); } // Toggle dataset through 3-way cycle toggleDataset(index); handleMoveToTop(index); }; // Move to top handlers var handleMoveToTop = function (index) { // Use local data for polygon ordering var currentData = localData; // Bring polygon to front (labels stay in place) var currentOrder = polygonOrder.length > 0 ? polygonOrder : Array.from({ length: currentData.length }, function (_, i) { return i; }); var newOrder = __spreadArray([], currentOrder, true); // Remove the clicked index from its current position var currentIndex = newOrder.indexOf(index); if (currentIndex > -1) { newOrder.splice(currentIndex, 1); } // Add it to the front (top of stack) newOrder.unshift(index); setPolygonOrder(newOrder); }; // Axis interaction handlers var handleAxisMouseDown = function (axisIndex) { // Start long press timer for axis var timer = setTimeout(function () { setIsAxisAnimating(true); setHighlightedAxis(axisIndex); setTimeout(function () { return setIsAxisAnimating(false); }, 200); }, 500); // 500ms long press delay setAxisLongPressTimer(timer); }; var handleAxisMouseUp = function (axisIndex) { // Clear long press timer if (axisLongPressTimer) { clearTimeout(axisLongPressTimer); setAxisLongPressTimer(null); } }; var handleAxisMouseEnter = function (axisIndex) { var _a; // Don't highlight if this axis is already highlighted (prevents flickering) if ((_a = internalChartState[axisIndex]) === null || _a === void 0 ? void 0 : _a.highlighted) { return; } // Highlight axis on hover (desktop) setIsAxisAnimating(true); setHighlightedAxis(axisIndex); setTimeout(function () { return setIsAxisAnimating(false); }, 200); }; var handleAxisMouseLeave = function (axisIndex) { var _a; // Clear long press timer if (axisLongPressTimer) { clearTimeout(axisLongPressTimer); setAxisLongPressTimer(null); } // Don't remove highlight if this axis is highlighted (prevents flickering) if ((_a = internalChartState[axisIndex]) === null || _a === void 0 ? void 0 : _a.highlighted) { return; } // Remove highlight on mouse leave setIsAxisAnimating(true); setHighlightedAxis(null); setTimeout(function () { return setIsAxisAnimating(false); }, 200); }; var handleAxisTouchStart = function (axisIndex) { handleAxisMouseDown(axisIndex); }; var handleAxisTouchEnd = function (axisIndex) { handleAxisMouseUp(); }; var handleAxisTouchCancel = function (axisIndex) { // Clear long press timer if (axisLongPressTimer) { clearTimeout(axisLongPressTimer); setAxisLongPressTimer(null); } // Remove highlight setHighlightedAxis(null); }; var handleAxisClick = function (axisIndex) { console.log('Axis clicked:', axisIndex, 'Current internal chart state:', internalChartState); // Clear active footnote when axis is clicked if (activeFootnote !== null) { setActiveFootnote(null); } // Toggle the highlighted state of the chart element var updatedChart = internalChartState.map(function (dataPoint, index) { var _a; return index === axisIndex ? __assign(__assign({}, dataPoint), { highlighted: !((_a = dataPoint.highlighted) !== null && _a !== void 0 ? _a : false) }) : dataPoint; }); console.log('Updated chart:', updatedChart, 'onChartChange provided:', !!onChartChange); // Update internal state setInternalChartState(updatedChart); // Call the callback to update the chart in the parent component if (onChartChange) { onChartChange(updatedChart); } else { console.log('onChartChange callback not provided - using internal state management'); } }; // Touch event handlers for mobile var handleTouchStart = function (index) { handleMouseDown(index); }; var handleTouchEnd = function (index) { handleMouseUp(index); }; var handleTouchCancel = function (index) { // Clear long press timer if (longPressTimer) { clearTimeout(longPressTimer); setLongPressTimer(null); } setHoveredDataset(null); // Remove highlight if it wasn't a long press if (longPressedDataset !== index) { var currentState = datasetStates.get(index); if (currentState && currentState.status === 'highlighted') { var newStates = new Map(datasetStates); newStates.set(index, { status: 'active' }); setDatasetStates(newStates); } } }; // Handle footnote click var handleFootnoteClick = function (footnoteIndex) { var footnote = footnotes[footnoteIndex]; if (!footnote) return; // Toggle footnote activation if (activeFootnote === footnoteIndex) { setActiveFootnote(null); // Don't change any dataset or chart state - just deactivate the footnote } else { setActiveFootnote(footnoteIndex); // Separate dataset labels from data point labels var datasetLabels_1 = new Set(); var dataPointLabels_1 = new Set(); footnote.highlight.forEach(function (label) { // Check if this label is a dataset label var isDatasetLabel = data.some(function (dataset) { return dataset.label === label; }); if (isDatasetLabel) { datasetLabels_1.add(label); } else { // Check if this label is a data point label (axis label) var isDataPointLabel = chart.some(function (dataPoint) { return dataPoint.label === label; }); if (isDataPointLabel) { dataPointLabels_1.add(label); } } }); // Apply dataset highlighting var newStates_2 = new Map(); data.forEach(function (dataset, index) { var isDatasetHighlighted = datasetLabels_1.has(dataset.label); if (isDatasetHighlighted) { newStates_2.set(index, { status: 'highlighted' }); } else if (dataset.status === 'hidden') { newStates_2.set(index, { status: 'hidden' }); } else { newStates_2.set(index, { status: 'inactive' }); } }); setDatasetStates(newStates_2); // Clear any hovered axis state setHighlightedAxis(null); // Set highlighted data points in chart state var updatedChartState = chart.map(function (dataPoint, index) { if (dataPointLabels_1.has(dataPoint.label)) { return __assign(__assign({}, dataPoint), { highlighted: true }); } else { // Clear any existing highlighting from footnotes return __assign(__assign({}, dataPoint), { highlighted: false }); } }); setInternalChartState(updatedChartState); // Reorder datasets to bring highlighted ones to the top var highlightedIndices = Array.from(newStates_2.entries()) .filter(function (_a) { _a[0]; var state = _a[1]; return state.status === 'highlighted'; }) .map(function (_a) { var index = _a[0]; _a[1]; return index; }); if (highlightedIndices.length > 0) { var currentOrder = polygonOrder.length > 0 ? polygonOrder : Array.from({ length: data.length }, function (_, i) { return i; }); var newOrder_1 = __spreadArray([], currentOrder, true); // Remove highlighted indices from their current positions highlightedIndices.forEach(function (index) { var currentIndex = newOrder_1.indexOf(index); if (currentIndex > -1) { newOrder_1.splice(currentIndex, 1); } }); // Add highlighted indices to the front (top of stack) highlightedIndices.forEach(function (index) { newOrder_1.unshift(index); }); setPolygonOrder(newOrder_1); } } }; // Calculate opacity based on dataset position in order var getDatasetOpacity = function (index) { var currentOrder = polygonOrder.length > 0 ? polygonOrder : Array.from({ length: allDatasetPoints.length }, function (_, i) { return i; }); var position = currentOrder.indexOf(index); // Top dataset (position 0) gets highest opacity, all others get medium opacity var strokeOpacity = position === 0 ? 0.75 : 0.45; var fillOpacity = position === 0 ? 0.65 : 0.35; return { strokeOpacity: strokeOpacity, fillOpacity: fillOpacity }; }; // Get dataset color based on status and hover state var getDatasetColor = function (index, baseColor) { var datasetState = datasetStates.get(index); if (!datasetState) return baseColor; // If dataset is inactive, return grayscale version if (datasetState.status === 'inactive') { var opacity = theme === 'dark' ? 0.9 : 0.6; // Higher opacity for dark theme // Handle both hex and rgba colors if (baseColor.startsWith('#')) { // Hex color conversion var hex = baseColor.slice(1); // Remove # var r = parseInt(hex.slice(0, 2), 16); var g = parseInt(hex.slice(2, 4), 16); var b = parseInt(hex.slice(4, 6), 16); var gray = Math.round(0.299 * r + 0.587 * g + 0.114 * b); return "rgba(".concat(gray, ", ").concat(gray, ", ").concat(gray, ", ").concat(opacity, ")"); } else if (baseColor.startsWith('rgba')) { // Extract RGB values from rgba var match = baseColor.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/); if (match) { var r = parseInt(match[1]); var g = parseInt(match[2]); var b = parseInt(match[3]); var gray = Math.round(0.299 * r + 0.587 * g + 0.114 * b); return "rgba(".concat(gray, ", ").concat(gray, ", ").concat(gray, ", ").concat(opacity, ")"); } } // Fallback to a default gray return "rgba(128, 128, 128, ".concat(opacity, ")"); } // For active and highlighted datasets, return the original color return baseColor; }; // Animation effect react.useEffect(function () { setIsAnimating(true); var timer = setTimeout(function () { return setIsAnimating(false); }, animationDuration); return function () { return clearTimeout(timer); }; }, [data, chart, animationDuration]); // Apply initial active footnote if provided react.useEffect(function () { if (initialActiveFootnote !== null && initialActiveFootnote !== undefined && footnotes.length > 0) { var footnote = footnotes[initialActiveFootnote]; if (footnote) { // Apply the footnote highlighting logic var datasetLabels_2 = new Set(); var dataPointLabels_2 = new Set(); footnote.highlight.forEach(function (label) { // Check if this label is a dataset label var isDatasetLabel = data.some(function (dataset) { return dataset.label === label; }); if (isDatasetLabel) { datasetLabels_2.add(label); } else { // Check if this label is a data point label (axis label) var isDataPointLabel = chart.some(function (dataPoint) { return dataPoint.label === label; }); if (isDataPointLabel) { dataPointLabels_2.add(label); } } }); // Apply dataset highlighting var newStates_3 = new Map(); data.forEach(function (dataset, index) { var isDatasetHighlighted = datasetLabels_2.has(dataset.label); if (isDatasetHighlighted) { newStates_3.set(index, { status: 'highlighted' }); } else if (dataset.status === 'hidden') { newStates_3.set(index, { status: 'hidden' }); } else { newStates_3.set(index, { status: 'inactive' }); } }); setDatasetStates(newStates_3); // Clear any hovered axis state setHighlightedAxis(null); // Set highlighted data points in chart state var updatedChartState = chart.map(function (dataPoint, index) { if (dataPointLabels_2.has(dataPoint.label)) { return __assign(__assign({}, dataPoint), { highlighted: true }); } else { // Clear any existing highlighting from footnotes return __assign(__assign({}, dataPoint), { highlighted: false }); } }); setInternalChartState(updatedChartState); // Reorder datasets to bring highlighted ones to the top var highlightedIndices = Array.from(newStates_3.entries()) .filter(function (_a) { _a[0]; var state = _a[1]; return state.status === 'highlighted'; }) .map(function (_a) { var index = _a[0]; _a[1]; return index; }); if (highlightedIndices.length > 0) { var currentOrder = polygonOrder.length > 0 ? polygonOrder : Array.from({ length: data.length }, function (_, i) { return i; }); var newOrder_2 = __spreadArray([], currentOrder, true); // Remove highlighted indices from their current positions highlightedIndices.forEach(function (index) { var currentIndex = newOrder_2.indexOf(index); if (currentIndex > -1) { newOrder_2.splice(currentIndex, 1); } }); // Add highlighted indices to the front (top of stack) highlightedIndices.forEach(function (index) { newOrder_2.unshift(index); }); setPolygonOrder(newOrder_2); } } } }, [initialActiveFootnote, footnotes, data, chart]); return (jsxRuntime.jsxs("div", __assign({ className: "r8r-chart ".concat(className), style: __assign({ width: chartConfig.actualWidth, background: currentTheme.backgroundColor, borderRadius: '8px', boxShadow: '0 4px 6px -1px rgba(0, 0, 0, 0.1)', border: showBorder ? "1px solid ".concat(currentTheme.gridColor) : 'none', display: 'flex', flexDirection: 'column', transition: "all ".concat(animationDuration, "ms ease-in-out"), opacity: isAnimating ? 0.8 : 1, margin: '0', overflow: 'hidden', boxSizing: 'border-box', fontFamily: 'sans-serif' }, style), ref: containerRef }, { children: [jsxRuntime.jsxs("div", __assign({ style: { display: 'flex', f