r8r
Version:
A modern, zero-dependency radar (spider) chart for visual comparisons in React
1,050 lines (1,042 loc) • 69 kB
JavaScript
'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