@stfrigerio/sito-template 0.1.21 → 0.1.23

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
@@ -2256,7 +2256,7 @@ const MoodChart = ({ moodData, width = 800, height = 400 }) => {
2256
2256
  }, children: [jsxRuntime.jsxs("div", { className: styles$6.tooltipHeader, children: [jsxRuntime.jsx("div", { className: styles$6.tooltipDate, children: selectedMood.date.toLocaleDateString() }), jsxRuntime.jsxs("div", { className: styles$6.tooltipRating, children: [jsxRuntime.jsx("span", { className: styles$6.ratingValue, children: selectedMood.rating }), jsxRuntime.jsx("span", { className: styles$6.ratingMax, children: "/10" })] })] }), selectedMood.tags.length > 0 && (jsxRuntime.jsx("div", { className: styles$6.tooltipTags, children: selectedMood.tags.map((tag, index) => (jsxRuntime.jsx("span", { className: styles$6.tag, children: tag }, index))) })), selectedMood.comment && (jsxRuntime.jsx("div", { className: styles$6.tooltipComment, children: selectedMood.comment }))] }))] }));
2257
2257
  };
2258
2258
 
2259
- var styles$5 = {"container":"QuantifiableHabitsChart-module_container__X5SBp","controls":"QuantifiableHabitsChart-module_controls__O-ObQ","viewToggle":"QuantifiableHabitsChart-module_viewToggle__24hKA","viewButton":"QuantifiableHabitsChart-module_viewButton__WFU6j","active":"QuantifiableHabitsChart-module_active__Pjqy9","viewIcon":"QuantifiableHabitsChart-module_viewIcon__b2mfk","viewLabel":"QuantifiableHabitsChart-module_viewLabel__9MjCU","legend":"QuantifiableHabitsChart-module_legend__3Ki7c","legendItem":"QuantifiableHabitsChart-module_legendItem__Zl7fz","inactive":"QuantifiableHabitsChart-module_inactive__TzZC-","legendColor":"QuantifiableHabitsChart-module_legendColor__zbPoV","legendEmoji":"QuantifiableHabitsChart-module_legendEmoji__HG9CZ","legendLabel":"QuantifiableHabitsChart-module_legendLabel__H3oFL","chart":"QuantifiableHabitsChart-module_chart__FMeA-","gridLine":"QuantifiableHabitsChart-module_gridLine__CTNIq","line":"QuantifiableHabitsChart-module_line__CpYip","xAxis":"QuantifiableHabitsChart-module_xAxis__lbgBG","yAxis":"QuantifiableHabitsChart-module_yAxis__Y6WeV","dataPoint":"QuantifiableHabitsChart-module_dataPoint__s8UMX"};
2259
+ var styles$5 = {"container":"QuantifiableHabitsChart-module_container__X5SBp","controls":"QuantifiableHabitsChart-module_controls__O-ObQ","viewToggle":"QuantifiableHabitsChart-module_viewToggle__24hKA","viewButton":"QuantifiableHabitsChart-module_viewButton__WFU6j","active":"QuantifiableHabitsChart-module_active__Pjqy9","viewIcon":"QuantifiableHabitsChart-module_viewIcon__b2mfk","viewLabel":"QuantifiableHabitsChart-module_viewLabel__9MjCU","legend":"QuantifiableHabitsChart-module_legend__3Ki7c","legendItem":"QuantifiableHabitsChart-module_legendItem__Zl7fz","inactive":"QuantifiableHabitsChart-module_inactive__TzZC-","legendColor":"QuantifiableHabitsChart-module_legendColor__zbPoV","legendEmoji":"QuantifiableHabitsChart-module_legendEmoji__HG9CZ","legendLabel":"QuantifiableHabitsChart-module_legendLabel__H3oFL","chart":"QuantifiableHabitsChart-module_chart__FMeA-","gridLine":"QuantifiableHabitsChart-module_gridLine__CTNIq","line":"QuantifiableHabitsChart-module_line__CpYip","xAxis":"QuantifiableHabitsChart-module_xAxis__lbgBG","yAxis":"QuantifiableHabitsChart-module_yAxis__Y6WeV","dataPoint":"QuantifiableHabitsChart-module_dataPoint__s8UMX","tooltip":"QuantifiableHabitsChart-module_tooltip__Fay8N","visible":"QuantifiableHabitsChart-module_visible__-KSJq","tooltipHeader":"QuantifiableHabitsChart-module_tooltipHeader__7Q2up","tooltipEmoji":"QuantifiableHabitsChart-module_tooltipEmoji__atV3T","tooltipDot":"QuantifiableHabitsChart-module_tooltipDot__YbdFh","tooltipInfo":"QuantifiableHabitsChart-module_tooltipInfo__XC7WF","tooltipDate":"QuantifiableHabitsChart-module_tooltipDate__6V6Xi","tooltipValue":"QuantifiableHabitsChart-module_tooltipValue__ldASB"};
2260
2260
 
2261
2261
  // Default colors as fallback
2262
2262
  const DEFAULT_HABIT_COLORS = {
@@ -2269,11 +2269,13 @@ const DEFAULT_HABIT_COLORS = {
2269
2269
  'Calories': '#FF9F1C',
2270
2270
  'Study': '#C774E8'
2271
2271
  };
2272
- const QuantifiableHabitsChart = ({ data, width = 800, height = 400, defaultViewType = 'daily', periodType = 'month', habitColors: customHabitColors = {}, habitEmojis: customHabitEmojis = {} }) => {
2272
+ const QuantifiableHabitsChart = ({ data, width = 800, height = 400, defaultViewType = 'daily', periodType = 'month', habitColors: customHabitColors = {}, habitEmojis: customHabitEmojis = {}, onDataPointClick }) => {
2273
2273
  const svgRef = React.useRef(null);
2274
+ const tooltipRef = React.useRef(null);
2274
2275
  const [viewType, setViewType] = React.useState(defaultViewType);
2275
2276
  const [activeHabits, setActiveHabits] = React.useState([]);
2276
2277
  const [hoveredHabit, setHoveredHabit] = React.useState(null);
2278
+ const [tooltipData, setTooltipData] = React.useState(null);
2277
2279
  const margin = { top: 20, right: 20, bottom: 50, left: 50 };
2278
2280
  const chartWidth = width - margin.left - margin.right;
2279
2281
  const chartHeight = height - margin.top - margin.bottom;
@@ -2281,6 +2283,16 @@ const QuantifiableHabitsChart = ({ data, width = 800, height = 400, defaultViewT
2281
2283
  React.useEffect(() => {
2282
2284
  setActiveHabits(habits);
2283
2285
  }, [habits]);
2286
+ // Hide tooltip on scroll
2287
+ React.useEffect(() => {
2288
+ const handleScroll = () => {
2289
+ setTooltipData(null);
2290
+ };
2291
+ window.addEventListener('scroll', handleScroll, true);
2292
+ return () => {
2293
+ window.removeEventListener('scroll', handleScroll, true);
2294
+ };
2295
+ }, []);
2284
2296
  const availableViewTypes = React.useMemo(() => {
2285
2297
  switch (periodType) {
2286
2298
  case 'week':
@@ -2369,6 +2381,15 @@ const QuantifiableHabitsChart = ({ data, width = 800, height = 400, defaultViewT
2369
2381
  return;
2370
2382
  const svg = d3__namespace.select(svgRef.current);
2371
2383
  svg.selectAll('*').remove();
2384
+ // Add invisible rect to detect mouse leave events
2385
+ svg.append('rect')
2386
+ .attr('width', width)
2387
+ .attr('height', height)
2388
+ .attr('fill', 'transparent')
2389
+ .style('pointer-events', 'all')
2390
+ .on('mouseleave', () => {
2391
+ setTooltipData(null);
2392
+ });
2372
2393
  const g = svg.append('g')
2373
2394
  .attr('transform', `translate(${margin.left},${margin.top})`);
2374
2395
  const dates = aggregateData.dates.map(d => new Date(d));
@@ -2399,15 +2420,56 @@ const QuantifiableHabitsChart = ({ data, width = 800, height = 400, defaultViewT
2399
2420
  .attr('d', line)
2400
2421
  .attr('stroke', getColor(habit))
2401
2422
  .attr('opacity', hoveredHabit && hoveredHabit !== habit ? 0.3 : 1);
2423
+ // Add invisible larger circles for better hover detection
2424
+ g.selectAll(`.hover-circle-${habit}`)
2425
+ .data(habitData)
2426
+ .enter().append('circle')
2427
+ .attr('cx', d => xScale(d[0]))
2428
+ .attr('cy', d => yScale(d[1]))
2429
+ .attr('r', 10) // Larger invisible area
2430
+ .attr('fill', 'transparent')
2431
+ .style('cursor', 'pointer')
2432
+ .on('mouseenter', function (_event, d) {
2433
+ const [date, value] = d;
2434
+ const rect = svgRef.current?.getBoundingClientRect();
2435
+ if (rect) {
2436
+ setTooltipData({
2437
+ habit,
2438
+ date: date.toISOString().split('T')[0],
2439
+ value,
2440
+ x: xScale(date) + margin.left + rect.left,
2441
+ y: yScale(value) + margin.top + rect.top
2442
+ });
2443
+ }
2444
+ // Find and enlarge the visible circle
2445
+ d3__namespace.select(g.node())
2446
+ .selectAll(`.circle-${habit}-${dates.indexOf(date)}`)
2447
+ .attr('r', 6);
2448
+ })
2449
+ .on('mouseleave', function (_event, _d) {
2450
+ setTooltipData(null);
2451
+ // Reset all circles for this habit
2452
+ d3__namespace.select(g.node())
2453
+ .selectAll('[class*="circle-' + habit + '"]')
2454
+ .attr('r', 4);
2455
+ })
2456
+ .on('click', function (_event, d) {
2457
+ const [date, value] = d;
2458
+ if (onDataPointClick) {
2459
+ onDataPointClick(habit, date.toISOString().split('T')[0], value);
2460
+ }
2461
+ });
2462
+ // Add visible circles
2402
2463
  g.selectAll(`.circle-${habit}`)
2403
2464
  .data(habitData)
2404
2465
  .enter().append('circle')
2405
- .attr('class', styles$5.dataPoint)
2466
+ .attr('class', (_d, i) => `${styles$5.dataPoint} circle-${habit}-${i}`)
2406
2467
  .attr('cx', d => xScale(d[0]))
2407
2468
  .attr('cy', d => yScale(d[1]))
2408
2469
  .attr('r', 4)
2409
2470
  .attr('fill', getColor(habit))
2410
- .attr('opacity', hoveredHabit && hoveredHabit !== habit ? 0.3 : 1);
2471
+ .attr('opacity', hoveredHabit && hoveredHabit !== habit ? 0.3 : 1)
2472
+ .style('pointer-events', 'none'); // Let the invisible circle handle events
2411
2473
  });
2412
2474
  // Helper function to get ISO week number
2413
2475
  const getISOWeek = (date) => {
@@ -2477,7 +2539,28 @@ const QuantifiableHabitsChart = ({ data, width = 800, height = 400, defaultViewT
2477
2539
  g.append('g')
2478
2540
  .attr('class', styles$5.yAxis)
2479
2541
  .call(d3__namespace.axisLeft(yScale));
2480
- }, [aggregateData, activeHabits, chartWidth, chartHeight, margin, hoveredHabit, customHabitColors]);
2542
+ }, [aggregateData, activeHabits, chartWidth, chartHeight, margin, hoveredHabit, customHabitColors, onDataPointClick]);
2543
+ // Format date for tooltip display
2544
+ const formatTooltipDate = (dateStr, viewType) => {
2545
+ const date = new Date(dateStr);
2546
+ switch (viewType) {
2547
+ case 'daily':
2548
+ return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
2549
+ case 'weekly': {
2550
+ const weekEnd = new Date(date);
2551
+ weekEnd.setDate(date.getDate() + 6);
2552
+ return `Week of ${date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}`;
2553
+ }
2554
+ case 'monthly':
2555
+ return date.toLocaleDateString('en-US', { month: 'long', year: 'numeric' });
2556
+ case 'quarterly': {
2557
+ const quarter = Math.floor(date.getMonth() / 3) + 1;
2558
+ return `Q${quarter} ${date.getFullYear()}`;
2559
+ }
2560
+ default:
2561
+ return dateStr;
2562
+ }
2563
+ };
2481
2564
  // View type icons and labels
2482
2565
  const viewTypeConfig = {
2483
2566
  daily: { icon: '📅', label: 'Daily' },
@@ -2485,42 +2568,57 @@ const QuantifiableHabitsChart = ({ data, width = 800, height = 400, defaultViewT
2485
2568
  monthly: { icon: '🗓️', label: 'Monthly' },
2486
2569
  quarterly: { icon: '📊', label: 'Quarterly' }
2487
2570
  };
2488
- return (jsxRuntime.jsxs("div", { className: styles$5.container, children: [jsxRuntime.jsx("div", { className: styles$5.controls, children: jsxRuntime.jsx("div", { className: styles$5.viewToggle, children: availableViewTypes.map(type => (jsxRuntime.jsxs("button", { className: `${styles$5.viewButton} ${viewType === type ? styles$5.active : ''}`, onClick: () => setViewType(type), title: viewTypeConfig[type].label, children: [jsxRuntime.jsx("span", { className: styles$5.viewIcon, children: viewTypeConfig[type].icon }), jsxRuntime.jsx("span", { className: styles$5.viewLabel, children: viewTypeConfig[type].label })] }, type))) }) }), jsxRuntime.jsx("div", { className: styles$5.legend, children: habits.map(habit => (jsxRuntime.jsxs("button", { className: `${styles$5.legendItem} ${!activeHabits.includes(habit) ? styles$5.inactive : ''}`, onClick: () => toggleHabit(habit), onMouseEnter: () => setHoveredHabit(habit), onMouseLeave: () => setHoveredHabit(null), children: [jsxRuntime.jsx("span", { className: styles$5.legendEmoji, children: customHabitEmojis[habit] || '📊' }), jsxRuntime.jsx("span", { className: styles$5.legendColor, style: { backgroundColor: getColor(habit) } }), jsxRuntime.jsx("span", { className: styles$5.legendLabel, children: habit })] }, habit))) }), jsxRuntime.jsx("svg", { ref: svgRef, width: width, height: height, className: styles$5.chart })] }));
2571
+ return (jsxRuntime.jsxs("div", { className: styles$5.container, children: [jsxRuntime.jsx("div", { className: styles$5.controls, children: jsxRuntime.jsx("div", { className: styles$5.viewToggle, children: availableViewTypes.map(type => (jsxRuntime.jsxs("button", { className: `${styles$5.viewButton} ${viewType === type ? styles$5.active : ''}`, onClick: () => setViewType(type), title: viewTypeConfig[type].label, children: [jsxRuntime.jsx("span", { className: styles$5.viewIcon, children: viewTypeConfig[type].icon }), jsxRuntime.jsx("span", { className: styles$5.viewLabel, children: viewTypeConfig[type].label })] }, type))) }) }), jsxRuntime.jsx("div", { className: styles$5.legend, children: habits.map(habit => (jsxRuntime.jsxs("button", { className: `${styles$5.legendItem} ${!activeHabits.includes(habit) ? styles$5.inactive : ''}`, onClick: () => toggleHabit(habit), onMouseEnter: () => setHoveredHabit(habit), onMouseLeave: () => setHoveredHabit(null), children: [jsxRuntime.jsx("span", { className: styles$5.legendEmoji, children: customHabitEmojis[habit] || '📊' }), jsxRuntime.jsx("span", { className: styles$5.legendColor, style: { backgroundColor: getColor(habit) } }), jsxRuntime.jsx("span", { className: styles$5.legendLabel, children: habit })] }, habit))) }), jsxRuntime.jsx("svg", { ref: svgRef, width: width, height: height, className: styles$5.chart }), tooltipData && (jsxRuntime.jsxs("div", { ref: tooltipRef, className: `${styles$5.tooltip} ${tooltipData ? styles$5.visible : ''}`, style: {
2572
+ left: `${tooltipData.x}px`,
2573
+ top: `${tooltipData.y - 80}px`
2574
+ }, children: [jsxRuntime.jsxs("div", { className: styles$5.tooltipHeader, children: [jsxRuntime.jsx("span", { className: styles$5.tooltipEmoji, children: customHabitEmojis[tooltipData.habit] || '📊' }), jsxRuntime.jsx("span", { className: styles$5.tooltipDot, style: { backgroundColor: getColor(tooltipData.habit) } }), jsxRuntime.jsx("span", { children: tooltipData.habit })] }), jsxRuntime.jsxs("div", { className: styles$5.tooltipInfo, children: [jsxRuntime.jsx("div", { className: styles$5.tooltipDate, children: formatTooltipDate(tooltipData.date, viewType) }), jsxRuntime.jsxs("div", { className: styles$5.tooltipValue, children: [jsxRuntime.jsx("strong", { children: Math.round(tooltipData.value * 10) / 10 }), jsxRuntime.jsx("span", { style: { fontSize: '12px', fontWeight: 'normal', opacity: 0.6 }, children: "units" })] })] })] }))] }));
2489
2575
  };
2490
2576
 
2491
- var styles$4 = {"container":"SleepChart-module_container__RjHVU","header":"SleepChart-module_header__bcoWe","title":"SleepChart-module_title__6-QII","legend":"SleepChart-module_legend__VsqQj","legendItem":"SleepChart-module_legendItem__2c1nd","sleepDot":"SleepChart-module_sleepDot__qCY6O","wakeDot":"SleepChart-module_wakeDot__-RcrT","chart":"SleepChart-module_chart__jC1nL","gridLine":"SleepChart-module_gridLine__hi715","sleepBar":"SleepChart-module_sleepBar__Hk76f","xAxis":"SleepChart-module_xAxis__xzXyM","yAxis":"SleepChart-module_yAxis__7N-LA"};
2577
+ var styles$4 = {"container":"SleepChart-module_container__RjHVU","header":"SleepChart-module_header__bcoWe","title":"SleepChart-module_title__6-QII","legend":"SleepChart-module_legend__VsqQj","legendItem":"SleepChart-module_legendItem__2c1nd","sleepDot":"SleepChart-module_sleepDot__qCY6O","wakeDot":"SleepChart-module_wakeDot__-RcrT","chart":"SleepChart-module_chart__jC1nL","gridLine":"SleepChart-module_gridLine__hi715","sleepBar":"SleepChart-module_sleepBar__Hk76f","xAxis":"SleepChart-module_xAxis__xzXyM","yAxis":"SleepChart-module_yAxis__7N-LA","tooltip":"SleepChart-module_tooltip__jQBv1","visible":"SleepChart-module_visible__wy0ck","tooltipHeader":"SleepChart-module_tooltipHeader__5BdPL","tooltipEmoji":"SleepChart-module_tooltipEmoji__c5vtz","tooltipInfo":"SleepChart-module_tooltipInfo__9Yrno","tooltipRow":"SleepChart-module_tooltipRow__CuDaE","tooltipLabel":"SleepChart-module_tooltipLabel__7SNzQ","tooltipValue":"SleepChart-module_tooltipValue__FoAVy","tooltipDuration":"SleepChart-module_tooltipDuration__d2wBW"};
2492
2578
 
2493
2579
  const parseTimeToDecimal = (time) => {
2494
2580
  const [hours, minutes] = time.split(':').map(Number);
2495
2581
  return hours + minutes / 60;
2496
2582
  };
2497
- const SleepChart = ({ sleepData, width = 800, height = 300, onDateClick }) => {
2583
+ const formatTime = (hour) => {
2584
+ const h24 = Math.floor(hour % 24);
2585
+ const h12 = h24 === 0 ? 12 : h24 > 12 ? h24 - 12 : h24;
2586
+ const ampm = h24 >= 12 ? 'PM' : 'AM';
2587
+ const minutes = Math.round((hour % 1) * 60);
2588
+ return `${Math.floor(h12)}:${minutes.toString().padStart(2, '0')} ${ampm}`;
2589
+ };
2590
+ const SleepChart = ({ sleepData, width = 800, height = 400, onDateClick }) => {
2498
2591
  const svgRef = React.useRef(null);
2499
- const margin = React.useMemo(() => ({ top: 20, right: 20, bottom: 40, left: 60 }), []);
2592
+ const [tooltipData, setTooltipData] = React.useState(null);
2593
+ const margin = React.useMemo(() => ({ top: 30, right: 30, bottom: 50, left: 80 }), []);
2500
2594
  const chartWidth = width - margin.left - margin.right;
2501
2595
  const chartHeight = height - margin.top - margin.bottom;
2596
+ // Hide tooltip on scroll
2597
+ React.useEffect(() => {
2598
+ const handleScroll = () => {
2599
+ setTooltipData(null);
2600
+ };
2601
+ window.addEventListener('scroll', handleScroll, true);
2602
+ return () => {
2603
+ window.removeEventListener('scroll', handleScroll, true);
2604
+ };
2605
+ }, []);
2502
2606
  React.useEffect(() => {
2503
2607
  if (!svgRef.current || sleepData.length === 0)
2504
2608
  return;
2505
2609
  const svg = d3__namespace.select(svgRef.current);
2506
2610
  svg.selectAll('*').remove();
2611
+ // Add invisible rect to detect mouse leave events
2612
+ svg.append('rect')
2613
+ .attr('width', width)
2614
+ .attr('height', height)
2615
+ .attr('fill', 'transparent')
2616
+ .style('pointer-events', 'all')
2617
+ .on('mouseleave', () => {
2618
+ setTooltipData(null);
2619
+ });
2507
2620
  const g = svg.append('g')
2508
2621
  .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)');
2524
2622
  const xScale = d3__namespace.scaleLinear()
2525
2623
  .domain([18, 42])
2526
2624
  .range([0, chartWidth]);
@@ -2528,16 +2626,39 @@ const SleepChart = ({ sleepData, width = 800, height = 300, onDateClick }) => {
2528
2626
  const yScale = d3__namespace.scaleBand()
2529
2627
  .domain(yDomain)
2530
2628
  .range([0, chartHeight])
2531
- .paddingInner(0.1)
2532
- .paddingOuter(0.05);
2629
+ .paddingInner(0.2)
2630
+ .paddingOuter(0.1);
2631
+ // Add vertical grid lines with midnight/noon highlights
2533
2632
  g.selectAll('.grid-line-x')
2534
- .data(d3__namespace.range(18, 43, 3))
2633
+ .data(d3__namespace.range(18, 43, 1))
2535
2634
  .enter().append('line')
2536
2635
  .attr('class', styles$4.gridLine)
2537
2636
  .attr('x1', d => xScale(d))
2538
2637
  .attr('y1', 0)
2539
2638
  .attr('x2', d => xScale(d))
2540
- .attr('y2', chartHeight);
2639
+ .attr('y2', chartHeight)
2640
+ .style('stroke', d => {
2641
+ const hour = d % 24;
2642
+ if (hour === 0)
2643
+ return 'var(--color-border)'; // Midnight
2644
+ if (hour === 12)
2645
+ return 'var(--color-border)'; // Noon
2646
+ return 'var(--color-border)';
2647
+ })
2648
+ .style('stroke-opacity', d => {
2649
+ const hour = d % 24;
2650
+ if (hour === 0 || hour === 12)
2651
+ return 0.3;
2652
+ if (hour % 3 === 0)
2653
+ return 0.15;
2654
+ return 0.05;
2655
+ })
2656
+ .style('stroke-width', d => {
2657
+ const hour = d % 24;
2658
+ if (hour === 0 || hour === 12)
2659
+ return 2;
2660
+ return 1;
2661
+ });
2541
2662
  sleepData.forEach((dayData) => {
2542
2663
  const yValue = yScale(dayData.date);
2543
2664
  if (yValue === undefined)
@@ -2575,37 +2696,60 @@ const SleepChart = ({ sleepData, width = 800, height = 300, onDateClick }) => {
2575
2696
  }
2576
2697
  // Draw the bar only if both values exist
2577
2698
  if (sleepHour !== null && wakeHour !== null) {
2578
- sleepGroup.append('rect')
2699
+ const rect = sleepGroup.append('rect')
2579
2700
  .attr('x', xScale(sleepHour))
2580
2701
  .attr('y', yValue)
2581
- .attr('width', Math.max(0, xScale(wakeHour) - xScale(sleepHour)))
2702
+ .attr('width', 0) // Start with 0 width for animation
2582
2703
  .attr('height', barHeight)
2583
- .attr('rx', 4)
2704
+ .attr('rx', barHeight / 2)
2584
2705
  .attr('fill', 'url(#sleepGradient)')
2585
- .attr('opacity', 0.8)
2586
- .attr('stroke', 'none');
2706
+ .attr('opacity', 0) // Start invisible
2707
+ .attr('stroke', 'rgba(255,255,255,0.3)')
2708
+ .attr('stroke-width', 1)
2709
+ .style('filter', 'drop-shadow(0 2px 8px rgba(155, 89, 182, 0.3))');
2710
+ // Animate the bar
2711
+ rect.transition()
2712
+ .duration(800)
2713
+ .delay(Math.random() * 200) // Stagger the animations
2714
+ .ease(d3__namespace.easeCubicOut)
2715
+ .attr('width', Math.max(0, xScale(wakeHour) - xScale(sleepHour)))
2716
+ .attr('opacity', 0.85);
2587
2717
  }
2588
2718
  // Draw sleep dot if sleep time exists
2589
2719
  if (sleepHour !== null) {
2590
- sleepGroup.append('circle')
2720
+ const sleepDot = sleepGroup.append('circle')
2591
2721
  .attr('cx', xScale(sleepHour))
2592
2722
  .attr('cy', yValue + barHeight / 2)
2593
- .attr('r', 4)
2723
+ .attr('r', 0) // Start with 0 radius
2594
2724
  .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))');
2725
+ .attr('stroke', '#ffffff')
2726
+ .attr('stroke-width', 2)
2727
+ .style('filter', 'drop-shadow(0 3px 6px rgba(155, 89, 182, 0.4))')
2728
+ .style('transition', 'all 0.3s ease');
2729
+ // Animate the dot
2730
+ sleepDot.transition()
2731
+ .duration(400)
2732
+ .delay(300 + Math.random() * 100)
2733
+ .ease(d3__namespace.easeBackOut)
2734
+ .attr('r', 6);
2598
2735
  }
2599
2736
  // Draw wake dot if wake hour exists
2600
2737
  if (wakeHour !== null) {
2601
- sleepGroup.append('circle')
2738
+ const wakeDot = sleepGroup.append('circle')
2602
2739
  .attr('cx', xScale(wakeHour))
2603
2740
  .attr('cy', yValue + barHeight / 2)
2604
- .attr('r', 4)
2741
+ .attr('r', 0) // Start with 0 radius
2605
2742
  .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))');
2743
+ .attr('stroke', '#ffffff')
2744
+ .attr('stroke-width', 2)
2745
+ .style('filter', 'drop-shadow(0 3px 6px rgba(52, 152, 219, 0.4))')
2746
+ .style('transition', 'all 0.3s ease');
2747
+ // Animate the dot
2748
+ wakeDot.transition()
2749
+ .duration(400)
2750
+ .delay(500 + Math.random() * 100)
2751
+ .ease(d3__namespace.easeBackOut)
2752
+ .attr('r', 6);
2609
2753
  }
2610
2754
  // Add hover interactions with tooltip
2611
2755
  sleepGroup
@@ -2615,48 +2759,50 @@ const SleepChart = ({ sleepData, width = 800, height = 300, onDateClick }) => {
2615
2759
  .transition()
2616
2760
  .duration(200)
2617
2761
  .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>`;
2762
+ .style('filter', 'drop-shadow(0 4px 12px rgba(155, 89, 182, 0.5)) brightness(1.1)');
2763
+ // Scale up the dots
2764
+ d3__namespace.select(this).selectAll('circle')
2765
+ .transition()
2766
+ .duration(200)
2767
+ .attr('r', 8);
2768
+ // Calculate tooltip data
2769
+ const rect = svgRef.current?.getBoundingClientRect();
2770
+ if (rect) {
2771
+ const duration = (sleepHour !== null && wakeHour !== null)
2772
+ ? wakeHour - sleepHour
2773
+ : null;
2774
+ setTooltipData({
2775
+ date: dayData.date,
2776
+ sleepTime: sleepHour !== null ? formatTime(sleepHour) : null,
2777
+ wakeTime: wakeHour !== null ? formatTime(wakeHour) : null,
2778
+ duration: duration !== null ? Math.round(duration * 100) / 100 : null,
2779
+ x: event.pageX,
2780
+ y: event.pageY - 80
2781
+ });
2639
2782
  }
2640
- tooltip.html(tooltipContent)
2641
- .style('opacity', 1)
2642
- .style('left', (event.pageX + 10) + 'px')
2643
- .style('top', (event.pageY - 10) + 'px');
2644
2783
  })
2645
2784
  .on('mouseleave', function () {
2646
2785
  // Reset bar appearance
2647
2786
  d3__namespace.select(this).select('rect')
2648
2787
  .transition()
2649
2788
  .duration(200)
2650
- .attr('opacity', 0.8)
2651
- .style('filter', 'none');
2789
+ .attr('opacity', 0.85)
2790
+ .style('filter', 'drop-shadow(0 2px 8px rgba(155, 89, 182, 0.3))');
2791
+ // Reset dots
2792
+ d3__namespace.select(this).selectAll('circle')
2793
+ .transition()
2794
+ .duration(200)
2795
+ .attr('r', 6);
2652
2796
  // Hide tooltip
2653
- tooltip.style('opacity', 0);
2797
+ setTooltipData(null);
2654
2798
  })
2655
2799
  .on('mousemove', function (event) {
2656
2800
  // Update tooltip position
2657
- tooltip
2658
- .style('left', (event.pageX + 10) + 'px')
2659
- .style('top', (event.pageY - 10) + 'px');
2801
+ setTooltipData(prev => prev ? {
2802
+ ...prev,
2803
+ x: event.pageX,
2804
+ y: event.pageY - 80
2805
+ } : null);
2660
2806
  });
2661
2807
  });
2662
2808
  // Create gradient definition
@@ -2668,16 +2814,23 @@ const SleepChart = ({ sleepData, width = 800, height = 300, onDateClick }) => {
2668
2814
  gradient.append('stop')
2669
2815
  .attr('offset', '0%')
2670
2816
  .attr('stop-color', '#9B59B6')
2671
- .attr('stop-opacity', 0.9);
2817
+ .attr('stop-opacity', 1);
2818
+ gradient.append('stop')
2819
+ .attr('offset', '50%')
2820
+ .attr('stop-color', '#7B68A6')
2821
+ .attr('stop-opacity', 1);
2672
2822
  gradient.append('stop')
2673
2823
  .attr('offset', '100%')
2674
2824
  .attr('stop-color', '#3498DB')
2675
- .attr('stop-opacity', 0.9);
2825
+ .attr('stop-opacity', 1);
2676
2826
  const xAxisTicks = d3__namespace.range(18, 43, 3).map(hour => ({
2677
2827
  value: hour,
2678
- label: (hour % 24).toString().padStart(2, '0') + ':00'
2828
+ label: hour === 24 ? '12 AM' : hour === 36 ? '12 PM' :
2829
+ (hour % 24) === 0 ? '12 AM' :
2830
+ (hour % 24) < 12 ? `${hour % 24} AM` :
2831
+ (hour % 24) === 12 ? '12 PM' : `${(hour % 24) - 12} PM`
2679
2832
  }));
2680
- g.append('g')
2833
+ const xAxis = g.append('g')
2681
2834
  .attr('class', styles$4.xAxis)
2682
2835
  .attr('transform', `translate(0,${chartHeight})`)
2683
2836
  .call(d3__namespace.axisBottom(xScale)
@@ -2685,7 +2838,14 @@ const SleepChart = ({ sleepData, width = 800, height = 300, onDateClick }) => {
2685
2838
  .tickFormat((d) => {
2686
2839
  const tick = xAxisTicks.find(t => t.value === d);
2687
2840
  return tick ? tick.label : '';
2688
- }));
2841
+ })
2842
+ .tickSizeOuter(0));
2843
+ // Style axis ticks
2844
+ xAxis.selectAll('text')
2845
+ .style('font-weight', d => {
2846
+ const hour = d % 24;
2847
+ return hour === 0 || hour === 12 ? '600' : '400';
2848
+ });
2689
2849
  const yAxisTicks = yDomain.filter((_, i) => i % Math.ceil(yDomain.length / 10) === 0);
2690
2850
  g.append('g')
2691
2851
  .attr('class', styles$4.yAxis)
@@ -2695,12 +2855,15 @@ const SleepChart = ({ sleepData, width = 800, height = 300, onDateClick }) => {
2695
2855
  const date = new Date(d);
2696
2856
  return `${(date.getMonth() + 1).toString().padStart(2, '0')}/${date.getDate().toString().padStart(2, '0')}`;
2697
2857
  }));
2698
- // Cleanup function
2699
- return () => {
2700
- tooltip.remove();
2701
- };
2702
2858
  }, [sleepData, chartWidth, chartHeight, margin, onDateClick]);
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 })] }));
2859
+ 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 }), tooltipData && (jsxRuntime.jsxs("div", { className: `${styles$4.tooltip} ${tooltipData ? styles$4.visible : ''}`, style: {
2860
+ left: `${tooltipData.x}px`,
2861
+ top: `${tooltipData.y}px`
2862
+ }, children: [jsxRuntime.jsxs("div", { className: styles$4.tooltipHeader, children: [jsxRuntime.jsx("span", { className: styles$4.tooltipEmoji, children: "\uD83D\uDE34" }), jsxRuntime.jsx("span", { children: new Date(tooltipData.date).toLocaleDateString('en-US', {
2863
+ weekday: 'short',
2864
+ month: 'short',
2865
+ day: 'numeric'
2866
+ }) })] }), jsxRuntime.jsxs("div", { className: styles$4.tooltipInfo, children: [tooltipData.sleepTime && (jsxRuntime.jsxs("div", { className: styles$4.tooltipRow, children: [jsxRuntime.jsxs("span", { className: styles$4.tooltipLabel, children: [jsxRuntime.jsx("span", { children: "\uD83C\uDF19" }), jsxRuntime.jsx("span", { children: "Sleep:" })] }), jsxRuntime.jsx("span", { className: styles$4.tooltipValue, children: tooltipData.sleepTime })] })), tooltipData.wakeTime && (jsxRuntime.jsxs("div", { className: styles$4.tooltipRow, children: [jsxRuntime.jsxs("span", { className: styles$4.tooltipLabel, children: [jsxRuntime.jsx("span", { children: "\u2600\uFE0F" }), jsxRuntime.jsx("span", { children: "Wake:" })] }), jsxRuntime.jsx("span", { className: styles$4.tooltipValue, children: tooltipData.wakeTime })] })), tooltipData.duration !== null && (jsxRuntime.jsxs("div", { className: styles$4.tooltipDuration, children: [jsxRuntime.jsx("span", { children: "\u23F1\uFE0F" }), jsxRuntime.jsxs("span", { children: [Math.floor(tooltipData.duration), "h ", Math.round((tooltipData.duration % 1) * 60), "m"] })] }))] })] }))] }));
2704
2867
  };
2705
2868
 
2706
2869
  var styles$3 = {"container":"BooleansHeatmap-module_container__IOyeU","title":"BooleansHeatmap-module_title__8DRRQ","habitEmoji":"BooleansHeatmap-module_habitEmoji__Mawv-","chart":"BooleansHeatmap-module_chart__-q0Pc","monthLabel":"BooleansHeatmap-module_monthLabel__MXbIg","dayLabel":"BooleansHeatmap-module_dayLabel__2RRtm","cell":"BooleansHeatmap-module_cell__WADVB","legend":"BooleansHeatmap-module_legend__WqGF8","legendItem":"BooleansHeatmap-module_legendItem__rDE2g","legendColor":"BooleansHeatmap-module_legendColor__Z34-d","tooltip":"BooleansHeatmap-module_tooltip__-fHl7"};
@@ -2929,8 +3092,8 @@ const SunburstChart = ({ data, width = 500, height = 500, title = 'Sunburst Char
2929
3092
  const interpolatedArc = d3__namespace.arc()
2930
3093
  .startAngle(() => currX0)
2931
3094
  .endAngle(() => currX1)
2932
- .innerRadius(() => Math.sqrt(currY0))
2933
- .outerRadius(() => Math.sqrt(currY1))
3095
+ .innerRadius(() => Math.min(radius, Math.sqrt(Math.max(0, currY0))))
3096
+ .outerRadius(() => Math.min(radius, Math.sqrt(Math.max(0, currY1))))
2934
3097
  .cornerRadius(3);
2935
3098
  return interpolatedArc(node) || '';
2936
3099
  };
@@ -2960,13 +3123,21 @@ const SunburstChart = ({ data, width = 500, height = 500, title = 'Sunburst Char
2960
3123
  .ease(d3__namespace.easeCubicInOut);
2961
3124
  // Zoom to clicked node - scale to fill the entire circle
2962
3125
  const clickedOriginal = clickedNode;
3126
+ // Ensure we use the original x values for proper scaling
3127
+ // The x-scale maps the clicked node's angular extent to the full circle
2963
3128
  const xScale = d3__namespace.scaleLinear()
2964
3129
  .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
3130
+ .range([0, 2 * Math.PI])
3131
+ .clamp(true); // Ensure values stay within bounds
3132
+ // For the radial scale, always use the full radius available
3133
+ // Find the deepest descendant to ensure all children fit
3134
+ const descendants = nodes.filter(d => isParentOf(clickedNode, d));
3135
+ const maxChildRadius = Math.max(...descendants.map(d => d.y1Original));
3136
+ // Always scale to use the full radius, regardless of original size
2967
3137
  const yScale = d3__namespace.scaleLinear()
2968
- .domain([clickedOriginal.y0Original, clickedOriginal.y1Original])
2969
- .range([0, radius * radius]);
3138
+ .domain([clickedOriginal.y0Original, maxChildRadius])
3139
+ .range([0, radius * radius])
3140
+ .clamp(true); // Ensure values stay within bounds
2970
3141
  // First, immediately hide elements that shouldn't be visible
2971
3142
  paths.style('opacity', (d) => isParentOf(clickedNode, d) ? 1 : 0);
2972
3143
  paths.transition(transition)
@@ -2978,17 +3149,33 @@ const SunburstChart = ({ data, width = 500, height = 500, title = 'Sunburst Char
2978
3149
  return () => '';
2979
3150
  }
2980
3151
  // 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
3152
+ // ALWAYS scale to fill the full circle, no minimum size preservation
3153
+ let newX0 = xScale(node.x0Original);
3154
+ let newX1 = xScale(node.x1Original);
3155
+ // Ensure proper scaling even for very small segments
3156
+ // The clicked node's extent should always map to full circle
3157
+ if (newX1 <= newX0) {
3158
+ // Force proper angular extent based on proportion
3159
+ const proportion = (node.x1Original - node.x0Original) / (clickedOriginal.x1Original - clickedOriginal.x0Original);
3160
+ newX1 = newX0 + (2 * Math.PI * proportion);
3161
+ }
3162
+ // For radial positions, scale appropriately
2984
3163
  let newY0, newY1;
2985
3164
  if (node === clickedNode) {
2986
- // The clicked node itself should fill from center to edge
3165
+ // The clicked node itself should fill from center
2987
3166
  newY0 = 0;
2988
- newY1 = radius * radius;
3167
+ // If it has children, leave room for them; otherwise fill to edge
3168
+ if (node.children && node.children.length > 0) {
3169
+ const childrenY0 = Math.min(...node.children.map(c => c.y0Original));
3170
+ newY1 = yScale(childrenY0) * 0.98; // Very slight gap for visual separation
3171
+ }
3172
+ else {
3173
+ newY1 = radius * radius * 0.98; // Fill almost to edge
3174
+ }
2989
3175
  }
2990
3176
  else {
2991
- // Child nodes should be scaled within the full radius
3177
+ // Child nodes should be scaled to fill the remaining space proportionally
3178
+ // NO minimum size enforcement - pure proportional scaling
2992
3179
  newY0 = yScale(node.y0Original);
2993
3180
  newY1 = yScale(node.y1Original);
2994
3181
  }
@@ -3011,8 +3198,8 @@ const SunburstChart = ({ data, width = 500, height = 500, title = 'Sunburst Char
3011
3198
  const interpolatedArc = d3__namespace.arc()
3012
3199
  .startAngle(() => currX0)
3013
3200
  .endAngle(() => currX1)
3014
- .innerRadius(() => Math.sqrt(currY0))
3015
- .outerRadius(() => Math.sqrt(currY1))
3201
+ .innerRadius(() => Math.min(radius, Math.sqrt(Math.max(0, currY0))))
3202
+ .outerRadius(() => Math.min(radius, Math.sqrt(Math.max(0, currY1))))
3016
3203
  .cornerRadius(3);
3017
3204
  return interpolatedArc(node) || '';
3018
3205
  };
@@ -3048,11 +3235,11 @@ const SunburstChart = ({ data, width = 500, height = 500, title = 'Sunburst Char
3048
3235
  return getColor(ancestor.data.name, d.depth);
3049
3236
  })
3050
3237
  .attr('stroke', () => {
3051
- // Get computed background color
3238
+ // Get computed border color from theme
3052
3239
  const computedStyle = window.getComputedStyle(svgRef.current);
3053
- return computedStyle.getPropertyValue('--bg-primary') || '#ffffff';
3240
+ return computedStyle.getPropertyValue('--color-border') || '#e2e8f0';
3054
3241
  })
3055
- .attr('stroke-width', 1)
3242
+ .attr('stroke-width', 2)
3056
3243
  .style('cursor', d => (!d.children || d.children.length === 0) ? 'default' : 'pointer')
3057
3244
  .on('click', handleClick)
3058
3245
  .on('mouseover', function (event, d) {
@@ -3112,7 +3299,9 @@ const SunburstChart = ({ data, width = 500, height = 500, title = 'Sunburst Char
3112
3299
  });
3113
3300
  const shouldDisplayLabel = (d) => {
3114
3301
  const angle = d.x1 - d.x0;
3115
- return angle > 0.15 && d.depth <= 2;
3302
+ // When zoomed in, be more lenient with label display since segments are larger
3303
+ const minAngle = focusedNodeRef.current ? 0.05 : 0.15;
3304
+ return angle > minAngle && d.depth <= 2;
3116
3305
  };
3117
3306
  // Function to update labels after transitions
3118
3307
  const updateLabels = (transition, visibleNodes, _isReset) => {