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..30b29a3ab9a6 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', '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: Array.from(CHART_FONT_FAMILY_NAMES),
axis: {
/** Number of Y-axis ticks (including zero) */
tickCount: 5,
diff --git a/src/components/Charts/context/ChartFontsContext.tsx b/src/components/Charts/context/ChartFontsContext.tsx
new file mode 100644
index 000000000000..03ef4db13915
--- /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 ChartFontsValue from '@components/Charts/types/chartFontsTypes';
+import type {ChartDefaultTypeface} from '@components/Charts/types/chartSkiaTypefaceTypes';
+
+const ChartFontsContext = createContext(null);
+
+function useChartFontsContext(): ChartFontsValue {
+ 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.tsx b/src/components/Charts/context/ChartFontsProvider.tsx
new file mode 100644
index 000000000000..b90c811285ae
--- /dev/null
+++ b/src/components/Charts/context/ChartFontsProvider.tsx
@@ -0,0 +1,25 @@
+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/hooks/index.ts b/src/components/Charts/hooks/index.ts
index c18d0c9934d4..7ff2f5556252 100644
--- a/src/components/Charts/hooks/index.ts
+++ b/src/components/Charts/hooks/index.ts
@@ -2,7 +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, useChartDefaultTypeface} from './useChartFontManager/useChartFontManager';
+export {useChartFontManager} from '@components/Charts/context/ChartFontsContext';
+export {default as ChartFontsProvider} from '@components/Charts/context/ChartFontsProvider';
+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/useChartFontManager/useChartFontManager.native.ts b/src/components/Charts/hooks/useChartFontManager/useChartFontManager.native.ts
deleted file mode 100644
index 04cd0344d420..000000000000
--- a/src/components/Charts/hooks/useChartFontManager/useChartFontManager.native.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-import type {DataModule, SkTypefaceFontProvider} from '@shopify/react-native-skia';
-import {useFonts, useTypeface} 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],
- });
-}
-
-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
deleted file mode 100644
index e0c8a8305fed..000000000000
--- a/src/components/Charts/hooks/useChartFontManager/useChartFontManager.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-import type {DataModule, 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;
-}
-
-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),
- ],
- NotoSansSymbols: [webFont(require('@assets/fonts/NotoSans-Symbols.ttf') as string)],
- NotoSansSCMonths: [webFont(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));
- return {regular, bold};
-}
-
-export {useChartDefaultTypeface};
-export default useChartFontManager;
diff --git a/src/components/Charts/hooks/useChartFonts.ts b/src/components/Charts/hooks/useChartFonts.ts
new file mode 100644
index 000000000000..5b012437a9d1
--- /dev/null
+++ b/src/components/Charts/hooks/useChartFonts.ts
@@ -0,0 +1,15 @@
+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();
+ }, []);
+
+ return fonts;
+}
+
+export default useChartFonts;
diff --git a/src/components/Charts/types/chartFontsTypes.ts b/src/components/Charts/types/chartFontsTypes.ts
new file mode 100644
index 000000000000..deaecc415114
--- /dev/null
+++ b/src/components/Charts/types/chartFontsTypes.ts
@@ -0,0 +1,9 @@
+import type {SkTypefaceFontProvider} from '@shopify/react-native-skia';
+import type {ChartDefaultTypeface} from './chartSkiaTypefaceTypes';
+
+type ChartFontsValue = {
+ typefaces: ChartDefaultTypeface;
+ fontMgr: SkTypefaceFontProvider | null;
+};
+
+export default ChartFontsValue;
diff --git a/src/components/Charts/types/chartSkiaTypefaceTypes.ts b/src/components/Charts/types/chartSkiaTypefaceTypes.ts
new file mode 100644
index 000000000000..fa26dde27a2a
--- /dev/null
+++ b/src/components/Charts/types/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/types.ts b/src/components/Charts/types/index.ts
similarity index 96%
rename from src/components/Charts/types.ts
rename to src/components/Charts/types/index.ts
index 2ed86d19ec06..b44954eb31e4 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 '@components/Charts/VictoryTheme';
type ChartDataPoint = {
/** Label displayed under the data point (e.g., "Amazon", "Nov 2025") */
diff --git a/src/components/Charts/utils/chartFontAssets.native.ts b/src/components/Charts/utils/chartFontAssets.native.ts
new file mode 100644
index 000000000000..585fdee494e4
--- /dev/null
+++ b/src/components/Charts/utils/chartFontAssets.native.ts
@@ -0,0 +1,37 @@
+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_MGR_SUPPLEMENTAL_ASSETS = {
+ NotoSansSymbols: SANS_SYMBOLS_FONT,
+ NotoSansSCMonths: SANS_SC_MONTHS_FONT,
+} as const;
+
+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
new file mode 100644
index 000000000000..a846320d3d7b
--- /dev/null
+++ b/src/components/Charts/utils/chartFontAssets.ts
@@ -0,0 +1,38 @@
+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_MGR_SUPPLEMENTAL_ASSETS = {
+ NotoSansSymbols: SANS_SYMBOLS_FONT,
+ NotoSansSCMonths: SANS_SC_MONTHS_FONT,
+} as const;
+
+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
new file mode 100644
index 000000000000..0ac924e91f90
--- /dev/null
+++ b/src/components/Charts/utils/chartFontConstants.ts
@@ -0,0 +1,16 @@
+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 = TupleToUnion;
+
+/** 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};
diff --git a/src/components/Charts/utils/chartFontsCache.ts b/src/components/Charts/utils/chartFontsCache.ts
new file mode 100644
index 000000000000..3945e4d57550
--- /dev/null
+++ b/src/components/Charts/utils/chartFontsCache.ts
@@ -0,0 +1,127 @@
+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 Log from '@libs/Log';
+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 = {
+ typefaces: Object.fromEntries((Object.keys(CHART_SKIA_TYPEFACE_ASSETS) as ChartSkiaTypefaceKey[]).map((key) => [key, null])) as ChartDefaultTypeface,
+ 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 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_MGR_SUPPLEMENTAL_ASSETS).map(async ([familyName, asset]) => {
+ const typeface = await loadTypefaceFromAsset(asset);
+
+ if (typeface) {
+ fontMgr.registerFont(typeface, familyName);
+ }
+ }),
+ ).then(() => ({typefaces, fontMgr}));
+}
+
+function loadChartFonts(): Promise {
+ return loadChartSkiaTypefaces().then(buildChartFontsValue);
+}
+
+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: unknown) => {
+ Log.hmmm('Chart fonts failed to load', {
+ error: error instanceof Error ? error.message : String(error),
+ });
+ loadPromise = null;
+ notifyChartFontLoadListeners();
+ return EMPTY_CHART_FONTS;
+ });
+ }
+
+ return loadPromise;
+}
+
+export {getChartFontsSnapshot, loadChartFontsOnce, subscribeToChartFonts};
diff --git a/src/components/Charts/utils/chartWebFont.ts b/src/components/Charts/utils/chartWebFont.ts
new file mode 100644
index 000000000000..a569e1424586
--- /dev/null
+++ b/src/components/Charts/utils/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/utils/getChartSkiaTypeface.ts b/src/components/Charts/utils/getChartSkiaTypeface.ts
new file mode 100644
index 000000000000..c1f0bd1521ee
--- /dev/null
+++ b/src/components/Charts/utils/getChartSkiaTypeface.ts
@@ -0,0 +1,59 @@
+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 {ChartLabelFontWeight} from './normalizeChartFontWeight';
+import normalizeChartFontWeight from './normalizeChartFontWeight';
+
+type ChartLabelFontStyle = 'normal' | 'italic';
+
+function normalizeFontStyle(fontStyle: string | undefined): ChartLabelFontStyle {
+ return fontStyle === 'italic' ? 'italic' : '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;
+
+ // 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';
+ }
+
+ 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 matchingKey ?? 'EXP_NEUE';
+}
+
+function getChartSkiaTypeface(
+ typefaces: ChartDefaultTypeface,
+ {
+ fontFamily,
+ fontStyle,
+ fontWeight,
+ }: {
+ fontFamily?: string;
+ fontStyle?: string;
+ fontWeight?: string | number;
+ },
+): SkTypeface | null {
+ const typefaceKey = getChartSkiaTypefaceKey(fontFamily, normalizeFontStyle(fontStyle), normalizeChartFontWeight(fontWeight));
+ return typefaces[typefaceKey];
+}
+
+export default getChartSkiaTypeface;
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..9fa433e9e6c6 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 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();
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/BaseVictoryChartRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/BaseVictoryChartRenderer.tsx
index da522351285b..2ceb4d4ac69f 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 {ChartFontsProvider} from '@components/Charts/hooks';
import VictoryChartContainer from './components/VictoryChartContainer';
import VictoryChartContent from './components/VictoryChartContent';
import {VictoryChartProvider} from './context/VictoryChartContext';
@@ -6,14 +7,14 @@ import type {VictoryChartRendererProps} from './types';
function BaseVictoryChartRenderer({tnode}: VictoryChartRendererProps) {
return (
-
-
-
-
-
+
+
+
+
+
+
+
);
}
-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 10cef4f0875c..a50e79bf578f 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,23 @@ function VictoryChartCartesian() {
domainPadding={domainPadding}
padding={padding}
renderOutside={(renderArgs) => (
-
- {labelItems.map((labelItem) => (
-
- ))}
- {legendItems.map((legendItem) => (
-
- ))}
-
+ // Chart font context does not propagate across the Skia renderOutside boundary.
+
+
+ {labelItems.map((labelItem) => (
+
+ ))}
+ {legendItems.map((legendItem) => (
+
+ ))}
+
+
)}
>
{(renderArgs) => (
@@ -56,6 +60,4 @@ function VictoryChartCartesian() {
);
}
-VictoryChartCartesian.displayName = 'VictoryChartCartesian';
-
export default VictoryChartCartesian;
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;
diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartLabel.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartLabel.tsx
index 76e3f963bda1..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 {useChartDefaultTypeface} 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';
@@ -20,15 +21,21 @@ 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, fontStyle, lineHeight, textAnchor = 'start', verticalAnchor = 'start'}: VictoryChartLabelsProps) {
+ const typefaces = useChartTypefaces();
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 lineFontStyle = fontStyle?.[index];
const lineLineHeight = lineHeight?.[index];
- const typeface = lineFontWeight === 'bold' ? boldTypeface : regularTypeface;
+ 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;
@@ -64,6 +71,4 @@ function VictoryChartLabel({x, y, text, color, fontSize, fontWeight, lineHeight,
});
}
-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 b7de61bca20f..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 {useChartDefaultTypeface} 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;
@@ -23,10 +24,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 = useChartTypefaces();
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;
@@ -75,6 +76,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 0843a623ce02..8226c772ea6a 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 {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,10 @@ type VictoryChartPieProps = {tnode: TNode};
const START_ANGLE = 270;
function VictoryChartPie({tnode}: VictoryChartPieProps) {
+ const renderEngine = useAmbientTRenderEngine();
+ 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;
const size = radius ? radius * 2 : undefined;
@@ -25,6 +32,7 @@ function VictoryChartPie({tnode}: VictoryChartPieProps) {
)}
@@ -32,6 +40,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 a9ff0dd3ee67..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,14 +30,15 @@ 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 ;
}
-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 ebaa6856e2f9..65c7e0233380 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 {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';
@@ -20,28 +21,29 @@ function VictoryChartPolar() {
valueKey={VALUE_KEY}
colorKey={COLOR_KEY}
>
- {tnode.children.map((child) => (
-
- ))}
- {labelItems.map((labelItem) => (
-
- ))}
- {legendItems.map((legendItem) => (
-
- ))}
+ {/* Chart font context does not propagate into polar Skia children. */}
+
+ {tnode.children.map((child) => (
+
+ ))}
+ {labelItems.map((labelItem) => (
+
+ ))}
+ {legendItems.map((legendItem) => (
+
+ ))}
+
);
}
-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 a6ee93bc96a9..332d34825f69 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,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 = 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);
const hasCartesianData = Object.values(data).some((entry) => X_KEY in entry);
@@ -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) {
diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/parsers/victoryLabelParser.ts b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/parsers/victoryLabelParser.ts
index a244f0b8c32e..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';
@@ -13,6 +14,8 @@ function parseVictoryLabelNode(tnode: TNode): PartialProcessNodeResult {
color: {},
fontSize: {},
fontWeight: {},
+ fontFamily: {},
+ fontStyle: {},
lineHeight: parseAttribute(tnode.attributes.lineheight),
textAnchor: parseAttribute(tnode.attributes.textanchor),
verticalAnchor: parseAttribute(tnode.attributes.verticalanchor),
@@ -37,7 +40,19 @@ 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) {
+ labelItem.fontFamily = {
+ ...labelItem.fontFamily,
+ [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..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,12 +14,14 @@ 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) => {
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 51f52d6656c3..4797b16ceb7b 100644
--- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/types.ts
+++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/types.ts
@@ -36,6 +36,8 @@ type RawLabelStyle = {
fill?: Color;
fontSize?: string | number;
fontWeight?: string | number;
+ fontFamily?: string;
+ fontStyle?: string;
};
type RawLegendStyle = {
@@ -43,6 +45,8 @@ type RawLegendStyle = {
fill?: Color;
fontSize?: string | number;
fontWeight?: string | number;
+ fontFamily?: string;
+ fontStyle?: string;
};
};
@@ -81,6 +85,12 @@ type LabelItem = {
/** Font weight (per line) */
fontWeight?: Record;
+ /** Font family (per line) */
+ fontFamily?: Record;
+
+ /** Font style (per line) */
+ fontStyle?: Record;
+
/** Line height (per line) */
lineHeight?: Record;
@@ -104,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/tests/unit/components/Charts/VictoryTheme.test.ts b/tests/unit/components/Charts/VictoryTheme.test.ts
index 44f821ce0a26..78525af7b963 100644
--- a/tests/unit/components/Charts/VictoryTheme.test.ts
+++ b/tests/unit/components/Charts/VictoryTheme.test.ts
@@ -1,3 +1,4 @@
+import {CHART_FONT_FAMILY_NAMES} from '@components/Charts/utils/chartFontConstants';
import type VictoryThemeType from '@components/Charts/VictoryTheme';
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', () => {
diff --git a/tests/unit/getChartSkiaTypefaceTest.ts b/tests/unit/getChartSkiaTypefaceTest.ts
new file mode 100644
index 000000000000..c627d98a480f
--- /dev/null
+++ b/tests/unit/getChartSkiaTypefaceTest.ts
@@ -0,0 +1,53 @@
+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';
+
+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;
+}
+
+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 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: '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: 'Expensify Neue',
+ fontStyle: 'italic',
+ fontWeight: 'bold',
+ });
+ expect(typeface).toBe(typefaces.EXP_NEUE_BOLD_ITALIC);
+ });
+});