@stfrigerio/sito-template 0.1.19 → 0.1.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/organisms/Calendar/Calendar.d.ts +2 -0
- package/dist/components/organisms/Calendar/Calendar.d.ts.map +1 -1
- package/dist/components/organisms/charts/SleepChart/SleepChart.d.ts.map +1 -1
- package/dist/components/organisms/charts/SunburstChart/SunburstChart.d.ts.map +1 -1
- package/dist/index.esm.js +450 -59
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +450 -59
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/dist/styles.css.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1957,6 +1957,23 @@ function Calendar({ events, onEventClick, onDateClick, viewMode = 'month', initi
|
|
|
1957
1957
|
}
|
|
1958
1958
|
return '#4A90E2'; // Default blue
|
|
1959
1959
|
};
|
|
1960
|
+
const getContrastColor = (bgColor) => {
|
|
1961
|
+
// Convert hex to RGB
|
|
1962
|
+
const hex = bgColor.replace('#', '');
|
|
1963
|
+
const r = parseInt(hex.substring(0, 2), 16);
|
|
1964
|
+
const g = parseInt(hex.substring(2, 4), 16);
|
|
1965
|
+
const b = parseInt(hex.substring(4, 6), 16);
|
|
1966
|
+
// Calculate luminance
|
|
1967
|
+
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
|
|
1968
|
+
// Return black or white based on luminance
|
|
1969
|
+
return luminance > 0.5 ? '#000000' : '#FFFFFF';
|
|
1970
|
+
};
|
|
1971
|
+
const getEventTextColor = (event) => {
|
|
1972
|
+
if (event.textColor)
|
|
1973
|
+
return event.textColor;
|
|
1974
|
+
const bgColor = getEventColor(event);
|
|
1975
|
+
return getContrastColor(bgColor);
|
|
1976
|
+
};
|
|
1960
1977
|
const handleEventClick = (event) => {
|
|
1961
1978
|
if (onEventClick) {
|
|
1962
1979
|
onEventClick(event);
|
|
@@ -2003,6 +2020,7 @@ function Calendar({ events, onEventClick, onDateClick, viewMode = 'month', initi
|
|
|
2003
2020
|
});
|
|
2004
2021
|
return (jsxRuntime.jsxs("div", { className: styles$7.hourSlot, children: [jsxRuntime.jsx("div", { className: styles$7.hourLine }), hourEvents.map((event, eventIndex) => (jsxRuntime.jsxs(framerMotion.motion.div, { className: `${styles$7.dayEvent} ${event.status === 'completed' ? styles$7.completed : ''}`, style: {
|
|
2005
2022
|
backgroundColor: getEventColor(event),
|
|
2023
|
+
color: getEventTextColor(event),
|
|
2006
2024
|
opacity: event.status === 'completed' ? 0.7 : 1
|
|
2007
2025
|
}, onClick: (e) => {
|
|
2008
2026
|
e.stopPropagation();
|
|
@@ -2030,6 +2048,7 @@ function Calendar({ events, onEventClick, onDateClick, viewMode = 'month', initi
|
|
|
2030
2048
|
ease: "easeOut"
|
|
2031
2049
|
}, onClick: () => handleDateClick(day), children: [isWeekStart && (jsxRuntime.jsx("div", { className: styles$7.weekNumber, children: getWeekNumber(day) })), jsxRuntime.jsx("div", { className: styles$7.dayNumber, children: day.getDate() }), dayEvents.length > 0 && (jsxRuntime.jsxs("div", { className: styles$7.events, children: [dayEvents.slice(0, maxEventsPerDay).map((event, eventIndex) => (jsxRuntime.jsxs(framerMotion.motion.div, { className: `${styles$7.event} ${event.status === 'completed' ? styles$7.completed : ''}`, style: {
|
|
2032
2050
|
backgroundColor: getEventColor(event),
|
|
2051
|
+
color: getEventTextColor(event),
|
|
2033
2052
|
opacity: event.status === 'completed' ? 0.7 : 1
|
|
2034
2053
|
}, onClick: (e) => {
|
|
2035
2054
|
e.stopPropagation();
|
|
@@ -2477,7 +2496,7 @@ const parseTimeToDecimal = (time) => {
|
|
|
2477
2496
|
};
|
|
2478
2497
|
const SleepChart = ({ sleepData, width = 800, height = 300, onDateClick }) => {
|
|
2479
2498
|
const svgRef = React.useRef(null);
|
|
2480
|
-
const margin = { top: 20, right: 20, bottom: 40, left: 60 };
|
|
2499
|
+
const margin = React.useMemo(() => ({ top: 20, right: 20, bottom: 40, left: 60 }), []);
|
|
2481
2500
|
const chartWidth = width - margin.left - margin.right;
|
|
2482
2501
|
const chartHeight = height - margin.top - margin.bottom;
|
|
2483
2502
|
React.useEffect(() => {
|
|
@@ -2487,6 +2506,21 @@ const SleepChart = ({ sleepData, width = 800, height = 300, onDateClick }) => {
|
|
|
2487
2506
|
svg.selectAll('*').remove();
|
|
2488
2507
|
const g = svg.append('g')
|
|
2489
2508
|
.attr('transform', `translate(${margin.left},${margin.top})`);
|
|
2509
|
+
// Create tooltip
|
|
2510
|
+
const tooltip = d3__namespace.select('body').append('div')
|
|
2511
|
+
.attr('class', 'sleep-chart-tooltip')
|
|
2512
|
+
.style('position', 'absolute')
|
|
2513
|
+
.style('background', 'var(--bg-secondary)')
|
|
2514
|
+
.style('color', 'var(--text-primary)')
|
|
2515
|
+
.style('padding', '12px')
|
|
2516
|
+
.style('border-radius', '8px')
|
|
2517
|
+
.style('box-shadow', '0 4px 12px rgba(0,0,0,0.25)')
|
|
2518
|
+
.style('border', '1px solid var(--border-primary)')
|
|
2519
|
+
.style('font-size', '14px')
|
|
2520
|
+
.style('pointer-events', 'none')
|
|
2521
|
+
.style('opacity', 0)
|
|
2522
|
+
.style('z-index', '1000')
|
|
2523
|
+
.style('backdrop-filter', 'blur(8px)');
|
|
2490
2524
|
const xScale = d3__namespace.scaleLinear()
|
|
2491
2525
|
.domain([18, 42])
|
|
2492
2526
|
.range([0, chartWidth]);
|
|
@@ -2518,17 +2552,25 @@ const SleepChart = ({ sleepData, width = 800, height = 300, onDateClick }) => {
|
|
|
2518
2552
|
let wakeHour = null;
|
|
2519
2553
|
if (dayData.sleep_time) {
|
|
2520
2554
|
sleepHour = parseTimeToDecimal(dayData.sleep_time);
|
|
2521
|
-
|
|
2555
|
+
// Convert evening hours to 24+ scale (e.g., 22:00 stays 22, but early morning like 02:00 becomes 26)
|
|
2556
|
+
if (sleepHour < 18) {
|
|
2522
2557
|
sleepHour += 24;
|
|
2558
|
+
}
|
|
2523
2559
|
}
|
|
2524
2560
|
if (dayData.wake_hour) {
|
|
2525
2561
|
wakeHour = parseTimeToDecimal(dayData.wake_hour);
|
|
2526
|
-
|
|
2527
|
-
|
|
2562
|
+
// If we have sleep time, wake time should be after sleep time
|
|
2563
|
+
if (sleepHour !== null) {
|
|
2564
|
+
// If wake hour is before sleep hour or very early, it's next day
|
|
2565
|
+
if (wakeHour < sleepHour || (sleepHour >= 18 && wakeHour < 18)) {
|
|
2566
|
+
wakeHour += 24;
|
|
2567
|
+
}
|
|
2528
2568
|
}
|
|
2529
|
-
else
|
|
2530
|
-
//
|
|
2531
|
-
wakeHour
|
|
2569
|
+
else {
|
|
2570
|
+
// No sleep time - if wake time is early morning, assume next day
|
|
2571
|
+
if (wakeHour < 12) {
|
|
2572
|
+
wakeHour += 24;
|
|
2573
|
+
}
|
|
2532
2574
|
}
|
|
2533
2575
|
}
|
|
2534
2576
|
// Draw the bar only if both values exist
|
|
@@ -2536,11 +2578,12 @@ const SleepChart = ({ sleepData, width = 800, height = 300, onDateClick }) => {
|
|
|
2536
2578
|
sleepGroup.append('rect')
|
|
2537
2579
|
.attr('x', xScale(sleepHour))
|
|
2538
2580
|
.attr('y', yValue)
|
|
2539
|
-
.attr('width', xScale(wakeHour) - xScale(sleepHour))
|
|
2581
|
+
.attr('width', Math.max(0, xScale(wakeHour) - xScale(sleepHour)))
|
|
2540
2582
|
.attr('height', barHeight)
|
|
2541
2583
|
.attr('rx', 4)
|
|
2542
2584
|
.attr('fill', 'url(#sleepGradient)')
|
|
2543
|
-
.attr('opacity', 0.8)
|
|
2585
|
+
.attr('opacity', 0.8)
|
|
2586
|
+
.attr('stroke', 'none');
|
|
2544
2587
|
}
|
|
2545
2588
|
// Draw sleep dot if sleep time exists
|
|
2546
2589
|
if (sleepHour !== null) {
|
|
@@ -2548,7 +2591,10 @@ const SleepChart = ({ sleepData, width = 800, height = 300, onDateClick }) => {
|
|
|
2548
2591
|
.attr('cx', xScale(sleepHour))
|
|
2549
2592
|
.attr('cy', yValue + barHeight / 2)
|
|
2550
2593
|
.attr('r', 4)
|
|
2551
|
-
.attr('fill', '#9B59B6')
|
|
2594
|
+
.attr('fill', '#9B59B6')
|
|
2595
|
+
.attr('stroke', '#fff')
|
|
2596
|
+
.attr('stroke-width', 1)
|
|
2597
|
+
.style('filter', 'drop-shadow(0 2px 4px rgba(0,0,0,0.1))');
|
|
2552
2598
|
}
|
|
2553
2599
|
// Draw wake dot if wake hour exists
|
|
2554
2600
|
if (wakeHour !== null) {
|
|
@@ -2556,20 +2602,64 @@ const SleepChart = ({ sleepData, width = 800, height = 300, onDateClick }) => {
|
|
|
2556
2602
|
.attr('cx', xScale(wakeHour))
|
|
2557
2603
|
.attr('cy', yValue + barHeight / 2)
|
|
2558
2604
|
.attr('r', 4)
|
|
2559
|
-
.attr('fill', '#3498DB')
|
|
2605
|
+
.attr('fill', '#3498DB')
|
|
2606
|
+
.attr('stroke', '#fff')
|
|
2607
|
+
.attr('stroke-width', 1)
|
|
2608
|
+
.style('filter', 'drop-shadow(0 2px 4px rgba(0,0,0,0.1))');
|
|
2560
2609
|
}
|
|
2561
|
-
|
|
2610
|
+
// Add hover interactions with tooltip
|
|
2611
|
+
sleepGroup
|
|
2612
|
+
.on('mouseenter', function (event) {
|
|
2613
|
+
// Highlight the bar
|
|
2562
2614
|
d3__namespace.select(this).select('rect')
|
|
2563
2615
|
.transition()
|
|
2564
2616
|
.duration(200)
|
|
2565
|
-
.attr('opacity', 1)
|
|
2566
|
-
|
|
2617
|
+
.attr('opacity', 1)
|
|
2618
|
+
.style('filter', 'brightness(1.1)');
|
|
2619
|
+
// Show tooltip
|
|
2620
|
+
const formatTime = (hour) => {
|
|
2621
|
+
const h24 = hour % 24;
|
|
2622
|
+
const h12 = h24 === 0 ? 12 : h24 > 12 ? h24 - 12 : h24;
|
|
2623
|
+
const ampm = h24 >= 12 ? 'PM' : 'AM';
|
|
2624
|
+
const minutes = Math.round((hour % 1) * 60);
|
|
2625
|
+
return `${h12}:${minutes.toString().padStart(2, '0')} ${ampm}`;
|
|
2626
|
+
};
|
|
2627
|
+
let tooltipContent = `<div><strong>${new Date(dayData.date).toLocaleDateString()}</strong></div>`;
|
|
2628
|
+
if (sleepHour !== null && wakeHour !== null) {
|
|
2629
|
+
const sleepDuration = wakeHour - sleepHour;
|
|
2630
|
+
tooltipContent += `<div>Sleep: ${formatTime(sleepHour)}</div>`;
|
|
2631
|
+
tooltipContent += `<div>Wake: ${formatTime(wakeHour)}</div>`;
|
|
2632
|
+
tooltipContent += `<div>Duration: ${Math.floor(sleepDuration)}h ${Math.round((sleepDuration % 1) * 60)}m</div>`;
|
|
2633
|
+
}
|
|
2634
|
+
else {
|
|
2635
|
+
if (sleepHour !== null)
|
|
2636
|
+
tooltipContent += `<div>Sleep: ${formatTime(sleepHour)}</div>`;
|
|
2637
|
+
if (wakeHour !== null)
|
|
2638
|
+
tooltipContent += `<div>Wake: ${formatTime(wakeHour)}</div>`;
|
|
2639
|
+
}
|
|
2640
|
+
tooltip.html(tooltipContent)
|
|
2641
|
+
.style('opacity', 1)
|
|
2642
|
+
.style('left', (event.pageX + 10) + 'px')
|
|
2643
|
+
.style('top', (event.pageY - 10) + 'px');
|
|
2644
|
+
})
|
|
2645
|
+
.on('mouseleave', function () {
|
|
2646
|
+
// Reset bar appearance
|
|
2567
2647
|
d3__namespace.select(this).select('rect')
|
|
2568
2648
|
.transition()
|
|
2569
2649
|
.duration(200)
|
|
2570
|
-
.attr('opacity', 0.8)
|
|
2650
|
+
.attr('opacity', 0.8)
|
|
2651
|
+
.style('filter', 'none');
|
|
2652
|
+
// Hide tooltip
|
|
2653
|
+
tooltip.style('opacity', 0);
|
|
2654
|
+
})
|
|
2655
|
+
.on('mousemove', function (event) {
|
|
2656
|
+
// Update tooltip position
|
|
2657
|
+
tooltip
|
|
2658
|
+
.style('left', (event.pageX + 10) + 'px')
|
|
2659
|
+
.style('top', (event.pageY - 10) + 'px');
|
|
2571
2660
|
});
|
|
2572
2661
|
});
|
|
2662
|
+
// Create gradient definition
|
|
2573
2663
|
const defs = svg.append('defs');
|
|
2574
2664
|
const gradient = defs.append('linearGradient')
|
|
2575
2665
|
.attr('id', 'sleepGradient')
|
|
@@ -2578,11 +2668,11 @@ const SleepChart = ({ sleepData, width = 800, height = 300, onDateClick }) => {
|
|
|
2578
2668
|
gradient.append('stop')
|
|
2579
2669
|
.attr('offset', '0%')
|
|
2580
2670
|
.attr('stop-color', '#9B59B6')
|
|
2581
|
-
.attr('stop-opacity', 0.
|
|
2671
|
+
.attr('stop-opacity', 0.9);
|
|
2582
2672
|
gradient.append('stop')
|
|
2583
2673
|
.attr('offset', '100%')
|
|
2584
2674
|
.attr('stop-color', '#3498DB')
|
|
2585
|
-
.attr('stop-opacity', 0.
|
|
2675
|
+
.attr('stop-opacity', 0.9);
|
|
2586
2676
|
const xAxisTicks = d3__namespace.range(18, 43, 3).map(hour => ({
|
|
2587
2677
|
value: hour,
|
|
2588
2678
|
label: (hour % 24).toString().padStart(2, '0') + ':00'
|
|
@@ -2605,6 +2695,10 @@ const SleepChart = ({ sleepData, width = 800, height = 300, onDateClick }) => {
|
|
|
2605
2695
|
const date = new Date(d);
|
|
2606
2696
|
return `${(date.getMonth() + 1).toString().padStart(2, '0')}/${date.getDate().toString().padStart(2, '0')}`;
|
|
2607
2697
|
}));
|
|
2698
|
+
// Cleanup function
|
|
2699
|
+
return () => {
|
|
2700
|
+
tooltip.remove();
|
|
2701
|
+
};
|
|
2608
2702
|
}, [sleepData, chartWidth, chartHeight, margin, onDateClick]);
|
|
2609
2703
|
return (jsxRuntime.jsxs("div", { className: styles$4.container, children: [jsxRuntime.jsxs("div", { className: styles$4.header, children: [jsxRuntime.jsx("h3", { className: styles$4.title, children: "Sleep Pattern" }), jsxRuntime.jsxs("div", { className: styles$4.legend, children: [jsxRuntime.jsxs("div", { className: styles$4.legendItem, children: [jsxRuntime.jsx("span", { className: styles$4.sleepDot }), jsxRuntime.jsx("span", { children: "Sleep Time" })] }), jsxRuntime.jsxs("div", { className: styles$4.legendItem, children: [jsxRuntime.jsx("span", { className: styles$4.wakeDot }), jsxRuntime.jsx("span", { children: "Wake Time" })] })] })] }), jsxRuntime.jsx("svg", { ref: svgRef, width: width, height: height, className: styles$4.chart })] }));
|
|
2610
2704
|
};
|
|
@@ -2737,19 +2831,29 @@ const COLOR_PALETTE = [
|
|
|
2737
2831
|
const getTextColor$1 = (backgroundColor) => {
|
|
2738
2832
|
const color = d3__namespace.color(backgroundColor);
|
|
2739
2833
|
if (!color)
|
|
2740
|
-
return '
|
|
2834
|
+
return 'var(--text-primary)';
|
|
2741
2835
|
const rgb = color.rgb();
|
|
2742
2836
|
// Calculate relative luminance using WCAG formula
|
|
2743
|
-
|
|
2744
|
-
|
|
2837
|
+
// Normalize RGB values to 0-1 range for proper calculation
|
|
2838
|
+
const r = rgb.r / 255;
|
|
2839
|
+
const g = rgb.g / 255;
|
|
2840
|
+
const b = rgb.b / 255;
|
|
2841
|
+
// Apply gamma correction
|
|
2842
|
+
const rLinear = r <= 0.03928 ? r / 12.92 : Math.pow((r + 0.055) / 1.055, 2.4);
|
|
2843
|
+
const gLinear = g <= 0.03928 ? g / 12.92 : Math.pow((g + 0.055) / 1.055, 2.4);
|
|
2844
|
+
const bLinear = b <= 0.03928 ? b / 12.92 : Math.pow((b + 0.055) / 1.055, 2.4);
|
|
2845
|
+
// Calculate relative luminance
|
|
2846
|
+
const luminance = 0.2126 * rLinear + 0.7152 * gLinear + 0.0722 * bLinear;
|
|
2847
|
+
// Use a threshold of 0.5 for better contrast
|
|
2745
2848
|
return luminance > 0.5 ? '#000000' : '#ffffff';
|
|
2746
2849
|
};
|
|
2747
2850
|
const SunburstChart = ({ data, width = 500, height = 500, title = 'Sunburst Chart', tagColors = {}, unit = 'items', centerLabel }) => {
|
|
2748
2851
|
const svgRef = React.useRef(null);
|
|
2749
2852
|
const colorMap = React.useRef(new Map()).current;
|
|
2750
2853
|
const colorIndex = React.useRef(0);
|
|
2854
|
+
const focusedNodeRef = React.useRef(null);
|
|
2751
2855
|
const radius = Math.min(width, height) / 2;
|
|
2752
|
-
const getColor = (name, depth) => {
|
|
2856
|
+
const getColor = React.useCallback((name, depth) => {
|
|
2753
2857
|
// First check if we have a tag color for this name
|
|
2754
2858
|
if (tagColors[name]) {
|
|
2755
2859
|
const color = d3__namespace.color(tagColors[name]);
|
|
@@ -2769,7 +2873,7 @@ const SunburstChart = ({ data, width = 500, height = 500, title = 'Sunburst Char
|
|
|
2769
2873
|
return color.darker(depth * 0.3).toString();
|
|
2770
2874
|
}
|
|
2771
2875
|
return baseColor;
|
|
2772
|
-
};
|
|
2876
|
+
}, [tagColors, colorMap]);
|
|
2773
2877
|
React.useEffect(() => {
|
|
2774
2878
|
if (!svgRef.current || !data)
|
|
2775
2879
|
return;
|
|
@@ -2783,6 +2887,13 @@ const SunburstChart = ({ data, width = 500, height = 500, title = 'Sunburst Char
|
|
|
2783
2887
|
const partition = d3__namespace.partition()
|
|
2784
2888
|
.size([2 * Math.PI, radius * radius]);
|
|
2785
2889
|
const nodes = partition(root).descendants();
|
|
2890
|
+
nodes.forEach((d) => {
|
|
2891
|
+
const node = d;
|
|
2892
|
+
node.x0Original = d.x0;
|
|
2893
|
+
node.x1Original = d.x1;
|
|
2894
|
+
node.y0Original = d.y0;
|
|
2895
|
+
node.y1Original = d.y1;
|
|
2896
|
+
});
|
|
2786
2897
|
const arc = d3__namespace.arc()
|
|
2787
2898
|
.startAngle(d => d.x0)
|
|
2788
2899
|
.endAngle(d => d.x1)
|
|
@@ -2793,7 +2904,139 @@ const SunburstChart = ({ data, width = 500, height = 500, title = 'Sunburst Char
|
|
|
2793
2904
|
.attr('class', styles$2.tooltip)
|
|
2794
2905
|
.style('opacity', 0)
|
|
2795
2906
|
.style('position', 'absolute');
|
|
2796
|
-
|
|
2907
|
+
// Function to reset to original view
|
|
2908
|
+
const resetView = () => {
|
|
2909
|
+
focusedNodeRef.current = null;
|
|
2910
|
+
const transition = d3__namespace.transition()
|
|
2911
|
+
.duration(750)
|
|
2912
|
+
.ease(d3__namespace.easeCubicInOut);
|
|
2913
|
+
// First show all elements
|
|
2914
|
+
paths.style('opacity', 1);
|
|
2915
|
+
paths.transition(transition)
|
|
2916
|
+
.attrTween('d', (d) => {
|
|
2917
|
+
const node = d;
|
|
2918
|
+
return (t) => {
|
|
2919
|
+
// Calculate interpolated positions
|
|
2920
|
+
const currX0 = d3__namespace.interpolate(node.x0, node.x0Original)(t);
|
|
2921
|
+
const currX1 = d3__namespace.interpolate(node.x1, node.x1Original)(t);
|
|
2922
|
+
const currY0 = d3__namespace.interpolate(node.y0, node.y0Original)(t);
|
|
2923
|
+
const currY1 = d3__namespace.interpolate(node.y1, node.y1Original)(t);
|
|
2924
|
+
// Update node positions continuously
|
|
2925
|
+
node.x0 = currX0;
|
|
2926
|
+
node.x1 = currX1;
|
|
2927
|
+
node.y0 = currY0;
|
|
2928
|
+
node.y1 = currY1;
|
|
2929
|
+
const interpolatedArc = d3__namespace.arc()
|
|
2930
|
+
.startAngle(() => currX0)
|
|
2931
|
+
.endAngle(() => currX1)
|
|
2932
|
+
.innerRadius(() => Math.sqrt(currY0))
|
|
2933
|
+
.outerRadius(() => Math.sqrt(currY1))
|
|
2934
|
+
.cornerRadius(3);
|
|
2935
|
+
return interpolatedArc(node) || '';
|
|
2936
|
+
};
|
|
2937
|
+
});
|
|
2938
|
+
// Restore cursor style based on whether nodes have children
|
|
2939
|
+
paths.style('cursor', d => (!d.children || d.children.length === 0) ? 'default' : 'pointer');
|
|
2940
|
+
// Reset center button cursor
|
|
2941
|
+
centerResetButton.style('cursor', 'default');
|
|
2942
|
+
// Update labels
|
|
2943
|
+
updateLabels(transition, nodes);
|
|
2944
|
+
};
|
|
2945
|
+
// Function to handle click and expand
|
|
2946
|
+
const handleClick = (event, clickedNode) => {
|
|
2947
|
+
event.stopPropagation();
|
|
2948
|
+
// If already focused on a node, don't allow clicking on other segments
|
|
2949
|
+
// Only allow reset through the center button
|
|
2950
|
+
if (focusedNodeRef.current) {
|
|
2951
|
+
return;
|
|
2952
|
+
}
|
|
2953
|
+
// Don't allow clicking on leaf nodes (nodes without children)
|
|
2954
|
+
if (!clickedNode.children || clickedNode.children.length === 0) {
|
|
2955
|
+
return;
|
|
2956
|
+
}
|
|
2957
|
+
focusedNodeRef.current = clickedNode;
|
|
2958
|
+
const transition = d3__namespace.transition()
|
|
2959
|
+
.duration(750)
|
|
2960
|
+
.ease(d3__namespace.easeCubicInOut);
|
|
2961
|
+
// Zoom to clicked node - scale to fill the entire circle
|
|
2962
|
+
const clickedOriginal = clickedNode;
|
|
2963
|
+
const xScale = d3__namespace.scaleLinear()
|
|
2964
|
+
.domain([clickedOriginal.x0Original, clickedOriginal.x1Original])
|
|
2965
|
+
.range([0, 2 * Math.PI]);
|
|
2966
|
+
// For the radial scale, we want to map the clicked node's radial range to the full circle
|
|
2967
|
+
const yScale = d3__namespace.scaleLinear()
|
|
2968
|
+
.domain([clickedOriginal.y0Original, clickedOriginal.y1Original])
|
|
2969
|
+
.range([0, radius * radius]);
|
|
2970
|
+
// First, immediately hide elements that shouldn't be visible
|
|
2971
|
+
paths.style('opacity', (d) => isParentOf(clickedNode, d) ? 1 : 0);
|
|
2972
|
+
paths.transition(transition)
|
|
2973
|
+
.attrTween('d', (d) => {
|
|
2974
|
+
const node = d;
|
|
2975
|
+
// Only transform nodes that are descendants of the clicked node
|
|
2976
|
+
if (!isParentOf(clickedNode, d)) {
|
|
2977
|
+
// Keep them hidden
|
|
2978
|
+
return () => '';
|
|
2979
|
+
}
|
|
2980
|
+
// Calculate new positions based on the original positions
|
|
2981
|
+
const newX0 = xScale(node.x0Original);
|
|
2982
|
+
const newX1 = xScale(node.x1Original);
|
|
2983
|
+
// For radial positions, map them within the clicked node's range
|
|
2984
|
+
let newY0, newY1;
|
|
2985
|
+
if (node === clickedNode) {
|
|
2986
|
+
// The clicked node itself should fill from center to edge
|
|
2987
|
+
newY0 = 0;
|
|
2988
|
+
newY1 = radius * radius;
|
|
2989
|
+
}
|
|
2990
|
+
else {
|
|
2991
|
+
// Child nodes should be scaled within the full radius
|
|
2992
|
+
newY0 = yScale(node.y0Original);
|
|
2993
|
+
newY1 = yScale(node.y1Original);
|
|
2994
|
+
}
|
|
2995
|
+
// Use current positions as start (they should be at original if not previously zoomed)
|
|
2996
|
+
const startX0 = node.x0;
|
|
2997
|
+
const startX1 = node.x1;
|
|
2998
|
+
const startY0 = node.y0;
|
|
2999
|
+
const startY1 = node.y1;
|
|
3000
|
+
return (t) => {
|
|
3001
|
+
// Calculate interpolated positions
|
|
3002
|
+
const currX0 = d3__namespace.interpolate(startX0, newX0)(t);
|
|
3003
|
+
const currX1 = d3__namespace.interpolate(startX1, newX1)(t);
|
|
3004
|
+
const currY0 = d3__namespace.interpolate(startY0, newY0)(t);
|
|
3005
|
+
const currY1 = d3__namespace.interpolate(startY1, newY1)(t);
|
|
3006
|
+
// Update node positions continuously
|
|
3007
|
+
node.x0 = currX0;
|
|
3008
|
+
node.x1 = currX1;
|
|
3009
|
+
node.y0 = currY0;
|
|
3010
|
+
node.y1 = currY1;
|
|
3011
|
+
const interpolatedArc = d3__namespace.arc()
|
|
3012
|
+
.startAngle(() => currX0)
|
|
3013
|
+
.endAngle(() => currX1)
|
|
3014
|
+
.innerRadius(() => Math.sqrt(currY0))
|
|
3015
|
+
.outerRadius(() => Math.sqrt(currY1))
|
|
3016
|
+
.cornerRadius(3);
|
|
3017
|
+
return interpolatedArc(node) || '';
|
|
3018
|
+
};
|
|
3019
|
+
});
|
|
3020
|
+
// Update labels for zoomed view
|
|
3021
|
+
updateLabels(transition, nodes.filter(d => isParentOf(clickedNode, d)));
|
|
3022
|
+
// Update cursor style to indicate segments are not clickable
|
|
3023
|
+
paths.style('cursor', 'default');
|
|
3024
|
+
// Update center button to be clickable
|
|
3025
|
+
centerResetButton.style('cursor', 'pointer');
|
|
3026
|
+
};
|
|
3027
|
+
// Helper function to check if a node is parent of another
|
|
3028
|
+
const isParentOf = (parent, descendant) => {
|
|
3029
|
+
if (parent === descendant)
|
|
3030
|
+
return true;
|
|
3031
|
+
let current = descendant.parent;
|
|
3032
|
+
while (current) {
|
|
3033
|
+
if (current === parent)
|
|
3034
|
+
return true;
|
|
3035
|
+
current = current.parent;
|
|
3036
|
+
}
|
|
3037
|
+
return false;
|
|
3038
|
+
};
|
|
3039
|
+
const paths = g.selectAll('path')
|
|
2797
3040
|
.data(nodes.filter(d => d.depth > 0 && d.value && d.value > 0))
|
|
2798
3041
|
.enter().append('path')
|
|
2799
3042
|
.attr('d', arc)
|
|
@@ -2804,23 +3047,21 @@ const SunburstChart = ({ data, width = 500, height = 500, title = 'Sunburst Char
|
|
|
2804
3047
|
}
|
|
2805
3048
|
return getColor(ancestor.data.name, d.depth);
|
|
2806
3049
|
})
|
|
2807
|
-
.attr('stroke',
|
|
2808
|
-
|
|
2809
|
-
.
|
|
2810
|
-
.
|
|
3050
|
+
.attr('stroke', () => {
|
|
3051
|
+
// Get computed background color
|
|
3052
|
+
const computedStyle = window.getComputedStyle(svgRef.current);
|
|
3053
|
+
return computedStyle.getPropertyValue('--bg-primary') || '#ffffff';
|
|
3054
|
+
})
|
|
3055
|
+
.attr('stroke-width', 1)
|
|
3056
|
+
.style('cursor', d => (!d.children || d.children.length === 0) ? 'default' : 'pointer')
|
|
3057
|
+
.on('click', handleClick)
|
|
2811
3058
|
.on('mouseover', function (event, d) {
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
d3__namespace.select(this)
|
|
2819
|
-
.transition()
|
|
2820
|
-
.duration(150)
|
|
2821
|
-
.attr('d', d => hoverArc(d))
|
|
2822
|
-
.style('filter', 'drop-shadow(0 2px 8px rgba(0,0,0,0.2))')
|
|
2823
|
-
.style('opacity', 0.9);
|
|
3059
|
+
// Don't show hover effect if already focused
|
|
3060
|
+
if (focusedNodeRef.current) {
|
|
3061
|
+
return;
|
|
3062
|
+
}
|
|
3063
|
+
const hoveredElement = this;
|
|
3064
|
+
// Show tooltip for all nodes
|
|
2824
3065
|
tooltip.transition()
|
|
2825
3066
|
.duration(200)
|
|
2826
3067
|
.style('opacity', 1);
|
|
@@ -2833,43 +3074,193 @@ const SunburstChart = ({ data, width = 500, height = 500, title = 'Sunburst Char
|
|
|
2833
3074
|
`)
|
|
2834
3075
|
.style('left', (event.pageX + 10) + 'px')
|
|
2835
3076
|
.style('top', (event.pageY - 28) + 'px');
|
|
3077
|
+
// Apply blur to all other segments
|
|
3078
|
+
paths.each(function () {
|
|
3079
|
+
if (this !== hoveredElement) {
|
|
3080
|
+
d3__namespace.select(this)
|
|
3081
|
+
.transition()
|
|
3082
|
+
.duration(200)
|
|
3083
|
+
.style('opacity', 0.3)
|
|
3084
|
+
.style('filter', 'blur(1px)');
|
|
3085
|
+
}
|
|
3086
|
+
else {
|
|
3087
|
+
d3__namespace.select(this)
|
|
3088
|
+
.transition()
|
|
3089
|
+
.duration(200)
|
|
3090
|
+
.style('opacity', 1)
|
|
3091
|
+
.style('filter', 'none');
|
|
3092
|
+
}
|
|
3093
|
+
});
|
|
2836
3094
|
})
|
|
2837
|
-
.on('mouseout', function (
|
|
2838
|
-
|
|
2839
|
-
.transition()
|
|
2840
|
-
.duration(150)
|
|
2841
|
-
.attr('d', d => arc(d))
|
|
2842
|
-
.style('filter', 'drop-shadow(0 1px 3px rgba(0,0,0,0.12))')
|
|
2843
|
-
.style('opacity', 1);
|
|
3095
|
+
.on('mouseout', function () {
|
|
3096
|
+
// Hide tooltip
|
|
2844
3097
|
tooltip.transition()
|
|
2845
3098
|
.duration(500)
|
|
2846
3099
|
.style('opacity', 0);
|
|
3100
|
+
// Reset all segments
|
|
3101
|
+
paths
|
|
3102
|
+
.transition()
|
|
3103
|
+
.duration(200)
|
|
3104
|
+
.style('opacity', 1)
|
|
3105
|
+
.style('filter', 'none');
|
|
3106
|
+
})
|
|
3107
|
+
.on('mousemove', function (event) {
|
|
3108
|
+
// Update tooltip position on mouse move
|
|
3109
|
+
tooltip
|
|
3110
|
+
.style('left', (event.pageX + 10) + 'px')
|
|
3111
|
+
.style('top', (event.pageY - 28) + 'px');
|
|
2847
3112
|
});
|
|
2848
3113
|
const shouldDisplayLabel = (d) => {
|
|
2849
3114
|
const angle = d.x1 - d.x0;
|
|
2850
3115
|
return angle > 0.15 && d.depth <= 2;
|
|
2851
3116
|
};
|
|
2852
|
-
//
|
|
2853
|
-
const
|
|
2854
|
-
|
|
2855
|
-
|
|
3117
|
+
// Function to update labels after transitions
|
|
3118
|
+
const updateLabels = (transition, visibleNodes, _isReset) => {
|
|
3119
|
+
// Remove old labels
|
|
3120
|
+
g.selectAll('text.segment-label').remove();
|
|
3121
|
+
g.selectAll('text.center-label').remove();
|
|
3122
|
+
// Wait for transition to complete before adding new labels
|
|
3123
|
+
transition.on('end', () => {
|
|
3124
|
+
// Filter out the focused node itself when zoomed (it's shown in center)
|
|
3125
|
+
const nodesToLabel = visibleNodes.filter(d => {
|
|
3126
|
+
if (focusedNodeRef.current && d === focusedNodeRef.current) {
|
|
3127
|
+
return false; // Don't show label for the zoomed parent node
|
|
3128
|
+
}
|
|
3129
|
+
return d.depth > 0 && d.value && d.value > 0 && shouldDisplayLabel(d);
|
|
3130
|
+
});
|
|
3131
|
+
g.selectAll('text.segment-label')
|
|
3132
|
+
.data(nodesToLabel)
|
|
3133
|
+
.enter().append('text')
|
|
3134
|
+
.attr('class', 'segment-label')
|
|
3135
|
+
.attr('transform', d => {
|
|
3136
|
+
const angle = (d.x0 + d.x1) / 2;
|
|
3137
|
+
const radiusVal = (Math.sqrt(d.y0) + Math.sqrt(d.y1)) / 2;
|
|
3138
|
+
const x = Math.cos(angle - Math.PI / 2) * radiusVal;
|
|
3139
|
+
const y = Math.sin(angle - Math.PI / 2) * radiusVal;
|
|
3140
|
+
return `translate(${x},${y})`;
|
|
3141
|
+
})
|
|
3142
|
+
.attr('text-anchor', 'middle')
|
|
3143
|
+
.attr('alignment-baseline', 'middle')
|
|
3144
|
+
.attr('font-size', d => d.depth === 1 ? '13px' : '11px')
|
|
3145
|
+
.attr('fill', d => {
|
|
3146
|
+
let ancestor = d;
|
|
3147
|
+
while (ancestor.depth > 1 && ancestor.parent) {
|
|
3148
|
+
ancestor = ancestor.parent;
|
|
3149
|
+
}
|
|
3150
|
+
const segmentColor = getColor(ancestor.data.name, d.depth);
|
|
3151
|
+
return getTextColor$1(segmentColor);
|
|
3152
|
+
})
|
|
3153
|
+
.attr('font-weight', '600')
|
|
3154
|
+
.style('pointer-events', 'none')
|
|
3155
|
+
.style('text-shadow', d => {
|
|
3156
|
+
let ancestor = d;
|
|
3157
|
+
while (ancestor.depth > 1 && ancestor.parent) {
|
|
3158
|
+
ancestor = ancestor.parent;
|
|
3159
|
+
}
|
|
3160
|
+
const segmentColor = getColor(ancestor.data.name, d.depth);
|
|
3161
|
+
const textColor = getTextColor$1(segmentColor);
|
|
3162
|
+
// Add subtle shadow for better readability
|
|
3163
|
+
const shadowColor = textColor === '#ffffff' ? 'rgba(0,0,0,0.3)' : 'rgba(255,255,255,0.3)';
|
|
3164
|
+
return `0 1px 1px ${shadowColor}`;
|
|
3165
|
+
})
|
|
3166
|
+
.text(d => {
|
|
3167
|
+
const maxLength = d.depth === 1 ? 12 : 8;
|
|
3168
|
+
return d.data.name.substring(0, maxLength);
|
|
3169
|
+
})
|
|
3170
|
+
.style('opacity', 0)
|
|
3171
|
+
.transition()
|
|
3172
|
+
.duration(200)
|
|
3173
|
+
.style('opacity', 1);
|
|
3174
|
+
// Update center text
|
|
3175
|
+
const currentNode = focusedNodeRef.current || root;
|
|
3176
|
+
const computedStyle = window.getComputedStyle(svgRef.current);
|
|
3177
|
+
const centerTextColor = computedStyle.getPropertyValue('--text-primary') ||
|
|
3178
|
+
(computedStyle.getPropertyValue('--bg-primary') === '#000000' ||
|
|
3179
|
+
computedStyle.getPropertyValue('color-scheme') === 'dark' ? '#ffffff' : '#000000');
|
|
3180
|
+
g.append('text')
|
|
3181
|
+
.attr('class', 'center-label')
|
|
3182
|
+
.attr('text-anchor', 'middle')
|
|
3183
|
+
.attr('alignment-baseline', 'middle')
|
|
3184
|
+
.attr('font-size', '18px')
|
|
3185
|
+
.attr('font-weight', 'bold')
|
|
3186
|
+
.attr('fill', centerTextColor)
|
|
3187
|
+
.style('pointer-events', 'none')
|
|
3188
|
+
.text(currentNode === root ? (centerLabel || data.name || 'Total') : currentNode.data.name)
|
|
3189
|
+
.style('opacity', 0)
|
|
3190
|
+
.transition()
|
|
3191
|
+
.duration(200)
|
|
3192
|
+
.style('opacity', 1);
|
|
3193
|
+
g.append('text')
|
|
3194
|
+
.attr('class', 'center-label')
|
|
3195
|
+
.attr('text-anchor', 'middle')
|
|
3196
|
+
.attr('alignment-baseline', 'middle')
|
|
3197
|
+
.attr('y', 20)
|
|
3198
|
+
.attr('font-size', '14px')
|
|
3199
|
+
.attr('font-weight', '500')
|
|
3200
|
+
.attr('fill', centerTextColor)
|
|
3201
|
+
.style('pointer-events', 'none')
|
|
3202
|
+
.text(`${(currentNode.value || 0).toLocaleString()} ${unit}`)
|
|
3203
|
+
.style('opacity', 0)
|
|
3204
|
+
.transition()
|
|
3205
|
+
.duration(200)
|
|
3206
|
+
.style('opacity', 1);
|
|
3207
|
+
});
|
|
3208
|
+
};
|
|
3209
|
+
// Add invisible center reset button (circle in the middle)
|
|
3210
|
+
const centerRadius = Math.sqrt(nodes[0].y0) || radius * 0.3;
|
|
3211
|
+
const centerResetButton = g.append('circle')
|
|
3212
|
+
.attr('class', 'center-reset-button')
|
|
3213
|
+
.attr('r', centerRadius)
|
|
3214
|
+
.attr('fill', 'transparent')
|
|
3215
|
+
.style('cursor', 'default')
|
|
3216
|
+
.on('click', () => {
|
|
3217
|
+
if (focusedNodeRef.current) {
|
|
3218
|
+
resetView();
|
|
3219
|
+
}
|
|
3220
|
+
})
|
|
3221
|
+
.on('mouseover', function () {
|
|
3222
|
+
if (focusedNodeRef.current) {
|
|
3223
|
+
d3__namespace.select(this)
|
|
3224
|
+
.transition()
|
|
3225
|
+
.duration(150)
|
|
3226
|
+
.attr('fill', 'rgba(100, 100, 100, 0.1)')
|
|
3227
|
+
.attr('stroke', 'rgba(100, 100, 100, 0.3)')
|
|
3228
|
+
.attr('stroke-width', 2);
|
|
3229
|
+
}
|
|
3230
|
+
})
|
|
3231
|
+
.on('mouseout', function () {
|
|
3232
|
+
if (focusedNodeRef.current) {
|
|
3233
|
+
d3__namespace.select(this)
|
|
3234
|
+
.transition()
|
|
3235
|
+
.duration(150)
|
|
3236
|
+
.attr('fill', 'transparent')
|
|
3237
|
+
.attr('stroke', 'none');
|
|
3238
|
+
}
|
|
3239
|
+
});
|
|
3240
|
+
// Calculate center text color - get computed theme color
|
|
3241
|
+
const computedStyle = window.getComputedStyle(svgRef.current);
|
|
3242
|
+
const centerTextColor = computedStyle.getPropertyValue('--text-primary') ||
|
|
3243
|
+
(computedStyle.getPropertyValue('--bg-primary') === '#000000' ||
|
|
3244
|
+
computedStyle.getPropertyValue('color-scheme') === 'dark' ? '#ffffff' : '#000000');
|
|
2856
3245
|
// Add center text
|
|
2857
3246
|
g.append('text')
|
|
3247
|
+
.attr('class', 'center-label')
|
|
2858
3248
|
.attr('text-anchor', 'middle')
|
|
2859
3249
|
.attr('alignment-baseline', 'middle')
|
|
2860
3250
|
.attr('font-size', '18px')
|
|
2861
3251
|
.attr('font-weight', 'bold')
|
|
2862
3252
|
.attr('fill', centerTextColor)
|
|
2863
|
-
.style('
|
|
3253
|
+
.style('pointer-events', 'none')
|
|
2864
3254
|
.text(centerLabel || data.name || 'Total');
|
|
2865
3255
|
g.append('text')
|
|
3256
|
+
.attr('class', 'center-label')
|
|
2866
3257
|
.attr('text-anchor', 'middle')
|
|
2867
3258
|
.attr('alignment-baseline', 'middle')
|
|
2868
3259
|
.attr('y', 20)
|
|
2869
3260
|
.attr('font-size', '14px')
|
|
2870
3261
|
.attr('font-weight', '500')
|
|
2871
3262
|
.attr('fill', centerTextColor)
|
|
2872
|
-
.style('
|
|
3263
|
+
.style('pointer-events', 'none')
|
|
2873
3264
|
.text(`${(root.value || 0).toLocaleString()} ${unit}`);
|
|
2874
3265
|
g.selectAll('text.segment-label')
|
|
2875
3266
|
.data(nodes.filter(d => d.depth > 0 && d.value && d.value > 0 && shouldDisplayLabel(d)))
|
|
@@ -2877,9 +3268,9 @@ const SunburstChart = ({ data, width = 500, height = 500, title = 'Sunburst Char
|
|
|
2877
3268
|
.attr('class', 'segment-label')
|
|
2878
3269
|
.attr('transform', d => {
|
|
2879
3270
|
const angle = (d.x0 + d.x1) / 2;
|
|
2880
|
-
const
|
|
2881
|
-
const x = Math.cos(angle - Math.PI / 2) *
|
|
2882
|
-
const y = Math.sin(angle - Math.PI / 2) *
|
|
3271
|
+
const radiusVal = (Math.sqrt(d.y0) + Math.sqrt(d.y1)) / 2;
|
|
3272
|
+
const x = Math.cos(angle - Math.PI / 2) * radiusVal;
|
|
3273
|
+
const y = Math.sin(angle - Math.PI / 2) * radiusVal;
|
|
2883
3274
|
return `translate(${x},${y})`;
|
|
2884
3275
|
})
|
|
2885
3276
|
.attr('text-anchor', 'middle')
|
|
@@ -2902,9 +3293,9 @@ const SunburstChart = ({ data, width = 500, height = 500, title = 'Sunburst Char
|
|
|
2902
3293
|
}
|
|
2903
3294
|
const segmentColor = getColor(ancestor.data.name, d.depth);
|
|
2904
3295
|
const textColor = getTextColor$1(segmentColor);
|
|
2905
|
-
//
|
|
2906
|
-
const shadowColor = textColor === '#ffffff' ? 'rgba(0,0,0,0.
|
|
2907
|
-
return `0 1px
|
|
3296
|
+
// Add subtle shadow for better readability
|
|
3297
|
+
const shadowColor = textColor === '#ffffff' ? 'rgba(0,0,0,0.3)' : 'rgba(255,255,255,0.3)';
|
|
3298
|
+
return `0 1px 1px ${shadowColor}`;
|
|
2908
3299
|
})
|
|
2909
3300
|
.text(d => {
|
|
2910
3301
|
const maxLength = d.depth === 1 ? 12 : 8;
|
|
@@ -2913,7 +3304,7 @@ const SunburstChart = ({ data, width = 500, height = 500, title = 'Sunburst Char
|
|
|
2913
3304
|
return () => {
|
|
2914
3305
|
tooltip.remove();
|
|
2915
3306
|
};
|
|
2916
|
-
}, [data, width, height, colorMap, radius]);
|
|
3307
|
+
}, [data, width, height, colorMap, radius, tagColors, unit, centerLabel]);
|
|
2917
3308
|
return (jsxRuntime.jsxs("div", { className: styles$2.container, children: [jsxRuntime.jsx("h3", { className: styles$2.title, children: title }), jsxRuntime.jsx("svg", { ref: svgRef, width: width, height: height, className: styles$2.chart })] }));
|
|
2918
3309
|
};
|
|
2919
3310
|
|