From 414997487cf96aa3f0b8f9264cf180b173ef6ea3 Mon Sep 17 00:00:00 2001 From: rory Date: Wed, 3 Jun 2026 07:47:50 -0700 Subject: [PATCH 01/35] Extract chartWebFont helper for chart font loading Co-authored-by: Cursor --- src/components/Charts/context/chartWebFont.ts | 11 ++++++++ .../useChartFontManager.ts | 27 +++++++------------ 2 files changed, 21 insertions(+), 17 deletions(-) create mode 100644 src/components/Charts/context/chartWebFont.ts diff --git a/src/components/Charts/context/chartWebFont.ts b/src/components/Charts/context/chartWebFont.ts new file mode 100644 index 000000000000..a569e1424586 --- /dev/null +++ b/src/components/Charts/context/chartWebFont.ts @@ -0,0 +1,11 @@ +import type {DataModule} from '@shopify/react-native-skia'; + +function chartWebFont(url: string): DataModule { + // We construct a fake ESModule-shaped object because react-native-skia's `useFonts` on web expects + // a DataModule (i.e. the result of a dynamic `require()` call), which always has the shape + // `{ __esModule: true, default: }`. The `__esModule` property uses a double-underscore prefix + // that violates the naming-convention rule, but it is mandated by the library's internal contract. + return {__esModule: true, default: url} as unknown as DataModule; +} + +export default chartWebFont; diff --git a/src/components/Charts/hooks/useChartFontManager/useChartFontManager.ts b/src/components/Charts/hooks/useChartFontManager/useChartFontManager.ts index e0c8a8305fed..f3dbf01b77f2 100644 --- a/src/components/Charts/hooks/useChartFontManager/useChartFontManager.ts +++ b/src/components/Charts/hooks/useChartFontManager/useChartFontManager.ts @@ -1,30 +1,23 @@ -import type {DataModule, SkTypefaceFontProvider} from '@shopify/react-native-skia'; +import type {SkTypefaceFontProvider} from '@shopify/react-native-skia'; import {useFonts, useTypeface} from '@shopify/react-native-skia'; - -function webFont(url: string): DataModule { - // We construct a fake ESModule-shaped object because react-native-skia's `useFonts` on web expects - // a DataModule (i.e. the result of a dynamic `require()` call), which always has the shape - // `{ __esModule: true, default: }`. The `__esModule` property uses a double-underscore prefix - // that violates the naming-convention rule, but it is mandated by the library's internal contract. - return {__esModule: true, default: url} as unknown as DataModule; -} +import chartWebFont from '@components/Charts/context/chartWebFont'; function useChartFontManager(): SkTypefaceFontProvider | null { return useFonts({ ExpensifyNeue: [ - webFont(require('@assets/fonts/web/ExpensifyNeue-Regular.woff2') as string), - webFont(require('@assets/fonts/web/ExpensifyNeue-Bold.woff2') as string), - webFont(require('@assets/fonts/web/ExpensifyNeue-Italic.woff2') as string), - webFont(require('@assets/fonts/web/ExpensifyNeue-BoldItalic.woff2') as string), + chartWebFont(require('@assets/fonts/web/ExpensifyNeue-Regular.woff2') as string), + chartWebFont(require('@assets/fonts/web/ExpensifyNeue-Bold.woff2') as string), + chartWebFont(require('@assets/fonts/web/ExpensifyNeue-Italic.woff2') as string), + chartWebFont(require('@assets/fonts/web/ExpensifyNeue-BoldItalic.woff2') as string), ], - NotoSansSymbols: [webFont(require('@assets/fonts/NotoSans-Symbols.ttf') as string)], - NotoSansSCMonths: [webFont(require('@assets/fonts/NotoSansSC-Months.ttf') as string)], + NotoSansSymbols: [chartWebFont(require('@assets/fonts/NotoSans-Symbols.ttf') as string)], + NotoSansSCMonths: [chartWebFont(require('@assets/fonts/NotoSansSC-Months.ttf') as string)], }); } function useChartDefaultTypeface() { - const regular = useTypeface(webFont(require('@assets/fonts/web/ExpensifyNeue-Regular.woff2') as string)); - const bold = useTypeface(webFont(require('@assets/fonts/web/ExpensifyNeue-Bold.woff2') as string)); + const regular = useTypeface(chartWebFont(require('@assets/fonts/web/ExpensifyNeue-Regular.woff2') as string)); + const bold = useTypeface(chartWebFont(require('@assets/fonts/web/ExpensifyNeue-Bold.woff2') as string)); return {regular, bold}; } From b82f7395f2350b04a576abdde0f6aa4e012400b8 Mon Sep 17 00:00:00 2001 From: rory Date: Wed, 3 Jun 2026 07:47:58 -0700 Subject: [PATCH 02/35] Add ChartDefaultTypefaceContext and platform providers Co-authored-by: Cursor --- .../context/ChartDefaultTypefaceContext.tsx | 20 +++++++++++++++++++ .../ChartDefaultTypefaceProvider.native.tsx | 15 ++++++++++++++ .../context/ChartDefaultTypefaceProvider.tsx | 15 ++++++++++++++ 3 files changed, 50 insertions(+) create mode 100644 src/components/Charts/context/ChartDefaultTypefaceContext.tsx create mode 100644 src/components/Charts/context/ChartDefaultTypefaceProvider.native.tsx create mode 100644 src/components/Charts/context/ChartDefaultTypefaceProvider.tsx diff --git a/src/components/Charts/context/ChartDefaultTypefaceContext.tsx b/src/components/Charts/context/ChartDefaultTypefaceContext.tsx new file mode 100644 index 000000000000..eba804949470 --- /dev/null +++ b/src/components/Charts/context/ChartDefaultTypefaceContext.tsx @@ -0,0 +1,20 @@ +import type {SkTypeface} from '@shopify/react-native-skia'; +import React, {createContext, useContext} from 'react'; + +type ChartDefaultTypeface = { + regular: SkTypeface | null; + bold: SkTypeface | null; +}; + +const ChartDefaultTypefaceContext = createContext(null); + +function useChartDefaultTypeface(): ChartDefaultTypeface { + const context = useContext(ChartDefaultTypefaceContext); + if (!context) { + throw new Error('useChartDefaultTypeface must be used within ChartDefaultTypefaceProvider'); + } + return context; +} + +export {ChartDefaultTypefaceContext, useChartDefaultTypeface}; +export type {ChartDefaultTypeface}; diff --git a/src/components/Charts/context/ChartDefaultTypefaceProvider.native.tsx b/src/components/Charts/context/ChartDefaultTypefaceProvider.native.tsx new file mode 100644 index 000000000000..1d3080eeef85 --- /dev/null +++ b/src/components/Charts/context/ChartDefaultTypefaceProvider.native.tsx @@ -0,0 +1,15 @@ +import type {DataModule} from '@shopify/react-native-skia'; +import {useTypeface} from '@shopify/react-native-skia'; +import React from 'react'; +import {ChartDefaultTypefaceContext} from '@components/Charts/context/ChartDefaultTypefaceContext'; + +function ChartDefaultTypefaceProvider({children}: {children: React.ReactNode}) { + const regular = useTypeface(require('@assets/fonts/native/ExpensifyNeue-Regular.otf') as DataModule); + const bold = useTypeface(require('@assets/fonts/native/ExpensifyNeue-Bold.otf') as DataModule); + + return {children}; +} + +ChartDefaultTypefaceProvider.displayName = 'ChartDefaultTypefaceProvider'; + +export default ChartDefaultTypefaceProvider; diff --git a/src/components/Charts/context/ChartDefaultTypefaceProvider.tsx b/src/components/Charts/context/ChartDefaultTypefaceProvider.tsx new file mode 100644 index 000000000000..574dce4d3ab7 --- /dev/null +++ b/src/components/Charts/context/ChartDefaultTypefaceProvider.tsx @@ -0,0 +1,15 @@ +import {useTypeface} from '@shopify/react-native-skia'; +import React from 'react'; +import {ChartDefaultTypefaceContext} from '@components/Charts/context/ChartDefaultTypefaceContext'; +import chartWebFont from '@components/Charts/context/chartWebFont'; + +function ChartDefaultTypefaceProvider({children}: {children: React.ReactNode}) { + const regular = useTypeface(chartWebFont(require('@assets/fonts/web/ExpensifyNeue-Regular.woff2') as string)); + const bold = useTypeface(chartWebFont(require('@assets/fonts/web/ExpensifyNeue-Bold.woff2') as string)); + + return {children}; +} + +ChartDefaultTypefaceProvider.displayName = 'ChartDefaultTypefaceProvider'; + +export default ChartDefaultTypefaceProvider; From a3b46874dc0685a218d3e6300ef0eeac647f5ba5 Mon Sep 17 00:00:00 2001 From: rory Date: Wed, 3 Jun 2026 07:48:10 -0700 Subject: [PATCH 03/35] Wire Victory charts through ChartDefaultTypefaceProvider Co-authored-by: Cursor --- src/components/Charts/hooks/index.ts | 5 ++++- .../useChartFontManager.native.ts | 9 +-------- .../useChartFontManager/useChartFontManager.ts | 9 +-------- .../BaseVictoryChartRenderer.tsx | 13 ++++++++----- 4 files changed, 14 insertions(+), 22 deletions(-) diff --git a/src/components/Charts/hooks/index.ts b/src/components/Charts/hooks/index.ts index c18d0c9934d4..0b2644373cd1 100644 --- a/src/components/Charts/hooks/index.ts +++ b/src/components/Charts/hooks/index.ts @@ -2,7 +2,10 @@ export {default as useChartLabelLayout} from './useChartLabelLayout'; export {default as useChartLabelMeasurements} from './useChartLabelMeasurements'; export {default as useChartParagraphs} from './useChartParagraphs'; export {default as useYAxisLabelWidth} from './useYAxisLabelWidth'; -export {default as useChartFontManager, useChartDefaultTypeface} from './useChartFontManager/useChartFontManager'; +export {default as useChartFontManager} from './useChartFontManager/useChartFontManager'; +export {default as ChartDefaultTypefaceProvider} from '../context/ChartDefaultTypefaceProvider'; +export {ChartDefaultTypefaceContext, useChartDefaultTypeface} from '../context/ChartDefaultTypefaceContext'; +export type {ChartDefaultTypeface} from '../context/ChartDefaultTypefaceContext'; export {useChartInteractions, TOOLTIP_BAR_GAP} from './useChartInteractions'; export type {HitTestArgs} from './useChartInteractions'; export {default as useChartLabelFormats} from './useChartLabelFormats'; diff --git a/src/components/Charts/hooks/useChartFontManager/useChartFontManager.native.ts b/src/components/Charts/hooks/useChartFontManager/useChartFontManager.native.ts index 04cd0344d420..5b3f293c2def 100644 --- a/src/components/Charts/hooks/useChartFontManager/useChartFontManager.native.ts +++ b/src/components/Charts/hooks/useChartFontManager/useChartFontManager.native.ts @@ -1,5 +1,5 @@ import type {DataModule, SkTypefaceFontProvider} from '@shopify/react-native-skia'; -import {useFonts, useTypeface} from '@shopify/react-native-skia'; +import {useFonts} from '@shopify/react-native-skia'; function useChartFontManager(): SkTypefaceFontProvider | null { return useFonts({ @@ -14,11 +14,4 @@ function useChartFontManager(): SkTypefaceFontProvider | null { }); } -function useChartDefaultTypeface() { - const regular = useTypeface(require('@assets/fonts/native/ExpensifyNeue-Regular.otf') as DataModule); - const bold = useTypeface(require('@assets/fonts/native/ExpensifyNeue-Bold.otf') as DataModule); - return {regular, bold}; -} - -export {useChartDefaultTypeface}; export default useChartFontManager; diff --git a/src/components/Charts/hooks/useChartFontManager/useChartFontManager.ts b/src/components/Charts/hooks/useChartFontManager/useChartFontManager.ts index f3dbf01b77f2..f43896f3fb14 100644 --- a/src/components/Charts/hooks/useChartFontManager/useChartFontManager.ts +++ b/src/components/Charts/hooks/useChartFontManager/useChartFontManager.ts @@ -1,5 +1,5 @@ import type {SkTypefaceFontProvider} from '@shopify/react-native-skia'; -import {useFonts, useTypeface} from '@shopify/react-native-skia'; +import {useFonts} from '@shopify/react-native-skia'; import chartWebFont from '@components/Charts/context/chartWebFont'; function useChartFontManager(): SkTypefaceFontProvider | null { @@ -15,11 +15,4 @@ function useChartFontManager(): SkTypefaceFontProvider | null { }); } -function useChartDefaultTypeface() { - const regular = useTypeface(chartWebFont(require('@assets/fonts/web/ExpensifyNeue-Regular.woff2') as string)); - const bold = useTypeface(chartWebFont(require('@assets/fonts/web/ExpensifyNeue-Bold.woff2') as string)); - return {regular, bold}; -} - -export {useChartDefaultTypeface}; export default useChartFontManager; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/BaseVictoryChartRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/BaseVictoryChartRenderer.tsx index da522351285b..a84bb3b17166 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/BaseVictoryChartRenderer.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/BaseVictoryChartRenderer.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import {ChartDefaultTypefaceProvider} from '@components/Charts/hooks'; import VictoryChartContainer from './components/VictoryChartContainer'; import VictoryChartContent from './components/VictoryChartContent'; import {VictoryChartProvider} from './context/VictoryChartContext'; @@ -6,11 +7,13 @@ import type {VictoryChartRendererProps} from './types'; function BaseVictoryChartRenderer({tnode}: VictoryChartRendererProps) { return ( - - - - - + + + + + + + ); } From 5af76f468145e665613b4445fa06bd858611479b Mon Sep 17 00:00:00 2001 From: rory Date: Wed, 3 Jun 2026 07:49:52 -0700 Subject: [PATCH 04/35] Fix lint in chart typeface context files Co-authored-by: Cursor --- src/components/Charts/context/ChartDefaultTypefaceContext.tsx | 2 +- .../Charts/context/ChartDefaultTypefaceProvider.native.tsx | 2 +- .../Charts/context/ChartDefaultTypefaceProvider.tsx | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/Charts/context/ChartDefaultTypefaceContext.tsx b/src/components/Charts/context/ChartDefaultTypefaceContext.tsx index eba804949470..c358c99a667c 100644 --- a/src/components/Charts/context/ChartDefaultTypefaceContext.tsx +++ b/src/components/Charts/context/ChartDefaultTypefaceContext.tsx @@ -1,5 +1,5 @@ import type {SkTypeface} from '@shopify/react-native-skia'; -import React, {createContext, useContext} from 'react'; +import {createContext, useContext} from 'react'; type ChartDefaultTypeface = { regular: SkTypeface | null; diff --git a/src/components/Charts/context/ChartDefaultTypefaceProvider.native.tsx b/src/components/Charts/context/ChartDefaultTypefaceProvider.native.tsx index 1d3080eeef85..99ab33957b55 100644 --- a/src/components/Charts/context/ChartDefaultTypefaceProvider.native.tsx +++ b/src/components/Charts/context/ChartDefaultTypefaceProvider.native.tsx @@ -1,7 +1,7 @@ import type {DataModule} from '@shopify/react-native-skia'; import {useTypeface} from '@shopify/react-native-skia'; import React from 'react'; -import {ChartDefaultTypefaceContext} from '@components/Charts/context/ChartDefaultTypefaceContext'; +import {ChartDefaultTypefaceContext} from './ChartDefaultTypefaceContext'; function ChartDefaultTypefaceProvider({children}: {children: React.ReactNode}) { const regular = useTypeface(require('@assets/fonts/native/ExpensifyNeue-Regular.otf') as DataModule); diff --git a/src/components/Charts/context/ChartDefaultTypefaceProvider.tsx b/src/components/Charts/context/ChartDefaultTypefaceProvider.tsx index 574dce4d3ab7..ca490da0a19d 100644 --- a/src/components/Charts/context/ChartDefaultTypefaceProvider.tsx +++ b/src/components/Charts/context/ChartDefaultTypefaceProvider.tsx @@ -1,7 +1,7 @@ import {useTypeface} from '@shopify/react-native-skia'; import React from 'react'; -import {ChartDefaultTypefaceContext} from '@components/Charts/context/ChartDefaultTypefaceContext'; -import chartWebFont from '@components/Charts/context/chartWebFont'; +import {ChartDefaultTypefaceContext} from './ChartDefaultTypefaceContext'; +import chartWebFont from './chartWebFont'; function ChartDefaultTypefaceProvider({children}: {children: React.ReactNode}) { const regular = useTypeface(chartWebFont(require('@assets/fonts/web/ExpensifyNeue-Regular.woff2') as string)); From a13a7a5bf38d204b65ff921d9c655cdf0cb5b9bb Mon Sep 17 00:00:00 2001 From: rory Date: Wed, 3 Jun 2026 08:09:52 -0700 Subject: [PATCH 05/35] Remove unused chart typeface exports for knip Co-authored-by: Cursor --- src/components/Charts/context/ChartDefaultTypefaceContext.tsx | 1 - src/components/Charts/hooks/index.ts | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/components/Charts/context/ChartDefaultTypefaceContext.tsx b/src/components/Charts/context/ChartDefaultTypefaceContext.tsx index c358c99a667c..0c8223d73640 100644 --- a/src/components/Charts/context/ChartDefaultTypefaceContext.tsx +++ b/src/components/Charts/context/ChartDefaultTypefaceContext.tsx @@ -17,4 +17,3 @@ function useChartDefaultTypeface(): ChartDefaultTypeface { } export {ChartDefaultTypefaceContext, useChartDefaultTypeface}; -export type {ChartDefaultTypeface}; diff --git a/src/components/Charts/hooks/index.ts b/src/components/Charts/hooks/index.ts index 0b2644373cd1..64d0ed2ae308 100644 --- a/src/components/Charts/hooks/index.ts +++ b/src/components/Charts/hooks/index.ts @@ -4,8 +4,7 @@ export {default as useChartParagraphs} from './useChartParagraphs'; export {default as useYAxisLabelWidth} from './useYAxisLabelWidth'; export {default as useChartFontManager} from './useChartFontManager/useChartFontManager'; export {default as ChartDefaultTypefaceProvider} from '../context/ChartDefaultTypefaceProvider'; -export {ChartDefaultTypefaceContext, useChartDefaultTypeface} from '../context/ChartDefaultTypefaceContext'; -export type {ChartDefaultTypeface} from '../context/ChartDefaultTypefaceContext'; +export {useChartDefaultTypeface} from '../context/ChartDefaultTypefaceContext'; export {useChartInteractions, TOOLTIP_BAR_GAP} from './useChartInteractions'; export type {HitTestArgs} from './useChartInteractions'; export {default as useChartLabelFormats} from './useChartLabelFormats'; From 90961765be326669dbe3f3a72fbeec0b450a5cf0 Mon Sep 17 00:00:00 2001 From: rory Date: Wed, 3 Jun 2026 08:41:22 -0700 Subject: [PATCH 06/35] Remove unnecessary displayName --- .../Charts/context/ChartDefaultTypefaceProvider.native.tsx | 2 -- src/components/Charts/context/ChartDefaultTypefaceProvider.tsx | 2 -- 2 files changed, 4 deletions(-) diff --git a/src/components/Charts/context/ChartDefaultTypefaceProvider.native.tsx b/src/components/Charts/context/ChartDefaultTypefaceProvider.native.tsx index 99ab33957b55..f9830eba9434 100644 --- a/src/components/Charts/context/ChartDefaultTypefaceProvider.native.tsx +++ b/src/components/Charts/context/ChartDefaultTypefaceProvider.native.tsx @@ -10,6 +10,4 @@ function ChartDefaultTypefaceProvider({children}: {children: React.ReactNode}) { return {children}; } -ChartDefaultTypefaceProvider.displayName = 'ChartDefaultTypefaceProvider'; - export default ChartDefaultTypefaceProvider; diff --git a/src/components/Charts/context/ChartDefaultTypefaceProvider.tsx b/src/components/Charts/context/ChartDefaultTypefaceProvider.tsx index ca490da0a19d..7dd6886d3300 100644 --- a/src/components/Charts/context/ChartDefaultTypefaceProvider.tsx +++ b/src/components/Charts/context/ChartDefaultTypefaceProvider.tsx @@ -10,6 +10,4 @@ function ChartDefaultTypefaceProvider({children}: {children: React.ReactNode}) { return {children}; } -ChartDefaultTypefaceProvider.displayName = 'ChartDefaultTypefaceProvider'; - export default ChartDefaultTypefaceProvider; From 4318681f8159e88cb7e22e599ae4409a550dceae Mon Sep 17 00:00:00 2001 From: rory Date: Wed, 3 Jun 2026 08:50:28 -0700 Subject: [PATCH 07/35] Wrap Skia chart overlays with ChartDefaultTypefaceProvider Co-authored-by: Cursor --- .../components/VictoryChartCartesian.tsx | 31 ++++++++------- .../components/VictoryChartPolar.tsx | 39 ++++++++++--------- 2 files changed, 38 insertions(+), 32 deletions(-) diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartCartesian.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartCartesian.tsx index 10cef4f0875c..2beed92cd08f 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartCartesian.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartCartesian.tsx @@ -1,5 +1,6 @@ import React from 'react'; import {CartesianChart} from 'victory-native'; +import {ChartDefaultTypefaceProvider} from '@components/Charts/hooks'; import {useVictoryChartContext} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartContext'; import {VictoryChartRenderArgsProvider} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartRenderArgsContext'; import getHierarchyID from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/getHierarchyID'; @@ -25,20 +26,22 @@ function VictoryChartCartesian() { domainPadding={domainPadding} padding={padding} renderOutside={(renderArgs) => ( - - {labelItems.map((labelItem) => ( - - ))} - {legendItems.map((legendItem) => ( - - ))} - + + + {labelItems.map((labelItem) => ( + + ))} + {legendItems.map((legendItem) => ( + + ))} + + )} > {(renderArgs) => ( diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartPolar.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartPolar.tsx index ebaa6856e2f9..d9c921e437bf 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartPolar.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartPolar.tsx @@ -1,5 +1,6 @@ import React from 'react'; import {PolarChart} from 'victory-native'; +import {ChartDefaultTypefaceProvider} from '@components/Charts/hooks'; import {COLOR_KEY, LABEL_KEY, VALUE_KEY} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/constants'; import {useVictoryChartContext} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartContext'; import getHierarchyID from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/getHierarchyID'; @@ -20,24 +21,26 @@ function VictoryChartPolar() { valueKey={VALUE_KEY} colorKey={COLOR_KEY} > - {tnode.children.map((child) => ( - - ))} - {labelItems.map((labelItem) => ( - - ))} - {legendItems.map((legendItem) => ( - - ))} + + {tnode.children.map((child) => ( + + ))} + {labelItems.map((labelItem) => ( + + ))} + {legendItems.map((legendItem) => ( + + ))} + ); } From 1e5309f7db60664666ab682a8acb27c44357f739 Mon Sep 17 00:00:00 2001 From: rory Date: Wed, 3 Jun 2026 09:08:24 -0700 Subject: [PATCH 08/35] Load Expensify New Kansas for Victory chart labels Polar chart HTML specifies fontFamily per label line; parse it and select the Kansas typeface from ChartDefaultTypefaceProvider. Co-authored-by: Cursor --- .../context/ChartDefaultTypefaceContext.tsx | 1 + .../ChartDefaultTypefaceProvider.native.tsx | 3 ++- .../context/ChartDefaultTypefaceProvider.tsx | 3 ++- src/components/Charts/hooks/index.ts | 1 + .../components/VictoryChartLabel.tsx | 8 +++++--- .../parsers/victoryLabelParser.ts | 7 +++++++ .../HTMLRenderers/VictoryChartRenderer/types.ts | 4 ++++ .../utils/getVictoryChartLabelTypeface.ts | 15 +++++++++++++++ 8 files changed, 37 insertions(+), 5 deletions(-) create mode 100644 src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/getVictoryChartLabelTypeface.ts diff --git a/src/components/Charts/context/ChartDefaultTypefaceContext.tsx b/src/components/Charts/context/ChartDefaultTypefaceContext.tsx index 0c8223d73640..37ff2010aead 100644 --- a/src/components/Charts/context/ChartDefaultTypefaceContext.tsx +++ b/src/components/Charts/context/ChartDefaultTypefaceContext.tsx @@ -4,6 +4,7 @@ import {createContext, useContext} from 'react'; type ChartDefaultTypeface = { regular: SkTypeface | null; bold: SkTypeface | null; + kansas: SkTypeface | null; }; const ChartDefaultTypefaceContext = createContext(null); diff --git a/src/components/Charts/context/ChartDefaultTypefaceProvider.native.tsx b/src/components/Charts/context/ChartDefaultTypefaceProvider.native.tsx index f9830eba9434..332a0d3577a4 100644 --- a/src/components/Charts/context/ChartDefaultTypefaceProvider.native.tsx +++ b/src/components/Charts/context/ChartDefaultTypefaceProvider.native.tsx @@ -6,8 +6,9 @@ import {ChartDefaultTypefaceContext} from './ChartDefaultTypefaceContext'; function ChartDefaultTypefaceProvider({children}: {children: React.ReactNode}) { const regular = useTypeface(require('@assets/fonts/native/ExpensifyNeue-Regular.otf') as DataModule); const bold = useTypeface(require('@assets/fonts/native/ExpensifyNeue-Bold.otf') as DataModule); + const kansas = useTypeface(require('@assets/fonts/native/ExpensifyNewKansas-Medium.otf') as DataModule); - return {children}; + return {children}; } export default ChartDefaultTypefaceProvider; diff --git a/src/components/Charts/context/ChartDefaultTypefaceProvider.tsx b/src/components/Charts/context/ChartDefaultTypefaceProvider.tsx index 7dd6886d3300..e541039477b2 100644 --- a/src/components/Charts/context/ChartDefaultTypefaceProvider.tsx +++ b/src/components/Charts/context/ChartDefaultTypefaceProvider.tsx @@ -6,8 +6,9 @@ import chartWebFont from './chartWebFont'; function ChartDefaultTypefaceProvider({children}: {children: React.ReactNode}) { const regular = useTypeface(chartWebFont(require('@assets/fonts/web/ExpensifyNeue-Regular.woff2') as string)); const bold = useTypeface(chartWebFont(require('@assets/fonts/web/ExpensifyNeue-Bold.woff2') as string)); + const kansas = useTypeface(chartWebFont(require('@assets/fonts/web/ExpensifyNewKansas-Medium.woff2') as string)); - return {children}; + return {children}; } export default ChartDefaultTypefaceProvider; diff --git a/src/components/Charts/hooks/index.ts b/src/components/Charts/hooks/index.ts index 64d0ed2ae308..988df7608392 100644 --- a/src/components/Charts/hooks/index.ts +++ b/src/components/Charts/hooks/index.ts @@ -5,6 +5,7 @@ export {default as useYAxisLabelWidth} from './useYAxisLabelWidth'; export {default as useChartFontManager} from './useChartFontManager/useChartFontManager'; export {default as ChartDefaultTypefaceProvider} from '../context/ChartDefaultTypefaceProvider'; export {useChartDefaultTypeface} from '../context/ChartDefaultTypefaceContext'; +export type {ChartDefaultTypeface} from '../context/ChartDefaultTypefaceContext'; export {useChartInteractions, TOOLTIP_BAR_GAP} from './useChartInteractions'; export type {HitTestArgs} from './useChartInteractions'; export {default as useChartLabelFormats} from './useChartLabelFormats'; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartLabel.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartLabel.tsx index 76e3f963bda1..ee634e570847 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartLabel.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartLabel.tsx @@ -4,6 +4,7 @@ import React from 'react'; import {useChartDefaultTypeface} from '@components/Charts/hooks'; import type {LabelItem} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/types'; import computeTextAnchorPosition from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/computeTextAnchorPosition'; +import getVictoryChartLabelTypeface from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/getVictoryChartLabelTypeface'; type VictoryChartLabelsProps = LabelItem; @@ -20,15 +21,16 @@ type ProcessedLine = { * Renders floating Skia text labels (from `` nodes) over the chart canvas. * Intended for use inside CartesianChart's `renderOutside` callback. */ -function VictoryChartLabel({x, y, text, color, fontSize, fontWeight, lineHeight, textAnchor = 'start', verticalAnchor = 'start'}: VictoryChartLabelsProps) { - const {regular: regularTypeface, bold: boldTypeface} = useChartDefaultTypeface(); +function VictoryChartLabel({x, y, text, color, fontSize, fontWeight, fontFamily, lineHeight, textAnchor = 'start', verticalAnchor = 'start'}: VictoryChartLabelsProps) { + const typefaces = useChartDefaultTypeface(); const processedLines = text.split('\n').reduce( (acc, line, index) => { const lineColor = color?.[index]; const lineFontSize = fontSize?.[index]; const lineFontWeight = fontWeight?.[index]; + const lineFontFamily = fontFamily?.[index]; const lineLineHeight = lineHeight?.[index]; - const typeface = lineFontWeight === 'bold' ? boldTypeface : regularTypeface; + const typeface = getVictoryChartLabelTypeface(lineFontFamily, lineFontWeight, typefaces); const lineFont = typeface && lineFontSize ? Skia.Font(typeface, lineFontSize) : null; const fontMetrics = lineFont?.getMetrics(); const lineWidth = lineFont?.getGlyphWidths(lineFont.getGlyphIDs(line)).reduce((totalWidth, width) => totalWidth + width, 0) ?? 0; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/parsers/victoryLabelParser.ts b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/parsers/victoryLabelParser.ts index a244f0b8c32e..1cb115c2d28d 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/parsers/victoryLabelParser.ts +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/parsers/victoryLabelParser.ts @@ -13,6 +13,7 @@ function parseVictoryLabelNode(tnode: TNode): PartialProcessNodeResult { color: {}, fontSize: {}, fontWeight: {}, + fontFamily: {}, lineHeight: parseAttribute(tnode.attributes.lineheight), textAnchor: parseAttribute(tnode.attributes.textanchor), verticalAnchor: parseAttribute(tnode.attributes.verticalanchor), @@ -40,6 +41,12 @@ function parseVictoryLabelNode(tnode: TNode): PartialProcessNodeResult { [index]: Number(textStyle.fontWeight) === 700 ? 'bold' : 'normal', }; } + if (textStyle.fontFamily) { + labelItem.fontFamily = { + ...labelItem.fontFamily, + [index]: textStyle.fontFamily, + }; + } } } diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/types.ts b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/types.ts index 51f52d6656c3..0b8007abfd83 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/types.ts +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/types.ts @@ -36,6 +36,7 @@ type RawLabelStyle = { fill?: Color; fontSize?: string | number; fontWeight?: string | number; + fontFamily?: string; }; type RawLegendStyle = { @@ -81,6 +82,9 @@ type LabelItem = { /** Font weight (per line) */ fontWeight?: Record; + /** Font family (per line) */ + fontFamily?: Record; + /** Line height (per line) */ lineHeight?: Record; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/getVictoryChartLabelTypeface.ts b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/getVictoryChartLabelTypeface.ts new file mode 100644 index 000000000000..62081790f762 --- /dev/null +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/getVictoryChartLabelTypeface.ts @@ -0,0 +1,15 @@ +import type {SkTypeface} from '@shopify/react-native-skia'; +import type {ChartDefaultTypeface} from '@components/Charts/hooks'; + +/** Matches `fontFamily` in Victory HTML from Web-Expensify (e.g. proactive CFO polar charts). */ +const EXPENSIFY_NEW_KANSAS_FONT_FAMILY = 'Expensify New Kansas'; + +function getVictoryChartLabelTypeface(fontFamily: string | undefined, fontWeight: 'normal' | 'bold' | undefined, typefaces: ChartDefaultTypeface): SkTypeface | null { + if (fontFamily === EXPENSIFY_NEW_KANSAS_FONT_FAMILY) { + return typefaces.kansas; + } + + return fontWeight === 'bold' ? typefaces.bold : typefaces.regular; +} + +export default getVictoryChartLabelTypeface; From b78dfa1aa632f5d18bc7b23e8c998a771a27b551 Mon Sep 17 00:00:00 2001 From: rory Date: Wed, 3 Jun 2026 09:13:50 -0700 Subject: [PATCH 09/35] Load all app Skia typefaces in ChartDefaultTypefaceProvider Expose every FontUtils family variant (Neue, Mono, Kansas, emoji) and resolve Victory label styles via getChartSkiaTypeface. Co-authored-by: Cursor --- .../context/ChartDefaultTypefaceContext.tsx | 8 +-- .../ChartDefaultTypefaceProvider.native.tsx | 11 ++-- .../context/ChartDefaultTypefaceProvider.tsx | 11 ++-- .../Charts/context/chartSkiaTypefaceTypes.ts | 9 +++ .../Charts/context/getChartSkiaTypeface.ts | 59 +++++++++++++++++++ .../context/useChartSkiaTypefaces.native.ts | 21 +++++++ .../Charts/context/useChartSkiaTypefaces.ts | 21 +++++++ src/components/Charts/hooks/index.ts | 3 +- .../components/VictoryChartLabel.tsx | 12 ++-- .../components/VictoryChartLegend.tsx | 8 +-- .../context/VictoryChartContext.tsx | 4 +- .../parsers/victoryLabelParser.ts | 7 +++ .../parsers/victoryLegendParser.ts | 4 +- .../VictoryChartRenderer/types.ts | 12 ++++ .../utils/getVictoryChartLabelTypeface.ts | 15 ----- 15 files changed, 159 insertions(+), 46 deletions(-) create mode 100644 src/components/Charts/context/chartSkiaTypefaceTypes.ts create mode 100644 src/components/Charts/context/getChartSkiaTypeface.ts create mode 100644 src/components/Charts/context/useChartSkiaTypefaces.native.ts create mode 100644 src/components/Charts/context/useChartSkiaTypefaces.ts delete mode 100644 src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/getVictoryChartLabelTypeface.ts diff --git a/src/components/Charts/context/ChartDefaultTypefaceContext.tsx b/src/components/Charts/context/ChartDefaultTypefaceContext.tsx index 37ff2010aead..6a0e783d8354 100644 --- a/src/components/Charts/context/ChartDefaultTypefaceContext.tsx +++ b/src/components/Charts/context/ChartDefaultTypefaceContext.tsx @@ -1,11 +1,5 @@ -import type {SkTypeface} from '@shopify/react-native-skia'; import {createContext, useContext} from 'react'; - -type ChartDefaultTypeface = { - regular: SkTypeface | null; - bold: SkTypeface | null; - kansas: SkTypeface | null; -}; +import type {ChartDefaultTypeface} from './chartSkiaTypefaceTypes'; const ChartDefaultTypefaceContext = createContext(null); diff --git a/src/components/Charts/context/ChartDefaultTypefaceProvider.native.tsx b/src/components/Charts/context/ChartDefaultTypefaceProvider.native.tsx index 332a0d3577a4..0a7384e5a622 100644 --- a/src/components/Charts/context/ChartDefaultTypefaceProvider.native.tsx +++ b/src/components/Charts/context/ChartDefaultTypefaceProvider.native.tsx @@ -1,14 +1,13 @@ -import type {DataModule} from '@shopify/react-native-skia'; -import {useTypeface} from '@shopify/react-native-skia'; import React from 'react'; import {ChartDefaultTypefaceContext} from './ChartDefaultTypefaceContext'; +import useChartSkiaTypefaces from './useChartSkiaTypefaces'; function ChartDefaultTypefaceProvider({children}: {children: React.ReactNode}) { - const regular = useTypeface(require('@assets/fonts/native/ExpensifyNeue-Regular.otf') as DataModule); - const bold = useTypeface(require('@assets/fonts/native/ExpensifyNeue-Bold.otf') as DataModule); - const kansas = useTypeface(require('@assets/fonts/native/ExpensifyNewKansas-Medium.otf') as DataModule); + const typefaces = useChartSkiaTypefaces(); - return {children}; + return {children}; } +ChartDefaultTypefaceProvider.displayName = 'ChartDefaultTypefaceProvider'; + export default ChartDefaultTypefaceProvider; diff --git a/src/components/Charts/context/ChartDefaultTypefaceProvider.tsx b/src/components/Charts/context/ChartDefaultTypefaceProvider.tsx index e541039477b2..0a7384e5a622 100644 --- a/src/components/Charts/context/ChartDefaultTypefaceProvider.tsx +++ b/src/components/Charts/context/ChartDefaultTypefaceProvider.tsx @@ -1,14 +1,13 @@ -import {useTypeface} from '@shopify/react-native-skia'; import React from 'react'; import {ChartDefaultTypefaceContext} from './ChartDefaultTypefaceContext'; -import chartWebFont from './chartWebFont'; +import useChartSkiaTypefaces from './useChartSkiaTypefaces'; function ChartDefaultTypefaceProvider({children}: {children: React.ReactNode}) { - const regular = useTypeface(chartWebFont(require('@assets/fonts/web/ExpensifyNeue-Regular.woff2') as string)); - const bold = useTypeface(chartWebFont(require('@assets/fonts/web/ExpensifyNeue-Bold.woff2') as string)); - const kansas = useTypeface(chartWebFont(require('@assets/fonts/web/ExpensifyNewKansas-Medium.woff2') as string)); + const typefaces = useChartSkiaTypefaces(); - return {children}; + return {children}; } +ChartDefaultTypefaceProvider.displayName = 'ChartDefaultTypefaceProvider'; + export default ChartDefaultTypefaceProvider; diff --git a/src/components/Charts/context/chartSkiaTypefaceTypes.ts b/src/components/Charts/context/chartSkiaTypefaceTypes.ts new file mode 100644 index 000000000000..fa26dde27a2a --- /dev/null +++ b/src/components/Charts/context/chartSkiaTypefaceTypes.ts @@ -0,0 +1,9 @@ +import type {SkTypeface} from '@shopify/react-native-skia'; +// eslint-disable-next-line no-restricted-imports +import type FontFamilyStyles from '@styles/utils/FontUtils/fontFamily/types'; + +type ChartSkiaTypefaceKey = Exclude; + +type ChartDefaultTypeface = Record; + +export type {ChartDefaultTypeface, ChartSkiaTypefaceKey}; diff --git a/src/components/Charts/context/getChartSkiaTypeface.ts b/src/components/Charts/context/getChartSkiaTypeface.ts new file mode 100644 index 000000000000..eecf10edfa9e --- /dev/null +++ b/src/components/Charts/context/getChartSkiaTypeface.ts @@ -0,0 +1,59 @@ +import type {SkTypeface} from '@shopify/react-native-skia'; +import type {TextStyle} from 'react-native'; +// eslint-disable-next-line no-restricted-imports +import singleFontFamily from '@styles/utils/FontUtils/fontFamily/singleFontFamily'; +import type {ChartDefaultTypeface, ChartSkiaTypefaceKey} from './chartSkiaTypefaceTypes'; + +type ChartLabelFontStyle = 'normal' | 'italic'; +type ChartLabelFontWeight = 'normal' | 'bold'; + +function normalizeFontStyle(fontStyle: string | undefined): ChartLabelFontStyle { + return fontStyle === 'italic' ? 'italic' : 'normal'; +} + +function normalizeFontWeight(fontWeight: string | number | undefined): ChartLabelFontWeight { + return Number(fontWeight) === 700 ? 'bold' : 'normal'; +} + +function fontWeightMatches(definitionWeight: TextStyle['fontWeight'], labelWeight: ChartLabelFontWeight): boolean { + const numericWeight = Number(definitionWeight); + + if (labelWeight === 'bold') { + return numericWeight >= 600 || definitionWeight === 'bold'; + } + + return numericWeight < 600 && definitionWeight !== 'bold'; +} + +function getChartSkiaTypefaceKey(fontFamily: string | undefined, fontStyle: ChartLabelFontStyle, fontWeight: ChartLabelFontWeight): ChartSkiaTypefaceKey { + const resolvedFontFamily = fontFamily ?? singleFontFamily.EXP_NEUE.fontFamily; + + if (resolvedFontFamily === singleFontFamily.EXP_NEW_KANSAS_MEDIUM.fontFamily) { + return fontStyle === 'italic' ? 'EXP_NEW_KANSAS_MEDIUM_ITALIC' : 'EXP_NEW_KANSAS_MEDIUM'; + } + + const matchingEntry = (Object.entries(singleFontFamily) as Array<[ChartSkiaTypefaceKey, (typeof singleFontFamily)[ChartSkiaTypefaceKey]]>).find( + ([, definition]) => definition.fontFamily === resolvedFontFamily && normalizeFontStyle(definition.fontStyle) === fontStyle && fontWeightMatches(definition.fontWeight, fontWeight), + ); + + return matchingEntry?.[0] ?? 'EXP_NEUE'; +} + +function getChartSkiaTypeface( + typefaces: ChartDefaultTypeface, + { + fontFamily, + fontStyle, + fontWeight, + }: { + fontFamily?: string; + fontStyle?: string; + fontWeight?: string | number; + }, +): SkTypeface | null { + const typefaceKey = getChartSkiaTypefaceKey(fontFamily, normalizeFontStyle(fontStyle), normalizeFontWeight(fontWeight)); + return typefaces[typefaceKey]; +} + +export {getChartSkiaTypeface}; +export type {ChartLabelFontStyle, ChartLabelFontWeight}; diff --git a/src/components/Charts/context/useChartSkiaTypefaces.native.ts b/src/components/Charts/context/useChartSkiaTypefaces.native.ts new file mode 100644 index 000000000000..9f47af1236fd --- /dev/null +++ b/src/components/Charts/context/useChartSkiaTypefaces.native.ts @@ -0,0 +1,21 @@ +import type {DataModule} from '@shopify/react-native-skia'; +import {useTypeface} from '@shopify/react-native-skia'; +import type {ChartDefaultTypeface} from './chartSkiaTypefaceTypes'; + +function useChartSkiaTypefaces(): ChartDefaultTypeface { + return { + MONOSPACE: useTypeface(require('@assets/fonts/native/ExpensifyMono-Regular.otf') as DataModule), + MONOSPACE_BOLD: useTypeface(require('@assets/fonts/native/ExpensifyMono-Bold.otf') as DataModule), + MONOSPACE_ITALIC: useTypeface(require('@assets/fonts/native/ExpensifyMono-Italic.otf') as DataModule), + MONOSPACE_BOLD_ITALIC: useTypeface(require('@assets/fonts/native/ExpensifyMono-BoldItalic.otf') as DataModule), + EXP_NEUE: useTypeface(require('@assets/fonts/native/ExpensifyNeue-Regular.otf') as DataModule), + EXP_NEUE_BOLD: useTypeface(require('@assets/fonts/native/ExpensifyNeue-Bold.otf') as DataModule), + EXP_NEUE_ITALIC: useTypeface(require('@assets/fonts/native/ExpensifyNeue-Italic.otf') as DataModule), + EXP_NEUE_BOLD_ITALIC: useTypeface(require('@assets/fonts/native/ExpensifyNeue-BoldItalic.otf') as DataModule), + EXP_NEW_KANSAS_MEDIUM: useTypeface(require('@assets/fonts/native/ExpensifyNewKansas-Medium.otf') as DataModule), + EXP_NEW_KANSAS_MEDIUM_ITALIC: useTypeface(require('@assets/fonts/native/ExpensifyNewKansas-MediumItalic.otf') as DataModule), + CUSTOM_EMOJI_FONT: useTypeface(require('@assets/fonts/native/CustomEmojiNativeFont.ttf') as DataModule), + }; +} + +export default useChartSkiaTypefaces; diff --git a/src/components/Charts/context/useChartSkiaTypefaces.ts b/src/components/Charts/context/useChartSkiaTypefaces.ts new file mode 100644 index 000000000000..1125675aa155 --- /dev/null +++ b/src/components/Charts/context/useChartSkiaTypefaces.ts @@ -0,0 +1,21 @@ +import {useTypeface} from '@shopify/react-native-skia'; +import type {ChartDefaultTypeface} from './chartSkiaTypefaceTypes'; +import chartWebFont from './chartWebFont'; + +function useChartSkiaTypefaces(): ChartDefaultTypeface { + return { + MONOSPACE: useTypeface(chartWebFont(require('@assets/fonts/web/ExpensifyMono-Regular.woff2') as string)), + MONOSPACE_BOLD: useTypeface(chartWebFont(require('@assets/fonts/web/ExpensifyMono-Bold.woff2') as string)), + MONOSPACE_ITALIC: useTypeface(chartWebFont(require('@assets/fonts/web/ExpensifyMono-Italic.woff2') as string)), + MONOSPACE_BOLD_ITALIC: useTypeface(chartWebFont(require('@assets/fonts/web/ExpensifyMono-BoldItalic.woff2') as string)), + EXP_NEUE: useTypeface(chartWebFont(require('@assets/fonts/web/ExpensifyNeue-Regular.woff2') as string)), + EXP_NEUE_BOLD: useTypeface(chartWebFont(require('@assets/fonts/web/ExpensifyNeue-Bold.woff2') as string)), + EXP_NEUE_ITALIC: useTypeface(chartWebFont(require('@assets/fonts/web/ExpensifyNeue-Italic.woff2') as string)), + EXP_NEUE_BOLD_ITALIC: useTypeface(chartWebFont(require('@assets/fonts/web/ExpensifyNeue-BoldItalic.woff2') as string)), + EXP_NEW_KANSAS_MEDIUM: useTypeface(chartWebFont(require('@assets/fonts/web/ExpensifyNewKansas-Medium.woff2') as string)), + EXP_NEW_KANSAS_MEDIUM_ITALIC: useTypeface(chartWebFont(require('@assets/fonts/web/ExpensifyNewKansas-MediumItalic.woff2') as string)), + CUSTOM_EMOJI_FONT: useTypeface(chartWebFont(require('@assets/fonts/web/CustomEmojiWebFont.ttf') as string)), + }; +} + +export default useChartSkiaTypefaces; diff --git a/src/components/Charts/hooks/index.ts b/src/components/Charts/hooks/index.ts index 988df7608392..f84d2716cbb8 100644 --- a/src/components/Charts/hooks/index.ts +++ b/src/components/Charts/hooks/index.ts @@ -4,8 +4,9 @@ export {default as useChartParagraphs} from './useChartParagraphs'; export {default as useYAxisLabelWidth} from './useYAxisLabelWidth'; export {default as useChartFontManager} from './useChartFontManager/useChartFontManager'; export {default as ChartDefaultTypefaceProvider} from '../context/ChartDefaultTypefaceProvider'; +export {getChartSkiaTypeface} from '../context/getChartSkiaTypeface'; export {useChartDefaultTypeface} from '../context/ChartDefaultTypefaceContext'; -export type {ChartDefaultTypeface} from '../context/ChartDefaultTypefaceContext'; +export type {ChartDefaultTypeface, ChartSkiaTypefaceKey} from '../context/chartSkiaTypefaceTypes'; export {useChartInteractions, TOOLTIP_BAR_GAP} from './useChartInteractions'; export type {HitTestArgs} from './useChartInteractions'; export {default as useChartLabelFormats} from './useChartLabelFormats'; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartLabel.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartLabel.tsx index ee634e570847..d59e52873a6c 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartLabel.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartLabel.tsx @@ -1,10 +1,9 @@ import {Skia, Text as SkText} from '@shopify/react-native-skia'; import type {Color, SkFont} from '@shopify/react-native-skia'; import React from 'react'; -import {useChartDefaultTypeface} from '@components/Charts/hooks'; +import {getChartSkiaTypeface, useChartDefaultTypeface} from '@components/Charts/hooks'; import type {LabelItem} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/types'; import computeTextAnchorPosition from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/computeTextAnchorPosition'; -import getVictoryChartLabelTypeface from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/getVictoryChartLabelTypeface'; type VictoryChartLabelsProps = LabelItem; @@ -21,7 +20,7 @@ type ProcessedLine = { * Renders floating Skia text labels (from `` nodes) over the chart canvas. * Intended for use inside CartesianChart's `renderOutside` callback. */ -function VictoryChartLabel({x, y, text, color, fontSize, fontWeight, fontFamily, lineHeight, textAnchor = 'start', verticalAnchor = 'start'}: VictoryChartLabelsProps) { +function VictoryChartLabel({x, y, text, color, fontSize, fontWeight, fontFamily, fontStyle, lineHeight, textAnchor = 'start', verticalAnchor = 'start'}: VictoryChartLabelsProps) { const typefaces = useChartDefaultTypeface(); const processedLines = text.split('\n').reduce( (acc, line, index) => { @@ -29,8 +28,13 @@ function VictoryChartLabel({x, y, text, color, fontSize, fontWeight, fontFamily, const lineFontSize = fontSize?.[index]; const lineFontWeight = fontWeight?.[index]; const lineFontFamily = fontFamily?.[index]; + const lineFontStyle = fontStyle?.[index]; const lineLineHeight = lineHeight?.[index]; - const typeface = getVictoryChartLabelTypeface(lineFontFamily, lineFontWeight, typefaces); + const typeface = getChartSkiaTypeface(typefaces, { + fontFamily: lineFontFamily, + fontStyle: lineFontStyle, + fontWeight: lineFontWeight, + }); const lineFont = typeface && lineFontSize ? Skia.Font(typeface, lineFontSize) : null; const fontMetrics = lineFont?.getMetrics(); const lineWidth = lineFont?.getGlyphWidths(lineFont.getGlyphIDs(line)).reduce((totalWidth, width) => totalWidth + width, 0) ?? 0; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartLegend.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartLegend.tsx index b7de61bca20f..f9aa0ebccc78 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartLegend.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartLegend.tsx @@ -1,7 +1,7 @@ import {Circle, Skia, Text as SkText} from '@shopify/react-native-skia'; import type {Color, SkFont} from '@shopify/react-native-skia'; import React, {Fragment} from 'react'; -import {useChartDefaultTypeface} from '@components/Charts/hooks'; +import {getChartSkiaTypeface, useChartDefaultTypeface} from '@components/Charts/hooks'; import type {LegendItem} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/types'; type VictoryChartLegendProps = LegendItem; @@ -23,10 +23,10 @@ type ProcessedEntry = { * Intended for use inside CartesianChart's `renderOutside` callback. */ function VictoryChartLegend({x, y, entries, gutter, symbolSpacer}: VictoryChartLegendProps) { - const {regular: regularTypeface, bold: boldTypeface} = useChartDefaultTypeface(); + const typefaces = useChartDefaultTypeface(); const processedEntries = entries.reduce( - (acc, {text, color, fontSize, fontWeight, symbolColor, symbolSize}) => { - const typeface = fontWeight === 'bold' ? boldTypeface : regularTypeface; + (acc, {text, color, fontSize, fontWeight, fontFamily, fontStyle, symbolColor, symbolSize}) => { + const typeface = getChartSkiaTypeface(typefaces, {fontFamily, fontStyle, fontWeight}); const font = typeface && fontSize ? Skia.Font(typeface, fontSize) : null; const fontMetrics = font?.getMetrics(); const lineHeight = fontMetrics ? fontMetrics.ascent + fontMetrics.descent + fontMetrics.leading : 0; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartContext.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartContext.tsx index a6ee93bc96a9..057903ebb03a 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartContext.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartContext.tsx @@ -32,8 +32,8 @@ const VictoryChartContext = createContext(null) * Returns null when the chart data is invalid (no data points, or mixed cartesian/polar content). */ function VictoryChartProvider({tnode, children}: {tnode: TNode; children: React.ReactNode}) { - const {regular: regularTypeface} = useChartDefaultTypeface(); - const {data, xKey, yKeys, xAxis, yAxis, domain, domainPadding, padding, isHorizontal, categories, labelItems, legendItems} = processVictoryChartTree(tnode, regularTypeface, null); + const typefaces = useChartDefaultTypeface(); + const {data, xKey, yKeys, xAxis, yAxis, domain, domainPadding, padding, isHorizontal, categories, labelItems, legendItems} = processVictoryChartTree(tnode, typefaces.EXP_NEUE, null); const {nodeStyles: chartContentStyles, parentNodeStyles: chartContainerStyles} = parseStyles(tnode); const hasCartesianData = Object.values(data).some((entry) => X_KEY in entry); diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/parsers/victoryLabelParser.ts b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/parsers/victoryLabelParser.ts index 1cb115c2d28d..e6e4f9649d79 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/parsers/victoryLabelParser.ts +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/parsers/victoryLabelParser.ts @@ -14,6 +14,7 @@ function parseVictoryLabelNode(tnode: TNode): PartialProcessNodeResult { fontSize: {}, fontWeight: {}, fontFamily: {}, + fontStyle: {}, lineHeight: parseAttribute(tnode.attributes.lineheight), textAnchor: parseAttribute(tnode.attributes.textanchor), verticalAnchor: parseAttribute(tnode.attributes.verticalanchor), @@ -47,6 +48,12 @@ function parseVictoryLabelNode(tnode: TNode): PartialProcessNodeResult { [index]: textStyle.fontFamily, }; } + if (textStyle.fontStyle) { + labelItem.fontStyle = { + ...labelItem.fontStyle, + [index]: textStyle.fontStyle, + }; + } } } diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/parsers/victoryLegendParser.ts b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/parsers/victoryLegendParser.ts index ebc83a9ac9fe..f7cb5bb0cd26 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/parsers/victoryLegendParser.ts +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/parsers/victoryLegendParser.ts @@ -14,11 +14,13 @@ function parseVictoryLegendNode(tnode: TNode): PartialProcessNodeResult { const color = style?.labels?.fill; const fontSize = style?.labels?.fontSize !== undefined ? Number(style.labels.fontSize) : undefined; const fontWeight = Number(style?.labels?.fontWeight) === 700 ? 'bold' : undefined; + const fontFamily = style?.labels?.fontFamily; + const fontStyle = style?.labels?.fontStyle; const entries: LegendItemEntry[] = (parseAttribute(tnode.attributes.data) ?? []).map((entry) => { const text = entry.name; const symbolColor = entry.symbol?.fill; const symbolSize = entry.symbol?.size !== undefined ? Number(entry.symbol.size) : undefined; - return {text, color, fontSize, fontWeight, symbolColor, symbolSize}; + return {text, color, fontSize, fontWeight, fontFamily, fontStyle, symbolColor, symbolSize}; }); const legendItem: LegendItem = {x, y, entries, gutter, symbolSpacer}; return {legendItems: [legendItem]}; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/types.ts b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/types.ts index 0b8007abfd83..4797b16ceb7b 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/types.ts +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/types.ts @@ -37,6 +37,7 @@ type RawLabelStyle = { fontSize?: string | number; fontWeight?: string | number; fontFamily?: string; + fontStyle?: string; }; type RawLegendStyle = { @@ -44,6 +45,8 @@ type RawLegendStyle = { fill?: Color; fontSize?: string | number; fontWeight?: string | number; + fontFamily?: string; + fontStyle?: string; }; }; @@ -85,6 +88,9 @@ type LabelItem = { /** Font family (per line) */ fontFamily?: Record; + /** Font style (per line) */ + fontStyle?: Record; + /** Line height (per line) */ lineHeight?: Record; @@ -108,6 +114,12 @@ type LegendItemEntry = { /** Font weight */ fontWeight?: 'normal' | 'bold'; + /** Font family */ + fontFamily?: string; + + /** Font style */ + fontStyle?: string; + /** The color of the symbol */ symbolColor?: Color; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/getVictoryChartLabelTypeface.ts b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/getVictoryChartLabelTypeface.ts deleted file mode 100644 index 62081790f762..000000000000 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/getVictoryChartLabelTypeface.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type {SkTypeface} from '@shopify/react-native-skia'; -import type {ChartDefaultTypeface} from '@components/Charts/hooks'; - -/** Matches `fontFamily` in Victory HTML from Web-Expensify (e.g. proactive CFO polar charts). */ -const EXPENSIFY_NEW_KANSAS_FONT_FAMILY = 'Expensify New Kansas'; - -function getVictoryChartLabelTypeface(fontFamily: string | undefined, fontWeight: 'normal' | 'bold' | undefined, typefaces: ChartDefaultTypeface): SkTypeface | null { - if (fontFamily === EXPENSIFY_NEW_KANSAS_FONT_FAMILY) { - return typefaces.kansas; - } - - return fontWeight === 'bold' ? typefaces.bold : typefaces.regular; -} - -export default getVictoryChartLabelTypeface; From 84b809bd3955b64a6ad0f4f88adbcb9722b600b3 Mon Sep 17 00:00:00 2001 From: rory Date: Wed, 3 Jun 2026 09:18:22 -0700 Subject: [PATCH 10/35] Fix VictoryChartPieLabel Skia context usage Precompute pie label config in VictoryChartPie so slice labels do not call useVictoryChartContext inside the Skia reconciler. Co-authored-by: Cursor --- .../components/VictoryChartPie.tsx | 24 +++++++++- .../components/VictoryChartPieLabel.tsx | 48 +++++++++---------- 2 files changed, 44 insertions(+), 28 deletions(-) diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartPie.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartPie.tsx index 0843a623ce02..4d9d9612f3f0 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartPie.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartPie.tsx @@ -1,7 +1,12 @@ -import React from 'react'; +import React, {useMemo} from 'react'; +import {useAmbientTRenderEngine} from 'react-native-render-html'; import type {TNode} from 'react-native-render-html'; import {Pie} from 'victory-native'; +import {useVictoryChartContext} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartContext'; +import parseVictoryLabelNode from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/parsers/victoryLabelParser'; +import type {PolarChartData} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/types'; import parseAttribute from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/parseAttribute'; +import parseComponent from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/parseComponent'; import VictoryChartPieLabel from './VictoryChartPieLabel'; type VictoryChartPieProps = {tnode: TNode}; @@ -10,9 +15,21 @@ type VictoryChartPieProps = {tnode: TNode}; const START_ANGLE = 270; function VictoryChartPie({tnode}: VictoryChartPieProps) { + const {data} = useVictoryChartContext(); + const renderEngine = useAmbientTRenderEngine(); const innerRadius = tnode.attributes.innerradius !== undefined ? Number(parseAttribute(tnode.attributes.innerradius)) : undefined; const radius = tnode.attributes.radius !== undefined ? Number(parseAttribute(tnode.attributes.radius)) : undefined; const size = radius ? radius * 2 : undefined; + const labelRadius = tnode.attributes.labelradius !== undefined ? Number(parseAttribute(tnode.attributes.labelradius)) : undefined; + + const pieLabelConfig = useMemo(() => { + const labelComponentNode = parseComponent(tnode.attributes.labelcomponent, renderEngine, 'victorylabel'); + const labelItemTemplate = labelComponentNode ? parseVictoryLabelNode(labelComponentNode).labelItems?.at(0) : undefined; + const dataLabels = Object.values(data).map((entry) => (entry as PolarChartData).label); + const labels = parseAttribute(tnode.attributes.labels); + + return {labelItemTemplate, dataLabels, labels}; + }, [data, renderEngine, tnode.attributes.labelcomponent, tnode.attributes.labels]); return ( ( )} diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartPieLabel.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartPieLabel.tsx index a9ff0dd3ee67..9b0e6178e474 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartPieLabel.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartPieLabel.tsx @@ -1,44 +1,40 @@ import React from 'react'; -import {useAmbientTRenderEngine} from 'react-native-render-html'; -import type {TNode} from 'react-native-render-html'; import type {PieSliceData} from 'victory-native'; -import {useVictoryChartContext} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartContext'; -import parseVictoryLabelNode from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/parsers/victoryLabelParser'; -import type {PolarChartData} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/types'; -import parseAttribute from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/parseAttribute'; -import parseComponent from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/parseComponent'; +import type {LabelItem} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/types'; import VictoryChartLabel from './VictoryChartLabel'; type VictoryChartPieLabelProps = { - tnode: TNode; slice: PieSliceData; + labelItemTemplate?: LabelItem; + dataLabels: Array; + labels?: string[]; + labelRadius?: number; }; const RADIAN = Math.PI / 180; -function VictoryChartPieLabel({tnode, slice}: VictoryChartPieLabelProps) { - const {data} = useVictoryChartContext(); - const renderEngine = useAmbientTRenderEngine(); - const labelComponentNode = parseComponent(tnode.attributes.labelcomponent, renderEngine, 'victorylabel'); - const labelItem = labelComponentNode ? parseVictoryLabelNode(labelComponentNode).labelItems?.at(0) : undefined; - - if (!labelItem) { +/** + * Renders a pie slice label inside the Skia canvas. Must not use React context hooks + * because this runs in victory-native's pie slice render callback. + */ +function VictoryChartPieLabel({slice, labelItemTemplate, dataLabels, labels, labelRadius}: VictoryChartPieLabelProps) { + if (!labelItemTemplate) { return null; } - const dataLabels = Object.values(data).map((entry) => (entry as PolarChartData).label); - const labels = parseAttribute(tnode.attributes.labels); const text = labels?.[dataLabels.indexOf(slice.label)] ?? slice.label; - - const labelRadius = tnode.attributes.labelradius !== undefined ? Number(parseAttribute(tnode.attributes.labelradius)) : slice.radius; + const resolvedLabelRadius = labelRadius ?? slice.radius; const midAngle = (slice.startAngle + slice.endAngle) / 2; - const x = slice.center.x + labelRadius * Math.cos(-midAngle * RADIAN); - const y = slice.center.y + labelRadius * Math.sin(midAngle * RADIAN); - - labelItem.text = text; - labelItem.x = x; - labelItem.y = y; - labelItem.textAnchor = 'middle'; + const x = slice.center.x + resolvedLabelRadius * Math.cos(-midAngle * RADIAN); + const y = slice.center.y + resolvedLabelRadius * Math.sin(midAngle * RADIAN); + + const labelItem: LabelItem = { + ...labelItemTemplate, + text, + x, + y, + textAnchor: 'middle', + }; return ; } From bf9370aecdabf3fcb32d519f12d75f17611b1315 Mon Sep 17 00:00:00 2001 From: rory Date: Wed, 3 Jun 2026 09:23:29 -0700 Subject: [PATCH 11/35] Remove unused chart typeface type exports for knip Co-authored-by: Cursor --- src/components/Charts/context/getChartSkiaTypeface.ts | 1 - src/components/Charts/hooks/index.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/src/components/Charts/context/getChartSkiaTypeface.ts b/src/components/Charts/context/getChartSkiaTypeface.ts index eecf10edfa9e..b26eda1cc977 100644 --- a/src/components/Charts/context/getChartSkiaTypeface.ts +++ b/src/components/Charts/context/getChartSkiaTypeface.ts @@ -56,4 +56,3 @@ function getChartSkiaTypeface( } export {getChartSkiaTypeface}; -export type {ChartLabelFontStyle, ChartLabelFontWeight}; diff --git a/src/components/Charts/hooks/index.ts b/src/components/Charts/hooks/index.ts index f84d2716cbb8..26ea730a2346 100644 --- a/src/components/Charts/hooks/index.ts +++ b/src/components/Charts/hooks/index.ts @@ -6,7 +6,6 @@ export {default as useChartFontManager} from './useChartFontManager/useChartFont export {default as ChartDefaultTypefaceProvider} from '../context/ChartDefaultTypefaceProvider'; export {getChartSkiaTypeface} from '../context/getChartSkiaTypeface'; export {useChartDefaultTypeface} from '../context/ChartDefaultTypefaceContext'; -export type {ChartDefaultTypeface, ChartSkiaTypefaceKey} from '../context/chartSkiaTypefaceTypes'; export {useChartInteractions, TOOLTIP_BAR_GAP} from './useChartInteractions'; export type {HitTestArgs} from './useChartInteractions'; export {default as useChartLabelFormats} from './useChartLabelFormats'; From 363c4bdf39c7e5b8cd965331846a62f6359783fb Mon Sep 17 00:00:00 2001 From: rory Date: Wed, 3 Jun 2026 09:30:27 -0700 Subject: [PATCH 12/35] Reorganize Charts module into utils, types, and hooks folders. Move chart helpers out of context so providers only own React context wiring. Co-authored-by: Cursor --- src/components/Charts/context/ChartDefaultTypefaceContext.tsx | 2 +- .../Charts/context/ChartDefaultTypefaceProvider.native.tsx | 2 +- .../Charts/context/ChartDefaultTypefaceProvider.tsx | 2 +- src/components/Charts/hooks/index.ts | 2 +- .../Charts/hooks/useChartFontManager/useChartFontManager.ts | 2 +- .../Charts/{context => hooks}/useChartSkiaTypefaces.native.ts | 2 +- .../Charts/{context => hooks}/useChartSkiaTypefaces.ts | 4 ++-- .../Charts/{context => types}/chartSkiaTypefaceTypes.ts | 0 src/components/Charts/{types.ts => types/index.ts} | 2 +- src/components/Charts/{context => utils}/chartWebFont.ts | 0 .../Charts/{context => utils}/getChartSkiaTypeface.ts | 2 +- src/components/Charts/{utils.ts => utils/index.ts} | 4 ++-- 12 files changed, 12 insertions(+), 12 deletions(-) rename src/components/Charts/{context => hooks}/useChartSkiaTypefaces.native.ts (95%) rename src/components/Charts/{context => hooks}/useChartSkiaTypefaces.ts (92%) rename src/components/Charts/{context => types}/chartSkiaTypefaceTypes.ts (100%) rename src/components/Charts/{types.ts => types/index.ts} (97%) rename src/components/Charts/{context => utils}/chartWebFont.ts (100%) rename src/components/Charts/{context => utils}/getChartSkiaTypeface.ts (98%) rename src/components/Charts/{utils.ts => utils/index.ts} (99%) diff --git a/src/components/Charts/context/ChartDefaultTypefaceContext.tsx b/src/components/Charts/context/ChartDefaultTypefaceContext.tsx index 6a0e783d8354..de54a4b00468 100644 --- a/src/components/Charts/context/ChartDefaultTypefaceContext.tsx +++ b/src/components/Charts/context/ChartDefaultTypefaceContext.tsx @@ -1,5 +1,5 @@ import {createContext, useContext} from 'react'; -import type {ChartDefaultTypeface} from './chartSkiaTypefaceTypes'; +import type {ChartDefaultTypeface} from '../types/chartSkiaTypefaceTypes'; const ChartDefaultTypefaceContext = createContext(null); diff --git a/src/components/Charts/context/ChartDefaultTypefaceProvider.native.tsx b/src/components/Charts/context/ChartDefaultTypefaceProvider.native.tsx index 0a7384e5a622..29e1010b1e04 100644 --- a/src/components/Charts/context/ChartDefaultTypefaceProvider.native.tsx +++ b/src/components/Charts/context/ChartDefaultTypefaceProvider.native.tsx @@ -1,6 +1,6 @@ import React from 'react'; +import useChartSkiaTypefaces from '../hooks/useChartSkiaTypefaces'; import {ChartDefaultTypefaceContext} from './ChartDefaultTypefaceContext'; -import useChartSkiaTypefaces from './useChartSkiaTypefaces'; function ChartDefaultTypefaceProvider({children}: {children: React.ReactNode}) { const typefaces = useChartSkiaTypefaces(); diff --git a/src/components/Charts/context/ChartDefaultTypefaceProvider.tsx b/src/components/Charts/context/ChartDefaultTypefaceProvider.tsx index 0a7384e5a622..29e1010b1e04 100644 --- a/src/components/Charts/context/ChartDefaultTypefaceProvider.tsx +++ b/src/components/Charts/context/ChartDefaultTypefaceProvider.tsx @@ -1,6 +1,6 @@ import React from 'react'; +import useChartSkiaTypefaces from '../hooks/useChartSkiaTypefaces'; import {ChartDefaultTypefaceContext} from './ChartDefaultTypefaceContext'; -import useChartSkiaTypefaces from './useChartSkiaTypefaces'; function ChartDefaultTypefaceProvider({children}: {children: React.ReactNode}) { const typefaces = useChartSkiaTypefaces(); diff --git a/src/components/Charts/hooks/index.ts b/src/components/Charts/hooks/index.ts index 26ea730a2346..f2dc08caf82d 100644 --- a/src/components/Charts/hooks/index.ts +++ b/src/components/Charts/hooks/index.ts @@ -4,7 +4,7 @@ export {default as useChartParagraphs} from './useChartParagraphs'; export {default as useYAxisLabelWidth} from './useYAxisLabelWidth'; export {default as useChartFontManager} from './useChartFontManager/useChartFontManager'; export {default as ChartDefaultTypefaceProvider} from '../context/ChartDefaultTypefaceProvider'; -export {getChartSkiaTypeface} from '../context/getChartSkiaTypeface'; +export {getChartSkiaTypeface} from '../utils/getChartSkiaTypeface'; export {useChartDefaultTypeface} from '../context/ChartDefaultTypefaceContext'; export {useChartInteractions, TOOLTIP_BAR_GAP} from './useChartInteractions'; export type {HitTestArgs} from './useChartInteractions'; diff --git a/src/components/Charts/hooks/useChartFontManager/useChartFontManager.ts b/src/components/Charts/hooks/useChartFontManager/useChartFontManager.ts index f43896f3fb14..3b11301f4646 100644 --- a/src/components/Charts/hooks/useChartFontManager/useChartFontManager.ts +++ b/src/components/Charts/hooks/useChartFontManager/useChartFontManager.ts @@ -1,6 +1,6 @@ import type {SkTypefaceFontProvider} from '@shopify/react-native-skia'; import {useFonts} from '@shopify/react-native-skia'; -import chartWebFont from '@components/Charts/context/chartWebFont'; +import chartWebFont from '@components/Charts/utils/chartWebFont'; function useChartFontManager(): SkTypefaceFontProvider | null { return useFonts({ diff --git a/src/components/Charts/context/useChartSkiaTypefaces.native.ts b/src/components/Charts/hooks/useChartSkiaTypefaces.native.ts similarity index 95% rename from src/components/Charts/context/useChartSkiaTypefaces.native.ts rename to src/components/Charts/hooks/useChartSkiaTypefaces.native.ts index 9f47af1236fd..551b095ec4d6 100644 --- a/src/components/Charts/context/useChartSkiaTypefaces.native.ts +++ b/src/components/Charts/hooks/useChartSkiaTypefaces.native.ts @@ -1,6 +1,6 @@ import type {DataModule} from '@shopify/react-native-skia'; import {useTypeface} from '@shopify/react-native-skia'; -import type {ChartDefaultTypeface} from './chartSkiaTypefaceTypes'; +import type {ChartDefaultTypeface} from '../types/chartSkiaTypefaceTypes'; function useChartSkiaTypefaces(): ChartDefaultTypeface { return { diff --git a/src/components/Charts/context/useChartSkiaTypefaces.ts b/src/components/Charts/hooks/useChartSkiaTypefaces.ts similarity index 92% rename from src/components/Charts/context/useChartSkiaTypefaces.ts rename to src/components/Charts/hooks/useChartSkiaTypefaces.ts index 1125675aa155..16f6357cf5e4 100644 --- a/src/components/Charts/context/useChartSkiaTypefaces.ts +++ b/src/components/Charts/hooks/useChartSkiaTypefaces.ts @@ -1,6 +1,6 @@ import {useTypeface} from '@shopify/react-native-skia'; -import type {ChartDefaultTypeface} from './chartSkiaTypefaceTypes'; -import chartWebFont from './chartWebFont'; +import type {ChartDefaultTypeface} from '../types/chartSkiaTypefaceTypes'; +import chartWebFont from '../utils/chartWebFont'; function useChartSkiaTypefaces(): ChartDefaultTypeface { return { diff --git a/src/components/Charts/context/chartSkiaTypefaceTypes.ts b/src/components/Charts/types/chartSkiaTypefaceTypes.ts similarity index 100% rename from src/components/Charts/context/chartSkiaTypefaceTypes.ts rename to src/components/Charts/types/chartSkiaTypefaceTypes.ts diff --git a/src/components/Charts/types.ts b/src/components/Charts/types/index.ts similarity index 97% rename from src/components/Charts/types.ts rename to src/components/Charts/types/index.ts index 2ed86d19ec06..dc3de15d5bb8 100644 --- a/src/components/Charts/types.ts +++ b/src/components/Charts/types/index.ts @@ -1,6 +1,6 @@ import type {SkParagraph} from '@shopify/react-native-skia'; import type {ValueOf} from 'type-fest'; -import type {LABEL_ROTATIONS} from './VictoryTheme'; +import type {LABEL_ROTATIONS} from '../VictoryTheme'; type ChartDataPoint = { /** Label displayed under the data point (e.g., "Amazon", "Nov 2025") */ diff --git a/src/components/Charts/context/chartWebFont.ts b/src/components/Charts/utils/chartWebFont.ts similarity index 100% rename from src/components/Charts/context/chartWebFont.ts rename to src/components/Charts/utils/chartWebFont.ts diff --git a/src/components/Charts/context/getChartSkiaTypeface.ts b/src/components/Charts/utils/getChartSkiaTypeface.ts similarity index 98% rename from src/components/Charts/context/getChartSkiaTypeface.ts rename to src/components/Charts/utils/getChartSkiaTypeface.ts index b26eda1cc977..57a96179c9f3 100644 --- a/src/components/Charts/context/getChartSkiaTypeface.ts +++ b/src/components/Charts/utils/getChartSkiaTypeface.ts @@ -2,7 +2,7 @@ import type {SkTypeface} from '@shopify/react-native-skia'; import type {TextStyle} from 'react-native'; // eslint-disable-next-line no-restricted-imports import singleFontFamily from '@styles/utils/FontUtils/fontFamily/singleFontFamily'; -import type {ChartDefaultTypeface, ChartSkiaTypefaceKey} from './chartSkiaTypefaceTypes'; +import type {ChartDefaultTypeface, ChartSkiaTypefaceKey} from '../types/chartSkiaTypefaceTypes'; type ChartLabelFontStyle = 'normal' | 'italic'; type ChartLabelFontWeight = 'normal' | 'bold'; diff --git a/src/components/Charts/utils.ts b/src/components/Charts/utils/index.ts similarity index 99% rename from src/components/Charts/utils.ts rename to src/components/Charts/utils/index.ts index 6db9a4455453..6990ddc4ba95 100644 --- a/src/components/Charts/utils.ts +++ b/src/components/Charts/utils/index.ts @@ -1,8 +1,8 @@ import {FontStyle, FontWeight, Skia} from '@shopify/react-native-skia'; import type {SkParagraph, SkParagraphBuilder, SkTypefaceFontProvider} from '@shopify/react-native-skia'; import variables from '@styles/variables'; -import type {ChartDataPoint, LabelRotation, PieSlice} from './types'; -import VictoryTheme, {DIAGONAL_ANGLE_RADIAN_THRESHOLD, ELLIPSIS, LABEL_PADDING, LABEL_ROTATIONS, MAX_X_AXIS_LABEL_WIDTH, SIN_45} from './VictoryTheme'; +import type {ChartDataPoint, LabelRotation, PieSlice} from '../types'; +import VictoryTheme, {DIAGONAL_ANGLE_RADIAN_THRESHOLD, ELLIPSIS, LABEL_PADDING, LABEL_ROTATIONS, MAX_X_AXIS_LABEL_WIDTH, SIN_45} from '../VictoryTheme'; /** One reusable ParagraphBuilder per fontMgr instance. Auto-GC'd when fontMgr is released. */ const builderCache = new WeakMap(); From db3b35b593d6b89a93e4e8a88248604dd93cdd34 Mon Sep 17 00:00:00 2001 From: rory Date: Wed, 3 Jun 2026 09:35:58 -0700 Subject: [PATCH 13/35] Fix Charts import aliases and default export for lint Use @components paths per prefer-alias and default-export getChartSkiaTypeface. Co-authored-by: Cursor --- .../Charts/context/ChartDefaultTypefaceContext.tsx | 2 +- .../Charts/context/ChartDefaultTypefaceProvider.native.tsx | 2 +- .../Charts/context/ChartDefaultTypefaceProvider.tsx | 2 +- src/components/Charts/hooks/index.ts | 6 +++--- src/components/Charts/hooks/useChartSkiaTypefaces.native.ts | 2 +- src/components/Charts/hooks/useChartSkiaTypefaces.ts | 4 ++-- src/components/Charts/types/index.ts | 2 +- src/components/Charts/utils/getChartSkiaTypeface.ts | 4 ++-- src/components/Charts/utils/index.ts | 4 ++-- 9 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/components/Charts/context/ChartDefaultTypefaceContext.tsx b/src/components/Charts/context/ChartDefaultTypefaceContext.tsx index de54a4b00468..6ffdb6ac6037 100644 --- a/src/components/Charts/context/ChartDefaultTypefaceContext.tsx +++ b/src/components/Charts/context/ChartDefaultTypefaceContext.tsx @@ -1,5 +1,5 @@ import {createContext, useContext} from 'react'; -import type {ChartDefaultTypeface} from '../types/chartSkiaTypefaceTypes'; +import type {ChartDefaultTypeface} from '@components/Charts/types/chartSkiaTypefaceTypes'; const ChartDefaultTypefaceContext = createContext(null); diff --git a/src/components/Charts/context/ChartDefaultTypefaceProvider.native.tsx b/src/components/Charts/context/ChartDefaultTypefaceProvider.native.tsx index 29e1010b1e04..a7fbfdc14df0 100644 --- a/src/components/Charts/context/ChartDefaultTypefaceProvider.native.tsx +++ b/src/components/Charts/context/ChartDefaultTypefaceProvider.native.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import useChartSkiaTypefaces from '../hooks/useChartSkiaTypefaces'; +import useChartSkiaTypefaces from '@components/Charts/hooks/useChartSkiaTypefaces'; import {ChartDefaultTypefaceContext} from './ChartDefaultTypefaceContext'; function ChartDefaultTypefaceProvider({children}: {children: React.ReactNode}) { diff --git a/src/components/Charts/context/ChartDefaultTypefaceProvider.tsx b/src/components/Charts/context/ChartDefaultTypefaceProvider.tsx index 29e1010b1e04..a7fbfdc14df0 100644 --- a/src/components/Charts/context/ChartDefaultTypefaceProvider.tsx +++ b/src/components/Charts/context/ChartDefaultTypefaceProvider.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import useChartSkiaTypefaces from '../hooks/useChartSkiaTypefaces'; +import useChartSkiaTypefaces from '@components/Charts/hooks/useChartSkiaTypefaces'; import {ChartDefaultTypefaceContext} from './ChartDefaultTypefaceContext'; function ChartDefaultTypefaceProvider({children}: {children: React.ReactNode}) { diff --git a/src/components/Charts/hooks/index.ts b/src/components/Charts/hooks/index.ts index f2dc08caf82d..3c9ebaaf64e9 100644 --- a/src/components/Charts/hooks/index.ts +++ b/src/components/Charts/hooks/index.ts @@ -3,9 +3,9 @@ export {default as useChartLabelMeasurements} from './useChartLabelMeasurements' export {default as useChartParagraphs} from './useChartParagraphs'; export {default as useYAxisLabelWidth} from './useYAxisLabelWidth'; export {default as useChartFontManager} from './useChartFontManager/useChartFontManager'; -export {default as ChartDefaultTypefaceProvider} from '../context/ChartDefaultTypefaceProvider'; -export {getChartSkiaTypeface} from '../utils/getChartSkiaTypeface'; -export {useChartDefaultTypeface} from '../context/ChartDefaultTypefaceContext'; +export {default as ChartDefaultTypefaceProvider} from '@components/Charts/context/ChartDefaultTypefaceProvider'; +export {default as getChartSkiaTypeface} from '@components/Charts/utils/getChartSkiaTypeface'; +export {useChartDefaultTypeface} from '@components/Charts/context/ChartDefaultTypefaceContext'; export {useChartInteractions, TOOLTIP_BAR_GAP} from './useChartInteractions'; export type {HitTestArgs} from './useChartInteractions'; export {default as useChartLabelFormats} from './useChartLabelFormats'; diff --git a/src/components/Charts/hooks/useChartSkiaTypefaces.native.ts b/src/components/Charts/hooks/useChartSkiaTypefaces.native.ts index 551b095ec4d6..f74cbfb96189 100644 --- a/src/components/Charts/hooks/useChartSkiaTypefaces.native.ts +++ b/src/components/Charts/hooks/useChartSkiaTypefaces.native.ts @@ -1,6 +1,6 @@ import type {DataModule} from '@shopify/react-native-skia'; import {useTypeface} from '@shopify/react-native-skia'; -import type {ChartDefaultTypeface} from '../types/chartSkiaTypefaceTypes'; +import type {ChartDefaultTypeface} from '@components/Charts/types/chartSkiaTypefaceTypes'; function useChartSkiaTypefaces(): ChartDefaultTypeface { return { diff --git a/src/components/Charts/hooks/useChartSkiaTypefaces.ts b/src/components/Charts/hooks/useChartSkiaTypefaces.ts index 16f6357cf5e4..aa95fe01dac0 100644 --- a/src/components/Charts/hooks/useChartSkiaTypefaces.ts +++ b/src/components/Charts/hooks/useChartSkiaTypefaces.ts @@ -1,6 +1,6 @@ import {useTypeface} from '@shopify/react-native-skia'; -import type {ChartDefaultTypeface} from '../types/chartSkiaTypefaceTypes'; -import chartWebFont from '../utils/chartWebFont'; +import type {ChartDefaultTypeface} from '@components/Charts/types/chartSkiaTypefaceTypes'; +import chartWebFont from '@components/Charts/utils/chartWebFont'; function useChartSkiaTypefaces(): ChartDefaultTypeface { return { diff --git a/src/components/Charts/types/index.ts b/src/components/Charts/types/index.ts index dc3de15d5bb8..b44954eb31e4 100644 --- a/src/components/Charts/types/index.ts +++ b/src/components/Charts/types/index.ts @@ -1,6 +1,6 @@ import type {SkParagraph} from '@shopify/react-native-skia'; import type {ValueOf} from 'type-fest'; -import type {LABEL_ROTATIONS} from '../VictoryTheme'; +import type {LABEL_ROTATIONS} from '@components/Charts/VictoryTheme'; type ChartDataPoint = { /** Label displayed under the data point (e.g., "Amazon", "Nov 2025") */ diff --git a/src/components/Charts/utils/getChartSkiaTypeface.ts b/src/components/Charts/utils/getChartSkiaTypeface.ts index 57a96179c9f3..4b62a0c34a2c 100644 --- a/src/components/Charts/utils/getChartSkiaTypeface.ts +++ b/src/components/Charts/utils/getChartSkiaTypeface.ts @@ -1,8 +1,8 @@ import type {SkTypeface} from '@shopify/react-native-skia'; import type {TextStyle} from 'react-native'; +import type {ChartDefaultTypeface, ChartSkiaTypefaceKey} from '@components/Charts/types/chartSkiaTypefaceTypes'; // eslint-disable-next-line no-restricted-imports import singleFontFamily from '@styles/utils/FontUtils/fontFamily/singleFontFamily'; -import type {ChartDefaultTypeface, ChartSkiaTypefaceKey} from '../types/chartSkiaTypefaceTypes'; type ChartLabelFontStyle = 'normal' | 'italic'; type ChartLabelFontWeight = 'normal' | 'bold'; @@ -55,4 +55,4 @@ function getChartSkiaTypeface( return typefaces[typefaceKey]; } -export {getChartSkiaTypeface}; +export default getChartSkiaTypeface; diff --git a/src/components/Charts/utils/index.ts b/src/components/Charts/utils/index.ts index 6990ddc4ba95..9fa433e9e6c6 100644 --- a/src/components/Charts/utils/index.ts +++ b/src/components/Charts/utils/index.ts @@ -1,8 +1,8 @@ import {FontStyle, FontWeight, Skia} from '@shopify/react-native-skia'; import type {SkParagraph, SkParagraphBuilder, SkTypefaceFontProvider} from '@shopify/react-native-skia'; +import type {ChartDataPoint, LabelRotation, PieSlice} from '@components/Charts/types'; +import VictoryTheme, {DIAGONAL_ANGLE_RADIAN_THRESHOLD, ELLIPSIS, LABEL_PADDING, LABEL_ROTATIONS, MAX_X_AXIS_LABEL_WIDTH, SIN_45} from '@components/Charts/VictoryTheme'; import variables from '@styles/variables'; -import type {ChartDataPoint, LabelRotation, PieSlice} from '../types'; -import VictoryTheme, {DIAGONAL_ANGLE_RADIAN_THRESHOLD, ELLIPSIS, LABEL_PADDING, LABEL_ROTATIONS, MAX_X_AXIS_LABEL_WIDTH, SIN_45} from '../VictoryTheme'; /** One reusable ParagraphBuilder per fontMgr instance. Auto-GC'd when fontMgr is released. */ const builderCache = new WeakMap(); From 0ec3e1596941c0301a2985f632d18ab33b6daf8e Mon Sep 17 00:00:00 2001 From: rory Date: Wed, 3 Jun 2026 10:50:16 -0700 Subject: [PATCH 14/35] chore(charts): add chart font registry and loader cache Centralize font asset paths and single-flight Skia loading so nested chart providers share one decoded fontMgr and typeface map. Co-authored-by: Cursor --- .../Charts/types/chartFontsTypes.ts | 11 ++ .../Charts/utils/chartFontAssets.native.ts | 40 ++++++ .../Charts/utils/chartFontAssets.ts | 41 ++++++ .../Charts/utils/chartFontsCache.ts | 120 ++++++++++++++++++ .../Charts/utils/createChartFontsValue.ts | 18 +++ 5 files changed, 230 insertions(+) create mode 100644 src/components/Charts/types/chartFontsTypes.ts create mode 100644 src/components/Charts/utils/chartFontAssets.native.ts create mode 100644 src/components/Charts/utils/chartFontAssets.ts create mode 100644 src/components/Charts/utils/chartFontsCache.ts create mode 100644 src/components/Charts/utils/createChartFontsValue.ts diff --git a/src/components/Charts/types/chartFontsTypes.ts b/src/components/Charts/types/chartFontsTypes.ts new file mode 100644 index 000000000000..eb55f5ad8c38 --- /dev/null +++ b/src/components/Charts/types/chartFontsTypes.ts @@ -0,0 +1,11 @@ +import type {SkTypefaceFontProvider} from '@shopify/react-native-skia'; +import type {ChartDefaultTypeface} from './chartSkiaTypefaceTypes'; + +type ChartFontsValue = { + typefaces: ChartDefaultTypeface; + fontMgr: SkTypefaceFontProvider | null; +}; + +type ChartFontsContextValue = ChartFontsValue; + +export type {ChartFontsContextValue, ChartFontsValue}; diff --git a/src/components/Charts/utils/chartFontAssets.native.ts b/src/components/Charts/utils/chartFontAssets.native.ts new file mode 100644 index 000000000000..ba261225a1b4 --- /dev/null +++ b/src/components/Charts/utils/chartFontAssets.native.ts @@ -0,0 +1,40 @@ +import type {DataModule} from '@shopify/react-native-skia'; +import type {ChartSkiaTypefaceKey} from '@components/Charts/types/chartSkiaTypefaceTypes'; + +const EXPENSIFY_MONO_REGULAR = require('@assets/fonts/native/ExpensifyMono-Regular.otf') as DataModule; +const EXPENSIFY_MONO_BOLD = require('@assets/fonts/native/ExpensifyMono-Bold.otf') as DataModule; +const EXPENSIFY_MONO_ITALIC = require('@assets/fonts/native/ExpensifyMono-Italic.otf') as DataModule; +const EXPENSIFY_MONO_BOLD_ITALIC = require('@assets/fonts/native/ExpensifyMono-BoldItalic.otf') as DataModule; +const EXPENSIFY_NEUE_REGULAR = require('@assets/fonts/native/ExpensifyNeue-Regular.otf') as DataModule; +const EXPENSIFY_NEUE_BOLD = require('@assets/fonts/native/ExpensifyNeue-Bold.otf') as DataModule; +const EXPENSIFY_NEUE_ITALIC = require('@assets/fonts/native/ExpensifyNeue-Italic.otf') as DataModule; +const EXPENSIFY_NEUE_BOLD_ITALIC = require('@assets/fonts/native/ExpensifyNeue-BoldItalic.otf') as DataModule; +const EXPENSIFY_NEW_KANSAS_MEDIUM = require('@assets/fonts/native/ExpensifyNewKansas-Medium.otf') as DataModule; +const EXPENSIFY_NEW_KANSAS_MEDIUM_ITALIC = require('@assets/fonts/native/ExpensifyNewKansas-MediumItalic.otf') as DataModule; +const CUSTOM_EMOJI_FONT = require('@assets/fonts/native/CustomEmojiNativeFont.ttf') as DataModule; +const SANS_SYMBOLS_FONT = require('@assets/fonts/NotoSans-Symbols.ttf') as DataModule; +const SANS_SC_MONTHS_FONT = require('@assets/fonts/NotoSansSC-Months.ttf') as DataModule; + +const CHART_SKIA_TYPEFACE_ASSETS: Record = { + MONOSPACE: EXPENSIFY_MONO_REGULAR, + MONOSPACE_BOLD: EXPENSIFY_MONO_BOLD, + MONOSPACE_ITALIC: EXPENSIFY_MONO_ITALIC, + MONOSPACE_BOLD_ITALIC: EXPENSIFY_MONO_BOLD_ITALIC, + EXP_NEUE: EXPENSIFY_NEUE_REGULAR, + EXP_NEUE_BOLD: EXPENSIFY_NEUE_BOLD, + EXP_NEUE_ITALIC: EXPENSIFY_NEUE_ITALIC, + EXP_NEUE_BOLD_ITALIC: EXPENSIFY_NEUE_BOLD_ITALIC, + EXP_NEW_KANSAS_MEDIUM: EXPENSIFY_NEW_KANSAS_MEDIUM, + EXP_NEW_KANSAS_MEDIUM_ITALIC: EXPENSIFY_NEW_KANSAS_MEDIUM_ITALIC, + CUSTOM_EMOJI_FONT, +}; + +const CHART_FONT_MANAGER_FAMILIES: Record = { + ExpensifyNeue: [EXPENSIFY_NEUE_REGULAR, EXPENSIFY_NEUE_BOLD, EXPENSIFY_NEUE_ITALIC, EXPENSIFY_NEUE_BOLD_ITALIC], + ExpensifyMono: [EXPENSIFY_MONO_REGULAR, EXPENSIFY_MONO_BOLD, EXPENSIFY_MONO_ITALIC, EXPENSIFY_MONO_BOLD_ITALIC], + ExpensifyNewKansas: [EXPENSIFY_NEW_KANSAS_MEDIUM, EXPENSIFY_NEW_KANSAS_MEDIUM_ITALIC], + NotoSansSymbols: [SANS_SYMBOLS_FONT], + NotoSansSCMonths: [SANS_SC_MONTHS_FONT], +}; + +export {CHART_FONT_MANAGER_FAMILIES, CHART_SKIA_TYPEFACE_ASSETS}; diff --git a/src/components/Charts/utils/chartFontAssets.ts b/src/components/Charts/utils/chartFontAssets.ts new file mode 100644 index 000000000000..26be32f9bbd9 --- /dev/null +++ b/src/components/Charts/utils/chartFontAssets.ts @@ -0,0 +1,41 @@ +import type {DataModule} from '@shopify/react-native-skia'; +import type {ChartSkiaTypefaceKey} from '@components/Charts/types/chartSkiaTypefaceTypes'; +import chartWebFont from './chartWebFont'; + +const EXPENSIFY_MONO_REGULAR = chartWebFont(require('@assets/fonts/web/ExpensifyMono-Regular.woff2') as string); +const EXPENSIFY_MONO_BOLD = chartWebFont(require('@assets/fonts/web/ExpensifyMono-Bold.woff2') as string); +const EXPENSIFY_MONO_ITALIC = chartWebFont(require('@assets/fonts/web/ExpensifyMono-Italic.woff2') as string); +const EXPENSIFY_MONO_BOLD_ITALIC = chartWebFont(require('@assets/fonts/web/ExpensifyMono-BoldItalic.woff2') as string); +const EXPENSIFY_NEUE_REGULAR = chartWebFont(require('@assets/fonts/web/ExpensifyNeue-Regular.woff2') as string); +const EXPENSIFY_NEUE_BOLD = chartWebFont(require('@assets/fonts/web/ExpensifyNeue-Bold.woff2') as string); +const EXPENSIFY_NEUE_ITALIC = chartWebFont(require('@assets/fonts/web/ExpensifyNeue-Italic.woff2') as string); +const EXPENSIFY_NEUE_BOLD_ITALIC = chartWebFont(require('@assets/fonts/web/ExpensifyNeue-BoldItalic.woff2') as string); +const EXPENSIFY_NEW_KANSAS_MEDIUM = chartWebFont(require('@assets/fonts/web/ExpensifyNewKansas-Medium.woff2') as string); +const EXPENSIFY_NEW_KANSAS_MEDIUM_ITALIC = chartWebFont(require('@assets/fonts/web/ExpensifyNewKansas-MediumItalic.woff2') as string); +const CUSTOM_EMOJI_FONT = chartWebFont(require('@assets/fonts/web/CustomEmojiWebFont.ttf') as string); +const SANS_SYMBOLS_FONT = chartWebFont(require('@assets/fonts/NotoSans-Symbols.ttf') as string); +const SANS_SC_MONTHS_FONT = chartWebFont(require('@assets/fonts/NotoSansSC-Months.ttf') as string); + +const CHART_SKIA_TYPEFACE_ASSETS: Record = { + MONOSPACE: EXPENSIFY_MONO_REGULAR, + MONOSPACE_BOLD: EXPENSIFY_MONO_BOLD, + MONOSPACE_ITALIC: EXPENSIFY_MONO_ITALIC, + MONOSPACE_BOLD_ITALIC: EXPENSIFY_MONO_BOLD_ITALIC, + EXP_NEUE: EXPENSIFY_NEUE_REGULAR, + EXP_NEUE_BOLD: EXPENSIFY_NEUE_BOLD, + EXP_NEUE_ITALIC: EXPENSIFY_NEUE_ITALIC, + EXP_NEUE_BOLD_ITALIC: EXPENSIFY_NEUE_BOLD_ITALIC, + EXP_NEW_KANSAS_MEDIUM: EXPENSIFY_NEW_KANSAS_MEDIUM, + EXP_NEW_KANSAS_MEDIUM_ITALIC: EXPENSIFY_NEW_KANSAS_MEDIUM_ITALIC, + CUSTOM_EMOJI_FONT, +}; + +const CHART_FONT_MANAGER_FAMILIES: Record = { + ExpensifyNeue: [EXPENSIFY_NEUE_REGULAR, EXPENSIFY_NEUE_BOLD, EXPENSIFY_NEUE_ITALIC, EXPENSIFY_NEUE_BOLD_ITALIC], + ExpensifyMono: [EXPENSIFY_MONO_REGULAR, EXPENSIFY_MONO_BOLD, EXPENSIFY_MONO_ITALIC, EXPENSIFY_MONO_BOLD_ITALIC], + ExpensifyNewKansas: [EXPENSIFY_NEW_KANSAS_MEDIUM, EXPENSIFY_NEW_KANSAS_MEDIUM_ITALIC], + NotoSansSymbols: [SANS_SYMBOLS_FONT], + NotoSansSCMonths: [SANS_SC_MONTHS_FONT], +}; + +export {CHART_FONT_MANAGER_FAMILIES, CHART_SKIA_TYPEFACE_ASSETS}; diff --git a/src/components/Charts/utils/chartFontsCache.ts b/src/components/Charts/utils/chartFontsCache.ts new file mode 100644 index 000000000000..62874391ca85 --- /dev/null +++ b/src/components/Charts/utils/chartFontsCache.ts @@ -0,0 +1,120 @@ +import type {DataModule, SkTypeface} from '@shopify/react-native-skia'; +import {Skia} from '@shopify/react-native-skia'; +import {Image} from 'react-native'; +import type {ChartFontsValue} from '@components/Charts/types/chartFontsTypes'; +import type {ChartDefaultTypeface, ChartSkiaTypefaceKey} from '@components/Charts/types/chartSkiaTypefaceTypes'; +import {CHART_FONT_MANAGER_FAMILIES, CHART_SKIA_TYPEFACE_ASSETS} from './chartFontAssets'; +import createChartFontsValue from './createChartFontsValue'; + +const EMPTY_CHART_FONTS: ChartFontsValue = { + typefaces: { + MONOSPACE: null, + MONOSPACE_BOLD: null, + MONOSPACE_ITALIC: null, + MONOSPACE_BOLD_ITALIC: null, + EXP_NEUE: null, + EXP_NEUE_BOLD: null, + EXP_NEUE_ITALIC: null, + EXP_NEUE_BOLD_ITALIC: null, + EXP_NEW_KANSAS_MEDIUM: null, + EXP_NEW_KANSAS_MEDIUM_ITALIC: null, + CUSTOM_EMOJI_FONT: null, + }, + fontMgr: null, +}; + +let cachedChartFonts: ChartFontsValue | null = null; +let loadPromise: Promise | null = null; +const chartFontLoadListeners = new Set<() => void>(); + +function resolveChartFontAsset(source: DataModule | string): string { + if (typeof source === 'string') { + return source; + } + + if (typeof source === 'number') { + return Image.resolveAssetSource(source).uri; + } + + if ('default' in source && typeof source.default === 'string') { + return source.default; + } + + if ('uri' in source && typeof source.uri === 'string') { + return source.uri; + } + + throw new Error('Unsupported chart font asset source'); +} + +function loadTypefaceFromAsset(source: DataModule | string): Promise { + const uri = resolveChartFontAsset(source); + + return Skia.Data.fromURI(uri).then((data) => Skia.Typeface.MakeFreeTypeFaceFromData(data)); +} + +function loadChartSkiaTypefaces(): Promise { + const typefaceKeys = Object.keys(CHART_SKIA_TYPEFACE_ASSETS) as ChartSkiaTypefaceKey[]; + + return Promise.all( + typefaceKeys.map(async (typefaceKey) => { + const typeface = await loadTypefaceFromAsset(CHART_SKIA_TYPEFACE_ASSETS[typefaceKey]); + return [typefaceKey, typeface] as const; + }), + ).then((entries) => Object.fromEntries(entries) as ChartDefaultTypeface); +} + +function loadFontManagerFamilies(): Promise> { + return Promise.all( + Object.entries(CHART_FONT_MANAGER_FAMILIES).map(async ([familyName, familyAssets]) => { + const familyTypefaces = await Promise.all(familyAssets.map((asset) => loadTypefaceFromAsset(asset))); + + return [familyName, familyTypefaces.filter((typeface): typeface is SkTypeface => typeface !== null)] as const; + }), + ).then((entries) => Object.fromEntries(entries)); +} + +function loadChartFonts(): Promise { + return Promise.all([loadChartSkiaTypefaces(), loadFontManagerFamilies()]).then(([typefaces, fontManagerFamilies]) => createChartFontsValue(typefaces, fontManagerFamilies)); +} + +function subscribeToChartFonts(listener: () => void): () => void { + chartFontLoadListeners.add(listener); + + return () => { + chartFontLoadListeners.delete(listener); + }; +} + +function getChartFontsSnapshot(): ChartFontsValue { + return cachedChartFonts ?? EMPTY_CHART_FONTS; +} + +function notifyChartFontLoadListeners(): void { + for (const listener of chartFontLoadListeners) { + listener(); + } +} + +function loadChartFontsOnce(): Promise { + if (cachedChartFonts) { + return Promise.resolve(cachedChartFonts); + } + + if (!loadPromise) { + loadPromise = loadChartFonts() + .then((fonts) => { + cachedChartFonts = fonts; + notifyChartFontLoadListeners(); + return fonts; + }) + .catch((error) => { + loadPromise = null; + throw error; + }); + } + + return loadPromise; +} + +export {getChartFontsSnapshot, loadChartFontsOnce, subscribeToChartFonts}; diff --git a/src/components/Charts/utils/createChartFontsValue.ts b/src/components/Charts/utils/createChartFontsValue.ts new file mode 100644 index 000000000000..5b5154f82030 --- /dev/null +++ b/src/components/Charts/utils/createChartFontsValue.ts @@ -0,0 +1,18 @@ +import type {SkTypeface} from '@shopify/react-native-skia'; +import {Skia} from '@shopify/react-native-skia'; +import type {ChartFontsValue} from '@components/Charts/types/chartFontsTypes'; +import type {ChartDefaultTypeface} from '@components/Charts/types/chartSkiaTypefaceTypes'; + +function createChartFontsValue(typefaces: ChartDefaultTypeface, fontManagerFamilies: Record): ChartFontsValue { + const fontMgr = Skia.TypefaceFontProvider.Make(); + + for (const [familyName, familyTypefaces] of Object.entries(fontManagerFamilies)) { + for (const typeface of familyTypefaces) { + fontMgr.registerFont(typeface, familyName); + } + } + + return {typefaces, fontMgr}; +} + +export default createChartFontsValue; From 43d3f0141ea71f1c2bf6b87a0725228567e115af Mon Sep 17 00:00:00 2001 From: rory Date: Wed, 3 Jun 2026 10:50:20 -0700 Subject: [PATCH 15/35] feat(charts): add ChartFontsProvider and context hooks Introduce ChartFontsProvider with injectable value for future CLI use, plus useChartTypefaces while legacy loaders remain for existing call sites. Co-authored-by: Cursor --- .../Charts/context/ChartFontsContext.tsx | 26 +++++++++++++++++ .../context/ChartFontsProvider.native.tsx | 29 +++++++++++++++++++ .../Charts/context/ChartFontsProvider.tsx | 29 +++++++++++++++++++ src/components/Charts/hooks/index.ts | 2 ++ src/components/Charts/hooks/useChartFonts.ts | 17 +++++++++++ 5 files changed, 103 insertions(+) create mode 100644 src/components/Charts/context/ChartFontsContext.tsx create mode 100644 src/components/Charts/context/ChartFontsProvider.native.tsx create mode 100644 src/components/Charts/context/ChartFontsProvider.tsx create mode 100644 src/components/Charts/hooks/useChartFonts.ts diff --git a/src/components/Charts/context/ChartFontsContext.tsx b/src/components/Charts/context/ChartFontsContext.tsx new file mode 100644 index 000000000000..56ef96b9511c --- /dev/null +++ b/src/components/Charts/context/ChartFontsContext.tsx @@ -0,0 +1,26 @@ +import type {SkTypefaceFontProvider} from '@shopify/react-native-skia'; +import {createContext, useContext} from 'react'; +import type {ChartFontsContextValue} from '@components/Charts/types/chartFontsTypes'; +import type {ChartDefaultTypeface} from '@components/Charts/types/chartSkiaTypefaceTypes'; + +const ChartFontsContext = createContext(null); + +function useChartFontsContext(): ChartFontsContextValue { + const context = useContext(ChartFontsContext); + + if (!context) { + throw new Error('Chart fonts hooks must be used within ChartFontsProvider'); + } + + return context; +} + +function useChartTypefaces(): ChartDefaultTypeface { + return useChartFontsContext().typefaces; +} + +function useChartFontManager(): SkTypefaceFontProvider | null { + return useChartFontsContext().fontMgr; +} + +export {ChartFontsContext, useChartFontManager, useChartTypefaces}; diff --git a/src/components/Charts/context/ChartFontsProvider.native.tsx b/src/components/Charts/context/ChartFontsProvider.native.tsx new file mode 100644 index 000000000000..4f98b3223b4f --- /dev/null +++ b/src/components/Charts/context/ChartFontsProvider.native.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import useChartFonts from '@components/Charts/hooks/useChartFonts'; +import type {ChartFontsValue} from '@components/Charts/types/chartFontsTypes'; +import {ChartFontsContext} from './ChartFontsContext'; + +type ChartFontsProviderProps = { + children: React.ReactNode; + value?: ChartFontsValue; +}; + +function ChartFontsLoaderProvider({children}: {children: React.ReactNode}) { + const fonts = useChartFonts(); + + return {children}; +} + +ChartFontsLoaderProvider.displayName = 'ChartFontsLoaderProvider'; + +function ChartFontsProvider({children, value}: ChartFontsProviderProps) { + if (value) { + return {children}; + } + + return {children}; +} + +ChartFontsProvider.displayName = 'ChartFontsProvider'; + +export default ChartFontsProvider; diff --git a/src/components/Charts/context/ChartFontsProvider.tsx b/src/components/Charts/context/ChartFontsProvider.tsx new file mode 100644 index 000000000000..4f98b3223b4f --- /dev/null +++ b/src/components/Charts/context/ChartFontsProvider.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import useChartFonts from '@components/Charts/hooks/useChartFonts'; +import type {ChartFontsValue} from '@components/Charts/types/chartFontsTypes'; +import {ChartFontsContext} from './ChartFontsContext'; + +type ChartFontsProviderProps = { + children: React.ReactNode; + value?: ChartFontsValue; +}; + +function ChartFontsLoaderProvider({children}: {children: React.ReactNode}) { + const fonts = useChartFonts(); + + return {children}; +} + +ChartFontsLoaderProvider.displayName = 'ChartFontsLoaderProvider'; + +function ChartFontsProvider({children, value}: ChartFontsProviderProps) { + if (value) { + return {children}; + } + + return {children}; +} + +ChartFontsProvider.displayName = 'ChartFontsProvider'; + +export default ChartFontsProvider; diff --git a/src/components/Charts/hooks/index.ts b/src/components/Charts/hooks/index.ts index 3c9ebaaf64e9..83685f385cd3 100644 --- a/src/components/Charts/hooks/index.ts +++ b/src/components/Charts/hooks/index.ts @@ -4,8 +4,10 @@ export {default as useChartParagraphs} from './useChartParagraphs'; export {default as useYAxisLabelWidth} from './useYAxisLabelWidth'; export {default as useChartFontManager} from './useChartFontManager/useChartFontManager'; export {default as ChartDefaultTypefaceProvider} from '@components/Charts/context/ChartDefaultTypefaceProvider'; +export {default as ChartFontsProvider} from '@components/Charts/context/ChartFontsProvider'; export {default as getChartSkiaTypeface} from '@components/Charts/utils/getChartSkiaTypeface'; export {useChartDefaultTypeface} from '@components/Charts/context/ChartDefaultTypefaceContext'; +export {useChartTypefaces} from '@components/Charts/context/ChartFontsContext'; export {useChartInteractions, TOOLTIP_BAR_GAP} from './useChartInteractions'; export type {HitTestArgs} from './useChartInteractions'; export {default as useChartLabelFormats} from './useChartLabelFormats'; diff --git a/src/components/Charts/hooks/useChartFonts.ts b/src/components/Charts/hooks/useChartFonts.ts new file mode 100644 index 000000000000..365cc260206b --- /dev/null +++ b/src/components/Charts/hooks/useChartFonts.ts @@ -0,0 +1,17 @@ +import {useEffect, useSyncExternalStore} from 'react'; +import type {ChartFontsValue} from '@components/Charts/types/chartFontsTypes'; +import {getChartFontsSnapshot, loadChartFontsOnce, subscribeToChartFonts} from '@components/Charts/utils/chartFontsCache'; + +function useChartFonts(): ChartFontsValue { + const fonts = useSyncExternalStore(subscribeToChartFonts, getChartFontsSnapshot, getChartFontsSnapshot); + + useEffect(() => { + loadChartFontsOnce().catch(() => { + // Chart consumers null-guard fontMgr and typefaces until load succeeds. + }); + }, []); + + return fonts; +} + +export default useChartFonts; From a99499f16eadb5e5b3b8c18f8c6a28754a33c2d6 Mon Sep 17 00:00:00 2001 From: rory Date: Wed, 3 Jun 2026 10:50:27 -0700 Subject: [PATCH 16/35] refactor(charts): migrate Victory HTML to ChartFontsProvider Switch Victory chart renderers to ChartFontsProvider and useChartTypefaces while Bar/Line charts continue using the legacy font hooks. Co-authored-by: Cursor --- .../VictoryChartRenderer/BaseVictoryChartRenderer.tsx | 6 +++--- .../components/VictoryChartCartesian.tsx | 6 +++--- .../VictoryChartRenderer/components/VictoryChartLabel.tsx | 4 ++-- .../VictoryChartRenderer/components/VictoryChartLegend.tsx | 4 ++-- .../VictoryChartRenderer/components/VictoryChartPolar.tsx | 6 +++--- .../VictoryChartRenderer/context/VictoryChartContext.tsx | 4 ++-- 6 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/BaseVictoryChartRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/BaseVictoryChartRenderer.tsx index a84bb3b17166..e058d52f31b2 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/BaseVictoryChartRenderer.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/BaseVictoryChartRenderer.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import {ChartDefaultTypefaceProvider} from '@components/Charts/hooks'; +import {ChartFontsProvider} from '@components/Charts/hooks'; import VictoryChartContainer from './components/VictoryChartContainer'; import VictoryChartContent from './components/VictoryChartContent'; import {VictoryChartProvider} from './context/VictoryChartContext'; @@ -7,13 +7,13 @@ import type {VictoryChartRendererProps} from './types'; function BaseVictoryChartRenderer({tnode}: VictoryChartRendererProps) { return ( - + - + ); } diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartCartesian.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartCartesian.tsx index 2beed92cd08f..e6ed3e7232a9 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartCartesian.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartCartesian.tsx @@ -1,6 +1,6 @@ import React from 'react'; import {CartesianChart} from 'victory-native'; -import {ChartDefaultTypefaceProvider} from '@components/Charts/hooks'; +import {ChartFontsProvider} from '@components/Charts/hooks'; import {useVictoryChartContext} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartContext'; import {VictoryChartRenderArgsProvider} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartRenderArgsContext'; import getHierarchyID from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/getHierarchyID'; @@ -26,7 +26,7 @@ function VictoryChartCartesian() { domainPadding={domainPadding} padding={padding} renderOutside={(renderArgs) => ( - + {labelItems.map((labelItem) => ( ))} - + )} > {(renderArgs) => ( diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartLabel.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartLabel.tsx index d59e52873a6c..0efc95c9e0af 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartLabel.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartLabel.tsx @@ -1,7 +1,7 @@ import {Skia, Text as SkText} from '@shopify/react-native-skia'; import type {Color, SkFont} from '@shopify/react-native-skia'; import React from 'react'; -import {getChartSkiaTypeface, useChartDefaultTypeface} from '@components/Charts/hooks'; +import {getChartSkiaTypeface, useChartTypefaces} from '@components/Charts/hooks'; import type {LabelItem} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/types'; import computeTextAnchorPosition from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/computeTextAnchorPosition'; @@ -21,7 +21,7 @@ type ProcessedLine = { * Intended for use inside CartesianChart's `renderOutside` callback. */ function VictoryChartLabel({x, y, text, color, fontSize, fontWeight, fontFamily, fontStyle, lineHeight, textAnchor = 'start', verticalAnchor = 'start'}: VictoryChartLabelsProps) { - const typefaces = useChartDefaultTypeface(); + const typefaces = useChartTypefaces(); const processedLines = text.split('\n').reduce( (acc, line, index) => { const lineColor = color?.[index]; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartLegend.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartLegend.tsx index f9aa0ebccc78..9ef0370ca409 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartLegend.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartLegend.tsx @@ -1,7 +1,7 @@ import {Circle, Skia, Text as SkText} from '@shopify/react-native-skia'; import type {Color, SkFont} from '@shopify/react-native-skia'; import React, {Fragment} from 'react'; -import {getChartSkiaTypeface, useChartDefaultTypeface} from '@components/Charts/hooks'; +import {getChartSkiaTypeface, useChartTypefaces} from '@components/Charts/hooks'; import type {LegendItem} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/types'; type VictoryChartLegendProps = LegendItem; @@ -23,7 +23,7 @@ type ProcessedEntry = { * Intended for use inside CartesianChart's `renderOutside` callback. */ function VictoryChartLegend({x, y, entries, gutter, symbolSpacer}: VictoryChartLegendProps) { - const typefaces = useChartDefaultTypeface(); + const typefaces = useChartTypefaces(); const processedEntries = entries.reduce( (acc, {text, color, fontSize, fontWeight, fontFamily, fontStyle, symbolColor, symbolSize}) => { const typeface = getChartSkiaTypeface(typefaces, {fontFamily, fontStyle, fontWeight}); diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartPolar.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartPolar.tsx index d9c921e437bf..684a667d6d05 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartPolar.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartPolar.tsx @@ -1,6 +1,6 @@ import React from 'react'; import {PolarChart} from 'victory-native'; -import {ChartDefaultTypefaceProvider} from '@components/Charts/hooks'; +import {ChartFontsProvider} from '@components/Charts/hooks'; import {COLOR_KEY, LABEL_KEY, VALUE_KEY} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/constants'; import {useVictoryChartContext} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartContext'; import getHierarchyID from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/getHierarchyID'; @@ -21,7 +21,7 @@ function VictoryChartPolar() { valueKey={VALUE_KEY} colorKey={COLOR_KEY} > - + {tnode.children.map((child) => ( ))} - + ); } diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartContext.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartContext.tsx index 057903ebb03a..45b051dd00e5 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartContext.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartContext.tsx @@ -1,6 +1,6 @@ import React, {createContext, useContext} from 'react'; import type {TNode} from 'react-native-render-html'; -import {useChartDefaultTypeface} from '@components/Charts/hooks'; +import {useChartTypefaces} from '@components/Charts/hooks'; import {CHART_TYPE, LABEL_KEY, X_KEY} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/constants'; import processVictoryChartTree from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/parsers/processVictoryChartTree'; import type {ChartType, ProcessNodeResult} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/types'; @@ -32,7 +32,7 @@ const VictoryChartContext = createContext(null) * Returns null when the chart data is invalid (no data points, or mixed cartesian/polar content). */ function VictoryChartProvider({tnode, children}: {tnode: TNode; children: React.ReactNode}) { - const typefaces = useChartDefaultTypeface(); + const typefaces = useChartTypefaces(); const {data, xKey, yKeys, xAxis, yAxis, domain, domainPadding, padding, isHorizontal, categories, labelItems, legendItems} = processVictoryChartTree(tnode, typefaces.EXP_NEUE, null); const {nodeStyles: chartContentStyles, parentNodeStyles: chartContainerStyles} = parseStyles(tnode); From 7e162d891008cabaf2a4b6e5070f7bd841d9404b Mon Sep 17 00:00:00 2001 From: rory Date: Wed, 3 Jun 2026 10:50:33 -0700 Subject: [PATCH 17/35] feat(charts): unify Bar/Line fonts via ChartFontsProvider Expand paragraph font fallbacks and wrap bar/line charts with ChartFontsProvider so axis labels use the shared cached fontMgr from context. Co-authored-by: Cursor --- src/components/Charts/BarChart/BarChartContent.tsx | 11 ++++++++++- src/components/Charts/LineChart/LineChartContent.tsx | 11 ++++++++++- src/components/Charts/VictoryTheme.ts | 2 +- src/components/Charts/hooks/index.ts | 4 +--- 4 files changed, 22 insertions(+), 6 deletions(-) diff --git a/src/components/Charts/BarChart/BarChartContent.tsx b/src/components/Charts/BarChart/BarChartContent.tsx index ecfb90e8a1b6..6a038b1ccef7 100644 --- a/src/components/Charts/BarChart/BarChartContent.tsx +++ b/src/components/Charts/BarChart/BarChartContent.tsx @@ -11,6 +11,7 @@ import ChartXAxisLabels from '@components/Charts/components/ChartXAxisLabels'; import ChartYAxisLabels from '@components/Charts/components/ChartYAxisLabels'; import type {HitTestArgs} from '@components/Charts/hooks'; import { + ChartFontsProvider, useChartFontManager, useChartInteractions, useChartLabelFormats, @@ -44,7 +45,7 @@ type BarChartProps = CartesianChartProps & { useSingleColor?: boolean; }; -function BarChartContent({data, isLoading, yAxisUnit, yAxisUnitPosition = 'left', useSingleColor = false, onBarPress}: BarChartProps) { +function BarChartContentBody({data, isLoading, yAxisUnit, yAxisUnitPosition = 'left', useSingleColor = false, onBarPress}: BarChartProps) { const theme = useTheme(); const styles = useThemeStyles(); const fontMgr = useChartFontManager(); @@ -301,6 +302,14 @@ function BarChartContent({data, isLoading, yAxisUnit, yAxisUnitPosition = 'left' ); } +function BarChartContent(props: BarChartProps) { + return ( + + + + ); +} + export default BarChartContent; export type {BarChartProps}; export {BAR_INNER_PADDING}; diff --git a/src/components/Charts/LineChart/LineChartContent.tsx b/src/components/Charts/LineChart/LineChartContent.tsx index f0129ac2ae4d..93b9e9a80051 100644 --- a/src/components/Charts/LineChart/LineChartContent.tsx +++ b/src/components/Charts/LineChart/LineChartContent.tsx @@ -13,6 +13,7 @@ import LeftFrameLine from '@components/Charts/components/LeftFrameLine'; import ScatterPoints from '@components/Charts/components/ScatterPoints'; import type {HitTestArgs} from '@components/Charts/hooks'; import { + ChartFontsProvider, useChartFontManager, useChartInteractions, useChartLabelFormats, @@ -47,7 +48,7 @@ type LineChartProps = CartesianChartProps & { onPointPress?: (dataPoint: ChartDataPoint, index: number) => void; }; -function LineChartContent({data, isLoading, yAxisUnit, yAxisUnitPosition = 'left', onPointPress}: LineChartProps) { +function LineChartContentBody({data, isLoading, yAxisUnit, yAxisUnitPosition = 'left', onPointPress}: LineChartProps) { const theme = useTheme(); const styles = useThemeStyles(); const fontMgr = useChartFontManager(); @@ -304,5 +305,13 @@ function LineChartContent({data, isLoading, yAxisUnit, yAxisUnitPosition = 'left ); } +function LineChartContent(props: LineChartProps) { + return ( + + + + ); +} + export default LineChartContent; export type {LineChartProps}; diff --git a/src/components/Charts/VictoryTheme.ts b/src/components/Charts/VictoryTheme.ts index 78c83379b0d0..8db0296cd17a 100644 --- a/src/components/Charts/VictoryTheme.ts +++ b/src/components/Charts/VictoryTheme.ts @@ -4,7 +4,7 @@ import colors from '@styles/theme/colors'; /** Font families used by all chart label components (Paragraph API multi-font fallback). */ -const CHART_FONT_FAMILIES = ['ExpensifyNeue', 'NotoSansSymbols', 'NotoSansSCMonths']; +const CHART_FONT_FAMILIES = ['ExpensifyNeue', 'ExpensifyMono', 'ExpensifyNewKansas', 'NotoSansSymbols', 'NotoSansSCMonths']; /** * Expensify Chart Color Palette. diff --git a/src/components/Charts/hooks/index.ts b/src/components/Charts/hooks/index.ts index 83685f385cd3..2710594037f3 100644 --- a/src/components/Charts/hooks/index.ts +++ b/src/components/Charts/hooks/index.ts @@ -2,11 +2,9 @@ export {default as useChartLabelLayout} from './useChartLabelLayout'; export {default as useChartLabelMeasurements} from './useChartLabelMeasurements'; export {default as useChartParagraphs} from './useChartParagraphs'; export {default as useYAxisLabelWidth} from './useYAxisLabelWidth'; -export {default as useChartFontManager} from './useChartFontManager/useChartFontManager'; -export {default as ChartDefaultTypefaceProvider} from '@components/Charts/context/ChartDefaultTypefaceProvider'; +export {useChartFontManager} from '@components/Charts/context/ChartFontsContext'; export {default as ChartFontsProvider} from '@components/Charts/context/ChartFontsProvider'; export {default as getChartSkiaTypeface} from '@components/Charts/utils/getChartSkiaTypeface'; -export {useChartDefaultTypeface} from '@components/Charts/context/ChartDefaultTypefaceContext'; export {useChartTypefaces} from '@components/Charts/context/ChartFontsContext'; export {useChartInteractions, TOOLTIP_BAR_GAP} from './useChartInteractions'; export type {HitTestArgs} from './useChartInteractions'; From e38912c74ee0675e129fa0c40275e8158b87b5c5 Mon Sep 17 00:00:00 2001 From: rory Date: Wed, 3 Jun 2026 10:50:37 -0700 Subject: [PATCH 18/35] refactor(charts): remove legacy chart font loading Delete ChartDefaultTypefaceProvider and per-hook Skia font loaders now replaced by the shared chartFontsCache and ChartFontsProvider. Co-authored-by: Cursor --- ...ry-native-cartesian-skia-context-bridge.md | 54 +++++++++++++++++++ .../context/ChartDefaultTypefaceContext.tsx | 14 ----- .../ChartDefaultTypefaceProvider.native.tsx | 13 ----- .../context/ChartDefaultTypefaceProvider.tsx | 13 ----- .../useChartFontManager.native.ts | 17 ------ .../useChartFontManager.ts | 18 ------- .../hooks/useChartSkiaTypefaces.native.ts | 21 -------- .../Charts/hooks/useChartSkiaTypefaces.ts | 21 -------- 8 files changed, 54 insertions(+), 117 deletions(-) create mode 100644 docs/victory-native-cartesian-skia-context-bridge.md delete mode 100644 src/components/Charts/context/ChartDefaultTypefaceContext.tsx delete mode 100644 src/components/Charts/context/ChartDefaultTypefaceProvider.native.tsx delete mode 100644 src/components/Charts/context/ChartDefaultTypefaceProvider.tsx delete mode 100644 src/components/Charts/hooks/useChartFontManager/useChartFontManager.native.ts delete mode 100644 src/components/Charts/hooks/useChartFontManager/useChartFontManager.ts delete mode 100644 src/components/Charts/hooks/useChartSkiaTypefaces.native.ts delete mode 100644 src/components/Charts/hooks/useChartSkiaTypefaces.ts diff --git a/docs/victory-native-cartesian-skia-context-bridge.md b/docs/victory-native-cartesian-skia-context-bridge.md new file mode 100644 index 000000000000..f86baf3b4ef5 --- /dev/null +++ b/docs/victory-native-cartesian-skia-context-bridge.md @@ -0,0 +1,54 @@ +# Victory Native: Cartesian chart Skia context bridge + +## Problem + +Custom React components rendered inside `CartesianChart` (including the `renderOutside` callback) cannot read React context from ancestors above the chart—for example, a `ChartFontsProvider` wrapping the HTML Victory chart tree. + +This surfaces as runtime errors such as: + +```text +useChartTypefaces must be used within ChartFontsProvider +``` + +The failure occurs in Skia overlay components (`VictoryChartLabel`, `VictoryChartLegend`) even when the provider is correctly placed on the main React tree (`BaseVictoryChartRenderer`). + +## Cause + +`victory-native` draws charts inside `@shopify/react-native-skia`’s ``, which uses a **separate React reconciler**. Context from the host app does not cross into that tree unless it is explicitly bridged. + +| Chart type | Context bridge (`its-fine`) | Notes | +| ----------------- | --------------------------- | ------------------------------------------ | +| `PolarChart` | Yes | Added in [victory-native-xl#440](https://github.com/FormidableLabs/victory-native-xl/pull/440) | +| `CartesianChart` | No | `children` and `renderOutside` run inside `` without a bridge | + +`ChartWrapper` already exposes `wrapCanvasContent` for this purpose; `PolarChart` uses it, `CartesianChart` does not. + +## Impact on Expensify App + +Victory HTML charts load default typefaces via `ChartFontsProvider` and consume them with `useChartTypefaces()` in label/legend components. After moving font loading into that provider, cartesian overlays broke until we added **nested** `ChartFontsProvider` instances inside `renderOutside` (and polar children)—which reloads fonts and duplicates logic. + +## Proposed upstream solution + +**Repository:** [FormidableLabs/victory-native-xl](https://github.com/FormidableLabs/victory-native-xl) (npm package: `victory-native`) + +**Change:** Give `CartesianChart` the same `its-fine` pattern as `PolarChart`: + +1. Wrap the cartesian chart entry in `` (from `its-fine`, already a dependency). +2. In the component that renders `ChartWrapper`, call `useContextBridge()` and pass + `wrapCanvasContent={(content) => {content}}` + (omit in headless mode, matching polar). + +All canvas content—including `children(renderArg)` and `renderOutside(renderArg)`—is built into `chartContent` and passed through `ChartWrapper`, so one bridge covers both. + +**Scope:** Small parity fix; no new public API. Aligns with the intent of [#433](https://github.com/FormidableLabs/victory-native-xl/issues/433) / [#440](https://github.com/FormidableLabs/victory-native-xl/pull/440). + +**Suggested PR title:** `Add its-fine context bridge to CartesianChart (parity with PolarChart)` + +## App workaround (until upstream ships) + +Until a `victory-native` release includes the bridge: + +- Keep a single `ChartFontsProvider` on `BaseVictoryChartRenderer` for `VictoryChartProvider` / tree parsing. +- Add nested providers (or a local `its-fine` bridge) at Skia boundaries where context is still missing—today: `VictoryChartCartesian` `renderOutside` and `VictoryChartPolar` children. + +After upgrading to a fixed `victory-native` version, remove nested providers and rely on the outer provider only. diff --git a/src/components/Charts/context/ChartDefaultTypefaceContext.tsx b/src/components/Charts/context/ChartDefaultTypefaceContext.tsx deleted file mode 100644 index 6ffdb6ac6037..000000000000 --- a/src/components/Charts/context/ChartDefaultTypefaceContext.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import {createContext, useContext} from 'react'; -import type {ChartDefaultTypeface} from '@components/Charts/types/chartSkiaTypefaceTypes'; - -const ChartDefaultTypefaceContext = createContext(null); - -function useChartDefaultTypeface(): ChartDefaultTypeface { - const context = useContext(ChartDefaultTypefaceContext); - if (!context) { - throw new Error('useChartDefaultTypeface must be used within ChartDefaultTypefaceProvider'); - } - return context; -} - -export {ChartDefaultTypefaceContext, useChartDefaultTypeface}; diff --git a/src/components/Charts/context/ChartDefaultTypefaceProvider.native.tsx b/src/components/Charts/context/ChartDefaultTypefaceProvider.native.tsx deleted file mode 100644 index a7fbfdc14df0..000000000000 --- a/src/components/Charts/context/ChartDefaultTypefaceProvider.native.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import React from 'react'; -import useChartSkiaTypefaces from '@components/Charts/hooks/useChartSkiaTypefaces'; -import {ChartDefaultTypefaceContext} from './ChartDefaultTypefaceContext'; - -function ChartDefaultTypefaceProvider({children}: {children: React.ReactNode}) { - const typefaces = useChartSkiaTypefaces(); - - return {children}; -} - -ChartDefaultTypefaceProvider.displayName = 'ChartDefaultTypefaceProvider'; - -export default ChartDefaultTypefaceProvider; diff --git a/src/components/Charts/context/ChartDefaultTypefaceProvider.tsx b/src/components/Charts/context/ChartDefaultTypefaceProvider.tsx deleted file mode 100644 index a7fbfdc14df0..000000000000 --- a/src/components/Charts/context/ChartDefaultTypefaceProvider.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import React from 'react'; -import useChartSkiaTypefaces from '@components/Charts/hooks/useChartSkiaTypefaces'; -import {ChartDefaultTypefaceContext} from './ChartDefaultTypefaceContext'; - -function ChartDefaultTypefaceProvider({children}: {children: React.ReactNode}) { - const typefaces = useChartSkiaTypefaces(); - - return {children}; -} - -ChartDefaultTypefaceProvider.displayName = 'ChartDefaultTypefaceProvider'; - -export default ChartDefaultTypefaceProvider; diff --git a/src/components/Charts/hooks/useChartFontManager/useChartFontManager.native.ts b/src/components/Charts/hooks/useChartFontManager/useChartFontManager.native.ts deleted file mode 100644 index 5b3f293c2def..000000000000 --- a/src/components/Charts/hooks/useChartFontManager/useChartFontManager.native.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type {DataModule, SkTypefaceFontProvider} from '@shopify/react-native-skia'; -import {useFonts} from '@shopify/react-native-skia'; - -function useChartFontManager(): SkTypefaceFontProvider | null { - return useFonts({ - ExpensifyNeue: [ - require('@assets/fonts/native/ExpensifyNeue-Regular.otf') as DataModule, - require('@assets/fonts/native/ExpensifyNeue-Bold.otf') as DataModule, - require('@assets/fonts/native/ExpensifyNeue-Italic.otf') as DataModule, - require('@assets/fonts/native/ExpensifyNeue-BoldItalic.otf') as DataModule, - ], - NotoSansSymbols: [require('@assets/fonts/NotoSans-Symbols.ttf') as DataModule], - NotoSansSCMonths: [require('@assets/fonts/NotoSansSC-Months.ttf') as DataModule], - }); -} - -export default useChartFontManager; diff --git a/src/components/Charts/hooks/useChartFontManager/useChartFontManager.ts b/src/components/Charts/hooks/useChartFontManager/useChartFontManager.ts deleted file mode 100644 index 3b11301f4646..000000000000 --- a/src/components/Charts/hooks/useChartFontManager/useChartFontManager.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type {SkTypefaceFontProvider} from '@shopify/react-native-skia'; -import {useFonts} from '@shopify/react-native-skia'; -import chartWebFont from '@components/Charts/utils/chartWebFont'; - -function useChartFontManager(): SkTypefaceFontProvider | null { - return useFonts({ - ExpensifyNeue: [ - chartWebFont(require('@assets/fonts/web/ExpensifyNeue-Regular.woff2') as string), - chartWebFont(require('@assets/fonts/web/ExpensifyNeue-Bold.woff2') as string), - chartWebFont(require('@assets/fonts/web/ExpensifyNeue-Italic.woff2') as string), - chartWebFont(require('@assets/fonts/web/ExpensifyNeue-BoldItalic.woff2') as string), - ], - NotoSansSymbols: [chartWebFont(require('@assets/fonts/NotoSans-Symbols.ttf') as string)], - NotoSansSCMonths: [chartWebFont(require('@assets/fonts/NotoSansSC-Months.ttf') as string)], - }); -} - -export default useChartFontManager; diff --git a/src/components/Charts/hooks/useChartSkiaTypefaces.native.ts b/src/components/Charts/hooks/useChartSkiaTypefaces.native.ts deleted file mode 100644 index f74cbfb96189..000000000000 --- a/src/components/Charts/hooks/useChartSkiaTypefaces.native.ts +++ /dev/null @@ -1,21 +0,0 @@ -import type {DataModule} from '@shopify/react-native-skia'; -import {useTypeface} from '@shopify/react-native-skia'; -import type {ChartDefaultTypeface} from '@components/Charts/types/chartSkiaTypefaceTypes'; - -function useChartSkiaTypefaces(): ChartDefaultTypeface { - return { - MONOSPACE: useTypeface(require('@assets/fonts/native/ExpensifyMono-Regular.otf') as DataModule), - MONOSPACE_BOLD: useTypeface(require('@assets/fonts/native/ExpensifyMono-Bold.otf') as DataModule), - MONOSPACE_ITALIC: useTypeface(require('@assets/fonts/native/ExpensifyMono-Italic.otf') as DataModule), - MONOSPACE_BOLD_ITALIC: useTypeface(require('@assets/fonts/native/ExpensifyMono-BoldItalic.otf') as DataModule), - EXP_NEUE: useTypeface(require('@assets/fonts/native/ExpensifyNeue-Regular.otf') as DataModule), - EXP_NEUE_BOLD: useTypeface(require('@assets/fonts/native/ExpensifyNeue-Bold.otf') as DataModule), - EXP_NEUE_ITALIC: useTypeface(require('@assets/fonts/native/ExpensifyNeue-Italic.otf') as DataModule), - EXP_NEUE_BOLD_ITALIC: useTypeface(require('@assets/fonts/native/ExpensifyNeue-BoldItalic.otf') as DataModule), - EXP_NEW_KANSAS_MEDIUM: useTypeface(require('@assets/fonts/native/ExpensifyNewKansas-Medium.otf') as DataModule), - EXP_NEW_KANSAS_MEDIUM_ITALIC: useTypeface(require('@assets/fonts/native/ExpensifyNewKansas-MediumItalic.otf') as DataModule), - CUSTOM_EMOJI_FONT: useTypeface(require('@assets/fonts/native/CustomEmojiNativeFont.ttf') as DataModule), - }; -} - -export default useChartSkiaTypefaces; diff --git a/src/components/Charts/hooks/useChartSkiaTypefaces.ts b/src/components/Charts/hooks/useChartSkiaTypefaces.ts deleted file mode 100644 index aa95fe01dac0..000000000000 --- a/src/components/Charts/hooks/useChartSkiaTypefaces.ts +++ /dev/null @@ -1,21 +0,0 @@ -import {useTypeface} from '@shopify/react-native-skia'; -import type {ChartDefaultTypeface} from '@components/Charts/types/chartSkiaTypefaceTypes'; -import chartWebFont from '@components/Charts/utils/chartWebFont'; - -function useChartSkiaTypefaces(): ChartDefaultTypeface { - return { - MONOSPACE: useTypeface(chartWebFont(require('@assets/fonts/web/ExpensifyMono-Regular.woff2') as string)), - MONOSPACE_BOLD: useTypeface(chartWebFont(require('@assets/fonts/web/ExpensifyMono-Bold.woff2') as string)), - MONOSPACE_ITALIC: useTypeface(chartWebFont(require('@assets/fonts/web/ExpensifyMono-Italic.woff2') as string)), - MONOSPACE_BOLD_ITALIC: useTypeface(chartWebFont(require('@assets/fonts/web/ExpensifyMono-BoldItalic.woff2') as string)), - EXP_NEUE: useTypeface(chartWebFont(require('@assets/fonts/web/ExpensifyNeue-Regular.woff2') as string)), - EXP_NEUE_BOLD: useTypeface(chartWebFont(require('@assets/fonts/web/ExpensifyNeue-Bold.woff2') as string)), - EXP_NEUE_ITALIC: useTypeface(chartWebFont(require('@assets/fonts/web/ExpensifyNeue-Italic.woff2') as string)), - EXP_NEUE_BOLD_ITALIC: useTypeface(chartWebFont(require('@assets/fonts/web/ExpensifyNeue-BoldItalic.woff2') as string)), - EXP_NEW_KANSAS_MEDIUM: useTypeface(chartWebFont(require('@assets/fonts/web/ExpensifyNewKansas-Medium.woff2') as string)), - EXP_NEW_KANSAS_MEDIUM_ITALIC: useTypeface(chartWebFont(require('@assets/fonts/web/ExpensifyNewKansas-MediumItalic.woff2') as string)), - CUSTOM_EMOJI_FONT: useTypeface(chartWebFont(require('@assets/fonts/web/CustomEmojiWebFont.ttf') as string)), - }; -} - -export default useChartSkiaTypefaces; From bdeaf13d5f0b14cb44aff36cf0196ab15c642d39 Mon Sep 17 00:00:00 2001 From: rory Date: Wed, 3 Jun 2026 10:52:08 -0700 Subject: [PATCH 19/35] chore: remove victory-native cartesian skia context bridge doc This doc was added by mistake and is not needed in the repo. Co-authored-by: Cursor --- ...ry-native-cartesian-skia-context-bridge.md | 54 ------------------- 1 file changed, 54 deletions(-) delete mode 100644 docs/victory-native-cartesian-skia-context-bridge.md diff --git a/docs/victory-native-cartesian-skia-context-bridge.md b/docs/victory-native-cartesian-skia-context-bridge.md deleted file mode 100644 index f86baf3b4ef5..000000000000 --- a/docs/victory-native-cartesian-skia-context-bridge.md +++ /dev/null @@ -1,54 +0,0 @@ -# Victory Native: Cartesian chart Skia context bridge - -## Problem - -Custom React components rendered inside `CartesianChart` (including the `renderOutside` callback) cannot read React context from ancestors above the chart—for example, a `ChartFontsProvider` wrapping the HTML Victory chart tree. - -This surfaces as runtime errors such as: - -```text -useChartTypefaces must be used within ChartFontsProvider -``` - -The failure occurs in Skia overlay components (`VictoryChartLabel`, `VictoryChartLegend`) even when the provider is correctly placed on the main React tree (`BaseVictoryChartRenderer`). - -## Cause - -`victory-native` draws charts inside `@shopify/react-native-skia`’s ``, which uses a **separate React reconciler**. Context from the host app does not cross into that tree unless it is explicitly bridged. - -| Chart type | Context bridge (`its-fine`) | Notes | -| ----------------- | --------------------------- | ------------------------------------------ | -| `PolarChart` | Yes | Added in [victory-native-xl#440](https://github.com/FormidableLabs/victory-native-xl/pull/440) | -| `CartesianChart` | No | `children` and `renderOutside` run inside `` without a bridge | - -`ChartWrapper` already exposes `wrapCanvasContent` for this purpose; `PolarChart` uses it, `CartesianChart` does not. - -## Impact on Expensify App - -Victory HTML charts load default typefaces via `ChartFontsProvider` and consume them with `useChartTypefaces()` in label/legend components. After moving font loading into that provider, cartesian overlays broke until we added **nested** `ChartFontsProvider` instances inside `renderOutside` (and polar children)—which reloads fonts and duplicates logic. - -## Proposed upstream solution - -**Repository:** [FormidableLabs/victory-native-xl](https://github.com/FormidableLabs/victory-native-xl) (npm package: `victory-native`) - -**Change:** Give `CartesianChart` the same `its-fine` pattern as `PolarChart`: - -1. Wrap the cartesian chart entry in `` (from `its-fine`, already a dependency). -2. In the component that renders `ChartWrapper`, call `useContextBridge()` and pass - `wrapCanvasContent={(content) => {content}}` - (omit in headless mode, matching polar). - -All canvas content—including `children(renderArg)` and `renderOutside(renderArg)`—is built into `chartContent` and passed through `ChartWrapper`, so one bridge covers both. - -**Scope:** Small parity fix; no new public API. Aligns with the intent of [#433](https://github.com/FormidableLabs/victory-native-xl/issues/433) / [#440](https://github.com/FormidableLabs/victory-native-xl/pull/440). - -**Suggested PR title:** `Add its-fine context bridge to CartesianChart (parity with PolarChart)` - -## App workaround (until upstream ships) - -Until a `victory-native` release includes the bridge: - -- Keep a single `ChartFontsProvider` on `BaseVictoryChartRenderer` for `VictoryChartProvider` / tree parsing. -- Add nested providers (or a local `its-fine` bridge) at Skia boundaries where context is still missing—today: `VictoryChartCartesian` `renderOutside` and `VictoryChartPolar` children. - -After upgrading to a fixed `victory-native` version, remove nested providers and rely on the outer provider only. From 7ce823bed9dcaf23b6339fd0b40fdbf2a96f44ec Mon Sep 17 00:00:00 2001 From: rory Date: Wed, 3 Jun 2026 11:04:44 -0700 Subject: [PATCH 20/35] refactor(charts): improve font load errors and drop useMemo Log chart font failures in chartFontsCache and remove hook-level catch. Drop useMemo from VictoryChartPie for React Compiler compliance. Co-authored-by: Cursor --- src/components/Charts/hooks/useChartFonts.ts | 4 +--- src/components/Charts/utils/chartFontsCache.ts | 9 +++++++-- .../components/VictoryChartPie.tsx | 15 ++++++--------- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/components/Charts/hooks/useChartFonts.ts b/src/components/Charts/hooks/useChartFonts.ts index 365cc260206b..bcfeb445bcce 100644 --- a/src/components/Charts/hooks/useChartFonts.ts +++ b/src/components/Charts/hooks/useChartFonts.ts @@ -6,9 +6,7 @@ function useChartFonts(): ChartFontsValue { const fonts = useSyncExternalStore(subscribeToChartFonts, getChartFontsSnapshot, getChartFontsSnapshot); useEffect(() => { - loadChartFontsOnce().catch(() => { - // Chart consumers null-guard fontMgr and typefaces until load succeeds. - }); + loadChartFontsOnce(); }, []); return fonts; diff --git a/src/components/Charts/utils/chartFontsCache.ts b/src/components/Charts/utils/chartFontsCache.ts index 62874391ca85..4b9ac4a4dbbd 100644 --- a/src/components/Charts/utils/chartFontsCache.ts +++ b/src/components/Charts/utils/chartFontsCache.ts @@ -3,6 +3,7 @@ import {Skia} from '@shopify/react-native-skia'; import {Image} from 'react-native'; import type {ChartFontsValue} from '@components/Charts/types/chartFontsTypes'; import type {ChartDefaultTypeface, ChartSkiaTypefaceKey} from '@components/Charts/types/chartSkiaTypefaceTypes'; +import Log from '@libs/Log'; import {CHART_FONT_MANAGER_FAMILIES, CHART_SKIA_TYPEFACE_ASSETS} from './chartFontAssets'; import createChartFontsValue from './createChartFontsValue'; @@ -108,9 +109,13 @@ function loadChartFontsOnce(): Promise { notifyChartFontLoadListeners(); return fonts; }) - .catch((error) => { + .catch((error: unknown) => { + Log.hmmm('Chart fonts failed to load', { + error: error instanceof Error ? error.message : String(error), + }); loadPromise = null; - throw error; + notifyChartFontLoadListeners(); + return EMPTY_CHART_FONTS; }); } diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartPie.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartPie.tsx index 4d9d9612f3f0..9d1f1f800dc6 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartPie.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartPie.tsx @@ -1,4 +1,4 @@ -import React, {useMemo} from 'react'; +import React from 'react'; import {useAmbientTRenderEngine} from 'react-native-render-html'; import type {TNode} from 'react-native-render-html'; import {Pie} from 'victory-native'; @@ -22,14 +22,11 @@ function VictoryChartPie({tnode}: VictoryChartPieProps) { const size = radius ? radius * 2 : undefined; const labelRadius = tnode.attributes.labelradius !== undefined ? Number(parseAttribute(tnode.attributes.labelradius)) : undefined; - const pieLabelConfig = useMemo(() => { - const labelComponentNode = parseComponent(tnode.attributes.labelcomponent, renderEngine, 'victorylabel'); - const labelItemTemplate = labelComponentNode ? parseVictoryLabelNode(labelComponentNode).labelItems?.at(0) : undefined; - const dataLabels = Object.values(data).map((entry) => (entry as PolarChartData).label); - const labels = parseAttribute(tnode.attributes.labels); - - return {labelItemTemplate, dataLabels, labels}; - }, [data, renderEngine, tnode.attributes.labelcomponent, tnode.attributes.labels]); + const labelComponentNode = parseComponent(tnode.attributes.labelcomponent, renderEngine, 'victorylabel'); + const labelItemTemplate = labelComponentNode ? parseVictoryLabelNode(labelComponentNode).labelItems?.at(0) : undefined; + const dataLabels = Object.values(data).map((entry) => (entry as PolarChartData).label); + const labels = parseAttribute(tnode.attributes.labels); + const pieLabelConfig = {labelItemTemplate, dataLabels, labels}; return ( Date: Wed, 3 Jun 2026 11:05:46 -0700 Subject: [PATCH 21/35] chore(charts): remove redundant component displayNames Drop displayName assignments that only repeat the function name. Co-authored-by: Cursor --- src/components/Charts/context/ChartFontsProvider.native.tsx | 4 ---- src/components/Charts/context/ChartFontsProvider.tsx | 4 ---- .../VictoryChartRenderer/BaseVictoryChartRenderer.tsx | 2 -- .../VictoryChartRenderer/components/VictoryChartCartesian.tsx | 2 -- .../VictoryChartRenderer/components/VictoryChartLabel.tsx | 2 -- .../VictoryChartRenderer/components/VictoryChartLegend.tsx | 2 -- .../VictoryChartRenderer/components/VictoryChartPie.tsx | 2 -- .../VictoryChartRenderer/components/VictoryChartPieLabel.tsx | 2 -- .../VictoryChartRenderer/components/VictoryChartPolar.tsx | 2 -- .../VictoryChartRenderer/context/VictoryChartContext.tsx | 2 -- 10 files changed, 24 deletions(-) diff --git a/src/components/Charts/context/ChartFontsProvider.native.tsx b/src/components/Charts/context/ChartFontsProvider.native.tsx index 4f98b3223b4f..3db66295e728 100644 --- a/src/components/Charts/context/ChartFontsProvider.native.tsx +++ b/src/components/Charts/context/ChartFontsProvider.native.tsx @@ -14,8 +14,6 @@ function ChartFontsLoaderProvider({children}: {children: React.ReactNode}) { return {children}; } -ChartFontsLoaderProvider.displayName = 'ChartFontsLoaderProvider'; - function ChartFontsProvider({children, value}: ChartFontsProviderProps) { if (value) { return {children}; @@ -24,6 +22,4 @@ function ChartFontsProvider({children, value}: ChartFontsProviderProps) { return {children}; } -ChartFontsProvider.displayName = 'ChartFontsProvider'; - export default ChartFontsProvider; diff --git a/src/components/Charts/context/ChartFontsProvider.tsx b/src/components/Charts/context/ChartFontsProvider.tsx index 4f98b3223b4f..3db66295e728 100644 --- a/src/components/Charts/context/ChartFontsProvider.tsx +++ b/src/components/Charts/context/ChartFontsProvider.tsx @@ -14,8 +14,6 @@ function ChartFontsLoaderProvider({children}: {children: React.ReactNode}) { return {children}; } -ChartFontsLoaderProvider.displayName = 'ChartFontsLoaderProvider'; - function ChartFontsProvider({children, value}: ChartFontsProviderProps) { if (value) { return {children}; @@ -24,6 +22,4 @@ function ChartFontsProvider({children, value}: ChartFontsProviderProps) { return {children}; } -ChartFontsProvider.displayName = 'ChartFontsProvider'; - export default ChartFontsProvider; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/BaseVictoryChartRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/BaseVictoryChartRenderer.tsx index e058d52f31b2..2ceb4d4ac69f 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/BaseVictoryChartRenderer.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/BaseVictoryChartRenderer.tsx @@ -17,6 +17,4 @@ function BaseVictoryChartRenderer({tnode}: VictoryChartRendererProps) { ); } -BaseVictoryChartRenderer.displayName = 'BaseVictoryChartRenderer'; - export default BaseVictoryChartRenderer; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartCartesian.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartCartesian.tsx index e6ed3e7232a9..819b691d8407 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartCartesian.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartCartesian.tsx @@ -59,6 +59,4 @@ function VictoryChartCartesian() { ); } -VictoryChartCartesian.displayName = 'VictoryChartCartesian'; - export default VictoryChartCartesian; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartLabel.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartLabel.tsx index 0efc95c9e0af..702c63dbca90 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartLabel.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartLabel.tsx @@ -70,6 +70,4 @@ function VictoryChartLabel({x, y, text, color, fontSize, fontWeight, fontFamily, }); } -VictoryChartLabel.displayName = 'VictoryChartLabel'; - export default VictoryChartLabel; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartLegend.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartLegend.tsx index 9ef0370ca409..6588c6fc9d61 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartLegend.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartLegend.tsx @@ -75,6 +75,4 @@ function VictoryChartLegend({x, y, entries, gutter, symbolSpacer}: VictoryChartL }); } -VictoryChartLegend.displayName = 'VictoryChartLegend'; - export default VictoryChartLegend; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartPie.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartPie.tsx index 9d1f1f800dc6..26a9a798cb48 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartPie.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartPie.tsx @@ -49,6 +49,4 @@ function VictoryChartPie({tnode}: VictoryChartPieProps) { ); } -VictoryChartPie.displayName = 'VictoryChartPie'; - export default VictoryChartPie; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartPieLabel.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartPieLabel.tsx index 9b0e6178e474..ae0dbb3bf80f 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartPieLabel.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartPieLabel.tsx @@ -39,6 +39,4 @@ function VictoryChartPieLabel({slice, labelItemTemplate, dataLabels, labels, lab return ; } -VictoryChartPieLabel.displayName = 'VictoryChartPieLabel'; - export default VictoryChartPieLabel; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartPolar.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartPolar.tsx index 684a667d6d05..ece38b95932b 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartPolar.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartPolar.tsx @@ -45,6 +45,4 @@ function VictoryChartPolar() { ); } -VictoryChartPolar.displayName = 'VictoryChartPolar'; - export default VictoryChartPolar; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartContext.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartContext.tsx index 45b051dd00e5..332d34825f69 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartContext.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartContext.tsx @@ -75,8 +75,6 @@ function VictoryChartProvider({tnode, children}: {tnode: TNode; children: React. return {children}; } -VictoryChartProvider.displayName = 'VictoryChartProvider'; - function useVictoryChartContext(): VictoryChartContextValue { const context = useContext(VictoryChartContext); if (!context) { From f8a020016257e97377ad8ea747abb9a2aa53f3c8 Mon Sep 17 00:00:00 2001 From: rory Date: Wed, 3 Jun 2026 11:45:08 -0700 Subject: [PATCH 22/35] fix(charts): keep VictoryChartContext out of VictoryChartPie Move chart context usage into VictoryChartPieLabel (prod API) so polar pie charts work without prop-drilling label config through the slice tree. Co-authored-by: Cursor --- .../components/VictoryChartPie.tsx | 19 +------- .../components/VictoryChartPieLabel.tsx | 48 ++++++++++--------- 2 files changed, 27 insertions(+), 40 deletions(-) diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartPie.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartPie.tsx index 26a9a798cb48..00bfbd3dbe0e 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartPie.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartPie.tsx @@ -1,12 +1,7 @@ import React from 'react'; -import {useAmbientTRenderEngine} from 'react-native-render-html'; import type {TNode} from 'react-native-render-html'; import {Pie} from 'victory-native'; -import {useVictoryChartContext} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartContext'; -import parseVictoryLabelNode from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/parsers/victoryLabelParser'; -import type {PolarChartData} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/types'; import parseAttribute from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/parseAttribute'; -import parseComponent from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/parseComponent'; import VictoryChartPieLabel from './VictoryChartPieLabel'; type VictoryChartPieProps = {tnode: TNode}; @@ -15,18 +10,9 @@ type VictoryChartPieProps = {tnode: TNode}; const START_ANGLE = 270; function VictoryChartPie({tnode}: VictoryChartPieProps) { - const {data} = useVictoryChartContext(); - const renderEngine = useAmbientTRenderEngine(); const innerRadius = tnode.attributes.innerradius !== undefined ? Number(parseAttribute(tnode.attributes.innerradius)) : undefined; const radius = tnode.attributes.radius !== undefined ? Number(parseAttribute(tnode.attributes.radius)) : undefined; const size = radius ? radius * 2 : undefined; - const labelRadius = tnode.attributes.labelradius !== undefined ? Number(parseAttribute(tnode.attributes.labelradius)) : undefined; - - const labelComponentNode = parseComponent(tnode.attributes.labelcomponent, renderEngine, 'victorylabel'); - const labelItemTemplate = labelComponentNode ? parseVictoryLabelNode(labelComponentNode).labelItems?.at(0) : undefined; - const dataLabels = Object.values(data).map((entry) => (entry as PolarChartData).label); - const labels = parseAttribute(tnode.attributes.labels); - const pieLabelConfig = {labelItemTemplate, dataLabels, labels}; return ( ( )} diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartPieLabel.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartPieLabel.tsx index ae0dbb3bf80f..4eda84bbfcf1 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartPieLabel.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartPieLabel.tsx @@ -1,40 +1,44 @@ import React from 'react'; +import {useAmbientTRenderEngine} from 'react-native-render-html'; +import type {TNode} from 'react-native-render-html'; import type {PieSliceData} from 'victory-native'; -import type {LabelItem} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/types'; +import {useVictoryChartContext} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartContext'; +import parseVictoryLabelNode from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/parsers/victoryLabelParser'; +import type {PolarChartData} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/types'; +import parseAttribute from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/parseAttribute'; +import parseComponent from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/parseComponent'; import VictoryChartLabel from './VictoryChartLabel'; type VictoryChartPieLabelProps = { + tnode: TNode; slice: PieSliceData; - labelItemTemplate?: LabelItem; - dataLabels: Array; - labels?: string[]; - labelRadius?: number; }; const RADIAN = Math.PI / 180; -/** - * Renders a pie slice label inside the Skia canvas. Must not use React context hooks - * because this runs in victory-native's pie slice render callback. - */ -function VictoryChartPieLabel({slice, labelItemTemplate, dataLabels, labels, labelRadius}: VictoryChartPieLabelProps) { - if (!labelItemTemplate) { +function VictoryChartPieLabel({tnode, slice}: VictoryChartPieLabelProps) { + const {data} = useVictoryChartContext(); + const renderEngine = useAmbientTRenderEngine(); + const labelComponentNode = parseComponent(tnode.attributes.labelcomponent, renderEngine, 'victorylabel'); + const labelItem = labelComponentNode ? parseVictoryLabelNode(labelComponentNode).labelItems?.at(0) : undefined; + + if (!labelItem) { return null; } + const dataLabels = Object.values(data).map((entry) => (entry as PolarChartData).label); + const labels = parseAttribute(tnode.attributes.labels); const text = labels?.[dataLabels.indexOf(slice.label)] ?? slice.label; - const resolvedLabelRadius = labelRadius ?? slice.radius; + + const labelRadius = tnode.attributes.labelradius !== undefined ? Number(parseAttribute(tnode.attributes.labelradius)) : slice.radius; const midAngle = (slice.startAngle + slice.endAngle) / 2; - const x = slice.center.x + resolvedLabelRadius * Math.cos(-midAngle * RADIAN); - const y = slice.center.y + resolvedLabelRadius * Math.sin(midAngle * RADIAN); - - const labelItem: LabelItem = { - ...labelItemTemplate, - text, - x, - y, - textAnchor: 'middle', - }; + const x = slice.center.x + labelRadius * Math.cos(-midAngle * RADIAN); + const y = slice.center.y + labelRadius * Math.sin(midAngle * RADIAN); + + labelItem.text = text; + labelItem.x = x; + labelItem.y = y; + labelItem.textAnchor = 'middle'; return ; } From e919a423e455e6b3f9c8ad8ed74fdf2e72914f44 Mon Sep 17 00:00:00 2001 From: rory Date: Wed, 3 Jun 2026 11:46:09 -0700 Subject: [PATCH 23/35] chore(charts): drop redundant ChartFontsProvider in cartesian overlays BaseVictoryChartRenderer already wraps charts with ChartFontsProvider; renderOutside labels can use that context like pie slice callbacks do. Co-authored-by: Cursor --- .../components/VictoryChartCartesian.tsx | 31 +++++++++---------- .../components/VictoryChartCategories.tsx | 2 -- 2 files changed, 14 insertions(+), 19 deletions(-) diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartCartesian.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartCartesian.tsx index 819b691d8407..8c663ee4f5b1 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartCartesian.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartCartesian.tsx @@ -1,6 +1,5 @@ import React from 'react'; import {CartesianChart} from 'victory-native'; -import {ChartFontsProvider} from '@components/Charts/hooks'; import {useVictoryChartContext} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartContext'; import {VictoryChartRenderArgsProvider} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartRenderArgsContext'; import getHierarchyID from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/getHierarchyID'; @@ -26,22 +25,20 @@ function VictoryChartCartesian() { domainPadding={domainPadding} padding={padding} renderOutside={(renderArgs) => ( - - - {labelItems.map((labelItem) => ( - - ))} - {legendItems.map((legendItem) => ( - - ))} - - + + {labelItems.map((labelItem) => ( + + ))} + {legendItems.map((legendItem) => ( + + ))} + )} > {(renderArgs) => ( diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartCategories.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartCategories.tsx index 348230c9c9da..5fdc144c38a8 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartCategories.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartCategories.tsx @@ -24,6 +24,4 @@ function VictoryChartCategories({tnode}: VictoryChartCategoriesProps) { return ; } -VictoryChartCategories.displayName = 'VictoryChartCategories'; - export default VictoryChartCategories; From 164b5fde6c04738cf61434ca487b8847d0cf6e9e Mon Sep 17 00:00:00 2001 From: rory Date: Wed, 3 Jun 2026 11:47:41 -0700 Subject: [PATCH 24/35] fix(charts): restore ChartFontsProvider in cartesian renderOutside ChartFontsContext does not reach renderOutside from the base provider; nested ChartFontsProvider is required for overlay labels and legends. Co-authored-by: Cursor --- .../components/VictoryChartCartesian.tsx | 31 ++++++++++--------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartCartesian.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartCartesian.tsx index 8c663ee4f5b1..819b691d8407 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartCartesian.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartCartesian.tsx @@ -1,5 +1,6 @@ import React from 'react'; import {CartesianChart} from 'victory-native'; +import {ChartFontsProvider} from '@components/Charts/hooks'; import {useVictoryChartContext} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartContext'; import {VictoryChartRenderArgsProvider} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartRenderArgsContext'; import getHierarchyID from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/getHierarchyID'; @@ -25,20 +26,22 @@ function VictoryChartCartesian() { domainPadding={domainPadding} padding={padding} renderOutside={(renderArgs) => ( - - {labelItems.map((labelItem) => ( - - ))} - {legendItems.map((legendItem) => ( - - ))} - + + + {labelItems.map((labelItem) => ( + + ))} + {legendItems.map((legendItem) => ( + + ))} + + )} > {(renderArgs) => ( From e18dd95cfb6f4418b0cee52771d1b935515f2178 Mon Sep 17 00:00:00 2001 From: rory Date: Wed, 3 Jun 2026 12:04:33 -0700 Subject: [PATCH 25/35] fix(charts): resolve bold string weight in getChartSkiaTypeface Treat fontWeight 'bold' like 700, add unit tests for typeface resolution, and iterate chart keys without casting SYSTEM entries. Co-authored-by: Cursor --- .../Charts/utils/getChartSkiaTypeface.ts | 17 +++++--- tests/unit/getChartSkiaTypefaceTest.ts | 43 +++++++++++++++++++ 2 files changed, 55 insertions(+), 5 deletions(-) create mode 100644 tests/unit/getChartSkiaTypefaceTest.ts diff --git a/src/components/Charts/utils/getChartSkiaTypeface.ts b/src/components/Charts/utils/getChartSkiaTypeface.ts index 4b62a0c34a2c..288b93468e72 100644 --- a/src/components/Charts/utils/getChartSkiaTypeface.ts +++ b/src/components/Charts/utils/getChartSkiaTypeface.ts @@ -12,7 +12,11 @@ function normalizeFontStyle(fontStyle: string | undefined): ChartLabelFontStyle } function normalizeFontWeight(fontWeight: string | number | undefined): ChartLabelFontWeight { - return Number(fontWeight) === 700 ? 'bold' : 'normal'; + if (fontWeight === 'bold' || Number(fontWeight) === 700) { + return 'bold'; + } + + return 'normal'; } function fontWeightMatches(definitionWeight: TextStyle['fontWeight'], labelWeight: ChartLabelFontWeight): boolean { @@ -32,11 +36,14 @@ function getChartSkiaTypefaceKey(fontFamily: string | undefined, fontStyle: Char return fontStyle === 'italic' ? 'EXP_NEW_KANSAS_MEDIUM_ITALIC' : 'EXP_NEW_KANSAS_MEDIUM'; } - const matchingEntry = (Object.entries(singleFontFamily) as Array<[ChartSkiaTypefaceKey, (typeof singleFontFamily)[ChartSkiaTypefaceKey]]>).find( - ([, definition]) => definition.fontFamily === resolvedFontFamily && normalizeFontStyle(definition.fontStyle) === fontStyle && fontWeightMatches(definition.fontWeight, fontWeight), - ); + const matchingKey = (Object.keys(singleFontFamily) as Array) + .filter((key): key is ChartSkiaTypefaceKey => key !== 'SYSTEM') + .find((key) => { + const definition = singleFontFamily[key]; + return definition.fontFamily === resolvedFontFamily && normalizeFontStyle(definition.fontStyle) === fontStyle && fontWeightMatches(definition.fontWeight, fontWeight); + }); - return matchingEntry?.[0] ?? 'EXP_NEUE'; + return matchingKey ?? 'EXP_NEUE'; } function getChartSkiaTypeface( diff --git a/tests/unit/getChartSkiaTypefaceTest.ts b/tests/unit/getChartSkiaTypefaceTest.ts new file mode 100644 index 000000000000..18c4877b79a0 --- /dev/null +++ b/tests/unit/getChartSkiaTypefaceTest.ts @@ -0,0 +1,43 @@ +import type {SkTypeface} from '@shopify/react-native-skia'; +import type {ChartDefaultTypeface, ChartSkiaTypefaceKey} from '@components/Charts/types/chartSkiaTypefaceTypes'; +import getChartSkiaTypeface from '@components/Charts/utils/getChartSkiaTypeface'; +import singleFontFamily from '@styles/utils/FontUtils/fontFamily/singleFontFamily'; + +const CHART_SKIA_TYPEFACE_KEYS = Object.keys(singleFontFamily).filter((key): key is ChartSkiaTypefaceKey => key !== 'SYSTEM'); + +function makeTypefaces(): ChartDefaultTypeface { + return Object.fromEntries(CHART_SKIA_TYPEFACE_KEYS.map((key) => [key, {id: key} as unknown as SkTypeface])) as ChartDefaultTypeface; +} + +describe('getChartSkiaTypeface', () => { + const typefaces = makeTypefaces(); + + it('should resolve numeric bold weight to the bold typeface', () => { + const typeface = getChartSkiaTypeface(typefaces, {fontWeight: 700}); + expect(typeface).toBe(typefaces.EXP_NEUE_BOLD); + }); + + it('should resolve string bold weight to the bold typeface', () => { + const typeface = getChartSkiaTypeface(typefaces, {fontWeight: 'bold'}); + expect(typeface).toBe(typefaces.EXP_NEUE_BOLD); + }); + + it('should resolve normal weight to the regular typeface', () => { + const typeface = getChartSkiaTypeface(typefaces, {fontWeight: 400}); + expect(typeface).toBe(typefaces.EXP_NEUE); + }); + + it('should resolve Expensify New Kansas by font family', () => { + const typeface = getChartSkiaTypeface(typefaces, {fontFamily: singleFontFamily.EXP_NEW_KANSAS_MEDIUM.fontFamily}); + expect(typeface).toBe(typefaces.EXP_NEW_KANSAS_MEDIUM); + }); + + it('should resolve italic Expensify Neue bold to the bold italic typeface', () => { + const typeface = getChartSkiaTypeface(typefaces, { + fontFamily: singleFontFamily.EXP_NEUE.fontFamily, + fontStyle: 'italic', + fontWeight: 'bold', + }); + expect(typeface).toBe(typefaces.EXP_NEUE_BOLD_ITALIC); + }); +}); From 0949e9a346fb28edd46ddf743bb3c01578cbaa36 Mon Sep 17 00:00:00 2001 From: rory Date: Wed, 3 Jun 2026 12:15:39 -0700 Subject: [PATCH 26/35] refactor(charts): dedupe font loads via shared constants Build fontMgr from decoded typefaces plus Noto-only assets instead of loading Neue, Mono, and Kansas twice. Share CHART_FONT_FAMILY_NAMES with VictoryTheme and derive the empty cache typeface map from assets. Co-authored-by: Cursor --- src/components/Charts/VictoryTheme.ts | 6 +-- .../Charts/utils/chartFontAssets.native.ts | 13 +++--- .../Charts/utils/chartFontAssets.ts | 13 +++--- .../Charts/utils/chartFontConstants.ts | 16 +++++++ .../Charts/utils/chartFontsCache.ts | 44 ++++++++++--------- .../Charts/utils/createChartFontsValue.ts | 18 -------- .../components/Charts/VictoryTheme.test.ts | 3 +- 7 files changed, 53 insertions(+), 60 deletions(-) create mode 100644 src/components/Charts/utils/chartFontConstants.ts delete mode 100644 src/components/Charts/utils/createChartFontsValue.ts diff --git a/src/components/Charts/VictoryTheme.ts b/src/components/Charts/VictoryTheme.ts index 8db0296cd17a..a0bcfe21cf0a 100644 --- a/src/components/Charts/VictoryTheme.ts +++ b/src/components/Charts/VictoryTheme.ts @@ -2,9 +2,7 @@ * Centralized styles and layout constants for the chart components. */ import colors from '@styles/theme/colors'; - -/** Font families used by all chart label components (Paragraph API multi-font fallback). */ -const CHART_FONT_FAMILIES = ['ExpensifyNeue', 'ExpensifyMono', 'ExpensifyNewKansas', 'NotoSansSymbols', 'NotoSansSCMonths']; +import {CHART_FONT_FAMILY_NAMES} from './utils/chartFontConstants'; /** * Expensify Chart Color Palette. @@ -54,7 +52,7 @@ const VictoryTheme = { default: getChartColor(DEFAULT_CHART_COLOR_INDEX), getColor: getChartColor, }, - fontFamilies: CHART_FONT_FAMILIES, + fontFamilies: [...CHART_FONT_FAMILY_NAMES], axis: { /** Number of Y-axis ticks (including zero) */ tickCount: 5, diff --git a/src/components/Charts/utils/chartFontAssets.native.ts b/src/components/Charts/utils/chartFontAssets.native.ts index ba261225a1b4..cfb0b204cebd 100644 --- a/src/components/Charts/utils/chartFontAssets.native.ts +++ b/src/components/Charts/utils/chartFontAssets.native.ts @@ -29,12 +29,9 @@ const CHART_SKIA_TYPEFACE_ASSETS: Record = { CUSTOM_EMOJI_FONT, }; -const CHART_FONT_MANAGER_FAMILIES: Record = { - ExpensifyNeue: [EXPENSIFY_NEUE_REGULAR, EXPENSIFY_NEUE_BOLD, EXPENSIFY_NEUE_ITALIC, EXPENSIFY_NEUE_BOLD_ITALIC], - ExpensifyMono: [EXPENSIFY_MONO_REGULAR, EXPENSIFY_MONO_BOLD, EXPENSIFY_MONO_ITALIC, EXPENSIFY_MONO_BOLD_ITALIC], - ExpensifyNewKansas: [EXPENSIFY_NEW_KANSAS_MEDIUM, EXPENSIFY_NEW_KANSAS_MEDIUM_ITALIC], - NotoSansSymbols: [SANS_SYMBOLS_FONT], - NotoSansSCMonths: [SANS_SC_MONTHS_FONT], -}; +const CHART_FONT_MGR_NOTO_ASSETS = { + NotoSansSymbols: SANS_SYMBOLS_FONT, + NotoSansSCMonths: SANS_SC_MONTHS_FONT, +} as const; -export {CHART_FONT_MANAGER_FAMILIES, CHART_SKIA_TYPEFACE_ASSETS}; +export {CHART_FONT_MGR_NOTO_ASSETS, CHART_SKIA_TYPEFACE_ASSETS}; diff --git a/src/components/Charts/utils/chartFontAssets.ts b/src/components/Charts/utils/chartFontAssets.ts index 26be32f9bbd9..1bd1654289d3 100644 --- a/src/components/Charts/utils/chartFontAssets.ts +++ b/src/components/Charts/utils/chartFontAssets.ts @@ -30,12 +30,9 @@ const CHART_SKIA_TYPEFACE_ASSETS: Record = { CUSTOM_EMOJI_FONT, }; -const CHART_FONT_MANAGER_FAMILIES: Record = { - ExpensifyNeue: [EXPENSIFY_NEUE_REGULAR, EXPENSIFY_NEUE_BOLD, EXPENSIFY_NEUE_ITALIC, EXPENSIFY_NEUE_BOLD_ITALIC], - ExpensifyMono: [EXPENSIFY_MONO_REGULAR, EXPENSIFY_MONO_BOLD, EXPENSIFY_MONO_ITALIC, EXPENSIFY_MONO_BOLD_ITALIC], - ExpensifyNewKansas: [EXPENSIFY_NEW_KANSAS_MEDIUM, EXPENSIFY_NEW_KANSAS_MEDIUM_ITALIC], - NotoSansSymbols: [SANS_SYMBOLS_FONT], - NotoSansSCMonths: [SANS_SC_MONTHS_FONT], -}; +const CHART_FONT_MGR_NOTO_ASSETS = { + NotoSansSymbols: SANS_SYMBOLS_FONT, + NotoSansSCMonths: SANS_SC_MONTHS_FONT, +} as const; -export {CHART_FONT_MANAGER_FAMILIES, CHART_SKIA_TYPEFACE_ASSETS}; +export {CHART_FONT_MGR_NOTO_ASSETS, CHART_SKIA_TYPEFACE_ASSETS}; diff --git a/src/components/Charts/utils/chartFontConstants.ts b/src/components/Charts/utils/chartFontConstants.ts new file mode 100644 index 000000000000..3d98703a116e --- /dev/null +++ b/src/components/Charts/utils/chartFontConstants.ts @@ -0,0 +1,16 @@ +import type {ChartSkiaTypefaceKey} from '@components/Charts/types/chartSkiaTypefaceTypes'; + +/** Font families registered on Skia fontMgr and used by Paragraph API multi-font fallback. */ +const CHART_FONT_FAMILY_NAMES = ['ExpensifyNeue', 'ExpensifyMono', 'ExpensifyNewKansas', 'NotoSansSymbols', 'NotoSansSCMonths'] as const; + +type ChartFontFamilyName = (typeof CHART_FONT_FAMILY_NAMES)[number]; + +/** Maps fontMgr family names to already-loaded chart typefaces (Noto families load separately). */ +const CHART_FONT_MGR_FROM_TYPEFACES: Record, ChartSkiaTypefaceKey[]> = { + ExpensifyNeue: ['EXP_NEUE', 'EXP_NEUE_BOLD', 'EXP_NEUE_ITALIC', 'EXP_NEUE_BOLD_ITALIC'], + ExpensifyMono: ['MONOSPACE', 'MONOSPACE_BOLD', 'MONOSPACE_ITALIC', 'MONOSPACE_BOLD_ITALIC'], + ExpensifyNewKansas: ['EXP_NEW_KANSAS_MEDIUM', 'EXP_NEW_KANSAS_MEDIUM_ITALIC'], +}; + +export {CHART_FONT_FAMILY_NAMES, CHART_FONT_MGR_FROM_TYPEFACES}; +export type {ChartFontFamilyName}; diff --git a/src/components/Charts/utils/chartFontsCache.ts b/src/components/Charts/utils/chartFontsCache.ts index 4b9ac4a4dbbd..cb530d49abe1 100644 --- a/src/components/Charts/utils/chartFontsCache.ts +++ b/src/components/Charts/utils/chartFontsCache.ts @@ -4,23 +4,11 @@ import {Image} from 'react-native'; import type {ChartFontsValue} from '@components/Charts/types/chartFontsTypes'; import type {ChartDefaultTypeface, ChartSkiaTypefaceKey} from '@components/Charts/types/chartSkiaTypefaceTypes'; import Log from '@libs/Log'; -import {CHART_FONT_MANAGER_FAMILIES, CHART_SKIA_TYPEFACE_ASSETS} from './chartFontAssets'; -import createChartFontsValue from './createChartFontsValue'; +import {CHART_FONT_MGR_NOTO_ASSETS, CHART_SKIA_TYPEFACE_ASSETS} from './chartFontAssets'; +import {CHART_FONT_MGR_FROM_TYPEFACES} from './chartFontConstants'; const EMPTY_CHART_FONTS: ChartFontsValue = { - typefaces: { - MONOSPACE: null, - MONOSPACE_BOLD: null, - MONOSPACE_ITALIC: null, - MONOSPACE_BOLD_ITALIC: null, - EXP_NEUE: null, - EXP_NEUE_BOLD: null, - EXP_NEUE_ITALIC: null, - EXP_NEUE_BOLD_ITALIC: null, - EXP_NEW_KANSAS_MEDIUM: null, - EXP_NEW_KANSAS_MEDIUM_ITALIC: null, - CUSTOM_EMOJI_FONT: null, - }, + typefaces: Object.fromEntries((Object.keys(CHART_SKIA_TYPEFACE_ASSETS) as ChartSkiaTypefaceKey[]).map((key) => [key, null])) as ChartDefaultTypeface, fontMgr: null, }; @@ -65,18 +53,32 @@ function loadChartSkiaTypefaces(): Promise { ).then((entries) => Object.fromEntries(entries) as ChartDefaultTypeface); } -function loadFontManagerFamilies(): Promise> { +function buildChartFontsValue(typefaces: ChartDefaultTypeface): Promise { + const fontMgr = Skia.TypefaceFontProvider.Make(); + + for (const [familyName, typefaceKeys] of Object.entries(CHART_FONT_MGR_FROM_TYPEFACES)) { + for (const typefaceKey of typefaceKeys) { + const typeface = typefaces[typefaceKey]; + + if (typeface) { + fontMgr.registerFont(typeface, familyName); + } + } + } + return Promise.all( - Object.entries(CHART_FONT_MANAGER_FAMILIES).map(async ([familyName, familyAssets]) => { - const familyTypefaces = await Promise.all(familyAssets.map((asset) => loadTypefaceFromAsset(asset))); + Object.entries(CHART_FONT_MGR_NOTO_ASSETS).map(async ([familyName, asset]) => { + const typeface = await loadTypefaceFromAsset(asset); - return [familyName, familyTypefaces.filter((typeface): typeface is SkTypeface => typeface !== null)] as const; + if (typeface) { + fontMgr.registerFont(typeface, familyName); + } }), - ).then((entries) => Object.fromEntries(entries)); + ).then(() => ({typefaces, fontMgr})); } function loadChartFonts(): Promise { - return Promise.all([loadChartSkiaTypefaces(), loadFontManagerFamilies()]).then(([typefaces, fontManagerFamilies]) => createChartFontsValue(typefaces, fontManagerFamilies)); + return loadChartSkiaTypefaces().then(buildChartFontsValue); } function subscribeToChartFonts(listener: () => void): () => void { diff --git a/src/components/Charts/utils/createChartFontsValue.ts b/src/components/Charts/utils/createChartFontsValue.ts deleted file mode 100644 index 5b5154f82030..000000000000 --- a/src/components/Charts/utils/createChartFontsValue.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type {SkTypeface} from '@shopify/react-native-skia'; -import {Skia} from '@shopify/react-native-skia'; -import type {ChartFontsValue} from '@components/Charts/types/chartFontsTypes'; -import type {ChartDefaultTypeface} from '@components/Charts/types/chartSkiaTypefaceTypes'; - -function createChartFontsValue(typefaces: ChartDefaultTypeface, fontManagerFamilies: Record): ChartFontsValue { - const fontMgr = Skia.TypefaceFontProvider.Make(); - - for (const [familyName, familyTypefaces] of Object.entries(fontManagerFamilies)) { - for (const typeface of familyTypefaces) { - fontMgr.registerFont(typeface, familyName); - } - } - - return {typefaces, fontMgr}; -} - -export default createChartFontsValue; diff --git a/tests/unit/components/Charts/VictoryTheme.test.ts b/tests/unit/components/Charts/VictoryTheme.test.ts index 44f821ce0a26..7960ec115f6a 100644 --- a/tests/unit/components/Charts/VictoryTheme.test.ts +++ b/tests/unit/components/Charts/VictoryTheme.test.ts @@ -1,4 +1,5 @@ import type VictoryThemeType from '@components/Charts/VictoryTheme'; +import {CHART_FONT_FAMILY_NAMES} from '@components/Charts/utils/chartFontConstants'; import colors from '@styles/theme/colors'; /** @@ -108,7 +109,7 @@ describe('VictoryTheme', () => { describe('static configuration values', () => { it('exposes the expected font families in order', () => { const VictoryTheme = loadVictoryTheme(); - expect(VictoryTheme.fontFamilies).toEqual(['ExpensifyNeue', 'NotoSansSymbols', 'NotoSansSCMonths']); + expect(VictoryTheme.fontFamilies).toEqual([...CHART_FONT_FAMILY_NAMES]); }); it('exposes axis values', () => { From 7a0a7f904c73962ae5070a62b65788082cb176f6 Mon Sep 17 00:00:00 2001 From: rory Date: Wed, 3 Jun 2026 12:15:43 -0700 Subject: [PATCH 27/35] refactor(charts): centralize chart font weight normalization Share normalizeChartFontWeight across parsers and getChartSkiaTypeface so string 'bold' and numeric semibold weights resolve consistently. Co-authored-by: Cursor --- .../Charts/utils/getChartSkiaTypeface.ts | 14 ++++---------- .../Charts/utils/normalizeChartFontWeight.ts | 18 ++++++++++++++++++ .../parsers/victoryLabelParser.ts | 3 ++- .../parsers/victoryLegendParser.ts | 3 ++- tests/unit/getChartSkiaTypefaceTest.ts | 10 ++++++++++ 5 files changed, 36 insertions(+), 12 deletions(-) create mode 100644 src/components/Charts/utils/normalizeChartFontWeight.ts diff --git a/src/components/Charts/utils/getChartSkiaTypeface.ts b/src/components/Charts/utils/getChartSkiaTypeface.ts index 288b93468e72..c1f0bd1521ee 100644 --- a/src/components/Charts/utils/getChartSkiaTypeface.ts +++ b/src/components/Charts/utils/getChartSkiaTypeface.ts @@ -3,22 +3,15 @@ import type {TextStyle} from 'react-native'; import type {ChartDefaultTypeface, ChartSkiaTypefaceKey} from '@components/Charts/types/chartSkiaTypefaceTypes'; // eslint-disable-next-line no-restricted-imports import singleFontFamily from '@styles/utils/FontUtils/fontFamily/singleFontFamily'; +import type {ChartLabelFontWeight} from './normalizeChartFontWeight'; +import normalizeChartFontWeight from './normalizeChartFontWeight'; type ChartLabelFontStyle = 'normal' | 'italic'; -type ChartLabelFontWeight = 'normal' | 'bold'; function normalizeFontStyle(fontStyle: string | undefined): ChartLabelFontStyle { return fontStyle === 'italic' ? 'italic' : 'normal'; } -function normalizeFontWeight(fontWeight: string | number | undefined): ChartLabelFontWeight { - if (fontWeight === 'bold' || Number(fontWeight) === 700) { - return 'bold'; - } - - return 'normal'; -} - function fontWeightMatches(definitionWeight: TextStyle['fontWeight'], labelWeight: ChartLabelFontWeight): boolean { const numericWeight = Number(definitionWeight); @@ -32,6 +25,7 @@ function fontWeightMatches(definitionWeight: TextStyle['fontWeight'], labelWeigh function getChartSkiaTypefaceKey(fontFamily: string | undefined, fontStyle: ChartLabelFontStyle, fontWeight: ChartLabelFontWeight): ChartSkiaTypefaceKey { const resolvedFontFamily = fontFamily ?? singleFontFamily.EXP_NEUE.fontFamily; + // Kansas has no bold variant; weight is ignored and Medium is always used. if (resolvedFontFamily === singleFontFamily.EXP_NEW_KANSAS_MEDIUM.fontFamily) { return fontStyle === 'italic' ? 'EXP_NEW_KANSAS_MEDIUM_ITALIC' : 'EXP_NEW_KANSAS_MEDIUM'; } @@ -58,7 +52,7 @@ function getChartSkiaTypeface( fontWeight?: string | number; }, ): SkTypeface | null { - const typefaceKey = getChartSkiaTypefaceKey(fontFamily, normalizeFontStyle(fontStyle), normalizeFontWeight(fontWeight)); + const typefaceKey = getChartSkiaTypefaceKey(fontFamily, normalizeFontStyle(fontStyle), normalizeChartFontWeight(fontWeight)); return typefaces[typefaceKey]; } diff --git a/src/components/Charts/utils/normalizeChartFontWeight.ts b/src/components/Charts/utils/normalizeChartFontWeight.ts new file mode 100644 index 000000000000..f2154fe10a66 --- /dev/null +++ b/src/components/Charts/utils/normalizeChartFontWeight.ts @@ -0,0 +1,18 @@ +type ChartLabelFontWeight = 'normal' | 'bold'; + +function normalizeChartFontWeight(fontWeight: string | number | undefined): ChartLabelFontWeight { + if (fontWeight === 'bold') { + return 'bold'; + } + + const numericWeight = Number(fontWeight); + + if (!Number.isNaN(numericWeight) && numericWeight >= 600) { + return 'bold'; + } + + return 'normal'; +} + +export default normalizeChartFontWeight; +export type {ChartLabelFontWeight}; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/parsers/victoryLabelParser.ts b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/parsers/victoryLabelParser.ts index e6e4f9649d79..78c4b7c61845 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/parsers/victoryLabelParser.ts +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/parsers/victoryLabelParser.ts @@ -1,4 +1,5 @@ import type {TNode} from 'react-native-render-html'; +import normalizeChartFontWeight from '@components/Charts/utils/normalizeChartFontWeight'; import type {LabelItem, PartialProcessNodeResult, RawLabelStyle, TextAnchor} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/types'; import parseAttribute from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/parseAttribute'; @@ -39,7 +40,7 @@ function parseVictoryLabelNode(tnode: TNode): PartialProcessNodeResult { if (textStyle.fontWeight) { labelItem.fontWeight = { ...labelItem.fontWeight, - [index]: Number(textStyle.fontWeight) === 700 ? 'bold' : 'normal', + [index]: normalizeChartFontWeight(textStyle.fontWeight), }; } if (textStyle.fontFamily) { diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/parsers/victoryLegendParser.ts b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/parsers/victoryLegendParser.ts index f7cb5bb0cd26..6b848196008c 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/parsers/victoryLegendParser.ts +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/parsers/victoryLegendParser.ts @@ -1,4 +1,5 @@ import type {TNode} from 'react-native-render-html'; +import normalizeChartFontWeight from '@components/Charts/utils/normalizeChartFontWeight'; import type {LegendItem, LegendItemEntry, PartialProcessNodeResult, RawLegendData, RawLegendStyle} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/types'; import parseAttribute from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/parseAttribute'; @@ -13,7 +14,7 @@ function parseVictoryLegendNode(tnode: TNode): PartialProcessNodeResult { const style = parseAttribute(tnode.attributes.style); const color = style?.labels?.fill; const fontSize = style?.labels?.fontSize !== undefined ? Number(style.labels.fontSize) : undefined; - const fontWeight = Number(style?.labels?.fontWeight) === 700 ? 'bold' : undefined; + const fontWeight = style?.labels?.fontWeight !== undefined ? normalizeChartFontWeight(style.labels.fontWeight) : undefined; const fontFamily = style?.labels?.fontFamily; const fontStyle = style?.labels?.fontStyle; const entries: LegendItemEntry[] = (parseAttribute(tnode.attributes.data) ?? []).map((entry) => { diff --git a/tests/unit/getChartSkiaTypefaceTest.ts b/tests/unit/getChartSkiaTypefaceTest.ts index 18c4877b79a0..9abdcd35843f 100644 --- a/tests/unit/getChartSkiaTypefaceTest.ts +++ b/tests/unit/getChartSkiaTypefaceTest.ts @@ -27,6 +27,16 @@ describe('getChartSkiaTypeface', () => { expect(typeface).toBe(typefaces.EXP_NEUE); }); + it('should resolve semibold numeric weight to the bold typeface', () => { + const typeface = getChartSkiaTypeface(typefaces, {fontWeight: 600}); + expect(typeface).toBe(typefaces.EXP_NEUE_BOLD); + }); + + it('should resolve medium numeric weight to the regular typeface', () => { + const typeface = getChartSkiaTypeface(typefaces, {fontWeight: 500}); + expect(typeface).toBe(typefaces.EXP_NEUE); + }); + it('should resolve Expensify New Kansas by font family', () => { const typeface = getChartSkiaTypeface(typefaces, {fontFamily: singleFontFamily.EXP_NEW_KANSAS_MEDIUM.fontFamily}); expect(typeface).toBe(typefaces.EXP_NEW_KANSAS_MEDIUM); From fad7fc07cdb2cbabd9ecad617537a6c23e62023e Mon Sep 17 00:00:00 2001 From: rory Date: Wed, 3 Jun 2026 12:15:46 -0700 Subject: [PATCH 28/35] refactor(charts): add ChartFontsBoundary for Skia overlays Re-establish font context in cartesian renderOutside and polar subtrees where React context does not cross the Skia boundary. Co-authored-by: Cursor --- .../Charts/context/ChartFontsBoundary.tsx | 16 ++++++++++++++++ src/components/Charts/hooks/index.ts | 2 +- .../components/VictoryChartCartesian.tsx | 6 +++--- .../components/VictoryChartPolar.tsx | 6 +++--- 4 files changed, 23 insertions(+), 7 deletions(-) create mode 100644 src/components/Charts/context/ChartFontsBoundary.tsx diff --git a/src/components/Charts/context/ChartFontsBoundary.tsx b/src/components/Charts/context/ChartFontsBoundary.tsx new file mode 100644 index 000000000000..d98f346ffc24 --- /dev/null +++ b/src/components/Charts/context/ChartFontsBoundary.tsx @@ -0,0 +1,16 @@ +import React from 'react'; +import ChartFontsProvider from './ChartFontsProvider'; + +type ChartFontsBoundaryProps = { + children: React.ReactNode; +}; + +/** + * Re-mounts chart font context inside Skia overlay subtrees (e.g. cartesian renderOutside, + * polar children) where React context does not propagate across the Skia boundary. + */ +function ChartFontsBoundary({children}: ChartFontsBoundaryProps) { + return {children}; +} + +export default ChartFontsBoundary; diff --git a/src/components/Charts/hooks/index.ts b/src/components/Charts/hooks/index.ts index 2710594037f3..4362541d8eae 100644 --- a/src/components/Charts/hooks/index.ts +++ b/src/components/Charts/hooks/index.ts @@ -4,7 +4,7 @@ export {default as useChartParagraphs} from './useChartParagraphs'; export {default as useYAxisLabelWidth} from './useYAxisLabelWidth'; export {useChartFontManager} from '@components/Charts/context/ChartFontsContext'; export {default as ChartFontsProvider} from '@components/Charts/context/ChartFontsProvider'; -export {default as getChartSkiaTypeface} from '@components/Charts/utils/getChartSkiaTypeface'; +export {default as ChartFontsBoundary} from '@components/Charts/context/ChartFontsBoundary'; export {useChartTypefaces} from '@components/Charts/context/ChartFontsContext'; export {useChartInteractions, TOOLTIP_BAR_GAP} from './useChartInteractions'; export type {HitTestArgs} from './useChartInteractions'; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartCartesian.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartCartesian.tsx index 819b691d8407..4ab6a47ce991 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartCartesian.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartCartesian.tsx @@ -1,6 +1,6 @@ import React from 'react'; import {CartesianChart} from 'victory-native'; -import {ChartFontsProvider} from '@components/Charts/hooks'; +import {ChartFontsBoundary} from '@components/Charts/hooks'; import {useVictoryChartContext} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartContext'; import {VictoryChartRenderArgsProvider} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartRenderArgsContext'; import getHierarchyID from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/getHierarchyID'; @@ -26,7 +26,7 @@ function VictoryChartCartesian() { domainPadding={domainPadding} padding={padding} renderOutside={(renderArgs) => ( - + {labelItems.map((labelItem) => ( ))} - + )} > {(renderArgs) => ( diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartPolar.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartPolar.tsx index ece38b95932b..5a1a8b068999 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartPolar.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartPolar.tsx @@ -1,6 +1,6 @@ import React from 'react'; import {PolarChart} from 'victory-native'; -import {ChartFontsProvider} from '@components/Charts/hooks'; +import {ChartFontsBoundary} from '@components/Charts/hooks'; import {COLOR_KEY, LABEL_KEY, VALUE_KEY} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/constants'; import {useVictoryChartContext} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartContext'; import getHierarchyID from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/getHierarchyID'; @@ -21,7 +21,7 @@ function VictoryChartPolar() { valueKey={VALUE_KEY} colorKey={COLOR_KEY} > - + {tnode.children.map((child) => ( ))} - + ); } From a79a81c5b5490ce3de4c6fb71b20207682b687b4 Mon Sep 17 00:00:00 2001 From: rory Date: Wed, 3 Jun 2026 12:15:49 -0700 Subject: [PATCH 29/35] refactor(charts): hoist pie label parse and trim font types Parse pie labelcomponent once in VictoryChartPie, import getChartSkiaTypeface from utils, collapse ChartFontsValue alias, and remove duplicate native provider. Co-authored-by: Cursor --- .../Charts/context/ChartFontsContext.tsx | 6 ++--- .../context/ChartFontsProvider.native.tsx | 25 ------------------- .../Charts/types/chartFontsTypes.ts | 4 +-- .../components/VictoryChartLabel.tsx | 3 ++- .../components/VictoryChartLegend.tsx | 3 ++- .../components/VictoryChartPie.tsx | 12 ++++++++- .../components/VictoryChartPieLabel.tsx | 24 ++++++++---------- 7 files changed, 30 insertions(+), 47 deletions(-) delete mode 100644 src/components/Charts/context/ChartFontsProvider.native.tsx diff --git a/src/components/Charts/context/ChartFontsContext.tsx b/src/components/Charts/context/ChartFontsContext.tsx index 56ef96b9511c..7eb079d6e1f2 100644 --- a/src/components/Charts/context/ChartFontsContext.tsx +++ b/src/components/Charts/context/ChartFontsContext.tsx @@ -1,11 +1,11 @@ import type {SkTypefaceFontProvider} from '@shopify/react-native-skia'; import {createContext, useContext} from 'react'; -import type {ChartFontsContextValue} from '@components/Charts/types/chartFontsTypes'; +import type {ChartFontsValue} from '@components/Charts/types/chartFontsTypes'; import type {ChartDefaultTypeface} from '@components/Charts/types/chartSkiaTypefaceTypes'; -const ChartFontsContext = createContext(null); +const ChartFontsContext = createContext(null); -function useChartFontsContext(): ChartFontsContextValue { +function useChartFontsContext(): ChartFontsValue { const context = useContext(ChartFontsContext); if (!context) { diff --git a/src/components/Charts/context/ChartFontsProvider.native.tsx b/src/components/Charts/context/ChartFontsProvider.native.tsx deleted file mode 100644 index 3db66295e728..000000000000 --- a/src/components/Charts/context/ChartFontsProvider.native.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import React from 'react'; -import useChartFonts from '@components/Charts/hooks/useChartFonts'; -import type {ChartFontsValue} from '@components/Charts/types/chartFontsTypes'; -import {ChartFontsContext} from './ChartFontsContext'; - -type ChartFontsProviderProps = { - children: React.ReactNode; - value?: ChartFontsValue; -}; - -function ChartFontsLoaderProvider({children}: {children: React.ReactNode}) { - const fonts = useChartFonts(); - - return {children}; -} - -function ChartFontsProvider({children, value}: ChartFontsProviderProps) { - if (value) { - return {children}; - } - - return {children}; -} - -export default ChartFontsProvider; diff --git a/src/components/Charts/types/chartFontsTypes.ts b/src/components/Charts/types/chartFontsTypes.ts index eb55f5ad8c38..e5d7e563c952 100644 --- a/src/components/Charts/types/chartFontsTypes.ts +++ b/src/components/Charts/types/chartFontsTypes.ts @@ -6,6 +6,4 @@ type ChartFontsValue = { fontMgr: SkTypefaceFontProvider | null; }; -type ChartFontsContextValue = ChartFontsValue; - -export type {ChartFontsContextValue, ChartFontsValue}; +export type {ChartFontsValue}; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartLabel.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartLabel.tsx index 702c63dbca90..bc3a2ca25cd6 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartLabel.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartLabel.tsx @@ -1,7 +1,8 @@ import {Skia, Text as SkText} from '@shopify/react-native-skia'; import type {Color, SkFont} from '@shopify/react-native-skia'; import React from 'react'; -import {getChartSkiaTypeface, useChartTypefaces} from '@components/Charts/hooks'; +import {useChartTypefaces} from '@components/Charts/hooks'; +import getChartSkiaTypeface from '@components/Charts/utils/getChartSkiaTypeface'; import type {LabelItem} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/types'; import computeTextAnchorPosition from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/computeTextAnchorPosition'; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartLegend.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartLegend.tsx index 6588c6fc9d61..70d654878b68 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartLegend.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartLegend.tsx @@ -1,7 +1,8 @@ import {Circle, Skia, Text as SkText} from '@shopify/react-native-skia'; import type {Color, SkFont} from '@shopify/react-native-skia'; import React, {Fragment} from 'react'; -import {getChartSkiaTypeface, useChartTypefaces} from '@components/Charts/hooks'; +import {useChartTypefaces} from '@components/Charts/hooks'; +import getChartSkiaTypeface from '@components/Charts/utils/getChartSkiaTypeface'; import type {LegendItem} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/types'; type VictoryChartLegendProps = LegendItem; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartPie.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartPie.tsx index 00bfbd3dbe0e..6313a52560b2 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartPie.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartPie.tsx @@ -1,7 +1,10 @@ -import React from 'react'; +import React, {useMemo} from 'react'; +import {useAmbientTRenderEngine} from 'react-native-render-html'; import type {TNode} from 'react-native-render-html'; import {Pie} from 'victory-native'; +import parseVictoryLabelNode from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/parsers/victoryLabelParser'; import parseAttribute from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/parseAttribute'; +import parseComponent from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/parseComponent'; import VictoryChartPieLabel from './VictoryChartPieLabel'; type VictoryChartPieProps = {tnode: TNode}; @@ -10,6 +13,12 @@ type VictoryChartPieProps = {tnode: TNode}; const START_ANGLE = 270; function VictoryChartPie({tnode}: VictoryChartPieProps) { + const renderEngine = useAmbientTRenderEngine(); + const labelItemTemplate = useMemo(() => { + const labelComponentNode = parseComponent(tnode.attributes.labelcomponent, renderEngine, 'victorylabel'); + return labelComponentNode ? parseVictoryLabelNode(labelComponentNode).labelItems?.at(0) : undefined; + }, [tnode.attributes.labelcomponent, renderEngine]); + const innerRadius = tnode.attributes.innerradius !== undefined ? Number(parseAttribute(tnode.attributes.innerradius)) : undefined; const radius = tnode.attributes.radius !== undefined ? Number(parseAttribute(tnode.attributes.radius)) : undefined; const size = radius ? radius * 2 : undefined; @@ -25,6 +34,7 @@ function VictoryChartPie({tnode}: VictoryChartPieProps) { )} diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartPieLabel.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartPieLabel.tsx index 4eda84bbfcf1..6b99a8361a74 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartPieLabel.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartPieLabel.tsx @@ -1,28 +1,23 @@ import React from 'react'; -import {useAmbientTRenderEngine} from 'react-native-render-html'; import type {TNode} from 'react-native-render-html'; import type {PieSliceData} from 'victory-native'; import {useVictoryChartContext} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartContext'; -import parseVictoryLabelNode from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/parsers/victoryLabelParser'; -import type {PolarChartData} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/types'; +import type {LabelItem, PolarChartData} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/types'; import parseAttribute from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/parseAttribute'; -import parseComponent from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/parseComponent'; import VictoryChartLabel from './VictoryChartLabel'; type VictoryChartPieLabelProps = { tnode: TNode; slice: PieSliceData; + labelItemTemplate?: LabelItem; }; const RADIAN = Math.PI / 180; -function VictoryChartPieLabel({tnode, slice}: VictoryChartPieLabelProps) { +function VictoryChartPieLabel({tnode, slice, labelItemTemplate}: VictoryChartPieLabelProps) { const {data} = useVictoryChartContext(); - const renderEngine = useAmbientTRenderEngine(); - const labelComponentNode = parseComponent(tnode.attributes.labelcomponent, renderEngine, 'victorylabel'); - const labelItem = labelComponentNode ? parseVictoryLabelNode(labelComponentNode).labelItems?.at(0) : undefined; - if (!labelItem) { + if (!labelItemTemplate) { return null; } @@ -35,10 +30,13 @@ function VictoryChartPieLabel({tnode, slice}: VictoryChartPieLabelProps) { const x = slice.center.x + labelRadius * Math.cos(-midAngle * RADIAN); const y = slice.center.y + labelRadius * Math.sin(midAngle * RADIAN); - labelItem.text = text; - labelItem.x = x; - labelItem.y = y; - labelItem.textAnchor = 'middle'; + const labelItem: LabelItem = { + ...labelItemTemplate, + text, + x, + y, + textAnchor: 'middle', + }; return ; } From 4b51eee9926905db58517b0c719a5023b21fc87d Mon Sep 17 00:00:00 2001 From: rory Date: Wed, 3 Jun 2026 12:28:41 -0700 Subject: [PATCH 30/35] chore(charts): fix ESLint violations in chart font modules Satisfy prefer-default-export, prefer-type-fest, no-negated-variables, and no-restricted-imports in chart font types, assets, and tests. Co-authored-by: Cursor --- src/components/Charts/context/ChartFontsContext.tsx | 2 +- src/components/Charts/context/ChartFontsProvider.tsx | 2 +- src/components/Charts/hooks/useChartFonts.ts | 2 +- src/components/Charts/types/chartFontsTypes.ts | 2 +- src/components/Charts/utils/chartFontAssets.native.ts | 4 ++-- src/components/Charts/utils/chartFontAssets.ts | 4 ++-- src/components/Charts/utils/chartFontConstants.ts | 3 ++- src/components/Charts/utils/chartFontsCache.ts | 6 +++--- tests/unit/getChartSkiaTypefaceTest.ts | 8 ++++---- 9 files changed, 17 insertions(+), 16 deletions(-) diff --git a/src/components/Charts/context/ChartFontsContext.tsx b/src/components/Charts/context/ChartFontsContext.tsx index 7eb079d6e1f2..03ef4db13915 100644 --- a/src/components/Charts/context/ChartFontsContext.tsx +++ b/src/components/Charts/context/ChartFontsContext.tsx @@ -1,6 +1,6 @@ import type {SkTypefaceFontProvider} from '@shopify/react-native-skia'; import {createContext, useContext} from 'react'; -import type {ChartFontsValue} from '@components/Charts/types/chartFontsTypes'; +import type ChartFontsValue from '@components/Charts/types/chartFontsTypes'; import type {ChartDefaultTypeface} from '@components/Charts/types/chartSkiaTypefaceTypes'; const ChartFontsContext = createContext(null); diff --git a/src/components/Charts/context/ChartFontsProvider.tsx b/src/components/Charts/context/ChartFontsProvider.tsx index 3db66295e728..b90c811285ae 100644 --- a/src/components/Charts/context/ChartFontsProvider.tsx +++ b/src/components/Charts/context/ChartFontsProvider.tsx @@ -1,6 +1,6 @@ import React from 'react'; import useChartFonts from '@components/Charts/hooks/useChartFonts'; -import type {ChartFontsValue} from '@components/Charts/types/chartFontsTypes'; +import type ChartFontsValue from '@components/Charts/types/chartFontsTypes'; import {ChartFontsContext} from './ChartFontsContext'; type ChartFontsProviderProps = { diff --git a/src/components/Charts/hooks/useChartFonts.ts b/src/components/Charts/hooks/useChartFonts.ts index bcfeb445bcce..5b012437a9d1 100644 --- a/src/components/Charts/hooks/useChartFonts.ts +++ b/src/components/Charts/hooks/useChartFonts.ts @@ -1,5 +1,5 @@ import {useEffect, useSyncExternalStore} from 'react'; -import type {ChartFontsValue} from '@components/Charts/types/chartFontsTypes'; +import type ChartFontsValue from '@components/Charts/types/chartFontsTypes'; import {getChartFontsSnapshot, loadChartFontsOnce, subscribeToChartFonts} from '@components/Charts/utils/chartFontsCache'; function useChartFonts(): ChartFontsValue { diff --git a/src/components/Charts/types/chartFontsTypes.ts b/src/components/Charts/types/chartFontsTypes.ts index e5d7e563c952..deaecc415114 100644 --- a/src/components/Charts/types/chartFontsTypes.ts +++ b/src/components/Charts/types/chartFontsTypes.ts @@ -6,4 +6,4 @@ type ChartFontsValue = { fontMgr: SkTypefaceFontProvider | null; }; -export type {ChartFontsValue}; +export default ChartFontsValue; diff --git a/src/components/Charts/utils/chartFontAssets.native.ts b/src/components/Charts/utils/chartFontAssets.native.ts index cfb0b204cebd..585fdee494e4 100644 --- a/src/components/Charts/utils/chartFontAssets.native.ts +++ b/src/components/Charts/utils/chartFontAssets.native.ts @@ -29,9 +29,9 @@ const CHART_SKIA_TYPEFACE_ASSETS: Record = { CUSTOM_EMOJI_FONT, }; -const CHART_FONT_MGR_NOTO_ASSETS = { +const CHART_FONT_MGR_SUPPLEMENTAL_ASSETS = { NotoSansSymbols: SANS_SYMBOLS_FONT, NotoSansSCMonths: SANS_SC_MONTHS_FONT, } as const; -export {CHART_FONT_MGR_NOTO_ASSETS, CHART_SKIA_TYPEFACE_ASSETS}; +export {CHART_FONT_MGR_SUPPLEMENTAL_ASSETS, CHART_SKIA_TYPEFACE_ASSETS}; diff --git a/src/components/Charts/utils/chartFontAssets.ts b/src/components/Charts/utils/chartFontAssets.ts index 1bd1654289d3..a846320d3d7b 100644 --- a/src/components/Charts/utils/chartFontAssets.ts +++ b/src/components/Charts/utils/chartFontAssets.ts @@ -30,9 +30,9 @@ const CHART_SKIA_TYPEFACE_ASSETS: Record = { CUSTOM_EMOJI_FONT, }; -const CHART_FONT_MGR_NOTO_ASSETS = { +const CHART_FONT_MGR_SUPPLEMENTAL_ASSETS = { NotoSansSymbols: SANS_SYMBOLS_FONT, NotoSansSCMonths: SANS_SC_MONTHS_FONT, } as const; -export {CHART_FONT_MGR_NOTO_ASSETS, CHART_SKIA_TYPEFACE_ASSETS}; +export {CHART_FONT_MGR_SUPPLEMENTAL_ASSETS, CHART_SKIA_TYPEFACE_ASSETS}; diff --git a/src/components/Charts/utils/chartFontConstants.ts b/src/components/Charts/utils/chartFontConstants.ts index 3d98703a116e..a97328a3bf3a 100644 --- a/src/components/Charts/utils/chartFontConstants.ts +++ b/src/components/Charts/utils/chartFontConstants.ts @@ -1,9 +1,10 @@ +import type {TupleToUnion} from 'type-fest'; import type {ChartSkiaTypefaceKey} from '@components/Charts/types/chartSkiaTypefaceTypes'; /** Font families registered on Skia fontMgr and used by Paragraph API multi-font fallback. */ const CHART_FONT_FAMILY_NAMES = ['ExpensifyNeue', 'ExpensifyMono', 'ExpensifyNewKansas', 'NotoSansSymbols', 'NotoSansSCMonths'] as const; -type ChartFontFamilyName = (typeof CHART_FONT_FAMILY_NAMES)[number]; +type ChartFontFamilyName = TupleToUnion; /** Maps fontMgr family names to already-loaded chart typefaces (Noto families load separately). */ const CHART_FONT_MGR_FROM_TYPEFACES: Record, ChartSkiaTypefaceKey[]> = { diff --git a/src/components/Charts/utils/chartFontsCache.ts b/src/components/Charts/utils/chartFontsCache.ts index cb530d49abe1..3945e4d57550 100644 --- a/src/components/Charts/utils/chartFontsCache.ts +++ b/src/components/Charts/utils/chartFontsCache.ts @@ -1,10 +1,10 @@ import type {DataModule, SkTypeface} from '@shopify/react-native-skia'; import {Skia} from '@shopify/react-native-skia'; import {Image} from 'react-native'; -import type {ChartFontsValue} from '@components/Charts/types/chartFontsTypes'; +import type ChartFontsValue from '@components/Charts/types/chartFontsTypes'; import type {ChartDefaultTypeface, ChartSkiaTypefaceKey} from '@components/Charts/types/chartSkiaTypefaceTypes'; import Log from '@libs/Log'; -import {CHART_FONT_MGR_NOTO_ASSETS, CHART_SKIA_TYPEFACE_ASSETS} from './chartFontAssets'; +import {CHART_FONT_MGR_SUPPLEMENTAL_ASSETS, CHART_SKIA_TYPEFACE_ASSETS} from './chartFontAssets'; import {CHART_FONT_MGR_FROM_TYPEFACES} from './chartFontConstants'; const EMPTY_CHART_FONTS: ChartFontsValue = { @@ -67,7 +67,7 @@ function buildChartFontsValue(typefaces: ChartDefaultTypeface): Promise { + Object.entries(CHART_FONT_MGR_SUPPLEMENTAL_ASSETS).map(async ([familyName, asset]) => { const typeface = await loadTypefaceFromAsset(asset); if (typeface) { diff --git a/tests/unit/getChartSkiaTypefaceTest.ts b/tests/unit/getChartSkiaTypefaceTest.ts index 9abdcd35843f..c627d98a480f 100644 --- a/tests/unit/getChartSkiaTypefaceTest.ts +++ b/tests/unit/getChartSkiaTypefaceTest.ts @@ -1,9 +1,9 @@ import type {SkTypeface} from '@shopify/react-native-skia'; import type {ChartDefaultTypeface, ChartSkiaTypefaceKey} from '@components/Charts/types/chartSkiaTypefaceTypes'; +import {CHART_SKIA_TYPEFACE_ASSETS} from '@components/Charts/utils/chartFontAssets'; import getChartSkiaTypeface from '@components/Charts/utils/getChartSkiaTypeface'; -import singleFontFamily from '@styles/utils/FontUtils/fontFamily/singleFontFamily'; -const CHART_SKIA_TYPEFACE_KEYS = Object.keys(singleFontFamily).filter((key): key is ChartSkiaTypefaceKey => key !== 'SYSTEM'); +const CHART_SKIA_TYPEFACE_KEYS = Object.keys(CHART_SKIA_TYPEFACE_ASSETS) as ChartSkiaTypefaceKey[]; function makeTypefaces(): ChartDefaultTypeface { return Object.fromEntries(CHART_SKIA_TYPEFACE_KEYS.map((key) => [key, {id: key} as unknown as SkTypeface])) as ChartDefaultTypeface; @@ -38,13 +38,13 @@ describe('getChartSkiaTypeface', () => { }); it('should resolve Expensify New Kansas by font family', () => { - const typeface = getChartSkiaTypeface(typefaces, {fontFamily: singleFontFamily.EXP_NEW_KANSAS_MEDIUM.fontFamily}); + const typeface = getChartSkiaTypeface(typefaces, {fontFamily: 'Expensify New Kansas'}); expect(typeface).toBe(typefaces.EXP_NEW_KANSAS_MEDIUM); }); it('should resolve italic Expensify Neue bold to the bold italic typeface', () => { const typeface = getChartSkiaTypeface(typefaces, { - fontFamily: singleFontFamily.EXP_NEUE.fontFamily, + fontFamily: 'Expensify Neue', fontStyle: 'italic', fontWeight: 'bold', }); From 739fbca93e14f4ae270fc9d6bbaada704812e108 Mon Sep 17 00:00:00 2001 From: rory Date: Wed, 3 Jun 2026 12:32:10 -0700 Subject: [PATCH 31/35] refactor(charts): inline ChartFontsProvider in Skia overlays Remove ChartFontsBoundary wrapper and use ChartFontsProvider directly where Skia subtrees need font context re-established. Co-authored-by: Cursor --- .../Charts/context/ChartFontsBoundary.tsx | 16 ---------------- src/components/Charts/hooks/index.ts | 1 - .../components/VictoryChartCartesian.tsx | 7 ++++--- .../components/VictoryChartPolar.tsx | 7 ++++--- 4 files changed, 8 insertions(+), 23 deletions(-) delete mode 100644 src/components/Charts/context/ChartFontsBoundary.tsx diff --git a/src/components/Charts/context/ChartFontsBoundary.tsx b/src/components/Charts/context/ChartFontsBoundary.tsx deleted file mode 100644 index d98f346ffc24..000000000000 --- a/src/components/Charts/context/ChartFontsBoundary.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import React from 'react'; -import ChartFontsProvider from './ChartFontsProvider'; - -type ChartFontsBoundaryProps = { - children: React.ReactNode; -}; - -/** - * Re-mounts chart font context inside Skia overlay subtrees (e.g. cartesian renderOutside, - * polar children) where React context does not propagate across the Skia boundary. - */ -function ChartFontsBoundary({children}: ChartFontsBoundaryProps) { - return {children}; -} - -export default ChartFontsBoundary; diff --git a/src/components/Charts/hooks/index.ts b/src/components/Charts/hooks/index.ts index 4362541d8eae..7ff2f5556252 100644 --- a/src/components/Charts/hooks/index.ts +++ b/src/components/Charts/hooks/index.ts @@ -4,7 +4,6 @@ export {default as useChartParagraphs} from './useChartParagraphs'; export {default as useYAxisLabelWidth} from './useYAxisLabelWidth'; export {useChartFontManager} from '@components/Charts/context/ChartFontsContext'; export {default as ChartFontsProvider} from '@components/Charts/context/ChartFontsProvider'; -export {default as ChartFontsBoundary} from '@components/Charts/context/ChartFontsBoundary'; export {useChartTypefaces} from '@components/Charts/context/ChartFontsContext'; export {useChartInteractions, TOOLTIP_BAR_GAP} from './useChartInteractions'; export type {HitTestArgs} from './useChartInteractions'; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartCartesian.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartCartesian.tsx index 4ab6a47ce991..a50e79bf578f 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartCartesian.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartCartesian.tsx @@ -1,6 +1,6 @@ import React from 'react'; import {CartesianChart} from 'victory-native'; -import {ChartFontsBoundary} from '@components/Charts/hooks'; +import {ChartFontsProvider} from '@components/Charts/hooks'; import {useVictoryChartContext} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartContext'; import {VictoryChartRenderArgsProvider} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartRenderArgsContext'; import getHierarchyID from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/getHierarchyID'; @@ -26,7 +26,8 @@ function VictoryChartCartesian() { domainPadding={domainPadding} padding={padding} renderOutside={(renderArgs) => ( - + // Chart font context does not propagate across the Skia renderOutside boundary. + {labelItems.map((labelItem) => ( ))} - + )} > {(renderArgs) => ( diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartPolar.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartPolar.tsx index 5a1a8b068999..65c7e0233380 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartPolar.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartPolar.tsx @@ -1,6 +1,6 @@ import React from 'react'; import {PolarChart} from 'victory-native'; -import {ChartFontsBoundary} from '@components/Charts/hooks'; +import {ChartFontsProvider} from '@components/Charts/hooks'; import {COLOR_KEY, LABEL_KEY, VALUE_KEY} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/constants'; import {useVictoryChartContext} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartContext'; import getHierarchyID from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/getHierarchyID'; @@ -21,7 +21,8 @@ function VictoryChartPolar() { valueKey={VALUE_KEY} colorKey={COLOR_KEY} > - + {/* Chart font context does not propagate into polar Skia children. */} + {tnode.children.map((child) => ( ))} - + ); } From 73c102ad06ef2b014eb53564b8b17586427e7c22 Mon Sep 17 00:00:00 2001 From: rory Date: Wed, 3 Jun 2026 12:36:21 -0700 Subject: [PATCH 32/35] chore(charts): drop useMemo from VictoryChartPie React Compiler handles memoization; parse label template inline. Co-authored-by: Cursor --- .../VictoryChartRenderer/components/VictoryChartPie.tsx | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartPie.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartPie.tsx index 6313a52560b2..8226c772ea6a 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartPie.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartPie.tsx @@ -1,4 +1,4 @@ -import React, {useMemo} from 'react'; +import React from 'react'; import {useAmbientTRenderEngine} from 'react-native-render-html'; import type {TNode} from 'react-native-render-html'; import {Pie} from 'victory-native'; @@ -14,10 +14,8 @@ const START_ANGLE = 270; function VictoryChartPie({tnode}: VictoryChartPieProps) { const renderEngine = useAmbientTRenderEngine(); - const labelItemTemplate = useMemo(() => { - const labelComponentNode = parseComponent(tnode.attributes.labelcomponent, renderEngine, 'victorylabel'); - return labelComponentNode ? parseVictoryLabelNode(labelComponentNode).labelItems?.at(0) : undefined; - }, [tnode.attributes.labelcomponent, renderEngine]); + const labelComponentNode = parseComponent(tnode.attributes.labelcomponent, renderEngine, 'victorylabel'); + const labelItemTemplate = labelComponentNode ? parseVictoryLabelNode(labelComponentNode).labelItems?.at(0) : undefined; const innerRadius = tnode.attributes.innerradius !== undefined ? Number(parseAttribute(tnode.attributes.innerradius)) : undefined; const radius = tnode.attributes.radius !== undefined ? Number(parseAttribute(tnode.attributes.radius)) : undefined; From 39940a1bffd01388611558bc4978f6cfaab24499 Mon Sep 17 00:00:00 2001 From: rory Date: Wed, 3 Jun 2026 12:39:15 -0700 Subject: [PATCH 33/35] Fix prettier --- tests/unit/components/Charts/VictoryTheme.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/components/Charts/VictoryTheme.test.ts b/tests/unit/components/Charts/VictoryTheme.test.ts index 7960ec115f6a..78525af7b963 100644 --- a/tests/unit/components/Charts/VictoryTheme.test.ts +++ b/tests/unit/components/Charts/VictoryTheme.test.ts @@ -1,5 +1,5 @@ -import type VictoryThemeType from '@components/Charts/VictoryTheme'; import {CHART_FONT_FAMILY_NAMES} from '@components/Charts/utils/chartFontConstants'; +import type VictoryThemeType from '@components/Charts/VictoryTheme'; import colors from '@styles/theme/colors'; /** From 0be9474b0d95e61ebffb67c23aba0200efa6f616 Mon Sep 17 00:00:00 2001 From: rory Date: Wed, 3 Jun 2026 12:42:47 -0700 Subject: [PATCH 34/35] chore(charts): remove unused ChartFontFamilyName export Fix knip unused type finding; type remains file-local only. Co-authored-by: Cursor --- src/components/Charts/utils/chartFontConstants.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/Charts/utils/chartFontConstants.ts b/src/components/Charts/utils/chartFontConstants.ts index a97328a3bf3a..0ac924e91f90 100644 --- a/src/components/Charts/utils/chartFontConstants.ts +++ b/src/components/Charts/utils/chartFontConstants.ts @@ -14,4 +14,3 @@ const CHART_FONT_MGR_FROM_TYPEFACES: Record Date: Wed, 3 Jun 2026 12:56:33 -0700 Subject: [PATCH 35/35] fix(charts): use mutable fontFamilies array for Skia API Array.from widens readonly CHART_FONT_FAMILY_NAMES for Paragraph pushStyle. Co-authored-by: Cursor --- src/components/Charts/VictoryTheme.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Charts/VictoryTheme.ts b/src/components/Charts/VictoryTheme.ts index a0bcfe21cf0a..30b29a3ab9a6 100644 --- a/src/components/Charts/VictoryTheme.ts +++ b/src/components/Charts/VictoryTheme.ts @@ -52,7 +52,7 @@ const VictoryTheme = { default: getChartColor(DEFAULT_CHART_COLOR_INDEX), getColor: getChartColor, }, - fontFamilies: [...CHART_FONT_FAMILY_NAMES], + fontFamilies: Array.from(CHART_FONT_FAMILY_NAMES), axis: { /** Number of Y-axis ticks (including zero) */ tickCount: 5,