11import { useEffect } from 'react' ;
22
33let initialized = false ;
4+ let themeDebounceTimer : ReturnType < typeof setTimeout > | null = null ;
45
5- /** Apply the GitHub Highcharts theme using live CSS variable values */
6- async function initAndApplyTheme ( ) {
6+ /**
7+ * Apply the GitHub Highcharts theme using live CSS variable values.
8+ * @param updateExisting - When true, re-applies theme to already-mounted charts.
9+ * Skipped on first init because `Highcharts.setOptions()` already covers
10+ * charts created after the call. Only needed for live theme switches.
11+ */
12+ async function initAndApplyTheme ( updateExisting = false ) {
713 const [ { default : Highcharts } , { buildGitHubChartTheme } ] = await Promise . all ( [
814 import ( 'highcharts' ) ,
915 import ( '../../lib/chart-theme' ) ,
@@ -23,15 +29,29 @@ async function initAndApplyTheme() {
2329 plotOptions : { series : { animation : false } } ,
2430 } ) ;
2531
26- // Re-apply visual styles (colors, backgrounds) to existing charts
27- // without clobbering per-chart options like title, series, etc.
28- // eslint-disable-next-line @typescript-eslint/no-unused-vars
29- const { title : _t , subtitle : _s , series : _sr , ...safeTheme } = theme ;
30- Highcharts . charts . forEach ( ( chart ) => {
31- if ( chart ) {
32- chart . update ( safeTheme , true , true ) ;
33- }
34- } ) ;
32+ // Only re-apply to existing charts on theme change (not first init)
33+ if ( updateExisting ) {
34+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
35+ const { title : _t , subtitle : _s , series : _sr , ...safeTheme } = theme ;
36+ Highcharts . charts . forEach ( ( chart ) => {
37+ if ( chart ) {
38+ chart . update ( safeTheme , true , true ) ;
39+ }
40+ } ) ;
41+ }
42+ }
43+
44+ /**
45+ * Debounced theme re-apply for live color scheme changes.
46+ * Coalesces rapid attribute mutations (Primer sets multiple attrs at once)
47+ * into a single chart update pass.
48+ */
49+ function scheduleThemeUpdate ( ) {
50+ if ( themeDebounceTimer ) clearTimeout ( themeDebounceTimer ) ;
51+ themeDebounceTimer = setTimeout ( ( ) => {
52+ themeDebounceTimer = null ;
53+ initAndApplyTheme ( true ) ;
54+ } , 100 ) ;
3555}
3656
3757/** Initialize Highcharts with GitHub theme + accessibility module.
@@ -40,28 +60,22 @@ export function useHighchartsInit() {
4060 useEffect ( ( ) => {
4161 if ( ! initialized ) {
4262 initialized = true ;
43- initAndApplyTheme ( ) ;
63+ initAndApplyTheme ( false ) ; // First init — no existing charts to update
4464 }
4565
4666 // Watch for OS color scheme changes (light ↔ dark)
4767 const mediaQuery = window . matchMedia ( '(prefers-color-scheme: dark)' ) ;
48- const handleChange = ( ) => {
49- // Small delay to let Primer's theme CSS vars update first
50- setTimeout ( ( ) => { initAndApplyTheme ( ) ; } , 50 ) ;
51- } ;
52- mediaQuery . addEventListener ( 'change' , handleChange ) ;
68+ mediaQuery . addEventListener ( 'change' , scheduleThemeUpdate ) ;
5369
54- // Also watch for Primer's data-color-mode attribute changes
55- const observer = new MutationObserver ( ( ) => {
56- setTimeout ( ( ) => { initAndApplyTheme ( ) ; } , 50 ) ;
57- } ) ;
70+ // Watch for Primer's data-color-mode attribute changes
71+ const observer = new MutationObserver ( scheduleThemeUpdate ) ;
5872 const primerRoot = document . querySelector ( '[data-color-mode]' ) ;
5973 if ( primerRoot ) {
6074 observer . observe ( primerRoot , { attributes : true , attributeFilter : [ 'data-color-mode' , 'data-light-theme' , 'data-dark-theme' ] } ) ;
6175 }
6276
6377 return ( ) => {
64- mediaQuery . removeEventListener ( 'change' , handleChange ) ;
78+ mediaQuery . removeEventListener ( 'change' , scheduleThemeUpdate ) ;
6579 observer . disconnect ( ) ;
6680 } ;
6781 } , [ ] ) ;
0 commit comments