useChartHoverState.tsx166 lines · main
1import { useCallback, useEffect, useState } from 'react'
2
3interface ChartHoverState {
4 hoveredIndex: number | null
5 hoveredChart: string | null
6 syncHover: boolean
7 syncTooltip: boolean
8}
9
10const CHART_HOVER_SYNC_STORAGE_KEY = 'briven-chart-hover-sync-enabled'
11const CHART_TOOLTIP_SYNC_STORAGE_KEY = 'briven-chart-tooltip-sync-enabled'
12
13// Global state shared across all hook instances
14let globalState: ChartHoverState = {
15 hoveredIndex: null,
16 hoveredChart: null,
17 syncHover: true,
18 syncTooltip: true,
19}
20
21// Subscribers for state changes
22const subscribers = new Set<(state: ChartHoverState) => void>()
23
24// Load initial sync settings from localStorage
25try {
26 if (typeof window !== 'undefined') {
27 const hoverSyncStored = localStorage.getItem(CHART_HOVER_SYNC_STORAGE_KEY)
28 const tooltipSyncStored = localStorage.getItem(CHART_TOOLTIP_SYNC_STORAGE_KEY)
29
30 if (hoverSyncStored !== null) {
31 globalState.syncHover = JSON.parse(hoverSyncStored)
32 }
33 if (tooltipSyncStored !== null) {
34 globalState.syncTooltip = JSON.parse(tooltipSyncStored)
35 }
36 }
37} catch (error) {
38 console.warn('Failed to load chart sync settings from localStorage:', error)
39}
40
41function notifySubscribers() {
42 subscribers.forEach((callback) => callback(globalState))
43}
44
45function updateGlobalState(updates: Partial<ChartHoverState>) {
46 const prevState = globalState
47 globalState = { ...globalState, ...updates }
48
49 // Save sync settings to localStorage when they change
50 if (updates.syncHover !== undefined) {
51 try {
52 localStorage.setItem(CHART_HOVER_SYNC_STORAGE_KEY, JSON.stringify(globalState.syncHover))
53 } catch (error) {
54 console.warn('Failed to save chart hover sync setting to localStorage:', error)
55 }
56 }
57 if (updates.syncTooltip !== undefined) {
58 try {
59 localStorage.setItem(CHART_TOOLTIP_SYNC_STORAGE_KEY, JSON.stringify(globalState.syncTooltip))
60 } catch (error) {
61 console.warn('Failed to save chart tooltip sync setting to localStorage:', error)
62 }
63 }
64
65 // Only notify if state actually changed
66 if (JSON.stringify(prevState) !== JSON.stringify(globalState)) {
67 notifySubscribers()
68 }
69}
70
71export function useChartHoverState(chartId: string) {
72 const [state, setState] = useState<ChartHoverState>(globalState)
73
74 // Subscribe to global state changes
75 useEffect(() => {
76 const callback = (newState: ChartHoverState) => {
77 setState(newState)
78 }
79
80 subscribers.add(callback)
81
82 return () => {
83 subscribers.delete(callback)
84 }
85 }, [])
86
87 // Set hover state for this chart
88 const setHover = useCallback(
89 (index: number | null) => {
90 if (globalState.syncHover) {
91 // If sync is enabled, update global state
92 updateGlobalState({
93 hoveredIndex: index,
94 hoveredChart: index !== null ? chartId : null,
95 })
96 } else {
97 // If sync is disabled, only update local state
98 setState((prev) => ({
99 ...prev,
100 hoveredIndex: index,
101 hoveredChart: index !== null ? chartId : null,
102 }))
103 }
104 },
105 [chartId]
106 )
107
108 // Clear hover state
109 const clearHover = useCallback(() => {
110 if (globalState.syncHover) {
111 updateGlobalState({
112 hoveredIndex: null,
113 hoveredChart: null,
114 })
115 } else {
116 setState((prev) => ({
117 ...prev,
118 hoveredIndex: null,
119 hoveredChart: null,
120 }))
121 }
122 }, [])
123
124 // Set sync settings (for settings component)
125 const setSyncHover = useCallback((enabled: boolean) => {
126 updateGlobalState({
127 syncHover: enabled,
128 // If turning off hover sync, also turn off tooltip sync
129 ...(enabled === false && { syncTooltip: false }),
130 })
131 }, [])
132
133 const setSyncTooltip = useCallback((enabled: boolean) => {
134 updateGlobalState({
135 syncTooltip: enabled,
136 // If turning on tooltip sync, also turn on hover sync
137 ...(enabled === true && { syncHover: true }),
138 })
139 }, [])
140
141 // Determine if this chart should show synced state
142 const isCurrentChart = state.hoveredChart === chartId
143 const shouldShowSyncedState = state.syncHover && state.hoveredChart !== null && !isCurrentChart
144
145 return {
146 // Current state
147 hoveredIndex: shouldShowSyncedState
148 ? state.hoveredIndex
149 : isCurrentChart
150 ? state.hoveredIndex
151 : null,
152 syncHover: state.syncHover,
153 syncTooltip: state.syncTooltip,
154 hoveredChart: state.hoveredChart,
155
156 // Actions
157 setHover,
158 clearHover,
159 setSyncHover,
160 setSyncTooltip,
161
162 // Helpers
163 isHovered: state.hoveredIndex !== null && (isCurrentChart || shouldShowSyncedState),
164 isCurrentChart,
165 }
166}