@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/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
- if (sleepHour < 18)
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
- if (sleepHour !== null && wakeHour < sleepHour - 18) {
2527
- wakeHour += 24;
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 if (sleepHour === null && wakeHour < 12) {
2530
- // If no sleep time but wake is in morning, assume it's next day
2531
- wakeHour += 24;
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
- sleepGroup.on('mouseenter', function () {
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
- }).on('mouseleave', function () {
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.8);
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.8);
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 '#ffffff';
2834
+ return 'var(--text-primary)';
2741
2835
  const rgb = color.rgb();
2742
2836
  // Calculate relative luminance using WCAG formula
2743
- const luminance = (0.299 * rgb.r + 0.587 * rgb.g + 0.114 * rgb.b) / 255;
2744
- // Return white text for dark backgrounds, black for light backgrounds
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
- g.selectAll('path')
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', 'var(--bg-primary)')
2808
- .attr('stroke-width', 2)
2809
- .style('cursor', 'pointer')
2810
- .style('filter', 'drop-shadow(0 1px 3px rgba(0,0,0,0.12))')
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
- const hoverArc = d3__namespace.arc()
2813
- .startAngle(d => d.x0)
2814
- .endAngle(d => d.x1)
2815
- .innerRadius(d => Math.sqrt(d.y0) - 2)
2816
- .outerRadius(d => Math.sqrt(d.y1) + 4)
2817
- .cornerRadius(3);
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 (event, d) {
2838
- d3__namespace.select(this)
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
- // Calculate average background color for center text contrast
2853
- const allSegments = nodes.filter(d => d.depth === 1);
2854
- const avgColor = allSegments.length > 0 ? d3__namespace.interpolateRgb.gamma(2.2)(...allSegments.map(d => getColor(d.data.name, 1)))(0.5) : '#ffffff';
2855
- const centerTextColor = getTextColor$1(avgColor);
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('text-shadow', centerTextColor === '#ffffff' ? '0 1px 3px rgba(0,0,0,0.5)' : '0 1px 3px rgba(255,255,255,0.5)')
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('text-shadow', centerTextColor === '#ffffff' ? '0 1px 2px rgba(0,0,0,0.4)' : '0 1px 2px rgba(255,255,255,0.4)')
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 radius = (Math.sqrt(d.y0) + Math.sqrt(d.y1)) / 2;
2881
- const x = Math.cos(angle - Math.PI / 2) * radius;
2882
- const y = Math.sin(angle - Math.PI / 2) * radius;
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
- // Use contrasting shadow color
2906
- const shadowColor = textColor === '#ffffff' ? 'rgba(0,0,0,0.5)' : 'rgba(255,255,255,0.5)';
2907
- return `0 1px 2px ${shadowColor}`;
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