From 48b27a6e0ffa5adfff94937d756ca1845fce889d Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Mon, 11 May 2026 21:25:51 +0100 Subject: [PATCH 01/69] wip --- .../BaseHTMLEngineProvider.tsx | 36 ++++++++++++++++++ .../VictoryBarRenderer.tsx | 26 +++++++++++++ .../VictoryChartRenderer/index.tsx | 37 +++++++++++++++++++ .../HTMLEngineProvider/HTMLRenderers/index.ts | 11 ++++++ 4 files changed, 110 insertions(+) create mode 100644 src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/VictoryBarRenderer.tsx create mode 100644 src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/index.tsx diff --git a/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx b/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx index 13dc7fbaaebb..315bc1469752 100755 --- a/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx +++ b/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx @@ -195,6 +195,42 @@ function BaseHTMLEngineProvider({textSelectable = false, children, enableExperim tagName: 'sparkles-icon', contentModel: HTMLContentModel.mixed, }), + victorychart: HTMLElementModel.fromCustomModel({ + tagName: 'victorychart', + contentModel: HTMLContentModel.block, + }), + victorybar: HTMLElementModel.fromCustomModel({ + tagName: 'victorybar', + contentModel: HTMLContentModel.block, + }), + /*victoryline: HTMLElementModel.fromCustomModel({ + tagName: 'victoryline', + contentModel: HTMLContentModel.block, + }), + victorypie: HTMLElementModel.fromCustomModel({ + tagName: 'victorypie', + contentModel: HTMLContentModel.block, + }), + victoryaxis: HTMLElementModel.fromCustomModel({ + tagName: 'victoryaxis', + contentModel: HTMLContentModel.block, + }), + victorylegend: HTMLElementModel.fromCustomModel({ + tagName: 'victorylegend', + contentModel: HTMLContentModel.block, + }), + victorylabel: HTMLElementModel.fromCustomModel({ + tagName: 'victorylabel', + contentModel: HTMLContentModel.textual, + }), + victorytooltip: HTMLElementModel.fromCustomModel({ + tagName: 'victorytooltip', + contentModel: HTMLContentModel.textual, + }), + victorygroup: HTMLElementModel.fromCustomModel({ + tagName: 'victorygroup', + contentModel: HTMLContentModel.block, + }),*/ }), [ styles.taskTitleMenuItem, diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/VictoryBarRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/VictoryBarRenderer.tsx new file mode 100644 index 000000000000..1f918f2ba2e0 --- /dev/null +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/VictoryBarRenderer.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import {View} from 'react-native'; +import type {GestureResponderEvent} from 'react-native'; +import {type CustomRendererProps, type TBlock, TNodeChildrenRenderer} from 'react-native-render-html'; +import {Bar, CartesianChart} from 'victory-native'; +import type {ChartBounds, PointsArray} from 'victory-native'; +import * as HTMLEngineUtils from '@components/HTMLEngineProvider/htmlEngineUtils'; +import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; +import {showContextMenuForReport, useShowContextMenuActions, useShowContextMenuState} from '@components/ShowContextMenuContext'; +import Text from '@components/Text'; +import useLocalize from '@hooks/useLocalize'; +import useStyleUtils from '@hooks/useStyleUtils'; +import useThemeStyles from '@hooks/useThemeStyles'; +import {isArchivedNonExpenseReport} from '@libs/ReportUtils'; +import CONST from '@src/CONST'; + +type VictoryBarRendererProps = CustomRendererProps & { + points: PointsArray; + chartBounds: ChartBounds; +}; + +function VictoryBarRenderer({TDefaultRenderer, points, chartBounds, ...defaultRendererProps}: VictoryBarRendererProps) { + return Why; +} + +export default VictoryBarRenderer; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/index.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/index.tsx new file mode 100644 index 000000000000..39b41dca6045 --- /dev/null +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/index.tsx @@ -0,0 +1,37 @@ +import React from 'react'; +import {View} from 'react-native'; +import type {GestureResponderEvent} from 'react-native'; +import {type CustomRendererProps, type TBlock, TNodeChildrenRenderer} from 'react-native-render-html'; +import {Bar, CartesianChart} from 'victory-native'; +import * as HTMLEngineUtils from '@components/HTMLEngineProvider/htmlEngineUtils'; +import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; +import {showContextMenuForReport, useShowContextMenuActions, useShowContextMenuState} from '@components/ShowContextMenuContext'; +import Text from '@components/Text'; +import useLocalize from '@hooks/useLocalize'; +import useStyleUtils from '@hooks/useStyleUtils'; +import useThemeStyles from '@hooks/useThemeStyles'; +import {isArchivedNonExpenseReport} from '@libs/ReportUtils'; +import CONST from '@src/CONST'; + +function VictoryChartRenderer({TDefaultRenderer, tnode, ...defaultRendererProps}: CustomRendererProps) { + return ( + + + {({points, chartBounds}) => ( + + )} + + + ); +} + +export default VictoryChartRenderer; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/index.ts b/src/components/HTMLEngineProvider/HTMLRenderers/index.ts index ef1d630ce007..c23d17215bd6 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/index.ts +++ b/src/components/HTMLEngineProvider/HTMLRenderers/index.ts @@ -21,6 +21,8 @@ import TaskTitleRenderer from './TaskTitleRenderer'; import TransactionHistoryLinkRenderer from './TransactionHistoryLinkRenderer'; import ULRenderer from './ULRenderer'; import UserDetailsRenderer from './UserDetailsRenderer'; +import VictoryChartRenderer from './VictoryChartRenderer'; +import VictoryBarRenderer from './VictoryChartRenderer/VictoryBarRenderer'; import VideoRenderer from './VideoRenderer'; /** @@ -54,6 +56,15 @@ const HTMLEngineProviderComponentList: CustomTagRendererRecord = { 'transaction-history-link': TransactionHistoryLinkRenderer, 'account-manager-link': AccountManagerLinkRenderer, 'sparkles-icon': SparklesIconRenderer, + victorychart: VictoryChartRenderer, + victorybar: VictoryBarRenderer, + /*victoryline: VictoryLineRenderer, + victorypie: VictoryPieRenderer, + victoryaxis: VictoryAxisRenderer, + victorylegend: VictoryLegendRenderer, + victorylabel: VictoryLabelRenderer, + victorytooltip: VictoryTooltipRenderer, + victorygroup: VictoryGroupRenderer,*/ /* eslint-enable @typescript-eslint/naming-convention */ }; From a09a744fb09444f7d4f15b4f383ca2e9cb04ceef Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Tue, 12 May 2026 02:47:14 +0100 Subject: [PATCH 02/69] temp setup --- .../BaseHTMLEngineProvider.tsx | 3 +- .../VictoryChartRenderer/index.tsx | 11 ++++- .../HTMLEngineProvider/HTMLRenderers/index.ts | 19 ++++---- src/setup/platformSetup/index.ts | 44 ++++++++++--------- 4 files changed, 45 insertions(+), 32 deletions(-) diff --git a/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx b/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx index 315bc1469752..1c7f0dd7bb4d 100755 --- a/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx +++ b/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx @@ -7,7 +7,7 @@ import convertToLTR from '@libs/convertToLTR'; import FontUtils from '@styles/utils/FontUtils'; import type ChildrenProps from '@src/types/utils/ChildrenProps'; import {computeEmbeddedMaxWidth, isChildOfTaskTitle} from './htmlEngineUtils'; -import htmlRenderers from './HTMLRenderers'; +import getHtmlRenderers from './HTMLRenderers'; type BaseHTMLEngineProviderProps = ChildrenProps & { /** Whether text elements should be selectable */ @@ -24,6 +24,7 @@ type BaseHTMLEngineProviderProps = ChildrenProps & { // costly invalidations and commits. function BaseHTMLEngineProvider({textSelectable = false, children, enableExperimentalBRCollapsing = false}: BaseHTMLEngineProviderProps) { const styles = useThemeStyles(); + const htmlRenderers = useMemo(() => getHtmlRenderers(), []); // Declare nonstandard tags and their content model here /* eslint-disable @typescript-eslint/naming-convention */ diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/index.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/index.tsx index 39b41dca6045..28abd40da9d4 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/index.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/index.tsx @@ -14,10 +14,17 @@ import {isArchivedNonExpenseReport} from '@libs/ReportUtils'; import CONST from '@src/CONST'; function VictoryChartRenderer({TDefaultRenderer, tnode, ...defaultRendererProps}: CustomRendererProps) { + const DATA = Array.from({length: 31}, (_, i) => ({ + x: i, + y: 40 + 30 * Math.random(), + })); + + console.log(tnode); + return ( - + diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/index.ts b/src/components/HTMLEngineProvider/HTMLRenderers/index.ts index c23d17215bd6..4929be499ec0 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/index.ts +++ b/src/components/HTMLEngineProvider/HTMLRenderers/index.ts @@ -21,14 +21,12 @@ import TaskTitleRenderer from './TaskTitleRenderer'; import TransactionHistoryLinkRenderer from './TransactionHistoryLinkRenderer'; import ULRenderer from './ULRenderer'; import UserDetailsRenderer from './UserDetailsRenderer'; -import VictoryChartRenderer from './VictoryChartRenderer'; -import VictoryBarRenderer from './VictoryChartRenderer/VictoryBarRenderer'; import VideoRenderer from './VideoRenderer'; /** * This collection defines our custom renderers. It is a mapping from HTML tag type to the corresponding component. */ -const HTMLEngineProviderComponentList: CustomTagRendererRecord = { +export default (): CustomTagRendererRecord => ({ // Standard HTML tag renderers a: AnchorRenderer, code: CodeRenderer, @@ -56,8 +54,14 @@ const HTMLEngineProviderComponentList: CustomTagRendererRecord = { 'transaction-history-link': TransactionHistoryLinkRenderer, 'account-manager-link': AccountManagerLinkRenderer, 'sparkles-icon': SparklesIconRenderer, - victorychart: VictoryChartRenderer, - victorybar: VictoryBarRenderer, + /* eslint-enable @typescript-eslint/naming-convention */ + + // VictoryChart components depend on Skia and should be loaded after Skia WASM + // + // Using `require` loads the components only when this function is executed, + // unlike `import` they'd be imported on module execution BEFORE Skia WASM is loaded. + victorychart: require('./VictoryChartRenderer').default, + victorybar: require('./VictoryChartRenderer/VictoryBarRenderer').default, /*victoryline: VictoryLineRenderer, victorypie: VictoryPieRenderer, victoryaxis: VictoryAxisRenderer, @@ -65,7 +69,4 @@ const HTMLEngineProviderComponentList: CustomTagRendererRecord = { victorylabel: VictoryLabelRenderer, victorytooltip: VictoryTooltipRenderer, victorygroup: VictoryGroupRenderer,*/ - /* eslint-enable @typescript-eslint/naming-convention */ -}; - -export default HTMLEngineProviderComponentList; +}); diff --git a/src/setup/platformSetup/index.ts b/src/setup/platformSetup/index.ts index 7bb35851057e..407850a0d0ae 100644 --- a/src/setup/platformSetup/index.ts +++ b/src/setup/platformSetup/index.ts @@ -1,3 +1,4 @@ +import {LoadSkiaWeb} from '@shopify/react-native-skia/lib/module/web'; import {AppRegistry} from 'react-native'; import CacheAPI from '@libs/CacheAPI'; import checkForUpdates from '@libs/checkForUpdates'; @@ -52,29 +53,32 @@ const webUpdater = (): PlatformSpecificUpdater => ({ }); export default function () { - // Initialize Cache API - CacheAPI.init(); + // s77rt tmp + LoadSkiaWeb().then(() => { + // Initialize Cache API + CacheAPI.init(); - AppRegistry.runApplication(Config.APP_NAME, { - rootTag: document.getElementById('root'), - }); + AppRegistry.runApplication(Config.APP_NAME, { + rootTag: document.getElementById('root'), + }); - // When app loads, get current version (production only) - if (Config.IS_IN_PRODUCTION) { - checkForUpdates(webUpdater()); - } + // When app loads, get current version (production only) + if (Config.IS_IN_PRODUCTION) { + checkForUpdates(webUpdater()); + } - // Start current date updater - DateUtils.startCurrentDateUpdater(); + // Start current date updater + DateUtils.startCurrentDateUpdater(); - // Service worker is only emitted in production builds (GenerateSW is skipped in dev). - if (Config.IS_IN_PRODUCTION && 'serviceWorker' in navigator) { - window.addEventListener('load', () => { - navigator.serviceWorker.register('/service-worker.js').catch((error) => { - // Soft-fail: app must continue working even if SW registration fails (Safari Private mode, etc.). - // eslint-disable-next-line no-console - console.warn('[SW] registration failed', error); + // Service worker is only emitted in production builds (GenerateSW is skipped in dev). + if (Config.IS_IN_PRODUCTION && 'serviceWorker' in navigator) { + window.addEventListener('load', () => { + navigator.serviceWorker.register('/service-worker.js').catch((error) => { + // Soft-fail: app must continue working even if SW registration fails (Safari Private mode, etc.). + // eslint-disable-next-line no-console + console.warn('[SW] registration failed', error); + }); }); - }); - } + } + }); } From e1ad4c95e48b2b67592c3b083e8e2a88fdb14f44 Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Tue, 12 May 2026 03:35:35 +0100 Subject: [PATCH 03/69] keep only one victorychart renderer --- .../BaseHTMLEngineProvider.tsx | 32 ------------------- .../index.tsx => VictoryChartRenderer.tsx} | 2 +- .../VictoryBarRenderer.tsx | 26 --------------- .../HTMLEngineProvider/HTMLRenderers/index.ts | 8 ----- 4 files changed, 1 insertion(+), 67 deletions(-) rename src/components/HTMLEngineProvider/HTMLRenderers/{VictoryChartRenderer/index.tsx => VictoryChartRenderer.tsx} (96%) delete mode 100644 src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/VictoryBarRenderer.tsx diff --git a/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx b/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx index 1c7f0dd7bb4d..5a8fd0ddfc54 100755 --- a/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx +++ b/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx @@ -200,38 +200,6 @@ function BaseHTMLEngineProvider({textSelectable = false, children, enableExperim tagName: 'victorychart', contentModel: HTMLContentModel.block, }), - victorybar: HTMLElementModel.fromCustomModel({ - tagName: 'victorybar', - contentModel: HTMLContentModel.block, - }), - /*victoryline: HTMLElementModel.fromCustomModel({ - tagName: 'victoryline', - contentModel: HTMLContentModel.block, - }), - victorypie: HTMLElementModel.fromCustomModel({ - tagName: 'victorypie', - contentModel: HTMLContentModel.block, - }), - victoryaxis: HTMLElementModel.fromCustomModel({ - tagName: 'victoryaxis', - contentModel: HTMLContentModel.block, - }), - victorylegend: HTMLElementModel.fromCustomModel({ - tagName: 'victorylegend', - contentModel: HTMLContentModel.block, - }), - victorylabel: HTMLElementModel.fromCustomModel({ - tagName: 'victorylabel', - contentModel: HTMLContentModel.textual, - }), - victorytooltip: HTMLElementModel.fromCustomModel({ - tagName: 'victorytooltip', - contentModel: HTMLContentModel.textual, - }), - victorygroup: HTMLElementModel.fromCustomModel({ - tagName: 'victorygroup', - contentModel: HTMLContentModel.block, - }),*/ }), [ styles.taskTitleMenuItem, diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/index.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer.tsx similarity index 96% rename from src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/index.tsx rename to src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer.tsx index 28abd40da9d4..df5858f5801c 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/index.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer.tsx @@ -2,7 +2,7 @@ import React from 'react'; import {View} from 'react-native'; import type {GestureResponderEvent} from 'react-native'; import {type CustomRendererProps, type TBlock, TNodeChildrenRenderer} from 'react-native-render-html'; -import {Bar, CartesianChart} from 'victory-native'; +import {Bar, CartesianChart, Line} from 'victory-native'; import * as HTMLEngineUtils from '@components/HTMLEngineProvider/htmlEngineUtils'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; import {showContextMenuForReport, useShowContextMenuActions, useShowContextMenuState} from '@components/ShowContextMenuContext'; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/VictoryBarRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/VictoryBarRenderer.tsx deleted file mode 100644 index 1f918f2ba2e0..000000000000 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/VictoryBarRenderer.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import React from 'react'; -import {View} from 'react-native'; -import type {GestureResponderEvent} from 'react-native'; -import {type CustomRendererProps, type TBlock, TNodeChildrenRenderer} from 'react-native-render-html'; -import {Bar, CartesianChart} from 'victory-native'; -import type {ChartBounds, PointsArray} from 'victory-native'; -import * as HTMLEngineUtils from '@components/HTMLEngineProvider/htmlEngineUtils'; -import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; -import {showContextMenuForReport, useShowContextMenuActions, useShowContextMenuState} from '@components/ShowContextMenuContext'; -import Text from '@components/Text'; -import useLocalize from '@hooks/useLocalize'; -import useStyleUtils from '@hooks/useStyleUtils'; -import useThemeStyles from '@hooks/useThemeStyles'; -import {isArchivedNonExpenseReport} from '@libs/ReportUtils'; -import CONST from '@src/CONST'; - -type VictoryBarRendererProps = CustomRendererProps & { - points: PointsArray; - chartBounds: ChartBounds; -}; - -function VictoryBarRenderer({TDefaultRenderer, points, chartBounds, ...defaultRendererProps}: VictoryBarRendererProps) { - return Why; -} - -export default VictoryBarRenderer; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/index.ts b/src/components/HTMLEngineProvider/HTMLRenderers/index.ts index 4929be499ec0..b8dd2a39fb51 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/index.ts +++ b/src/components/HTMLEngineProvider/HTMLRenderers/index.ts @@ -61,12 +61,4 @@ export default (): CustomTagRendererRecord => ({ // Using `require` loads the components only when this function is executed, // unlike `import` they'd be imported on module execution BEFORE Skia WASM is loaded. victorychart: require('./VictoryChartRenderer').default, - victorybar: require('./VictoryChartRenderer/VictoryBarRenderer').default, - /*victoryline: VictoryLineRenderer, - victorypie: VictoryPieRenderer, - victoryaxis: VictoryAxisRenderer, - victorylegend: VictoryLegendRenderer, - victorylabel: VictoryLabelRenderer, - victorytooltip: VictoryTooltipRenderer, - victorygroup: VictoryGroupRenderer,*/ }); From 1b030cb679a5427e3ebaaffa73502ef64112941f Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Tue, 12 May 2026 04:34:44 +0100 Subject: [PATCH 04/69] wip still --- .../HTMLRenderers/VictoryChartRenderer.tsx | 57 +++++++++++++++---- 1 file changed, 45 insertions(+), 12 deletions(-) diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer.tsx index df5858f5801c..b3795acd0c39 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer.tsx @@ -1,7 +1,7 @@ -import React from 'react'; +import React, {useCallback, useMemo} from 'react'; import {View} from 'react-native'; import type {GestureResponderEvent} from 'react-native'; -import {type CustomRendererProps, type TBlock, TNodeChildrenRenderer} from 'react-native-render-html'; +import {type CustomRendererProps, type TBlock, TNode, TNodeChildrenRenderer} from 'react-native-render-html'; import {Bar, CartesianChart, Line} from 'victory-native'; import * as HTMLEngineUtils from '@components/HTMLEngineProvider/htmlEngineUtils'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; @@ -17,25 +17,58 @@ function VictoryChartRenderer({TDefaultRenderer, tnode, ...defaultRendererProps} const DATA = Array.from({length: 31}, (_, i) => ({ x: i, y: 40 + 30 * Math.random(), + z: 50, })); - console.log(tnode); + window.tnode = tnode; + + const data = useMemo(() => { + let currentNode: TNode | null = tnode; + while (currentNode) { + console.log(currentNode); + currentNode = null; + } + }, []); + + const renderChild = useCallback((tnode, index, points, chartBounds) => { + const key = `${tnode.tagName ?? 'node'}-${index}`; + switch (tnode.tagName) { + case 'victorybar': + return ( + + {tnode.children.map((child, childIndex) => renderChild(child, childIndex, points, chartBounds))} + + ); + case 'victoryline': + return ( + + {tnode.children.map((child, childIndex) => renderChild(child, childIndex, points, chartBounds))} + + ); + default: + return null; + } + }, []); return ( - {({points, chartBounds}) => ( - - )} + {({points, chartBounds}) => tnode.children.map((child, childIndex) => renderChild(child, childIndex, points, chartBounds))} ); From 4561344e68e6fe577a01edabdf9741ec774c7690 Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Tue, 12 May 2026 04:42:21 +0100 Subject: [PATCH 05/69] use JSON5 to parse data --- .../HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer.tsx index b3795acd0c39..3bcaeb7e66a4 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer.tsx @@ -1,3 +1,4 @@ +import JSON5 from 'json5'; import React, {useCallback, useMemo} from 'react'; import {View} from 'react-native'; import type {GestureResponderEvent} from 'react-native'; @@ -20,6 +21,8 @@ function VictoryChartRenderer({TDefaultRenderer, tnode, ...defaultRendererProps} z: 50, })); + console.log('parsed data', JSON5.parse("[ {x: 'Jan', y: 3}, {x: 'Feb', y: 5}, {x: 'Mar', y: 2}, {x: 'Apr', y: 7} ]")); + window.tnode = tnode; const data = useMemo(() => { From 8148ba5534a357fee7d770a1b80fd0625e41a8f2 Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Wed, 13 May 2026 04:06:18 +0100 Subject: [PATCH 06/69] extract and process data - wip --- .../HTMLRenderers/VictoryChartRenderer.tsx | 68 ++++++++++++++----- 1 file changed, 50 insertions(+), 18 deletions(-) diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer.tsx index 3bcaeb7e66a4..37e200cd31b3 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer.tsx @@ -14,24 +14,56 @@ import useThemeStyles from '@hooks/useThemeStyles'; import {isArchivedNonExpenseReport} from '@libs/ReportUtils'; import CONST from '@src/CONST'; -function VictoryChartRenderer({TDefaultRenderer, tnode, ...defaultRendererProps}: CustomRendererProps) { - const DATA = Array.from({length: 31}, (_, i) => ({ - x: i, - y: 40 + 30 * Math.random(), - z: 50, - })); +/** + * Traverse node and extract and parse `data` attributes + * The retured array is 1D - All nested data are flattened + */ +function extractData(tnode: TNode): Array> { + const data: Array> = []; + if (tnode.attributes?.data) { + const parsedData = JSON5.parse(tnode.attributes.data); + if (Array.isArray(parsedData)) { + data.push(...parsedData); + } + } + data.push(...tnode.children.flatMap((child) => extractData(child))); + return data; +} + +/** + * Process raw data and combines points based on shared xKey + */ +function processDataForCartesianChart(rawData: Array>) { + const xKey = 'x'; + const yKeys = []; + const data = Object.values( + rawData.reduce((points, point) => { + const yLevel = (points[point.x]?.yLevel ?? 0) + 1; + const yKey = 'y' + yLevel; + yKeys.push(yKey); + points[point.x] = { + ...points[point.x], + [xKey]: point.x, + [yKey]: point.y, + yLevel, + }; + return points; + }, {}), + ); + return { + data, + xKey, + yKeys, + }; +} - console.log('parsed data', JSON5.parse("[ {x: 'Jan', y: 3}, {x: 'Feb', y: 5}, {x: 'Mar', y: 2}, {x: 'Apr', y: 7} ]")); +function VictoryChartRenderer({TDefaultRenderer, tnode, ...defaultRendererProps}: CustomRendererProps) { + const rawData = useMemo(() => extractData(tnode), []); + const isPolarChart = useMemo(() => false, [rawData]); + const {data, xKey, yKeys} = useMemo(() => (isPolarChart ? {} : processDataForCartesianChart(rawData)), [rawData, isPolarChart]); window.tnode = tnode; - - const data = useMemo(() => { - let currentNode: TNode | null = tnode; - while (currentNode) { - console.log(currentNode); - currentNode = null; - } - }, []); + console.log({data}); const renderChild = useCallback((tnode, index, points, chartBounds) => { const key = `${tnode.tagName ?? 'node'}-${index}`; @@ -67,9 +99,9 @@ function VictoryChartRenderer({TDefaultRenderer, tnode, ...defaultRendererProps} return ( {({points, chartBounds}) => tnode.children.map((child, childIndex) => renderChild(child, childIndex, points, chartBounds))} From 5a3e343527c19d2cf1ed637574454f368d0b157a Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Wed, 13 May 2026 05:12:34 +0100 Subject: [PATCH 07/69] add getHierarchyID --- .../HTMLRenderers/VictoryChartRenderer.tsx | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer.tsx index 37e200cd31b3..a4edb5bf9455 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer.tsx @@ -14,6 +14,19 @@ import useThemeStyles from '@hooks/useThemeStyles'; import {isArchivedNonExpenseReport} from '@libs/ReportUtils'; import CONST from '@src/CONST'; +/** + * Get node unique ID based on hierarchy + */ +function getHierarchyID(tnode: TNode): string { + let id = String(tnode.nodeIndex); + let parent = tnode.parent; + while (parent) { + id = `${parent.nodeIndex}-${id}`; + parent = parent.parent; + } + return id; +} + /** * Traverse node and extract and parse `data` attributes * The retured array is 1D - All nested data are flattened @@ -31,7 +44,7 @@ function extractData(tnode: TNode): Array> { } /** - * Process raw data and combines points based on shared xKey + * Process raw data and combine points based on shared xKey */ function processDataForCartesianChart(rawData: Array>) { const xKey = 'x'; @@ -67,6 +80,7 @@ function VictoryChartRenderer({TDefaultRenderer, tnode, ...defaultRendererProps} const renderChild = useCallback((tnode, index, points, chartBounds) => { const key = `${tnode.tagName ?? 'node'}-${index}`; + const hierarchyID = getHierarchyID(tnode); switch (tnode.tagName) { case 'victorybar': return ( From 326294094b785f90c593e9ee7757a03960028215 Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Wed, 13 May 2026 06:30:10 +0100 Subject: [PATCH 08/69] got data extracted correctly --- .../HTMLRenderers/VictoryChartRenderer.tsx | 54 +++++++++---------- 1 file changed, 25 insertions(+), 29 deletions(-) diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer.tsx index a4edb5bf9455..2905a7bbd0e6 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer.tsx @@ -1,4 +1,5 @@ import JSON5 from 'json5'; +import lodashMerge from 'lodash/merge'; import React, {useCallback, useMemo} from 'react'; import {View} from 'react-native'; import type {GestureResponderEvent} from 'react-native'; @@ -14,6 +15,9 @@ import useThemeStyles from '@hooks/useThemeStyles'; import {isArchivedNonExpenseReport} from '@libs/ReportUtils'; import CONST from '@src/CONST'; +const X_KEY = 'x'; +const Y_KEY_PREFIX = 'y'; + /** * Get node unique ID based on hierarchy */ @@ -28,41 +32,33 @@ function getHierarchyID(tnode: TNode): string { } /** - * Traverse node and extract and parse `data` attributes - * The retured array is 1D - All nested data are flattened + * Traverse node and extract all points from `data` attributes */ -function extractData(tnode: TNode): Array> { - const data: Array> = []; +function extractData(tnode: TNode): Record> { + const rawData = {}; if (tnode.attributes?.data) { const parsedData = JSON5.parse(tnode.attributes.data); if (Array.isArray(parsedData)) { - data.push(...parsedData); + const xKey = X_KEY; + const yKey = Y_KEY_PREFIX + getHierarchyID(tnode); + parsedData.forEach((point) => { + rawData[point.x] = { + [xKey]: point.x, + [yKey]: point.y, + }; + }); } } - data.push(...tnode.children.flatMap((child) => extractData(child))); - return data; + return lodashMerge(rawData, ...tnode.children.map((child) => extractData(child))); } /** - * Process raw data and combine points based on shared xKey + * Returns required attributes for `` */ -function processDataForCartesianChart(rawData: Array>) { - const xKey = 'x'; - const yKeys = []; - const data = Object.values( - rawData.reduce((points, point) => { - const yLevel = (points[point.x]?.yLevel ?? 0) + 1; - const yKey = 'y' + yLevel; - yKeys.push(yKey); - points[point.x] = { - ...points[point.x], - [xKey]: point.x, - [yKey]: point.y, - yLevel, - }; - return points; - }, {}), - ); +function prepareDataForCartesianChart(rawData: Record>) { + const data = Object.values(rawData); + const xKey = X_KEY; + const yKeys = data.length > 0 ? Object.keys(data[0]).filter((key) => key !== xKey) : []; return { data, xKey, @@ -73,20 +69,20 @@ function processDataForCartesianChart(rawData: Array>) { function VictoryChartRenderer({TDefaultRenderer, tnode, ...defaultRendererProps}: CustomRendererProps) { const rawData = useMemo(() => extractData(tnode), []); const isPolarChart = useMemo(() => false, [rawData]); - const {data, xKey, yKeys} = useMemo(() => (isPolarChart ? {} : processDataForCartesianChart(rawData)), [rawData, isPolarChart]); + const {data, xKey, yKeys} = useMemo(() => (isPolarChart ? {} : prepareDataForCartesianChart(rawData)), [rawData, isPolarChart]); window.tnode = tnode; console.log({data}); const renderChild = useCallback((tnode, index, points, chartBounds) => { const key = `${tnode.tagName ?? 'node'}-${index}`; - const hierarchyID = getHierarchyID(tnode); + const yKey = Y_KEY_PREFIX + getHierarchyID(tnode); switch (tnode.tagName) { case 'victorybar': return ( From 29468778c035be44d53992119e8f4945b4668e55 Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Thu, 14 May 2026 16:16:55 +0100 Subject: [PATCH 09/69] some cleanup --- .../HTMLRenderers/VictoryChartRenderer.tsx | 57 ++++++++++++------- 1 file changed, 36 insertions(+), 21 deletions(-) diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer.tsx index 2905a7bbd0e6..605b0253d82d 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer.tsx @@ -2,8 +2,8 @@ import JSON5 from 'json5'; import lodashMerge from 'lodash/merge'; import React, {useCallback, useMemo} from 'react'; import {View} from 'react-native'; -import type {GestureResponderEvent} from 'react-native'; import {type CustomRendererProps, type TBlock, TNode, TNodeChildrenRenderer} from 'react-native-render-html'; +import type {CartesianChartRenderArg, ChartBounds, PointsArray} from 'victory-native'; import {Bar, CartesianChart, Line} from 'victory-native'; import * as HTMLEngineUtils from '@components/HTMLEngineProvider/htmlEngineUtils'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; @@ -18,6 +18,8 @@ import CONST from '@src/CONST'; const X_KEY = 'x'; const Y_KEY_PREFIX = 'y'; +type RawData = Record; + /** * Get node unique ID based on hierarchy */ @@ -34,14 +36,14 @@ function getHierarchyID(tnode: TNode): string { /** * Traverse node and extract all points from `data` attributes */ -function extractData(tnode: TNode): Record> { - const rawData = {}; +function extractData(tnode: TNode): Record { + const rawData: Record = {}; if (tnode.attributes?.data) { - const parsedData = JSON5.parse(tnode.attributes.data); + const parsedData = parseAttribute(tnode.attributes.data); if (Array.isArray(parsedData)) { const xKey = X_KEY; const yKey = Y_KEY_PREFIX + getHierarchyID(tnode); - parsedData.forEach((point) => { + (parsedData as RawData[]).forEach((point) => { rawData[point.x] = { [xKey]: point.x, [yKey]: point.y, @@ -53,9 +55,9 @@ function extractData(tnode: TNode): Record> { } /** - * Returns required attributes for `` + * Returns required props for `` */ -function prepareDataForCartesianChart(rawData: Record>) { +function prepareDataForCartesianChart(rawData: Record) { const data = Object.values(rawData); const xKey = X_KEY; const yKeys = data.length > 0 ? Object.keys(data[0]).filter((key) => key !== xKey) : []; @@ -66,17 +68,32 @@ function prepareDataForCartesianChart(rawData: Record) { - const rawData = useMemo(() => extractData(tnode), []); - const isPolarChart = useMemo(() => false, [rawData]); - const {data, xKey, yKeys} = useMemo(() => (isPolarChart ? {} : prepareDataForCartesianChart(rawData)), [rawData, isPolarChart]); +/** + * Parse attribute as JSON or fallback to input as is + * Example: "20" -> 20 + * : "[ {x: 'Jan', y: 3} ]" -> `[{"x": "Jan", "y": 3}]` (Valid RFC 8259) + * : "Green" -> "Green" + */ +function parseAttribute(attribute: string): any { + if (!attribute) { + return undefined; + } + try { + return JSON5.parse(attribute); + } catch { + return attribute; + } +} - window.tnode = tnode; - console.log({data}); +function VictoryChartRenderer({tnode}: CustomRendererProps) { + const rawData = useMemo(() => extractData(tnode), [tnode]); + const isPolarChart = useMemo(() => false, [rawData]); + const {data, xKey, yKeys} = useMemo(() => prepareDataForCartesianChart(rawData), [rawData, isPolarChart]); - const renderChild = useCallback((tnode, index, points, chartBounds) => { + const renderCartesianChartChild = useCallback((tnode: TNode, index: Number, renderArgs: CartesianChartRenderArg) => { const key = `${tnode.tagName ?? 'node'}-${index}`; const yKey = Y_KEY_PREFIX + getHierarchyID(tnode); + const {points, chartBounds} = renderArgs; switch (tnode.tagName) { case 'victorybar': return ( @@ -84,10 +101,8 @@ function VictoryChartRenderer({TDefaultRenderer, tnode, ...defaultRendererProps} key={key} points={points[yKey]} chartBounds={chartBounds} - color="red" - roundedCorners={{topLeft: 10, topRight: 10}} > - {tnode.children.map((child, childIndex) => renderChild(child, childIndex, points, chartBounds))} + {tnode.children.map((child, childIndex) => renderCartesianChartChild(child, childIndex, renderArgs))} ); case 'victoryline': @@ -95,10 +110,8 @@ function VictoryChartRenderer({TDefaultRenderer, tnode, ...defaultRendererProps} - {tnode.children.map((child, childIndex) => renderChild(child, childIndex, points, chartBounds))} + {tnode.children.map((child, childIndex) => renderCartesianChartChild(child, childIndex, renderArgs))} ); default: @@ -112,8 +125,10 @@ function VictoryChartRenderer({TDefaultRenderer, tnode, ...defaultRendererProps} data={data} xKey={xKey} yKeys={yKeys} + domain={parseAttribute(tnode.attributes.domain)} + domainPadding={parseAttribute(tnode.attributes.domainPadding)} > - {({points, chartBounds}) => tnode.children.map((child, childIndex) => renderChild(child, childIndex, points, chartBounds))} + {(renderArgs) => tnode.children.map((child, childIndex) => renderCartesianChartChild(child, childIndex, renderArgs))} ); From da0a9dcb012df4b2f5de8da405c34c0d4464414f Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Thu, 14 May 2026 18:20:13 +0100 Subject: [PATCH 10/69] add some Bar props --- src/components/Charts/BarChart/BarChartContent.tsx | 1 + .../HTMLRenderers/VictoryChartRenderer.tsx | 10 ++++++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/components/Charts/BarChart/BarChartContent.tsx b/src/components/Charts/BarChart/BarChartContent.tsx index 6eab22c00f57..42b96217ae2e 100644 --- a/src/components/Charts/BarChart/BarChartContent.tsx +++ b/src/components/Charts/BarChart/BarChartContent.tsx @@ -303,3 +303,4 @@ function BarChartContent({data, isLoading, yAxisUnit, yAxisUnitPosition = 'left' export default BarChartContent; export type {BarChartProps}; +export {BAR_INNER_PADDING}; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer.tsx index 605b0253d82d..ba41aeed5255 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer.tsx @@ -5,6 +5,7 @@ import {View} from 'react-native'; import {type CustomRendererProps, type TBlock, TNode, TNodeChildrenRenderer} from 'react-native-render-html'; import type {CartesianChartRenderArg, ChartBounds, PointsArray} from 'victory-native'; import {Bar, CartesianChart, Line} from 'victory-native'; +import {BAR_INNER_PADDING} from '@components/Charts/BarChart/BarChartContent'; import * as HTMLEngineUtils from '@components/HTMLEngineProvider/htmlEngineUtils'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; import {showContextMenuForReport, useShowContextMenuActions, useShowContextMenuState} from '@components/ShowContextMenuContext'; @@ -90,6 +91,8 @@ function VictoryChartRenderer({tnode}: CustomRendererProps) { const isPolarChart = useMemo(() => false, [rawData]); const {data, xKey, yKeys} = useMemo(() => prepareDataForCartesianChart(rawData), [rawData, isPolarChart]); + window.tnode = tnode; + const renderCartesianChartChild = useCallback((tnode: TNode, index: Number, renderArgs: CartesianChartRenderArg) => { const key = `${tnode.tagName ?? 'node'}-${index}`; const yKey = Y_KEY_PREFIX + getHierarchyID(tnode); @@ -101,6 +104,8 @@ function VictoryChartRenderer({tnode}: CustomRendererProps) { key={key} points={points[yKey]} chartBounds={chartBounds} + innerPadding={BAR_INNER_PADDING} + roundedCorners={parseAttribute(tnode.attributes.cornerradius)} > {tnode.children.map((child, childIndex) => renderCartesianChartChild(child, childIndex, renderArgs))} @@ -120,13 +125,14 @@ function VictoryChartRenderer({tnode}: CustomRendererProps) { }, []); return ( - + {(renderArgs) => tnode.children.map((child, childIndex) => renderCartesianChartChild(child, childIndex, renderArgs))} From 3343cada35d5983fcaf1ce2dd99892f35fb9db82 Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Thu, 14 May 2026 18:47:25 +0100 Subject: [PATCH 11/69] add props for victorybar and victoryline --- .../HTMLRenderers/VictoryChartRenderer.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer.tsx index ba41aeed5255..c51b06c25ca4 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer.tsx @@ -6,6 +6,7 @@ import {type CustomRendererProps, type TBlock, TNode, TNodeChildrenRenderer} fro import type {CartesianChartRenderArg, ChartBounds, PointsArray} from 'victory-native'; import {Bar, CartesianChart, Line} from 'victory-native'; import {BAR_INNER_PADDING} from '@components/Charts/BarChart/BarChartContent'; +import {DEFAULT_CHART_COLOR} from '@components/Charts/utils'; import * as HTMLEngineUtils from '@components/HTMLEngineProvider/htmlEngineUtils'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; import {showContextMenuForReport, useShowContextMenuActions, useShowContextMenuState} from '@components/ShowContextMenuContext'; @@ -91,8 +92,6 @@ function VictoryChartRenderer({tnode}: CustomRendererProps) { const isPolarChart = useMemo(() => false, [rawData]); const {data, xKey, yKeys} = useMemo(() => prepareDataForCartesianChart(rawData), [rawData, isPolarChart]); - window.tnode = tnode; - const renderCartesianChartChild = useCallback((tnode: TNode, index: Number, renderArgs: CartesianChartRenderArg) => { const key = `${tnode.tagName ?? 'node'}-${index}`; const yKey = Y_KEY_PREFIX + getHierarchyID(tnode); @@ -104,8 +103,11 @@ function VictoryChartRenderer({tnode}: CustomRendererProps) { key={key} points={points[yKey]} chartBounds={chartBounds} + color={DEFAULT_CHART_COLOR} innerPadding={BAR_INNER_PADDING} roundedCorners={parseAttribute(tnode.attributes.cornerradius)} + barWidth={parseAttribute(tnode.attributes.barwidth)} + labels={parseAttribute(tnode.attributes.labels)} > {tnode.children.map((child, childIndex) => renderCartesianChartChild(child, childIndex, renderArgs))} @@ -115,6 +117,9 @@ function VictoryChartRenderer({tnode}: CustomRendererProps) { {tnode.children.map((child, childIndex) => renderCartesianChartChild(child, childIndex, renderArgs))} From f2090cef775897c598e1259264bdc516392e1940 Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Thu, 14 May 2026 23:42:30 +0100 Subject: [PATCH 12/69] parse styles --- .../HTMLRenderers/VictoryChartRenderer.tsx | 123 +++++++++++++----- 1 file changed, 91 insertions(+), 32 deletions(-) diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer.tsx index c51b06c25ca4..884017ab014a 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer.tsx @@ -1,9 +1,12 @@ +import {topLeft} from '@shopify/react-native-skia'; import JSON5 from 'json5'; +import lodashIsObject from 'lodash/isObject'; import lodashMerge from 'lodash/merge'; import React, {useCallback, useMemo} from 'react'; import {View} from 'react-native'; +import type {ColorValue, ViewStyle} from 'react-native'; import {type CustomRendererProps, type TBlock, TNode, TNodeChildrenRenderer} from 'react-native-render-html'; -import type {CartesianChartRenderArg, ChartBounds, PointsArray} from 'victory-native'; +import type {CartesianChartRenderArg, ChartBounds, PointsArray, RoundedCorners} from 'victory-native'; import {Bar, CartesianChart, Line} from 'victory-native'; import {BAR_INNER_PADDING} from '@components/Charts/BarChart/BarChartContent'; import {DEFAULT_CHART_COLOR} from '@components/Charts/utils'; @@ -38,31 +41,31 @@ function getHierarchyID(tnode: TNode): string { /** * Traverse node and extract all points from `data` attributes */ -function extractData(tnode: TNode): Record { - const rawData: Record = {}; +function extractData(tnode: TNode) { + const data: Record = {}; + const xKey: string = X_KEY; + const yKeys: string[] = []; + if (tnode.attributes?.data) { const parsedData = parseAttribute(tnode.attributes.data); if (Array.isArray(parsedData)) { - const xKey = X_KEY; const yKey = Y_KEY_PREFIX + getHierarchyID(tnode); + yKeys.push(yKey); (parsedData as RawData[]).forEach((point) => { - rawData[point.x] = { + data[point.x] = { [xKey]: point.x, [yKey]: point.y, }; }); } } - return lodashMerge(rawData, ...tnode.children.map((child) => extractData(child))); -} -/** - * Returns required props for `` - */ -function prepareDataForCartesianChart(rawData: Record) { - const data = Object.values(rawData); - const xKey = X_KEY; - const yKeys = data.length > 0 ? Object.keys(data[0]).filter((key) => key !== xKey) : []; + tnode.children.forEach((child) => { + const {data: childData, yKeys: childYkeys} = extractData(child); + lodashMerge(data, childData); + yKeys.push(...childYkeys); + }); + return { data, xKey, @@ -87,15 +90,69 @@ function parseAttribute(attribute: string): any { } } +/** + * Helper to parse VC's `cornerRadius` into VN's `roundedCorners` + */ +function parseCornerRadius(attribute: string): RoundedCorners | undefined { + const cornerRadius = parseAttribute(attribute); + if (typeof cornerRadius === 'number') { + return { + topLeft: cornerRadius, + topRight: cornerRadius, + bottomLeft: cornerRadius, + bottomRight: cornerRadius, + }; + } + if (lodashIsObject(cornerRadius)) { + return { + topLeft: 'topLeft' in cornerRadius ? (cornerRadius.topLeft as number) : 'top' in cornerRadius ? (cornerRadius.top as number) : undefined, + topRight: 'topRight' in cornerRadius ? (cornerRadius.topRight as number) : 'top' in cornerRadius ? (cornerRadius.top as number) : undefined, + bottomLeft: 'bottomLeft' in cornerRadius ? (cornerRadius.bottomLeft as number) : 'bottom' in cornerRadius ? (cornerRadius.bottom as number) : undefined, + bottomRight: 'bottomRight' in cornerRadius ? (cornerRadius.bottomRight as number) : 'bottom' in cornerRadius ? (cornerRadius.bottom as number) : undefined, + }; + } + return undefined; +} + +/** + * Helper to parse common styles + */ +function parseStyles(tnode: TNode) { + const nodeStyles: ViewStyle = {}; + const parentNodeStyles: ViewStyle = {}; + + const parsedHeight = parseAttribute(tnode.attributes.height); + if (typeof parsedHeight === 'number') { + nodeStyles.height = parsedHeight; + } + const parsedWidth = parseAttribute(tnode.attributes.width); + if (typeof parsedWidth === 'number') { + nodeStyles.width = parsedWidth; + } + + const parsedStyle = parseAttribute(tnode.attributes.style); + if (lodashIsObject(parsedStyle)) { + if ('parent' in parsedStyle && lodashIsObject(parsedStyle.parent)) { + Object.assign(parentNodeStyles, parsedStyle.parent); + } + if ('data' in parsedStyle && lodashIsObject(parsedStyle.data)) { + Object.assign(nodeStyles, parsedStyle.data); + } + } + + return {nodeStyles, parentNodeStyles}; +} + function VictoryChartRenderer({tnode}: CustomRendererProps) { - const rawData = useMemo(() => extractData(tnode), [tnode]); - const isPolarChart = useMemo(() => false, [rawData]); - const {data, xKey, yKeys} = useMemo(() => prepareDataForCartesianChart(rawData), [rawData, isPolarChart]); + const styles = useThemeStyles(); + const {data, xKey, yKeys} = useMemo(() => extractData(tnode), [tnode]); + const {nodeStyles, parentNodeStyles} = useMemo(() => parseStyles(tnode), [tnode]); - const renderCartesianChartChild = useCallback((tnode: TNode, index: Number, renderArgs: CartesianChartRenderArg) => { + const renderCartesianChartChild = useCallback((tnode: TNode, index: Number, renderArgs: CartesianChartRenderArg) => { const key = `${tnode.tagName ?? 'node'}-${index}`; const yKey = Y_KEY_PREFIX + getHierarchyID(tnode); const {points, chartBounds} = renderArgs; + const {nodeStyles} = parseStyles(tnode); switch (tnode.tagName) { case 'victorybar': return ( @@ -103,9 +160,9 @@ function VictoryChartRenderer({tnode}: CustomRendererProps) { key={key} points={points[yKey]} chartBounds={chartBounds} - color={DEFAULT_CHART_COLOR} + color={nodeStyles.fill ?? DEFAULT_CHART_COLOR} innerPadding={BAR_INNER_PADDING} - roundedCorners={parseAttribute(tnode.attributes.cornerradius)} + roundedCorners={parseCornerRadius(tnode.attributes.cornerradius)} barWidth={parseAttribute(tnode.attributes.barwidth)} labels={parseAttribute(tnode.attributes.labels)} > @@ -117,7 +174,7 @@ function VictoryChartRenderer({tnode}: CustomRendererProps) { @@ -130,17 +187,19 @@ function VictoryChartRenderer({tnode}: CustomRendererProps) { }, []); return ( - - - {(renderArgs) => tnode.children.map((child, childIndex) => renderCartesianChartChild(child, childIndex, renderArgs))} - + + + + {(renderArgs) => tnode.children.map((child, childIndex) => renderCartesianChartChild(child, childIndex, renderArgs))} + + ); } From 1dc1a3f19a1a50a5128bb7e6787b868717acefb7 Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Thu, 14 May 2026 23:55:57 +0100 Subject: [PATCH 13/69] remove unused labels prop --- .../HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer.tsx index 884017ab014a..8dc1221bd0bb 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer.tsx @@ -164,7 +164,6 @@ function VictoryChartRenderer({tnode}: CustomRendererProps) { innerPadding={BAR_INNER_PADDING} roundedCorners={parseCornerRadius(tnode.attributes.cornerradius)} barWidth={parseAttribute(tnode.attributes.barwidth)} - labels={parseAttribute(tnode.attributes.labels)} > {tnode.children.map((child, childIndex) => renderCartesianChartChild(child, childIndex, renderArgs))} From e26d318a1432151bb380efa7af041838188df85b Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Fri, 15 May 2026 03:36:59 +0100 Subject: [PATCH 14/69] handle xaxis/yaxis - still missing font --- .../HTMLRenderers/VictoryChartRenderer.tsx | 82 +++++++++++++++++-- 1 file changed, 74 insertions(+), 8 deletions(-) diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer.tsx index 8dc1221bd0bb..d76388da8402 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer.tsx @@ -1,8 +1,8 @@ -import {topLeft} from '@shopify/react-native-skia'; +import {Color, topLeft} from '@shopify/react-native-skia'; import JSON5 from 'json5'; import lodashIsObject from 'lodash/isObject'; import lodashMerge from 'lodash/merge'; -import React, {useCallback, useMemo} from 'react'; +import React, {ComponentProps, useCallback, useMemo} from 'react'; import {View} from 'react-native'; import type {ColorValue, ViewStyle} from 'react-native'; import {type CustomRendererProps, type TBlock, TNode, TNodeChildrenRenderer} from 'react-native-render-html'; @@ -39,14 +39,16 @@ function getHierarchyID(tnode: TNode): string { } /** - * Traverse node and extract all points from `data` attributes + * Traverse all nodes to extract points from `data` attributes and other config e.g. axis configuration */ function extractData(tnode: TNode) { const data: Record = {}; const xKey: string = X_KEY; const yKeys: string[] = []; + let xAxis: ComponentProps>['xAxis']; + let yAxis: ComponentProps>['yAxis']; - if (tnode.attributes?.data) { + if (tnode.tagName === 'victorybar' || tnode.tagName === 'victorychart') { const parsedData = parseAttribute(tnode.attributes.data); if (Array.isArray(parsedData)) { const yKey = Y_KEY_PREFIX + getHierarchyID(tnode); @@ -58,18 +60,78 @@ function extractData(tnode: TNode) { }; }); } + } else if (tnode.tagName === 'victoryaxis') { + const isDependentAxis = 'dependentaxis' in tnode.attributes && tnode.attributes.dependentaxis !== 'false'; + const tickCount = parseAttribute(tnode.attributes.tickcount); + const tickValues = parseAttribute(tnode.attributes.tickvalues); + const orientation = parseAttribute(tnode.attributes.orientation); + const style = parseAttribute(tnode.attributes.style); + let lineColor: Color | undefined; + let lineWidth: number | undefined; + let labelColor: string | undefined; + let labelOffset: number | undefined; + if (lodashIsObject(style)) { + // VC uses separate colors for axis and grid but VN uses a unifed one. + if ('grid' in style && lodashIsObject(style.grid)) { + lineColor ??= 'stroke' in style.grid ? (style.grid.stroke as Color) : undefined; + lineWidth ??= 'strokeWidth' in style.grid ? (style.grid.strokeWidth as number) : undefined; + } + if ('axis' in style && lodashIsObject(style.axis)) { + lineColor ??= 'stroke' in style.axis ? (style.axis.stroke as Color) : undefined; + lineWidth ??= 'strokeWidth' in style.axis ? (style.axis.strokeWidth as number) : undefined; + } + if ('tickLabels' in style && lodashIsObject(style.tickLabels)) { + labelColor ??= 'fill' in style.tickLabels ? (style.tickLabels.fill as string) : undefined; + labelOffset ??= 'padding' in style.tickLabels ? (style.tickLabels.padding as number) : undefined; + } + } + if (isDependentAxis) { + yAxis = [ + { + tickCount, + tickValues, + axisSide: orientation === 'right' ? 'right' : 'left', + lineColor, + lineWidth, + labelColor, + labelOffset, + }, + ]; + } else { + xAxis = { + tickCount, + tickValues, + axisSide: orientation === 'top' ? 'top' : 'bottom', + lineColor, + lineWidth, + labelColor, + labelOffset, + }; + } } tnode.children.forEach((child) => { - const {data: childData, yKeys: childYkeys} = extractData(child); - lodashMerge(data, childData); - yKeys.push(...childYkeys); + const childData = extractData(child); + yKeys.push(...childData.yKeys); + if (childData.xAxis) { + // It's safe to replace because there should be at most one xAxis + xAxis = childData.xAxis; + } + if (childData.yAxis) { + if (!yAxis) { + yAxis = []; + } + yAxis.push(...childData.yAxis); + } + lodashMerge(data, childData.data); }); return { data, xKey, yKeys, + xAxis, + yAxis, }; } @@ -145,9 +207,11 @@ function parseStyles(tnode: TNode) { function VictoryChartRenderer({tnode}: CustomRendererProps) { const styles = useThemeStyles(); - const {data, xKey, yKeys} = useMemo(() => extractData(tnode), [tnode]); + const {data, xKey, yKeys, xAxis, yAxis} = useMemo(() => extractData(tnode), [tnode]); const {nodeStyles, parentNodeStyles} = useMemo(() => parseStyles(tnode), [tnode]); + window.tnode = tnode; + const renderCartesianChartChild = useCallback((tnode: TNode, index: Number, renderArgs: CartesianChartRenderArg) => { const key = `${tnode.tagName ?? 'node'}-${index}`; const yKey = Y_KEY_PREFIX + getHierarchyID(tnode); @@ -192,6 +256,8 @@ function VictoryChartRenderer({tnode}: CustomRendererProps) { data={Object.values(data)} xKey={xKey} yKeys={yKeys} + xAxis={xAxis} + yAxis={yAxis} domain={parseAttribute(tnode.attributes.domain)} domainPadding={parseAttribute(tnode.attributes.domainpadding)} padding={parseAttribute(tnode.attributes.padding)} From 54758aacdba2d113d454244e3e5541cb5fa08293 Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Fri, 15 May 2026 07:17:29 +0100 Subject: [PATCH 15/69] use default font --- src/components/Charts/hooks/index.ts | 2 +- .../useChartFontManager.native.ts | 7 +++- .../useChartFontManager.ts | 7 +++- .../HTMLRenderers/VictoryChartRenderer.tsx | 36 +++++++++++-------- 4 files changed, 34 insertions(+), 18 deletions(-) diff --git a/src/components/Charts/hooks/index.ts b/src/components/Charts/hooks/index.ts index 13781a736967..2d6c24713d34 100644 --- a/src/components/Charts/hooks/index.ts +++ b/src/components/Charts/hooks/index.ts @@ -2,7 +2,7 @@ export {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 useChartFontManager, useChartDefaultTypeface} from './useChartFontManager/useChartFontManager'; 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 5b3f293c2def..0770190a7cbe 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} from '@shopify/react-native-skia'; +import {useFonts, useTypeface} from '@shopify/react-native-skia'; function useChartFontManager(): SkTypefaceFontProvider | null { return useFonts({ @@ -14,4 +14,9 @@ function useChartFontManager(): SkTypefaceFontProvider | null { }); } +function useChartDefaultTypeface() { + return useTypeface(require('@assets/fonts/native/ExpensifyNeue-Regular.otf') as DataModule); +} + +export {useChartDefaultTypeface}; export default useChartFontManager; diff --git a/src/components/Charts/hooks/useChartFontManager/useChartFontManager.ts b/src/components/Charts/hooks/useChartFontManager/useChartFontManager.ts index 6c5062a02086..58020daec4c5 100644 --- a/src/components/Charts/hooks/useChartFontManager/useChartFontManager.ts +++ b/src/components/Charts/hooks/useChartFontManager/useChartFontManager.ts @@ -1,5 +1,5 @@ import type {DataModule, SkTypefaceFontProvider} from '@shopify/react-native-skia'; -import {useFonts} 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 @@ -22,4 +22,9 @@ function useChartFontManager(): SkTypefaceFontProvider | null { }); } +function useChartDefaultTypeface() { + return useTypeface(webFont(require('@assets/fonts/web/ExpensifyNeue-Regular.woff2') as string)); +} + +export {useChartDefaultTypeface}; export default useChartFontManager; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer.tsx index d76388da8402..510286b87345 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer.tsx @@ -1,14 +1,15 @@ -import {Color, topLeft} from '@shopify/react-native-skia'; +import {type Color, listFontFamilies, matchFont, type SkFont, Skia, SkTypeface, type SkTypefaceFontProvider} from '@shopify/react-native-skia'; import JSON5 from 'json5'; import lodashIsObject from 'lodash/isObject'; import lodashMerge from 'lodash/merge'; import React, {ComponentProps, useCallback, useMemo} from 'react'; import {View} from 'react-native'; -import type {ColorValue, ViewStyle} from 'react-native'; +import type {ColorValue, TextStyle, ViewStyle} from 'react-native'; import {type CustomRendererProps, type TBlock, TNode, TNodeChildrenRenderer} from 'react-native-render-html'; import type {CartesianChartRenderArg, ChartBounds, PointsArray, RoundedCorners} from 'victory-native'; import {Bar, CartesianChart, Line} from 'victory-native'; import {BAR_INNER_PADDING} from '@components/Charts/BarChart/BarChartContent'; +import {useChartDefaultTypeface} from '@components/Charts/hooks'; import {DEFAULT_CHART_COLOR} from '@components/Charts/utils'; import * as HTMLEngineUtils from '@components/HTMLEngineProvider/htmlEngineUtils'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; @@ -41,7 +42,7 @@ function getHierarchyID(tnode: TNode): string { /** * Traverse all nodes to extract points from `data` attributes and other config e.g. axis configuration */ -function extractData(tnode: TNode) { +function processNode(tnode: TNode, typeface: SkTypeface | null) { const data: Record = {}; const xKey: string = X_KEY; const yKeys: string[] = []; @@ -70,21 +71,24 @@ function extractData(tnode: TNode) { let lineWidth: number | undefined; let labelColor: string | undefined; let labelOffset: number | undefined; + let fontSize: number | undefined; if (lodashIsObject(style)) { // VC uses separate colors for axis and grid but VN uses a unifed one. if ('grid' in style && lodashIsObject(style.grid)) { lineColor ??= 'stroke' in style.grid ? (style.grid.stroke as Color) : undefined; - lineWidth ??= 'strokeWidth' in style.grid ? (style.grid.strokeWidth as number) : undefined; + lineWidth ??= 'strokeWidth' in style.grid ? Number(style.grid.strokeWidth) : undefined; } if ('axis' in style && lodashIsObject(style.axis)) { lineColor ??= 'stroke' in style.axis ? (style.axis.stroke as Color) : undefined; - lineWidth ??= 'strokeWidth' in style.axis ? (style.axis.strokeWidth as number) : undefined; + lineWidth ??= 'strokeWidth' in style.axis ? Number(style.axis.strokeWidth) : undefined; } if ('tickLabels' in style && lodashIsObject(style.tickLabels)) { - labelColor ??= 'fill' in style.tickLabels ? (style.tickLabels.fill as string) : undefined; - labelOffset ??= 'padding' in style.tickLabels ? (style.tickLabels.padding as number) : undefined; + labelColor ??= 'fill' in style.tickLabels ? String(style.tickLabels.fill) : undefined; + labelOffset ??= 'padding' in style.tickLabels ? Number(style.tickLabels.padding) : undefined; + fontSize ??= 'fontSize' in style.tickLabels ? Number(style.tickLabels.fontSize) : undefined; } } + const font = typeface ? Skia.Font(typeface, fontSize) : null; if (isDependentAxis) { yAxis = [ { @@ -95,6 +99,7 @@ function extractData(tnode: TNode) { lineWidth, labelColor, labelOffset, + font, }, ]; } else { @@ -106,12 +111,13 @@ function extractData(tnode: TNode) { lineWidth, labelColor, labelOffset, + font, }; } } tnode.children.forEach((child) => { - const childData = extractData(child); + const childData = processNode(child, typeface); yKeys.push(...childData.yKeys); if (childData.xAxis) { // It's safe to replace because there should be at most one xAxis @@ -167,10 +173,10 @@ function parseCornerRadius(attribute: string): RoundedCorners | undefined { } if (lodashIsObject(cornerRadius)) { return { - topLeft: 'topLeft' in cornerRadius ? (cornerRadius.topLeft as number) : 'top' in cornerRadius ? (cornerRadius.top as number) : undefined, - topRight: 'topRight' in cornerRadius ? (cornerRadius.topRight as number) : 'top' in cornerRadius ? (cornerRadius.top as number) : undefined, - bottomLeft: 'bottomLeft' in cornerRadius ? (cornerRadius.bottomLeft as number) : 'bottom' in cornerRadius ? (cornerRadius.bottom as number) : undefined, - bottomRight: 'bottomRight' in cornerRadius ? (cornerRadius.bottomRight as number) : 'bottom' in cornerRadius ? (cornerRadius.bottom as number) : undefined, + topLeft: 'topLeft' in cornerRadius ? Number(cornerRadius.topLeft) : 'top' in cornerRadius ? Number(cornerRadius.top) : undefined, + topRight: 'topRight' in cornerRadius ? Number(cornerRadius.topRight) : 'top' in cornerRadius ? Number(cornerRadius.top) : undefined, + bottomLeft: 'bottomLeft' in cornerRadius ? Number(cornerRadius.bottomLeft) : 'bottom' in cornerRadius ? Number(cornerRadius.bottom) : undefined, + bottomRight: 'bottomRight' in cornerRadius ? Number(cornerRadius.bottomRight) : 'bottom' in cornerRadius ? Number(cornerRadius.bottom) : undefined, }; } return undefined; @@ -207,11 +213,10 @@ function parseStyles(tnode: TNode) { function VictoryChartRenderer({tnode}: CustomRendererProps) { const styles = useThemeStyles(); - const {data, xKey, yKeys, xAxis, yAxis} = useMemo(() => extractData(tnode), [tnode]); + const typeface = useChartDefaultTypeface(); + const {data, xKey, yKeys, xAxis, yAxis} = useMemo(() => processNode(tnode, typeface), [tnode, typeface]); const {nodeStyles, parentNodeStyles} = useMemo(() => parseStyles(tnode), [tnode]); - window.tnode = tnode; - const renderCartesianChartChild = useCallback((tnode: TNode, index: Number, renderArgs: CartesianChartRenderArg) => { const key = `${tnode.tagName ?? 'node'}-${index}`; const yKey = Y_KEY_PREFIX + getHierarchyID(tnode); @@ -259,6 +264,7 @@ function VictoryChartRenderer({tnode}: CustomRendererProps) { xAxis={xAxis} yAxis={yAxis} domain={parseAttribute(tnode.attributes.domain)} + // s77rt TOOD fix compatibility domainPadding={parseAttribute(tnode.attributes.domainpadding)} padding={parseAttribute(tnode.attributes.padding)} > From 4ac420e06b1e17f00048cd31adfeaccfa561d3ec Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Fri, 15 May 2026 07:43:52 +0100 Subject: [PATCH 16/69] parse domain padding correctly --- .../HTMLRenderers/VictoryChartRenderer.tsx | 43 ++++++++++++++++++- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer.tsx index 510286b87345..05c5514fde25 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer.tsx @@ -182,6 +182,46 @@ function parseCornerRadius(attribute: string): RoundedCorners | undefined { return undefined; } +/** + * Helper to parse VC's `domainPadding` into VN's `domainPadding` + */ +function parseDomainPadding(attribute: string): ComponentProps>['domainPadding'] | undefined { + const domainPadding = parseAttribute(attribute); + if (typeof domainPadding === 'number') { + return domainPadding; + } + if (Array.isArray(domainPadding)) { + return { + left: domainPadding[0], + right: domainPadding[1], + }; + } + if (lodashIsObject(domainPadding)) { + let left: number | undefined, right: number | undefined, top: number | undefined, bottom: number | undefined; + if ('x' in domainPadding && typeof domainPadding.x === 'number') { + left = domainPadding.x; + right = domainPadding.x; + } else if ('x' in domainPadding && Array.isArray(domainPadding.x)) { + left = domainPadding.x[0]; + right = domainPadding.x[1]; + } + if ('y' in domainPadding && typeof domainPadding.y === 'number') { + top = domainPadding.y; + bottom = domainPadding.y; + } else if ('y' in domainPadding && Array.isArray(domainPadding.y)) { + top = domainPadding.y[1]; + bottom = domainPadding.y[0]; + } + return { + left, + right, + top, + bottom, + }; + } + return undefined; +} + /** * Helper to parse common styles */ @@ -264,8 +304,7 @@ function VictoryChartRenderer({tnode}: CustomRendererProps) { xAxis={xAxis} yAxis={yAxis} domain={parseAttribute(tnode.attributes.domain)} - // s77rt TOOD fix compatibility - domainPadding={parseAttribute(tnode.attributes.domainpadding)} + domainPadding={parseDomainPadding(tnode.attributes.domainpadding)} padding={parseAttribute(tnode.attributes.padding)} > {(renderArgs) => tnode.children.map((child, childIndex) => renderCartesianChartChild(child, childIndex, renderArgs))} From 14d2e6ae87682e464f1fc36b88424ea0e2b6b03d Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Tue, 19 May 2026 01:57:57 +0100 Subject: [PATCH 17/69] support VictoryLabel --- .../HTMLRenderers/VictoryChartRenderer.tsx | 78 ++++++++++++++++++- 1 file changed, 76 insertions(+), 2 deletions(-) diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer.tsx index 05c5514fde25..11d52c2623cf 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer.tsx @@ -1,4 +1,4 @@ -import {type Color, listFontFamilies, matchFont, type SkFont, Skia, SkTypeface, type SkTypefaceFontProvider} from '@shopify/react-native-skia'; +import {Circle, type Color, listFontFamilies, matchFont, Paragraph, type SkFont, Skia, Text as SkText, SkTypeface, type SkTypefaceFontProvider} from '@shopify/react-native-skia'; import JSON5 from 'json5'; import lodashIsObject from 'lodash/isObject'; import lodashMerge from 'lodash/merge'; @@ -26,6 +26,26 @@ const Y_KEY_PREFIX = 'y'; type RawData = Record; +type LabelItem = { + /** Position on the X-axis */ + x: number; + + /** Position on the Y-axis */ + y: number; + + /** Text to draw */ + text: string; + + /** The color of the text */ + color: Color; + + /** Font size */ + fontSize: number; + + /** Font weight */ + fontWeight: 'normal' | 'bold'; +}; + /** * Get node unique ID based on hierarchy */ @@ -48,6 +68,7 @@ function processNode(tnode: TNode, typeface: SkTypeface | null) { const yKeys: string[] = []; let xAxis: ComponentProps>['xAxis']; let yAxis: ComponentProps>['yAxis']; + const labelItems: LabelItem[] = []; if (tnode.tagName === 'victorybar' || tnode.tagName === 'victorychart') { const parsedData = parseAttribute(tnode.attributes.data); @@ -114,6 +135,33 @@ function processNode(tnode: TNode, typeface: SkTypeface | null) { font, }; } + } else if (tnode.tagName === 'victorylabel') { + const x = parseAttribute(tnode.attributes.x); + const y = parseAttribute(tnode.attributes.y); + const text = parseAttribute(tnode.attributes.text); + const style = parseAttribute(tnode.attributes.style); + let color: Color = 'black'; + let fontSize: number = 16; + let fontWeight: 'normal' | 'bold' = 'normal'; + if (lodashIsObject(style)) { + if ('fill' in style) { + color = style.fill as Color; + } + if ('fontSize' in style) { + fontSize = Number(style.fontSize); + } + if ('fontWeight' in style && Number(style.fontWeight) === 700) { + fontWeight = 'bold'; + } + } + labelItems.push({ + x, + y, + text, + color, + fontSize, + fontWeight, + }); } tnode.children.forEach((child) => { @@ -129,6 +177,7 @@ function processNode(tnode: TNode, typeface: SkTypeface | null) { } yAxis.push(...childData.yAxis); } + labelItems.push(...childData.labelItems); lodashMerge(data, childData.data); }); @@ -138,6 +187,7 @@ function processNode(tnode: TNode, typeface: SkTypeface | null) { yKeys, xAxis, yAxis, + labelItems, }; } @@ -254,7 +304,7 @@ function parseStyles(tnode: TNode) { function VictoryChartRenderer({tnode}: CustomRendererProps) { const styles = useThemeStyles(); const typeface = useChartDefaultTypeface(); - const {data, xKey, yKeys, xAxis, yAxis} = useMemo(() => processNode(tnode, typeface), [tnode, typeface]); + const {data, xKey, yKeys, xAxis, yAxis, labelItems} = useMemo(() => processNode(tnode, typeface), [tnode, typeface]); const {nodeStyles, parentNodeStyles} = useMemo(() => parseStyles(tnode), [tnode]); const renderCartesianChartChild = useCallback((tnode: TNode, index: Number, renderArgs: CartesianChartRenderArg) => { @@ -294,6 +344,29 @@ function VictoryChartRenderer({tnode}: CustomRendererProps) { } }, []); + const renderOutside = useCallback( + (renderArgs: CartesianChartRenderArg) => { + return ( + <> + {labelItems.map(({x, y, text, color, fontSize, fontWeight}) => { + const font = typeface ? Skia.Font(typeface, fontSize) : null; + return ( + + ); + })} + + ); + }, + [labelItems, typeface], + ); + return ( @@ -306,6 +379,7 @@ function VictoryChartRenderer({tnode}: CustomRendererProps) { domain={parseAttribute(tnode.attributes.domain)} domainPadding={parseDomainPadding(tnode.attributes.domainpadding)} padding={parseAttribute(tnode.attributes.padding)} + renderOutside={renderOutside} > {(renderArgs) => tnode.children.map((child, childIndex) => renderCartesianChartChild(child, childIndex, renderArgs))} From f87cdd7e97197b6c61adce33f77a675b2c9002d8 Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Tue, 19 May 2026 02:06:07 +0100 Subject: [PATCH 18/69] support bold font weight --- .../useChartFontManager/useChartFontManager.native.ts | 4 +++- .../hooks/useChartFontManager/useChartFontManager.ts | 4 +++- .../HTMLRenderers/VictoryChartRenderer.tsx | 7 ++++--- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/components/Charts/hooks/useChartFontManager/useChartFontManager.native.ts b/src/components/Charts/hooks/useChartFontManager/useChartFontManager.native.ts index 0770190a7cbe..04cd0344d420 100644 --- a/src/components/Charts/hooks/useChartFontManager/useChartFontManager.native.ts +++ b/src/components/Charts/hooks/useChartFontManager/useChartFontManager.native.ts @@ -15,7 +15,9 @@ function useChartFontManager(): SkTypefaceFontProvider | null { } function useChartDefaultTypeface() { - return useTypeface(require('@assets/fonts/native/ExpensifyNeue-Regular.otf') as DataModule); + 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}; diff --git a/src/components/Charts/hooks/useChartFontManager/useChartFontManager.ts b/src/components/Charts/hooks/useChartFontManager/useChartFontManager.ts index 58020daec4c5..e0c8a8305fed 100644 --- a/src/components/Charts/hooks/useChartFontManager/useChartFontManager.ts +++ b/src/components/Charts/hooks/useChartFontManager/useChartFontManager.ts @@ -23,7 +23,9 @@ function useChartFontManager(): SkTypefaceFontProvider | null { } function useChartDefaultTypeface() { - return useTypeface(webFont(require('@assets/fonts/web/ExpensifyNeue-Regular.woff2') as string)); + 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}; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer.tsx index 11d52c2623cf..cff2432e39fc 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer.tsx @@ -303,8 +303,8 @@ function parseStyles(tnode: TNode) { function VictoryChartRenderer({tnode}: CustomRendererProps) { const styles = useThemeStyles(); - const typeface = useChartDefaultTypeface(); - const {data, xKey, yKeys, xAxis, yAxis, labelItems} = useMemo(() => processNode(tnode, typeface), [tnode, typeface]); + const {regular: regularTypeface, bold: boldTypeface} = useChartDefaultTypeface(); + const {data, xKey, yKeys, xAxis, yAxis, labelItems} = useMemo(() => processNode(tnode, regularTypeface), [tnode, regularTypeface]); const {nodeStyles, parentNodeStyles} = useMemo(() => parseStyles(tnode), [tnode]); const renderCartesianChartChild = useCallback((tnode: TNode, index: Number, renderArgs: CartesianChartRenderArg) => { @@ -349,6 +349,7 @@ function VictoryChartRenderer({tnode}: CustomRendererProps) { return ( <> {labelItems.map(({x, y, text, color, fontSize, fontWeight}) => { + const typeface = fontWeight === 'bold' ? boldTypeface : regularTypeface; const font = typeface ? Skia.Font(typeface, fontSize) : null; return ( ) { ); }, - [labelItems, typeface], + [labelItems, regularTypeface, boldTypeface], ); return ( From 1fed4efde51021df9dee739af6c3a5ba16f81516 Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Tue, 19 May 2026 03:34:20 +0100 Subject: [PATCH 19/69] support VictoryLegend --- .../HTMLRenderers/VictoryChartRenderer.tsx | 121 +++++++++++++++++- 1 file changed, 118 insertions(+), 3 deletions(-) diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer.tsx index cff2432e39fc..f57c992e929a 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer.tsx @@ -2,7 +2,7 @@ import {Circle, type Color, listFontFamilies, matchFont, Paragraph, type SkFont, import JSON5 from 'json5'; import lodashIsObject from 'lodash/isObject'; import lodashMerge from 'lodash/merge'; -import React, {ComponentProps, useCallback, useMemo} from 'react'; +import React, {ComponentProps, Fragment, useCallback, useMemo} from 'react'; import {View} from 'react-native'; import type {ColorValue, TextStyle, ViewStyle} from 'react-native'; import {type CustomRendererProps, type TBlock, TNode, TNodeChildrenRenderer} from 'react-native-render-html'; @@ -46,6 +46,43 @@ type LabelItem = { fontWeight: 'normal' | 'bold'; }; +type LegendItemEntry = { + /** Text to draw */ + text: string; + + /** The color of the text */ + color: Color; + + /** Font size */ + fontSize: number; + + /** Font weight */ + fontWeight: 'normal' | 'bold'; + + /** The color of the symbol */ + symbolColor: Color; + + /** Symbol size */ + symbolSize: number; +}; + +type LegendItem = { + /** Position on the X-axis */ + x: number; + + /** Position on the Y-axis */ + y: number; + + /** Legend entries */ + entries: LegendItemEntry[]; + + /** Space between entries */ + gutter: number; + + /** Space between entry's text and symbol */ + symbolSpacer: number; +}; + /** * Get node unique ID based on hierarchy */ @@ -69,6 +106,7 @@ function processNode(tnode: TNode, typeface: SkTypeface | null) { let xAxis: ComponentProps>['xAxis']; let yAxis: ComponentProps>['yAxis']; const labelItems: LabelItem[] = []; + const legendItems: LegendItem[] = []; if (tnode.tagName === 'victorybar' || tnode.tagName === 'victorychart') { const parsedData = parseAttribute(tnode.attributes.data); @@ -88,6 +126,7 @@ function processNode(tnode: TNode, typeface: SkTypeface | null) { const tickValues = parseAttribute(tnode.attributes.tickvalues); const orientation = parseAttribute(tnode.attributes.orientation); const style = parseAttribute(tnode.attributes.style); + // s77rt clean up the `let`s and nested code let lineColor: Color | undefined; let lineWidth: number | undefined; let labelColor: string | undefined; @@ -162,6 +201,50 @@ function processNode(tnode: TNode, typeface: SkTypeface | null) { fontSize, fontWeight, }); + } else if (tnode.tagName === 'victorylegend') { + const x = parseAttribute(tnode.attributes.x); + const y = parseAttribute(tnode.attributes.y); + const gutter = parseAttribute(tnode.attributes.gutter); + const symbolSpacer = parseAttribute(tnode.attributes.symbolspacer); + const data = parseAttribute(tnode.attributes.data); + const style = parseAttribute(tnode.attributes.style); + let color: Color = 'black'; + let fontSize: number = 13; + let fontWeight: 'normal' | 'bold' = 'normal'; + if (lodashIsObject(style) && 'labels' in style && lodashIsObject(style.labels)) { + if ('fill' in style.labels) { + color = style.labels.fill as Color; + } + if ('fontSize' in style.labels) { + fontSize = Number(style.labels.fontSize); + } + if ('fontWeight' in style.labels && Number(style.labels.fontWeight) === 700) { + fontWeight = 'bold'; + } + } + const entries: LegendItemEntry[] = []; + if (Array.isArray(data)) { + (data as Array>).forEach((entry) => { + const text = entry.name; + const symbolColor: Color = entry.symbol?.fill ?? 'black'; + const symbolSize: number = Number(entry.symbol?.size ?? 6); + entries.push({ + text, + color, + fontSize, + fontWeight, + symbolColor, + symbolSize, + }); + }); + } + legendItems.push({ + x, + y, + entries, + gutter, + symbolSpacer, + }); } tnode.children.forEach((child) => { @@ -178,6 +261,7 @@ function processNode(tnode: TNode, typeface: SkTypeface | null) { yAxis.push(...childData.yAxis); } labelItems.push(...childData.labelItems); + legendItems.push(...childData.legendItems); lodashMerge(data, childData.data); }); @@ -188,6 +272,7 @@ function processNode(tnode: TNode, typeface: SkTypeface | null) { xAxis, yAxis, labelItems, + legendItems, }; } @@ -304,7 +389,7 @@ function parseStyles(tnode: TNode) { function VictoryChartRenderer({tnode}: CustomRendererProps) { const styles = useThemeStyles(); const {regular: regularTypeface, bold: boldTypeface} = useChartDefaultTypeface(); - const {data, xKey, yKeys, xAxis, yAxis, labelItems} = useMemo(() => processNode(tnode, regularTypeface), [tnode, regularTypeface]); + const {data, xKey, yKeys, xAxis, yAxis, labelItems, legendItems} = useMemo(() => processNode(tnode, regularTypeface), [tnode, regularTypeface]); const {nodeStyles, parentNodeStyles} = useMemo(() => parseStyles(tnode), [tnode]); const renderCartesianChartChild = useCallback((tnode: TNode, index: Number, renderArgs: CartesianChartRenderArg) => { @@ -362,10 +447,40 @@ function VictoryChartRenderer({tnode}: CustomRendererProps) { /> ); })} + {legendItems.map(({x: startX, y, entries, gutter, symbolSpacer}, legendIndex) => { + let x = startX; + return entries.map(({text, color, fontSize, fontWeight, symbolColor, symbolSize}) => { + const typeface = fontWeight === 'bold' ? boldTypeface : regularTypeface; + const font = typeface ? Skia.Font(typeface, fontSize) : null; + const fontMetrics = font?.getMetrics(); + const lineHeight = fontMetrics ? fontMetrics.ascent + fontMetrics.descent + fontMetrics.leading : 0; + const symbolX = x; + x += symbolSize + symbolSpacer; + const textX = x; + x += (font?.getGlyphWidths(font.getGlyphIDs(text)).reduce((acc, width) => acc + width, 0) ?? 0) + gutter; + return ( + + + + + ); + }); + })} ); }, - [labelItems, regularTypeface, boldTypeface], + [labelItems, legendItems, regularTypeface, boldTypeface], ); return ( From 8091d60469a4e1da485d0bf9b2adff5f2ad172f5 Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Thu, 21 May 2026 00:46:48 +0100 Subject: [PATCH 20/69] clean up --- .../HTMLRenderers/VictoryChartRenderer.tsx | 265 ++++++++---------- 1 file changed, 110 insertions(+), 155 deletions(-) diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer.tsx index f57c992e929a..f13d93b475b3 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer.tsx @@ -1,31 +1,27 @@ -import {Circle, type Color, listFontFamilies, matchFont, Paragraph, type SkFont, Skia, Text as SkText, SkTypeface, type SkTypefaceFontProvider} from '@shopify/react-native-skia'; +import {Circle, Skia, Text as SkText, SkTypeface} from '@shopify/react-native-skia'; +import type {Color} from '@shopify/react-native-skia'; import JSON5 from 'json5'; import lodashIsObject from 'lodash/isObject'; import lodashMerge from 'lodash/merge'; import React, {ComponentProps, Fragment, useCallback, useMemo} from 'react'; import {View} from 'react-native'; -import type {ColorValue, TextStyle, ViewStyle} from 'react-native'; -import {type CustomRendererProps, type TBlock, TNode, TNodeChildrenRenderer} from 'react-native-render-html'; -import type {CartesianChartRenderArg, ChartBounds, PointsArray, RoundedCorners} from 'victory-native'; +import type {ViewStyle} from 'react-native'; +import {TNode} from 'react-native-render-html'; +import type {CustomRendererProps, TBlock} from 'react-native-render-html'; +import type {CartesianChartRenderArg, RoundedCorners} from 'victory-native'; import {Bar, CartesianChart, Line} from 'victory-native'; import {BAR_INNER_PADDING} from '@components/Charts/BarChart/BarChartContent'; import {useChartDefaultTypeface} from '@components/Charts/hooks'; import {DEFAULT_CHART_COLOR} from '@components/Charts/utils'; -import * as HTMLEngineUtils from '@components/HTMLEngineProvider/htmlEngineUtils'; -import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; -import {showContextMenuForReport, useShowContextMenuActions, useShowContextMenuState} from '@components/ShowContextMenuContext'; -import Text from '@components/Text'; -import useLocalize from '@hooks/useLocalize'; -import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; -import {isArchivedNonExpenseReport} from '@libs/ReportUtils'; -import CONST from '@src/CONST'; const X_KEY = 'x'; const Y_KEY_PREFIX = 'y'; type RawData = Record; +type StyleObject = Record; + type LabelItem = { /** Position on the X-axis */ x: number; @@ -109,45 +105,26 @@ function processNode(tnode: TNode, typeface: SkTypeface | null) { const legendItems: LegendItem[] = []; if (tnode.tagName === 'victorybar' || tnode.tagName === 'victorychart') { - const parsedData = parseAttribute(tnode.attributes.data); - if (Array.isArray(parsedData)) { - const yKey = Y_KEY_PREFIX + getHierarchyID(tnode); - yKeys.push(yKey); - (parsedData as RawData[]).forEach((point) => { - data[point.x] = { - [xKey]: point.x, - [yKey]: point.y, - }; - }); - } + const points = parseAttribute(tnode.attributes.data); + const yKey = Y_KEY_PREFIX + getHierarchyID(tnode); + yKeys.push(yKey); + points?.forEach((point) => { + data[point.x] = { + [xKey]: point.x, + [yKey]: point.y, + }; + }); } else if (tnode.tagName === 'victoryaxis') { const isDependentAxis = 'dependentaxis' in tnode.attributes && tnode.attributes.dependentaxis !== 'false'; - const tickCount = parseAttribute(tnode.attributes.tickcount); - const tickValues = parseAttribute(tnode.attributes.tickvalues); - const orientation = parseAttribute(tnode.attributes.orientation); - const style = parseAttribute(tnode.attributes.style); - // s77rt clean up the `let`s and nested code - let lineColor: Color | undefined; - let lineWidth: number | undefined; - let labelColor: string | undefined; - let labelOffset: number | undefined; - let fontSize: number | undefined; - if (lodashIsObject(style)) { - // VC uses separate colors for axis and grid but VN uses a unifed one. - if ('grid' in style && lodashIsObject(style.grid)) { - lineColor ??= 'stroke' in style.grid ? (style.grid.stroke as Color) : undefined; - lineWidth ??= 'strokeWidth' in style.grid ? Number(style.grid.strokeWidth) : undefined; - } - if ('axis' in style && lodashIsObject(style.axis)) { - lineColor ??= 'stroke' in style.axis ? (style.axis.stroke as Color) : undefined; - lineWidth ??= 'strokeWidth' in style.axis ? Number(style.axis.strokeWidth) : undefined; - } - if ('tickLabels' in style && lodashIsObject(style.tickLabels)) { - labelColor ??= 'fill' in style.tickLabels ? String(style.tickLabels.fill) : undefined; - labelOffset ??= 'padding' in style.tickLabels ? Number(style.tickLabels.padding) : undefined; - fontSize ??= 'fontSize' in style.tickLabels ? Number(style.tickLabels.fontSize) : undefined; - } - } + const tickCount = parseAttribute(tnode.attributes.tickcount); + const tickValues = parseAttribute(tnode.attributes.tickvalues); + const orientation = parseAttribute(tnode.attributes.orientation); + const style = parseAttribute(tnode.attributes.style); + const lineColor = style?.axis?.stroke !== undefined ? (style.axis.stroke as Color) : undefined; + const lineWidth = style?.axis?.strokeWidth !== undefined ? Number(style.axis.strokeWidth) : undefined; + const labelColor = style?.tickLabels?.fill !== undefined ? String(style.tickLabels.fill) : undefined; + const labelOffset = style?.tickLabels?.padding !== undefined ? Number(style.tickLabels.padding) : undefined; + const fontSize = style?.tickLabels?.fontSize !== undefined ? Number(style.tickLabels.fontSize) : undefined; const font = typeface ? Skia.Font(typeface, fontSize) : null; if (isDependentAxis) { yAxis = [ @@ -175,24 +152,13 @@ function processNode(tnode: TNode, typeface: SkTypeface | null) { }; } } else if (tnode.tagName === 'victorylabel') { - const x = parseAttribute(tnode.attributes.x); - const y = parseAttribute(tnode.attributes.y); - const text = parseAttribute(tnode.attributes.text); - const style = parseAttribute(tnode.attributes.style); - let color: Color = 'black'; - let fontSize: number = 16; - let fontWeight: 'normal' | 'bold' = 'normal'; - if (lodashIsObject(style)) { - if ('fill' in style) { - color = style.fill as Color; - } - if ('fontSize' in style) { - fontSize = Number(style.fontSize); - } - if ('fontWeight' in style && Number(style.fontWeight) === 700) { - fontWeight = 'bold'; - } - } + const x = parseAttribute(tnode.attributes.x) ?? 0; + const y = parseAttribute(tnode.attributes.y) ?? 0; + const text = parseAttribute(tnode.attributes.text) ?? ''; + const style = parseAttribute(tnode.attributes.style); + const color = style?.fill !== undefined ? (style.fill as Color) : 'black'; + const fontSize = style?.fontSize !== undefined ? Number(style.fontSize) : 16; + const fontWeight = Number(style?.fontWeight) === 700 ? 'bold' : 'normal'; labelItems.push({ x, y, @@ -202,42 +168,28 @@ function processNode(tnode: TNode, typeface: SkTypeface | null) { fontWeight, }); } else if (tnode.tagName === 'victorylegend') { - const x = parseAttribute(tnode.attributes.x); - const y = parseAttribute(tnode.attributes.y); - const gutter = parseAttribute(tnode.attributes.gutter); - const symbolSpacer = parseAttribute(tnode.attributes.symbolspacer); + const x = parseAttribute(tnode.attributes.x) ?? 0; + const y = parseAttribute(tnode.attributes.y) ?? 0; + const gutter = parseAttribute(tnode.attributes.gutter) ?? 32; + const symbolSpacer = parseAttribute(tnode.attributes.symbolspacer) ?? 8; const data = parseAttribute(tnode.attributes.data); - const style = parseAttribute(tnode.attributes.style); - let color: Color = 'black'; - let fontSize: number = 13; - let fontWeight: 'normal' | 'bold' = 'normal'; - if (lodashIsObject(style) && 'labels' in style && lodashIsObject(style.labels)) { - if ('fill' in style.labels) { - color = style.labels.fill as Color; - } - if ('fontSize' in style.labels) { - fontSize = Number(style.labels.fontSize); - } - if ('fontWeight' in style.labels && Number(style.labels.fontWeight) === 700) { - fontWeight = 'bold'; - } - } - const entries: LegendItemEntry[] = []; - if (Array.isArray(data)) { - (data as Array>).forEach((entry) => { - const text = entry.name; - const symbolColor: Color = entry.symbol?.fill ?? 'black'; - const symbolSize: number = Number(entry.symbol?.size ?? 6); - entries.push({ - text, - color, - fontSize, - fontWeight, - symbolColor, - symbolSize, - }); - }); - } + const style = parseAttribute(tnode.attributes.style); + const color = style?.labels?.fill !== undefined ? (style.labels.fill as Color) : 'black'; + const fontSize = style?.labels?.fontSize !== undefined ? Number(style.labels.fontSize) : 12; + const fontWeight = Number(style?.labels?.fontWeight) === 700 ? 'bold' : 'normal'; + const entries: LegendItemEntry[] = (parseAttribute(tnode.attributes.data) ?? []).map((entry) => { + const text = entry.name !== undefined ? String(entry.name) : ''; + const symbolColor = entry.symbol?.fill !== undefined ? (entry.symbol.fill as Color) : 'black'; + const symbolSize = entry.symbol?.size !== undefined ? Number(entry.symbol.size) : 4; + return { + text, + color, + fontSize, + fontWeight, + symbolColor, + symbolSize, + }; + }); legendItems.push({ x, y, @@ -282,14 +234,14 @@ function processNode(tnode: TNode, typeface: SkTypeface | null) { * : "[ {x: 'Jan', y: 3} ]" -> `[{"x": "Jan", "y": 3}]` (Valid RFC 8259) * : "Green" -> "Green" */ -function parseAttribute(attribute: string): any { +function parseAttribute(attribute: string): T | undefined { if (!attribute) { return undefined; } try { - return JSON5.parse(attribute); + return JSON5.parse(attribute); } catch { - return attribute; + return attribute as T; } } @@ -391,6 +343,7 @@ function VictoryChartRenderer({tnode}: CustomRendererProps) { const {regular: regularTypeface, bold: boldTypeface} = useChartDefaultTypeface(); const {data, xKey, yKeys, xAxis, yAxis, labelItems, legendItems} = useMemo(() => processNode(tnode, regularTypeface), [tnode, regularTypeface]); const {nodeStyles, parentNodeStyles} = useMemo(() => parseStyles(tnode), [tnode]); + const [isCartesianChart, isPolarChart] = useMemo(() => [Object.keys(data).length > 0, false], [data]); const renderCartesianChartChild = useCallback((tnode: TNode, index: Number, renderArgs: CartesianChartRenderArg) => { const key = `${tnode.tagName ?? 'node'}-${index}`; @@ -429,59 +382,61 @@ function VictoryChartRenderer({tnode}: CustomRendererProps) { } }, []); - const renderOutside = useCallback( - (renderArgs: CartesianChartRenderArg) => { - return ( - <> - {labelItems.map(({x, y, text, color, fontSize, fontWeight}) => { + const renderCartesianChartOutside = useCallback(() => { + return ( + <> + {labelItems.map(({x, y, text, color, fontSize, fontWeight}) => { + const typeface = fontWeight === 'bold' ? boldTypeface : regularTypeface; + const font = typeface ? Skia.Font(typeface, fontSize) : null; + return ( + + ); + })} + {legendItems.map(({x: startX, y, entries, gutter, symbolSpacer}, legendIndex) => { + let x = startX; + return entries.map(({text, color, fontSize, fontWeight, symbolColor, symbolSize}) => { const typeface = fontWeight === 'bold' ? boldTypeface : regularTypeface; const font = typeface ? Skia.Font(typeface, fontSize) : null; + const fontMetrics = font?.getMetrics(); + const lineHeight = fontMetrics ? fontMetrics.ascent + fontMetrics.descent + fontMetrics.leading : 0; + const symbolX = x; + x += symbolSize + symbolSpacer; + const textX = x; + x += (font?.getGlyphWidths(font.getGlyphIDs(text)).reduce((acc, width) => acc + width, 0) ?? 0) + gutter; return ( - + + + + ); - })} - {legendItems.map(({x: startX, y, entries, gutter, symbolSpacer}, legendIndex) => { - let x = startX; - return entries.map(({text, color, fontSize, fontWeight, symbolColor, symbolSize}) => { - const typeface = fontWeight === 'bold' ? boldTypeface : regularTypeface; - const font = typeface ? Skia.Font(typeface, fontSize) : null; - const fontMetrics = font?.getMetrics(); - const lineHeight = fontMetrics ? fontMetrics.ascent + fontMetrics.descent + fontMetrics.leading : 0; - const symbolX = x; - x += symbolSize + symbolSpacer; - const textX = x; - x += (font?.getGlyphWidths(font.getGlyphIDs(text)).reduce((acc, width) => acc + width, 0) ?? 0) + gutter; - return ( - - - - - ); - }); - })} - - ); - }, - [labelItems, legendItems, regularTypeface, boldTypeface], - ); + }); + })} + + ); + }, [labelItems, legendItems, regularTypeface, boldTypeface]); + + // Invalid chart (no charts or mixed charts) + if (isCartesianChart === isPolarChart) { + return null; + } return ( @@ -495,7 +450,7 @@ function VictoryChartRenderer({tnode}: CustomRendererProps) { domain={parseAttribute(tnode.attributes.domain)} domainPadding={parseDomainPadding(tnode.attributes.domainpadding)} padding={parseAttribute(tnode.attributes.padding)} - renderOutside={renderOutside} + renderOutside={renderCartesianChartOutside} > {(renderArgs) => tnode.children.map((child, childIndex) => renderCartesianChartChild(child, childIndex, renderArgs))} From 67a4d2c5190d6059bcd643631599ec91a342f8ed Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Thu, 21 May 2026 01:13:08 +0100 Subject: [PATCH 21/69] make some props optional and avoid magic numbers --- .../HTMLRenderers/VictoryChartRenderer.tsx | 59 ++++++++++--------- 1 file changed, 30 insertions(+), 29 deletions(-) diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer.tsx index f13d93b475b3..92000cceba76 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer.tsx @@ -33,13 +33,13 @@ type LabelItem = { text: string; /** The color of the text */ - color: Color; + color?: Color; /** Font size */ - fontSize: number; + fontSize?: number; /** Font weight */ - fontWeight: 'normal' | 'bold'; + fontWeight?: 'normal' | 'bold'; }; type LegendItemEntry = { @@ -47,19 +47,19 @@ type LegendItemEntry = { text: string; /** The color of the text */ - color: Color; + color?: Color; /** Font size */ - fontSize: number; + fontSize?: number; /** Font weight */ - fontWeight: 'normal' | 'bold'; + fontWeight?: 'normal' | 'bold'; /** The color of the symbol */ - symbolColor: Color; + symbolColor?: Color; /** Symbol size */ - symbolSize: number; + symbolSize?: number; }; type LegendItem = { @@ -73,10 +73,10 @@ type LegendItem = { entries: LegendItemEntry[]; /** Space between entries */ - gutter: number; + gutter?: number; /** Space between entry's text and symbol */ - symbolSpacer: number; + symbolSpacer?: number; }; /** @@ -156,9 +156,9 @@ function processNode(tnode: TNode, typeface: SkTypeface | null) { const y = parseAttribute(tnode.attributes.y) ?? 0; const text = parseAttribute(tnode.attributes.text) ?? ''; const style = parseAttribute(tnode.attributes.style); - const color = style?.fill !== undefined ? (style.fill as Color) : 'black'; - const fontSize = style?.fontSize !== undefined ? Number(style.fontSize) : 16; - const fontWeight = Number(style?.fontWeight) === 700 ? 'bold' : 'normal'; + const color = style?.fill !== undefined ? (style.fill as Color) : undefined; + const fontSize = style?.fontSize !== undefined ? Number(style.fontSize) : undefined; + const fontWeight = Number(style?.fontWeight) === 700 ? 'bold' : undefined; labelItems.push({ x, y, @@ -170,17 +170,16 @@ function processNode(tnode: TNode, typeface: SkTypeface | null) { } else if (tnode.tagName === 'victorylegend') { const x = parseAttribute(tnode.attributes.x) ?? 0; const y = parseAttribute(tnode.attributes.y) ?? 0; - const gutter = parseAttribute(tnode.attributes.gutter) ?? 32; - const symbolSpacer = parseAttribute(tnode.attributes.symbolspacer) ?? 8; - const data = parseAttribute(tnode.attributes.data); + const gutter = parseAttribute(tnode.attributes.gutter) ?? undefined; + const symbolSpacer = parseAttribute(tnode.attributes.symbolspacer) ?? undefined; const style = parseAttribute(tnode.attributes.style); - const color = style?.labels?.fill !== undefined ? (style.labels.fill as Color) : 'black'; - const fontSize = style?.labels?.fontSize !== undefined ? Number(style.labels.fontSize) : 12; - const fontWeight = Number(style?.labels?.fontWeight) === 700 ? 'bold' : 'normal'; + const color = style?.labels?.fill !== undefined ? (style.labels.fill as Color) : undefined; + const fontSize = style?.labels?.fontSize !== undefined ? Number(style.labels.fontSize) : undefined; + const fontWeight = Number(style?.labels?.fontWeight) === 700 ? 'bold' : undefined; const entries: LegendItemEntry[] = (parseAttribute(tnode.attributes.data) ?? []).map((entry) => { const text = entry.name !== undefined ? String(entry.name) : ''; - const symbolColor = entry.symbol?.fill !== undefined ? (entry.symbol.fill as Color) : 'black'; - const symbolSize = entry.symbol?.size !== undefined ? Number(entry.symbol.size) : 4; + const symbolColor = entry.symbol?.fill !== undefined ? (entry.symbol.fill as Color) : undefined; + const symbolSize = entry.symbol?.size !== undefined ? Number(entry.symbol.size) : undefined; return { text, color, @@ -407,17 +406,19 @@ function VictoryChartRenderer({tnode}: CustomRendererProps) { const fontMetrics = font?.getMetrics(); const lineHeight = fontMetrics ? fontMetrics.ascent + fontMetrics.descent + fontMetrics.leading : 0; const symbolX = x; - x += symbolSize + symbolSpacer; + x += (symbolSize ?? 0) + (symbolSpacer ?? 0); const textX = x; - x += (font?.getGlyphWidths(font.getGlyphIDs(text)).reduce((acc, width) => acc + width, 0) ?? 0) + gutter; + x += (font?.getGlyphWidths(font.getGlyphIDs(text)).reduce((acc, width) => acc + width, 0) ?? 0) + (gutter ?? 0); return ( - + {!!symbolSize && ( + + )} Date: Thu, 21 May 2026 01:23:37 +0100 Subject: [PATCH 22/69] fix grid compatibility --- .../HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer.tsx index 92000cceba76..b01179ba3317 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer.tsx @@ -120,8 +120,8 @@ function processNode(tnode: TNode, typeface: SkTypeface | null) { const tickValues = parseAttribute(tnode.attributes.tickvalues); const orientation = parseAttribute(tnode.attributes.orientation); const style = parseAttribute(tnode.attributes.style); - const lineColor = style?.axis?.stroke !== undefined ? (style.axis.stroke as Color) : undefined; - const lineWidth = style?.axis?.strokeWidth !== undefined ? Number(style.axis.strokeWidth) : undefined; + const lineColor = style?.grid?.stroke !== undefined ? (style.grid.stroke as Color) : undefined; + const lineWidth = style?.grid?.strokeWidth !== undefined ? Number(style.grid.strokeWidth) : 0; // 0 Not to draw the lines for compatibility with VictoryChart const labelColor = style?.tickLabels?.fill !== undefined ? String(style.tickLabels.fill) : undefined; const labelOffset = style?.tickLabels?.padding !== undefined ? Number(style.tickLabels.padding) : undefined; const fontSize = style?.tickLabels?.fontSize !== undefined ? Number(style.tickLabels.fontSize) : undefined; From c43d6922d0307be915688e933bf2a24f80e10d8f Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Thu, 21 May 2026 02:10:12 +0100 Subject: [PATCH 23/69] support formatted labels --- .../HTMLRenderers/VictoryChartRenderer.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer.tsx index b01179ba3317..fa64f6e32eaa 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer.tsx @@ -116,9 +116,11 @@ function processNode(tnode: TNode, typeface: SkTypeface | null) { }); } else if (tnode.tagName === 'victoryaxis') { const isDependentAxis = 'dependentaxis' in tnode.attributes && tnode.attributes.dependentaxis !== 'false'; + const orientation = parseAttribute(tnode.attributes.orientation); const tickCount = parseAttribute(tnode.attributes.tickcount); const tickValues = parseAttribute(tnode.attributes.tickvalues); - const orientation = parseAttribute(tnode.attributes.orientation); + const tickFormat = parseAttribute(tnode.attributes.tickformat); + const formatLabel = (value: number) => tickFormat?.[tickValues?.indexOf(value) ?? -1] ?? String(value); const style = parseAttribute(tnode.attributes.style); const lineColor = style?.grid?.stroke !== undefined ? (style.grid.stroke as Color) : undefined; const lineWidth = style?.grid?.strokeWidth !== undefined ? Number(style.grid.strokeWidth) : 0; // 0 Not to draw the lines for compatibility with VictoryChart @@ -131,6 +133,7 @@ function processNode(tnode: TNode, typeface: SkTypeface | null) { { tickCount, tickValues, + formatYLabel: formatLabel, axisSide: orientation === 'right' ? 'right' : 'left', lineColor, lineWidth, @@ -143,6 +146,7 @@ function processNode(tnode: TNode, typeface: SkTypeface | null) { xAxis = { tickCount, tickValues, + formatXLabel: formatLabel, axisSide: orientation === 'top' ? 'top' : 'bottom', lineColor, lineWidth, From 95610e924e50f110a43e3efb493f6a91463672f9 Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Thu, 21 May 2026 03:20:13 +0100 Subject: [PATCH 24/69] fix tagname --- .../HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer.tsx index fa64f6e32eaa..28f227bb6fe2 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer.tsx @@ -104,7 +104,7 @@ function processNode(tnode: TNode, typeface: SkTypeface | null) { const labelItems: LabelItem[] = []; const legendItems: LegendItem[] = []; - if (tnode.tagName === 'victorybar' || tnode.tagName === 'victorychart') { + if (tnode.tagName === 'victorybar' || tnode.tagName === 'victoryline') { const points = parseAttribute(tnode.attributes.data); const yKey = Y_KEY_PREFIX + getHierarchyID(tnode); yKeys.push(yKey); From 788d4dfd9792110be10f865dcff548071a8681e5 Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Thu, 21 May 2026 03:24:46 +0100 Subject: [PATCH 25/69] register victory chart tags --- .../BaseHTMLEngineProvider.tsx | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx b/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx index a86b6e71c636..e25c62f8734b 100755 --- a/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx +++ b/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx @@ -205,6 +205,26 @@ function BaseHTMLEngineProvider({textSelectable = false, children, enableExperim tagName: 'victorychart', contentModel: HTMLContentModel.block, }), + victorybar: HTMLElementModel.fromCustomModel({ + tagName: 'victorybar', + contentModel: HTMLContentModel.block, + }), + victoryline: HTMLElementModel.fromCustomModel({ + tagName: 'victoryline', + contentModel: HTMLContentModel.block, + }), + victoryaxis: HTMLElementModel.fromCustomModel({ + tagName: 'victoryaxis', + contentModel: HTMLContentModel.block, + }), + victorylabel: HTMLElementModel.fromCustomModel({ + tagName: 'victorylabel', + contentModel: HTMLContentModel.textual, + }), + victorylegend: HTMLElementModel.fromCustomModel({ + tagName: 'victorylegend', + contentModel: HTMLContentModel.block, + }), }), [ styles.taskTitleMenuItem, From cab2e375cf2c3654d2701f0daedbb5cf3ac7dc37 Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Thu, 21 May 2026 04:04:49 +0100 Subject: [PATCH 26/69] fix tickCount padding weird issue --- .../HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer.tsx index 28f227bb6fe2..724713ef9078 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer.tsx @@ -117,7 +117,7 @@ function processNode(tnode: TNode, typeface: SkTypeface | null) { } else if (tnode.tagName === 'victoryaxis') { const isDependentAxis = 'dependentaxis' in tnode.attributes && tnode.attributes.dependentaxis !== 'false'; const orientation = parseAttribute(tnode.attributes.orientation); - const tickCount = parseAttribute(tnode.attributes.tickcount); + const tickCount = parseAttribute(tnode.attributes.tickcount) ?? 0; const tickValues = parseAttribute(tnode.attributes.tickvalues); const tickFormat = parseAttribute(tnode.attributes.tickformat); const formatLabel = (value: number) => tickFormat?.[tickValues?.indexOf(value) ?? -1] ?? String(value); From 9be68bcc7e2683b95344bea28dd6a03e7c8998cd Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Thu, 21 May 2026 05:03:03 +0100 Subject: [PATCH 27/69] load Skia properly before rendering Skia components --- .../BaseHTMLEngineProvider.tsx | 3 +- .../BaseVictoryChartRenderer.tsx} | 4 +- .../VictoryChartRenderer/index.native.tsx | 9 ++++ .../VictoryChartRenderer/index.tsx | 14 ++++++ .../HTMLEngineProvider/HTMLRenderers/index.ts | 13 +++--- src/setup/platformSetup/index.ts | 44 +++++++++---------- 6 files changed, 52 insertions(+), 35 deletions(-) rename src/components/HTMLEngineProvider/HTMLRenderers/{VictoryChartRenderer.tsx => VictoryChartRenderer/BaseVictoryChartRenderer.tsx} (99%) create mode 100644 src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/index.native.tsx create mode 100644 src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/index.tsx diff --git a/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx b/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx index e25c62f8734b..56ce3e399c8e 100755 --- a/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx +++ b/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx @@ -7,7 +7,7 @@ import convertToLTR from '@libs/convertToLTR'; import FontUtils from '@styles/utils/FontUtils'; import type ChildrenProps from '@src/types/utils/ChildrenProps'; import {computeEmbeddedMaxWidth, isChildOfTaskTitle} from './htmlEngineUtils'; -import getHtmlRenderers from './HTMLRenderers'; +import htmlRenderers from './HTMLRenderers'; type BaseHTMLEngineProviderProps = ChildrenProps & { /** Whether text elements should be selectable */ @@ -24,7 +24,6 @@ type BaseHTMLEngineProviderProps = ChildrenProps & { // costly invalidations and commits. function BaseHTMLEngineProvider({textSelectable = false, children, enableExperimentalBRCollapsing = false}: BaseHTMLEngineProviderProps) { const styles = useThemeStyles(); - const htmlRenderers = useMemo(() => getHtmlRenderers(), []); // Declare nonstandard tags and their content model here /* eslint-disable @typescript-eslint/naming-convention */ diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/BaseVictoryChartRenderer.tsx similarity index 99% rename from src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer.tsx rename to src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/BaseVictoryChartRenderer.tsx index 724713ef9078..69efdb6b9e9b 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/BaseVictoryChartRenderer.tsx @@ -341,7 +341,7 @@ function parseStyles(tnode: TNode) { return {nodeStyles, parentNodeStyles}; } -function VictoryChartRenderer({tnode}: CustomRendererProps) { +function BaseVictoryChartRenderer({tnode}: CustomRendererProps) { const styles = useThemeStyles(); const {regular: regularTypeface, bold: boldTypeface} = useChartDefaultTypeface(); const {data, xKey, yKeys, xAxis, yAxis, labelItems, legendItems} = useMemo(() => processNode(tnode, regularTypeface), [tnode, regularTypeface]); @@ -464,4 +464,4 @@ function VictoryChartRenderer({tnode}: CustomRendererProps) { ); } -export default VictoryChartRenderer; +export default BaseVictoryChartRenderer; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/index.native.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/index.native.tsx new file mode 100644 index 000000000000..2522cdf14db5 --- /dev/null +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/index.native.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import type {CustomRendererProps, TBlock} from 'react-native-render-html'; +import BaseVictoryChartRenderer from './BaseVictoryChartRenderer'; + +function VictoryChartRenderer(props: CustomRendererProps) { + return ; +} + +export default VictoryChartRenderer; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/index.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/index.tsx new file mode 100644 index 000000000000..a2fc27868950 --- /dev/null +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/index.tsx @@ -0,0 +1,14 @@ +import {WithSkiaWeb} from '@shopify/react-native-skia/lib/module/web'; +import React from 'react'; +import type {CustomRendererProps, TBlock} from 'react-native-render-html'; + +function VictoryChartRenderer(props: CustomRendererProps) { + return ( + import('./BaseVictoryChartRenderer')} + componentProps={props} + /> + ); +} + +export default VictoryChartRenderer; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/index.ts b/src/components/HTMLEngineProvider/HTMLRenderers/index.ts index b3d8fb1662e2..86be9404b18d 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/index.ts +++ b/src/components/HTMLEngineProvider/HTMLRenderers/index.ts @@ -22,12 +22,13 @@ import TaskTitleRenderer from './TaskTitleRenderer'; import TransactionHistoryLinkRenderer from './TransactionHistoryLinkRenderer'; import ULRenderer from './ULRenderer'; import UserDetailsRenderer from './UserDetailsRenderer'; +import VictoryChartRenderer from './VictoryChartRenderer'; import VideoRenderer from './VideoRenderer'; /** * This collection defines our custom renderers. It is a mapping from HTML tag type to the corresponding component. */ -export default (): CustomTagRendererRecord => ({ +const HTMLEngineProviderComponentList: CustomTagRendererRecord = { // Standard HTML tag renderers a: AnchorRenderer, code: CodeRenderer, @@ -58,9 +59,7 @@ export default (): CustomTagRendererRecord => ({ 'sparkles-icon': SparklesIconRenderer, /* eslint-enable @typescript-eslint/naming-convention */ - // VictoryChart components depend on Skia and should be loaded after Skia WASM - // - // Using `require` loads the components only when this function is executed, - // unlike `import` they'd be imported on module execution BEFORE Skia WASM is loaded. - victorychart: require('./VictoryChartRenderer').default, -}); + victorychart: VictoryChartRenderer, +}; + +export default HTMLEngineProviderComponentList; diff --git a/src/setup/platformSetup/index.ts b/src/setup/platformSetup/index.ts index 407850a0d0ae..7bb35851057e 100644 --- a/src/setup/platformSetup/index.ts +++ b/src/setup/platformSetup/index.ts @@ -1,4 +1,3 @@ -import {LoadSkiaWeb} from '@shopify/react-native-skia/lib/module/web'; import {AppRegistry} from 'react-native'; import CacheAPI from '@libs/CacheAPI'; import checkForUpdates from '@libs/checkForUpdates'; @@ -53,32 +52,29 @@ const webUpdater = (): PlatformSpecificUpdater => ({ }); export default function () { - // s77rt tmp - LoadSkiaWeb().then(() => { - // Initialize Cache API - CacheAPI.init(); + // Initialize Cache API + CacheAPI.init(); - AppRegistry.runApplication(Config.APP_NAME, { - rootTag: document.getElementById('root'), - }); + AppRegistry.runApplication(Config.APP_NAME, { + rootTag: document.getElementById('root'), + }); - // When app loads, get current version (production only) - if (Config.IS_IN_PRODUCTION) { - checkForUpdates(webUpdater()); - } + // When app loads, get current version (production only) + if (Config.IS_IN_PRODUCTION) { + checkForUpdates(webUpdater()); + } - // Start current date updater - DateUtils.startCurrentDateUpdater(); + // Start current date updater + DateUtils.startCurrentDateUpdater(); - // Service worker is only emitted in production builds (GenerateSW is skipped in dev). - if (Config.IS_IN_PRODUCTION && 'serviceWorker' in navigator) { - window.addEventListener('load', () => { - navigator.serviceWorker.register('/service-worker.js').catch((error) => { - // Soft-fail: app must continue working even if SW registration fails (Safari Private mode, etc.). - // eslint-disable-next-line no-console - console.warn('[SW] registration failed', error); - }); + // Service worker is only emitted in production builds (GenerateSW is skipped in dev). + if (Config.IS_IN_PRODUCTION && 'serviceWorker' in navigator) { + window.addEventListener('load', () => { + navigator.serviceWorker.register('/service-worker.js').catch((error) => { + // Soft-fail: app must continue working even if SW registration fails (Safari Private mode, etc.). + // eslint-disable-next-line no-console + console.warn('[SW] registration failed', error); }); - } - }); + }); + } } From e97ceb2fe14d230d70daa4d5b5b4b85b21ea2242 Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Thu, 21 May 2026 05:10:25 +0100 Subject: [PATCH 28/69] define type separately --- .../HTMLRenderers/VictoryChartRenderer/index.native.tsx | 4 ++-- .../HTMLRenderers/VictoryChartRenderer/index.tsx | 5 +++-- .../HTMLRenderers/VictoryChartRenderer/types.ts | 5 +++++ 3 files changed, 10 insertions(+), 4 deletions(-) create mode 100644 src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/types.ts diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/index.native.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/index.native.tsx index 2522cdf14db5..d176700ec083 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/index.native.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/index.native.tsx @@ -1,8 +1,8 @@ import React from 'react'; -import type {CustomRendererProps, TBlock} from 'react-native-render-html'; import BaseVictoryChartRenderer from './BaseVictoryChartRenderer'; +import type {VictoryChartRendererProps} from './types'; -function VictoryChartRenderer(props: CustomRendererProps) { +function VictoryChartRenderer(props: VictoryChartRendererProps) { return ; } diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/index.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/index.tsx index a2fc27868950..8631241357cf 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/index.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/index.tsx @@ -1,8 +1,9 @@ import {WithSkiaWeb} from '@shopify/react-native-skia/lib/module/web'; import React from 'react'; -import type {CustomRendererProps, TBlock} from 'react-native-render-html'; +import type {VictoryChartRendererProps} from './types'; -function VictoryChartRenderer(props: CustomRendererProps) { +function VictoryChartRenderer(props: VictoryChartRendererProps) { + // Victory Chart uses Skia internally and it uses a WASM module that must be loaded before rendering any Skia-based component. return ( import('./BaseVictoryChartRenderer')} diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/types.ts b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/types.ts new file mode 100644 index 000000000000..e695fcbad295 --- /dev/null +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/types.ts @@ -0,0 +1,5 @@ +import type {CustomRendererProps, TBlock} from 'react-native-render-html'; + +type VictoryChartRendererProps = CustomRendererProps; + +export type {VictoryChartRendererProps}; From 8d166b7a1c0268d7f19705dfa31a2d020983bfe9 Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Thu, 21 May 2026 16:48:32 +0100 Subject: [PATCH 29/69] fix wasm resolution --- .../VictoryChartRenderer/index.tsx | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/index.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/index.tsx index 8631241357cf..6a4b0ab12c03 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/index.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/index.tsx @@ -1,13 +1,31 @@ import {WithSkiaWeb} from '@shopify/react-native-skia/lib/module/web'; import React from 'react'; +import {View} from 'react-native'; +import ActivityIndicator from '@components/ActivityIndicator'; +import useThemeStyles from '@hooks/useThemeStyles'; +import type {SkeletonSpanReasonAttributes} from '@libs/telemetry/useSkeletonSpan'; import type {VictoryChartRendererProps} from './types'; +const getBaseVictoryChartRenderer = () => import('./BaseVictoryChartRenderer'); + function VictoryChartRenderer(props: VictoryChartRendererProps) { + const styles = useThemeStyles(); + const reasonAttributes: SkeletonSpanReasonAttributes = {context: 'VictoryChartRenderer.SkiaWebLoading'}; + // Victory Chart uses Skia internally and it uses a WASM module that must be loaded before rendering any Skia-based component. return ( import('./BaseVictoryChartRenderer')} + opts={{locateFile: (file: string) => `/${file}`}} + getComponent={getBaseVictoryChartRenderer} componentProps={props} + fallback={ + + + + } /> ); } From eb58ba1cdca189630c4d6626db36069b5acf4dfa Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Thu, 21 May 2026 17:08:36 +0100 Subject: [PATCH 30/69] fix line styles --- .../VictoryChartRenderer/BaseVictoryChartRenderer.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/BaseVictoryChartRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/BaseVictoryChartRenderer.tsx index 69efdb6b9e9b..de69a0b8a19b 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/BaseVictoryChartRenderer.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/BaseVictoryChartRenderer.tsx @@ -373,9 +373,9 @@ function BaseVictoryChartRenderer({tnode}: CustomRendererProps) { {tnode.children.map((child, childIndex) => renderCartesianChartChild(child, childIndex, renderArgs))} From b39a47a03ea19bfa871d3b8df0534df3b7c20ab4 Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Thu, 21 May 2026 17:19:44 +0100 Subject: [PATCH 31/69] lint --- .../BaseVictoryChartRenderer.tsx | 12 ++++-------- .../HTMLRenderers/VictoryChartRenderer/types.ts | 1 + 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/BaseVictoryChartRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/BaseVictoryChartRenderer.tsx index de69a0b8a19b..094cb6e727d8 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/BaseVictoryChartRenderer.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/BaseVictoryChartRenderer.tsx @@ -7,13 +7,13 @@ import React, {ComponentProps, Fragment, useCallback, useMemo} from 'react'; import {View} from 'react-native'; import type {ViewStyle} from 'react-native'; import {TNode} from 'react-native-render-html'; -import type {CustomRendererProps, TBlock} from 'react-native-render-html'; import type {CartesianChartRenderArg, RoundedCorners} from 'victory-native'; import {Bar, CartesianChart, Line} from 'victory-native'; import {BAR_INNER_PADDING} from '@components/Charts/BarChart/BarChartContent'; import {useChartDefaultTypeface} from '@components/Charts/hooks'; import {DEFAULT_CHART_COLOR} from '@components/Charts/utils'; import useThemeStyles from '@hooks/useThemeStyles'; +import type {VictoryChartRendererProps} from './types'; const X_KEY = 'x'; const Y_KEY_PREFIX = 'y'; @@ -341,7 +341,7 @@ function parseStyles(tnode: TNode) { return {nodeStyles, parentNodeStyles}; } -function BaseVictoryChartRenderer({tnode}: CustomRendererProps) { +function BaseVictoryChartRenderer({tnode}: VictoryChartRendererProps) { const styles = useThemeStyles(); const {regular: regularTypeface, bold: boldTypeface} = useChartDefaultTypeface(); const {data, xKey, yKeys, xAxis, yAxis, labelItems, legendItems} = useMemo(() => processNode(tnode, regularTypeface), [tnode, regularTypeface]); @@ -364,9 +364,7 @@ function BaseVictoryChartRenderer({tnode}: CustomRendererProps) { innerPadding={BAR_INNER_PADDING} roundedCorners={parseCornerRadius(tnode.attributes.cornerradius)} barWidth={parseAttribute(tnode.attributes.barwidth)} - > - {tnode.children.map((child, childIndex) => renderCartesianChartChild(child, childIndex, renderArgs))} - + /> ); case 'victoryline': return ( @@ -376,9 +374,7 @@ function BaseVictoryChartRenderer({tnode}: CustomRendererProps) { color={nodeStyles.stroke ?? DEFAULT_CHART_COLOR} strokeWidth={nodeStyles.strokeWidth !== undefined ? Number(nodeStyles.strokeWidth) : undefined} curveType={parseAttribute(tnode.attributes.interpolation)} - > - {tnode.children.map((child, childIndex) => renderCartesianChartChild(child, childIndex, renderArgs))} - + /> ); default: return null; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/types.ts b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/types.ts index e695fcbad295..a288b85f425e 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/types.ts +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/types.ts @@ -2,4 +2,5 @@ import type {CustomRendererProps, TBlock} from 'react-native-render-html'; type VictoryChartRendererProps = CustomRendererProps; +// eslint-disable-next-line import/prefer-default-export export type {VictoryChartRendererProps}; From 4c9dceca3720d0ced570fe69a495330960ef93b8 Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Thu, 21 May 2026 17:44:49 +0100 Subject: [PATCH 32/69] add displayName --- .../VictoryChartRenderer/BaseVictoryChartRenderer.tsx | 2 ++ .../HTMLRenderers/VictoryChartRenderer/index.native.tsx | 2 ++ .../HTMLRenderers/VictoryChartRenderer/index.tsx | 2 ++ 3 files changed, 6 insertions(+) diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/BaseVictoryChartRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/BaseVictoryChartRenderer.tsx index 094cb6e727d8..84cb5eaa0485 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/BaseVictoryChartRenderer.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/BaseVictoryChartRenderer.tsx @@ -460,4 +460,6 @@ function BaseVictoryChartRenderer({tnode}: VictoryChartRendererProps) { ); } +BaseVictoryChartRenderer.displayName = 'BaseVictoryChartRenderer'; + export default BaseVictoryChartRenderer; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/index.native.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/index.native.tsx index d176700ec083..16c2e46abc3c 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/index.native.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/index.native.tsx @@ -6,4 +6,6 @@ function VictoryChartRenderer(props: VictoryChartRendererProps) { return ; } +VictoryChartRenderer.displayName = 'VictoryChartRenderer'; + export default VictoryChartRenderer; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/index.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/index.tsx index 6a4b0ab12c03..d3b5db67548f 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/index.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/index.tsx @@ -30,4 +30,6 @@ function VictoryChartRenderer(props: VictoryChartRendererProps) { ); } +VictoryChartRenderer.displayName = 'VictoryChartRenderer'; + export default VictoryChartRenderer; From a25d3314a20224089de77268a37c7767f10505c9 Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Thu, 21 May 2026 17:57:38 +0100 Subject: [PATCH 33/69] add words to spell check dictionary --- cspell.json | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/cspell.json b/cspell.json index c59a20125d46..87438b273945 100644 --- a/cspell.json +++ b/cspell.json @@ -78,6 +78,7 @@ "autosync", "avds", "AVURL", + "barwidth", "bamboohr", "Bartek", "basehead", @@ -154,6 +155,7 @@ "copiloted", "copiloting", "copyable", + "cornerradius", "Corpay", "Countertop", "CPPFLAGS", @@ -175,6 +177,8 @@ "deburred", "dedupe", "Deel", + "dependentaxis", + "domainpadding", "deeplink", "deeplinked", "deeplinking", @@ -784,6 +788,9 @@ "tsgo", "twocards", "Typeform", + "tickcount", + "tickvalues", + "tickformat", "uatp", "UATP", "UBOI", @@ -832,6 +839,12 @@ "Valuska", "VBBA", "Venmo", + "victorychart", + "victorybar", + "victoryline", + "victoryaxis", + "victorylabel", + "victorylegend", "viewability", "viewport", "viewports", From b157fb28c01a06d11cebab2b1c9cb3ff852e6e5a Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Thu, 21 May 2026 21:09:12 +0100 Subject: [PATCH 34/69] better typescript --- .../BaseVictoryChartRenderer.tsx | 116 +++++------------- .../VictoryChartRenderer/constants.ts | 4 + .../VictoryChartRenderer/types.ts | 86 ++++++++++++- 3 files changed, 117 insertions(+), 89 deletions(-) create mode 100644 src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/constants.ts diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/BaseVictoryChartRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/BaseVictoryChartRenderer.tsx index 84cb5eaa0485..d638ff1b0c42 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/BaseVictoryChartRenderer.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/BaseVictoryChartRenderer.tsx @@ -1,83 +1,21 @@ -import {Circle, Skia, Text as SkText, SkTypeface} from '@shopify/react-native-skia'; -import type {Color} from '@shopify/react-native-skia'; +import {Circle, Skia, Text as SkText} from '@shopify/react-native-skia'; +import type {Color, SkTypeface} from '@shopify/react-native-skia'; import JSON5 from 'json5'; import lodashIsObject from 'lodash/isObject'; import lodashMerge from 'lodash/merge'; -import React, {ComponentProps, Fragment, useCallback, useMemo} from 'react'; +import React, {Fragment, useCallback, useMemo} from 'react'; +import type {ComponentProps} from 'react'; import {View} from 'react-native'; import type {ViewStyle} from 'react-native'; -import {TNode} from 'react-native-render-html'; +import type {TNode} from 'react-native-render-html'; import type {CartesianChartRenderArg, RoundedCorners} from 'victory-native'; import {Bar, CartesianChart, Line} from 'victory-native'; import {BAR_INNER_PADDING} from '@components/Charts/BarChart/BarChartContent'; import {useChartDefaultTypeface} from '@components/Charts/hooks'; import {DEFAULT_CHART_COLOR} from '@components/Charts/utils'; import useThemeStyles from '@hooks/useThemeStyles'; -import type {VictoryChartRendererProps} from './types'; - -const X_KEY = 'x'; -const Y_KEY_PREFIX = 'y'; - -type RawData = Record; - -type StyleObject = Record; - -type LabelItem = { - /** Position on the X-axis */ - x: number; - - /** Position on the Y-axis */ - y: number; - - /** Text to draw */ - text: string; - - /** The color of the text */ - color?: Color; - - /** Font size */ - fontSize?: number; - - /** Font weight */ - fontWeight?: 'normal' | 'bold'; -}; - -type LegendItemEntry = { - /** Text to draw */ - text: string; - - /** The color of the text */ - color?: Color; - - /** Font size */ - fontSize?: number; - - /** Font weight */ - fontWeight?: 'normal' | 'bold'; - - /** The color of the symbol */ - symbolColor?: Color; - - /** Symbol size */ - symbolSize?: number; -}; - -type LegendItem = { - /** Position on the X-axis */ - x: number; - - /** Position on the Y-axis */ - y: number; - - /** Legend entries */ - entries: LegendItemEntry[]; - - /** Space between entries */ - gutter?: number; - - /** Space between entry's text and symbol */ - symbolSpacer?: number; -}; +import {X_KEY, Y_KEY_PREFIX} from './constants'; +import type {CartesianChartData, LabelItem, LegendItem, LegendItemEntry, RawChartData, RawLegendData, StyleObject, VictoryChartRendererProps, yKey} from './types'; /** * Get node unique ID based on hierarchy @@ -92,35 +30,39 @@ function getHierarchyID(tnode: TNode): string { return id; } +function getYKey(tnode: TNode): yKey { + return `${Y_KEY_PREFIX}${getHierarchyID(tnode)}`; +} + /** * Traverse all nodes to extract points from `data` attributes and other config e.g. axis configuration */ function processNode(tnode: TNode, typeface: SkTypeface | null) { - const data: Record = {}; - const xKey: string = X_KEY; - const yKeys: string[] = []; - let xAxis: ComponentProps>['xAxis']; - let yAxis: ComponentProps>['yAxis']; + const data: Record = {}; + const xKey = X_KEY; + const yKeys: yKey[] = []; + let xAxis: ComponentProps>['xAxis']; + let yAxis: ComponentProps>['yAxis']; const labelItems: LabelItem[] = []; const legendItems: LegendItem[] = []; if (tnode.tagName === 'victorybar' || tnode.tagName === 'victoryline') { - const points = parseAttribute(tnode.attributes.data); - const yKey = Y_KEY_PREFIX + getHierarchyID(tnode); + const points = parseAttribute(tnode.attributes.data) ?? []; + const yKey = getYKey(tnode); yKeys.push(yKey); - points?.forEach((point) => { + for (const point of points) { data[point.x] = { [xKey]: point.x, [yKey]: point.y, - }; - }); + } as CartesianChartData; + } } else if (tnode.tagName === 'victoryaxis') { const isDependentAxis = 'dependentaxis' in tnode.attributes && tnode.attributes.dependentaxis !== 'false'; const orientation = parseAttribute(tnode.attributes.orientation); const tickCount = parseAttribute(tnode.attributes.tickcount) ?? 0; const tickValues = parseAttribute(tnode.attributes.tickvalues); const tickFormat = parseAttribute(tnode.attributes.tickformat); - const formatLabel = (value: number) => tickFormat?.[tickValues?.indexOf(value) ?? -1] ?? String(value); + const formatLabel = (label: string | number) => tickFormat?.[tickValues?.indexOf(Number(label)) ?? -1] ?? String(label); const style = parseAttribute(tnode.attributes.style); const lineColor = style?.grid?.stroke !== undefined ? (style.grid.stroke as Color) : undefined; const lineWidth = style?.grid?.strokeWidth !== undefined ? Number(style.grid.strokeWidth) : 0; // 0 Not to draw the lines for compatibility with VictoryChart @@ -180,9 +122,9 @@ function processNode(tnode: TNode, typeface: SkTypeface | null) { const color = style?.labels?.fill !== undefined ? (style.labels.fill as Color) : undefined; const fontSize = style?.labels?.fontSize !== undefined ? Number(style.labels.fontSize) : undefined; const fontWeight = Number(style?.labels?.fontWeight) === 700 ? 'bold' : undefined; - const entries: LegendItemEntry[] = (parseAttribute(tnode.attributes.data) ?? []).map((entry) => { - const text = entry.name !== undefined ? String(entry.name) : ''; - const symbolColor = entry.symbol?.fill !== undefined ? (entry.symbol.fill as Color) : undefined; + 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, @@ -228,7 +170,7 @@ function processNode(tnode: TNode, typeface: SkTypeface | null) { yAxis, labelItems, legendItems, - }; + } as const; } /** @@ -275,7 +217,7 @@ function parseCornerRadius(attribute: string): RoundedCorners | undefined { /** * Helper to parse VC's `domainPadding` into VN's `domainPadding` */ -function parseDomainPadding(attribute: string): ComponentProps>['domainPadding'] | undefined { +function parseDomainPadding(attribute: string): ComponentProps>['domainPadding'] | undefined { const domainPadding = parseAttribute(attribute); if (typeof domainPadding === 'number') { return domainPadding; @@ -348,9 +290,9 @@ function BaseVictoryChartRenderer({tnode}: VictoryChartRendererProps) { const {nodeStyles, parentNodeStyles} = useMemo(() => parseStyles(tnode), [tnode]); const [isCartesianChart, isPolarChart] = useMemo(() => [Object.keys(data).length > 0, false], [data]); - const renderCartesianChartChild = useCallback((tnode: TNode, index: Number, renderArgs: CartesianChartRenderArg) => { + const renderCartesianChartChild = useCallback((tnode: TNode, index: Number, renderArgs: CartesianChartRenderArg) => { const key = `${tnode.tagName ?? 'node'}-${index}`; - const yKey = Y_KEY_PREFIX + getHierarchyID(tnode); + const yKey = getYKey(tnode); const {points, chartBounds} = renderArgs; const {nodeStyles} = parseStyles(tnode); switch (tnode.tagName) { diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/constants.ts b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/constants.ts new file mode 100644 index 000000000000..4424f7f1ff5e --- /dev/null +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/constants.ts @@ -0,0 +1,4 @@ +const X_KEY = 'x'; +const Y_KEY_PREFIX = 'y'; + +export {X_KEY, Y_KEY_PREFIX}; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/types.ts b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/types.ts index a288b85f425e..ddfd68ec4f0a 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/types.ts +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/types.ts @@ -1,6 +1,88 @@ +import type {Color} from '@shopify/react-native-skia'; import type {CustomRendererProps, TBlock} from 'react-native-render-html'; +import {X_KEY, Y_KEY_PREFIX} from './constants'; type VictoryChartRendererProps = CustomRendererProps; -// eslint-disable-next-line import/prefer-default-export -export type {VictoryChartRendererProps}; +type RawChartData = { + x: string | number; + y: number; +}; + +type RawLegendData = { + name: string; + symbol?: { + fill?: Color; + size?: string | number; + }; +}; + +type xKey = typeof X_KEY; +type yKey = `${typeof Y_KEY_PREFIX}${string}`; + +type CartesianChartData = { + [X_KEY]: string | number; + [key: `${yKey}`]: number; +}; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type StyleObject = Record; + +type LabelItem = { + /** Position on the X-axis */ + x: number; + + /** Position on the Y-axis */ + y: number; + + /** Text to draw */ + text: string; + + /** The color of the text */ + color?: Color; + + /** Font size */ + fontSize?: number; + + /** Font weight */ + fontWeight?: 'normal' | 'bold'; +}; + +type LegendItemEntry = { + /** Text to draw */ + text: string; + + /** The color of the text */ + color?: Color; + + /** Font size */ + fontSize?: number; + + /** Font weight */ + fontWeight?: 'normal' | 'bold'; + + /** The color of the symbol */ + symbolColor?: Color; + + /** Symbol size */ + symbolSize?: number; +}; + +type LegendItem = { + /** Position on the X-axis */ + x: number; + + /** Position on the Y-axis */ + y: number; + + /** Legend entries */ + entries: LegendItemEntry[]; + + /** Space between entries */ + gutter?: number; + + /** Space between entry's text and symbol */ + symbolSpacer?: number; +}; + +export type {VictoryChartRendererProps, RawChartData, RawLegendData, xKey, yKey, CartesianChartData, StyleObject, LabelItem, LegendItemEntry, LegendItem}; From 07b8addf82ae191a5529f591b547b8ea01480d64 Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Thu, 21 May 2026 21:19:59 +0100 Subject: [PATCH 35/69] avoid use of any type --- .../BaseVictoryChartRenderer.tsx | 26 +++++++++++----- .../VictoryChartRenderer/types.ts | 31 ++++++++++++++++--- 2 files changed, 46 insertions(+), 11 deletions(-) diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/BaseVictoryChartRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/BaseVictoryChartRenderer.tsx index d638ff1b0c42..954a728f1254 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/BaseVictoryChartRenderer.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/BaseVictoryChartRenderer.tsx @@ -15,7 +15,19 @@ import {useChartDefaultTypeface} from '@components/Charts/hooks'; import {DEFAULT_CHART_COLOR} from '@components/Charts/utils'; import useThemeStyles from '@hooks/useThemeStyles'; import {X_KEY, Y_KEY_PREFIX} from './constants'; -import type {CartesianChartData, LabelItem, LegendItem, LegendItemEntry, RawChartData, RawLegendData, StyleObject, VictoryChartRendererProps, yKey} from './types'; +import type { + CartesianChartData, + LabelItem, + LegendItem, + LegendItemEntry, + RawAxisStyle, + RawChartData, + RawLabelStyle, + RawLegendData, + RawLegendStyle, + VictoryChartRendererProps, + yKey, +} from './types'; /** * Get node unique ID based on hierarchy @@ -63,8 +75,8 @@ function processNode(tnode: TNode, typeface: SkTypeface | null) { const tickValues = parseAttribute(tnode.attributes.tickvalues); const tickFormat = parseAttribute(tnode.attributes.tickformat); const formatLabel = (label: string | number) => tickFormat?.[tickValues?.indexOf(Number(label)) ?? -1] ?? String(label); - const style = parseAttribute(tnode.attributes.style); - const lineColor = style?.grid?.stroke !== undefined ? (style.grid.stroke as Color) : undefined; + const style = parseAttribute(tnode.attributes.style); + const lineColor = style?.grid?.stroke; const lineWidth = style?.grid?.strokeWidth !== undefined ? Number(style.grid.strokeWidth) : 0; // 0 Not to draw the lines for compatibility with VictoryChart const labelColor = style?.tickLabels?.fill !== undefined ? String(style.tickLabels.fill) : undefined; const labelOffset = style?.tickLabels?.padding !== undefined ? Number(style.tickLabels.padding) : undefined; @@ -101,8 +113,8 @@ function processNode(tnode: TNode, typeface: SkTypeface | null) { const x = parseAttribute(tnode.attributes.x) ?? 0; const y = parseAttribute(tnode.attributes.y) ?? 0; const text = parseAttribute(tnode.attributes.text) ?? ''; - const style = parseAttribute(tnode.attributes.style); - const color = style?.fill !== undefined ? (style.fill as Color) : undefined; + const style = parseAttribute(tnode.attributes.style); + const color = style?.fill; const fontSize = style?.fontSize !== undefined ? Number(style.fontSize) : undefined; const fontWeight = Number(style?.fontWeight) === 700 ? 'bold' : undefined; labelItems.push({ @@ -118,8 +130,8 @@ function processNode(tnode: TNode, typeface: SkTypeface | null) { const y = parseAttribute(tnode.attributes.y) ?? 0; const gutter = parseAttribute(tnode.attributes.gutter) ?? undefined; const symbolSpacer = parseAttribute(tnode.attributes.symbolspacer) ?? undefined; - const style = parseAttribute(tnode.attributes.style); - const color = style?.labels?.fill !== undefined ? (style.labels.fill as Color) : undefined; + 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 entries: LegendItemEntry[] = (parseAttribute(tnode.attributes.data) ?? []).map((entry) => { diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/types.ts b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/types.ts index ddfd68ec4f0a..11c93e69383b 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/types.ts +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/types.ts @@ -17,6 +17,32 @@ type RawLegendData = { }; }; +type RawAxisStyle = { + grid?: { + stroke?: Color; + strokeWidth?: string | number; + }; + tickLabels?: { + fill?: Color; + padding?: string | number; + fontSize?: string | number; + }; +}; + +type RawLabelStyle = { + fill?: Color; + fontSize?: string | number; + fontWeight?: string | number; +}; + +type RawLegendStyle = { + labels?: { + fill?: Color; + fontSize?: string | number; + fontWeight?: string | number; + }; +}; + type xKey = typeof X_KEY; type yKey = `${typeof Y_KEY_PREFIX}${string}`; @@ -25,9 +51,6 @@ type CartesianChartData = { [key: `${yKey}`]: number; }; -// eslint-disable-next-line @typescript-eslint/no-explicit-any -type StyleObject = Record; - type LabelItem = { /** Position on the X-axis */ x: number; @@ -85,4 +108,4 @@ type LegendItem = { symbolSpacer?: number; }; -export type {VictoryChartRendererProps, RawChartData, RawLegendData, xKey, yKey, CartesianChartData, StyleObject, LabelItem, LegendItemEntry, LegendItem}; +export type {VictoryChartRendererProps, RawChartData, RawLegendData, RawAxisStyle, RawLabelStyle, RawLegendStyle, xKey, yKey, CartesianChartData, LabelItem, LegendItemEntry, LegendItem}; From b0489598dc1c21ead3e4d60147970ab135ae5ddc Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Thu, 21 May 2026 22:23:16 +0100 Subject: [PATCH 36/69] lint --- .../BaseVictoryChartRenderer.tsx | 84 +++++++++++++------ 1 file changed, 57 insertions(+), 27 deletions(-) diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/BaseVictoryChartRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/BaseVictoryChartRenderer.tsx index 954a728f1254..a7b9b64cfe80 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/BaseVictoryChartRenderer.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/BaseVictoryChartRenderer.tsx @@ -1,5 +1,5 @@ import {Circle, Skia, Text as SkText} from '@shopify/react-native-skia'; -import type {Color, SkTypeface} from '@shopify/react-native-skia'; +import type {SkTypeface} from '@shopify/react-native-skia'; import JSON5 from 'json5'; import lodashIsObject from 'lodash/isObject'; import lodashMerge from 'lodash/merge'; @@ -42,6 +42,9 @@ function getHierarchyID(tnode: TNode): string { return id; } +/** + * Get the Y-axis key for a given node + */ function getYKey(tnode: TNode): yKey { return `${Y_KEY_PREFIX}${getHierarchyID(tnode)}`; } @@ -156,7 +159,7 @@ function processNode(tnode: TNode, typeface: SkTypeface | null) { }); } - tnode.children.forEach((child) => { + for (const child of tnode.children) { const childData = processNode(child, typeface); yKeys.push(...childData.yKeys); if (childData.xAxis) { @@ -172,7 +175,7 @@ function processNode(tnode: TNode, typeface: SkTypeface | null) { labelItems.push(...childData.labelItems); legendItems.push(...childData.legendItems); lodashMerge(data, childData.data); - }); + } return { data, @@ -216,11 +219,35 @@ function parseCornerRadius(attribute: string): RoundedCorners | undefined { }; } if (lodashIsObject(cornerRadius)) { + let topLeft: number | undefined; + let topRight: number | undefined; + let bottomLeft: number | undefined; + let bottomRight: number | undefined; + if ('topLeft' in cornerRadius) { + topLeft = Number(cornerRadius.topLeft); + } else if ('top' in cornerRadius) { + topLeft = Number(cornerRadius.top); + } + if ('topRight' in cornerRadius) { + topLeft = Number(cornerRadius.topRight); + } else if ('top' in cornerRadius) { + topRight = Number(cornerRadius.top); + } + if ('bottomLeft' in cornerRadius) { + bottomLeft = Number(cornerRadius.bottomLeft); + } else if ('bottom' in cornerRadius) { + bottomLeft = Number(cornerRadius.bottom); + } + if ('bottomRight' in cornerRadius) { + bottomRight = Number(cornerRadius.bottomRight); + } else if ('bottom' in cornerRadius) { + bottomRight = Number(cornerRadius.bottom); + } return { - topLeft: 'topLeft' in cornerRadius ? Number(cornerRadius.topLeft) : 'top' in cornerRadius ? Number(cornerRadius.top) : undefined, - topRight: 'topRight' in cornerRadius ? Number(cornerRadius.topRight) : 'top' in cornerRadius ? Number(cornerRadius.top) : undefined, - bottomLeft: 'bottomLeft' in cornerRadius ? Number(cornerRadius.bottomLeft) : 'bottom' in cornerRadius ? Number(cornerRadius.bottom) : undefined, - bottomRight: 'bottomRight' in cornerRadius ? Number(cornerRadius.bottomRight) : 'bottom' in cornerRadius ? Number(cornerRadius.bottom) : undefined, + topLeft, + topRight, + bottomLeft, + bottomRight, }; } return undefined; @@ -236,25 +263,28 @@ function parseDomainPadding(attribute: string): ComponentProps processNode(tnode, regularTypeface), [tnode, regularTypeface]); - const {nodeStyles, parentNodeStyles} = useMemo(() => parseStyles(tnode), [tnode]); + const {nodeStyles: chartContentStyles, parentNodeStyles: chartContainerStyles} = useMemo(() => parseStyles(tnode), [tnode]); const [isCartesianChart, isPolarChart] = useMemo(() => [Object.keys(data).length > 0, false], [data]); - const renderCartesianChartChild = useCallback((tnode: TNode, index: Number, renderArgs: CartesianChartRenderArg) => { - const key = `${tnode.tagName ?? 'node'}-${index}`; - const yKey = getYKey(tnode); + const renderCartesianChartChild = useCallback((tnodeChild: TNode, index: number, renderArgs: CartesianChartRenderArg) => { + const key = `${tnodeChild.tagName ?? 'node'}-${index}`; + const yKey = getYKey(tnodeChild); const {points, chartBounds} = renderArgs; - const {nodeStyles} = parseStyles(tnode); - switch (tnode.tagName) { + const {nodeStyles} = parseStyles(tnodeChild); + switch (tnodeChild.tagName) { case 'victorybar': return ( ); case 'victoryline': @@ -327,7 +357,7 @@ function BaseVictoryChartRenderer({tnode}: VictoryChartRendererProps) { points={points[yKey]} color={nodeStyles.stroke ?? DEFAULT_CHART_COLOR} strokeWidth={nodeStyles.strokeWidth !== undefined ? Number(nodeStyles.strokeWidth) : undefined} - curveType={parseAttribute(tnode.attributes.interpolation)} + curveType={parseAttribute(tnodeChild.attributes.interpolation)} /> ); default: @@ -352,7 +382,7 @@ function BaseVictoryChartRenderer({tnode}: VictoryChartRendererProps) { /> ); })} - {legendItems.map(({x: startX, y, entries, gutter, symbolSpacer}, legendIndex) => { + {legendItems.map(({x: startX, y, entries, gutter, symbolSpacer}) => { let x = startX; return entries.map(({text, color, fontSize, fontWeight, symbolColor, symbolSize}) => { const typeface = fontWeight === 'bold' ? boldTypeface : regularTypeface; @@ -364,7 +394,7 @@ function BaseVictoryChartRenderer({tnode}: VictoryChartRendererProps) { const textX = x; x += (font?.getGlyphWidths(font.getGlyphIDs(text)).reduce((acc, width) => acc + width, 0) ?? 0) + (gutter ?? 0); return ( - + {!!symbolSize && ( - + + Date: Thu, 21 May 2026 22:28:40 +0100 Subject: [PATCH 37/69] add word to spellcheck dictionary --- cspell.json | 1 + 1 file changed, 1 insertion(+) diff --git a/cspell.json b/cspell.json index 87438b273945..5ed2e087392a 100644 --- a/cspell.json +++ b/cspell.json @@ -760,6 +760,7 @@ "symbolicated", "Symbolicates", "symbolication", + "symbolspacer", "systempreferences", "tabindex", "Talkspace", From cf2be8552a272543723ca8f0c54a16eca2c99378 Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Thu, 21 May 2026 22:52:16 +0100 Subject: [PATCH 38/69] copy resolve.fallback from common webpack to storybook to fix build --- .storybook/webpack.config.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.storybook/webpack.config.ts b/.storybook/webpack.config.ts index ffe6fe201d24..391abe32b0ac 100644 --- a/.storybook/webpack.config.ts +++ b/.storybook/webpack.config.ts @@ -73,7 +73,9 @@ const webpackConfig = async ({config}: {config: Configuration}) => { definePlugin.definitions.__REACT_WEB_CONFIG__ = JSON.stringify(env); } } + config.resolve.extensions = custom.resolve.extensions; + config.resolve.fallback = custom.resolve.fallback; const babelRulesIndex = custom.module.rules.findIndex((rule) => rule.loader === 'babel-loader'); const babelRule = custom.module.rules.at(babelRulesIndex); From 8a18b2ef6aef3a3a6459caa45310bfc9565b6b62 Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Thu, 21 May 2026 22:54:23 +0100 Subject: [PATCH 39/69] more lint --- .../BaseVictoryChartRenderer.tsx | 14 +++++++------- .../HTMLRenderers/VictoryChartRenderer/types.ts | 8 ++++---- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/BaseVictoryChartRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/BaseVictoryChartRenderer.tsx index a7b9b64cfe80..43b7c2f87158 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/BaseVictoryChartRenderer.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/BaseVictoryChartRenderer.tsx @@ -26,7 +26,7 @@ import type { RawLegendData, RawLegendStyle, VictoryChartRendererProps, - yKey, + YKey, } from './types'; /** @@ -45,7 +45,7 @@ function getHierarchyID(tnode: TNode): string { /** * Get the Y-axis key for a given node */ -function getYKey(tnode: TNode): yKey { +function getYKey(tnode: TNode): YKey { return `${Y_KEY_PREFIX}${getHierarchyID(tnode)}`; } @@ -55,9 +55,9 @@ function getYKey(tnode: TNode): yKey { function processNode(tnode: TNode, typeface: SkTypeface | null) { const data: Record = {}; const xKey = X_KEY; - const yKeys: yKey[] = []; - let xAxis: ComponentProps>['xAxis']; - let yAxis: ComponentProps>['yAxis']; + const yKeys: YKey[] = []; + let xAxis: ComponentProps>['xAxis']; + let yAxis: ComponentProps>['yAxis']; const labelItems: LabelItem[] = []; const legendItems: LegendItem[] = []; @@ -256,7 +256,7 @@ function parseCornerRadius(attribute: string): RoundedCorners | undefined { /** * Helper to parse VC's `domainPadding` into VN's `domainPadding` */ -function parseDomainPadding(attribute: string): ComponentProps>['domainPadding'] | undefined { +function parseDomainPadding(attribute: string): ComponentProps>['domainPadding'] | undefined { const domainPadding = parseAttribute(attribute); if (typeof domainPadding === 'number') { return domainPadding; @@ -332,7 +332,7 @@ function BaseVictoryChartRenderer({tnode}: VictoryChartRendererProps) { const {nodeStyles: chartContentStyles, parentNodeStyles: chartContainerStyles} = useMemo(() => parseStyles(tnode), [tnode]); const [isCartesianChart, isPolarChart] = useMemo(() => [Object.keys(data).length > 0, false], [data]); - const renderCartesianChartChild = useCallback((tnodeChild: TNode, index: number, renderArgs: CartesianChartRenderArg) => { + const renderCartesianChartChild = useCallback((tnodeChild: TNode, index: number, renderArgs: CartesianChartRenderArg) => { const key = `${tnodeChild.tagName ?? 'node'}-${index}`; const yKey = getYKey(tnodeChild); const {points, chartBounds} = renderArgs; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/types.ts b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/types.ts index 11c93e69383b..c8b08e339d5f 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/types.ts +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/types.ts @@ -43,12 +43,12 @@ type RawLegendStyle = { }; }; -type xKey = typeof X_KEY; -type yKey = `${typeof Y_KEY_PREFIX}${string}`; +type XKey = typeof X_KEY; +type YKey = `${typeof Y_KEY_PREFIX}${string}`; type CartesianChartData = { [X_KEY]: string | number; - [key: `${yKey}`]: number; + [key: `${YKey}`]: number; }; type LabelItem = { @@ -108,4 +108,4 @@ type LegendItem = { symbolSpacer?: number; }; -export type {VictoryChartRendererProps, RawChartData, RawLegendData, RawAxisStyle, RawLabelStyle, RawLegendStyle, xKey, yKey, CartesianChartData, LabelItem, LegendItemEntry, LegendItem}; +export type {VictoryChartRendererProps, RawChartData, RawLegendData, RawAxisStyle, RawLabelStyle, RawLegendStyle, XKey, YKey, CartesianChartData, LabelItem, LegendItemEntry, LegendItem}; From c0ca46ba069028973b34922581203b731402ee22 Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Thu, 21 May 2026 22:56:51 +0100 Subject: [PATCH 40/69] remove useMemo and useCallback usage --- .../BaseVictoryChartRenderer.tsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/BaseVictoryChartRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/BaseVictoryChartRenderer.tsx index 43b7c2f87158..17edccc91093 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/BaseVictoryChartRenderer.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/BaseVictoryChartRenderer.tsx @@ -3,7 +3,7 @@ import type {SkTypeface} from '@shopify/react-native-skia'; import JSON5 from 'json5'; import lodashIsObject from 'lodash/isObject'; import lodashMerge from 'lodash/merge'; -import React, {Fragment, useCallback, useMemo} from 'react'; +import React, {Fragment} from 'react'; import type {ComponentProps} from 'react'; import {View} from 'react-native'; import type {ViewStyle} from 'react-native'; @@ -328,11 +328,11 @@ function parseStyles(tnode: TNode) { function BaseVictoryChartRenderer({tnode}: VictoryChartRendererProps) { const styles = useThemeStyles(); const {regular: regularTypeface, bold: boldTypeface} = useChartDefaultTypeface(); - const {data, xKey, yKeys, xAxis, yAxis, labelItems, legendItems} = useMemo(() => processNode(tnode, regularTypeface), [tnode, regularTypeface]); - const {nodeStyles: chartContentStyles, parentNodeStyles: chartContainerStyles} = useMemo(() => parseStyles(tnode), [tnode]); - const [isCartesianChart, isPolarChart] = useMemo(() => [Object.keys(data).length > 0, false], [data]); + const {data, xKey, yKeys, xAxis, yAxis, labelItems, legendItems} = processNode(tnode, regularTypeface); + const {nodeStyles: chartContentStyles, parentNodeStyles: chartContainerStyles} = parseStyles(tnode); + const [isCartesianChart, isPolarChart] = [Object.keys(data).length > 0, false]; - const renderCartesianChartChild = useCallback((tnodeChild: TNode, index: number, renderArgs: CartesianChartRenderArg) => { + const renderCartesianChartChild = (tnodeChild: TNode, index: number, renderArgs: CartesianChartRenderArg) => { const key = `${tnodeChild.tagName ?? 'node'}-${index}`; const yKey = getYKey(tnodeChild); const {points, chartBounds} = renderArgs; @@ -363,9 +363,9 @@ function BaseVictoryChartRenderer({tnode}: VictoryChartRendererProps) { default: return null; } - }, []); + }; - const renderCartesianChartOutside = useCallback(() => { + const renderCartesianChartOutside = () => { return ( <> {labelItems.map(({x, y, text, color, fontSize, fontWeight}) => { @@ -416,7 +416,7 @@ function BaseVictoryChartRenderer({tnode}: VictoryChartRendererProps) { })} ); - }, [labelItems, legendItems, regularTypeface, boldTypeface]); + }; // Invalid chart (no charts or mixed charts) if (isCartesianChart === isPolarChart) { From ff9c5e0898512afa5504fdf52b91c65022b0a5e1 Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Thu, 21 May 2026 23:02:34 +0100 Subject: [PATCH 41/69] add json5 --- package-lock.json | 3 +++ package.json | 1 + 2 files changed, 4 insertions(+) diff --git a/package-lock.json b/package-lock.json index 9cb31a77c5aa..a11216d95746 100644 --- a/package-lock.json +++ b/package-lock.json @@ -79,6 +79,7 @@ "howler": "^2.2.4", "htmlparser2": "10.0.0", "idb-keyval": "^6.2.1", + "json5": "^2.2.2", "lodash-es": "4.17.21", "lottie-react-native": "6.5.1", "mapbox-gl": "^2.15.0", @@ -30373,6 +30374,8 @@ }, "node_modules/json5": { "version": "2.2.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.2.tgz", + "integrity": "sha512-46Tk9JiOL2z7ytNQWFLpj99RZkVgeHf87yGQKsIkaPz1qSH9UczKH1rO7K3wgRselo0tYMUNfecYpm/p1vC7tQ==", "license": "MIT", "bin": { "json5": "lib/cli.js" diff --git a/package.json b/package.json index 3fdfbf1b6c9f..33caab290662 100644 --- a/package.json +++ b/package.json @@ -143,6 +143,7 @@ "howler": "^2.2.4", "htmlparser2": "10.0.0", "idb-keyval": "^6.2.1", + "json5": "^2.2.2", "lodash-es": "4.17.21", "lottie-react-native": "6.5.1", "mapbox-gl": "^2.15.0", From f95e7fa4ac30e11e9904839c4b303fc8da619d69 Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Thu, 21 May 2026 23:06:53 +0100 Subject: [PATCH 42/69] explain why using JSON5 --- .../VictoryChartRenderer/BaseVictoryChartRenderer.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/BaseVictoryChartRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/BaseVictoryChartRenderer.tsx index 17edccc91093..e6081f93d651 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/BaseVictoryChartRenderer.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/BaseVictoryChartRenderer.tsx @@ -199,6 +199,7 @@ function parseAttribute(attribute: string): T | undefined { return undefined; } try { + // Using JSON5 instead of JSON because the former is not as strict as the later e.g. can parse objects with non-stringified fields `'{x: 100}'` return JSON5.parse(attribute); } catch { return attribute as T; From 5b81896279f60d7f76a0e686b2b180505c695dd2 Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Fri, 22 May 2026 00:14:43 +0100 Subject: [PATCH 43/69] Revert "add json5" This reverts commit ff9c5e0898512afa5504fdf52b91c65022b0a5e1. --- package-lock.json | 3 --- package.json | 1 - 2 files changed, 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index a11216d95746..9cb31a77c5aa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -79,7 +79,6 @@ "howler": "^2.2.4", "htmlparser2": "10.0.0", "idb-keyval": "^6.2.1", - "json5": "^2.2.2", "lodash-es": "4.17.21", "lottie-react-native": "6.5.1", "mapbox-gl": "^2.15.0", @@ -30374,8 +30373,6 @@ }, "node_modules/json5": { "version": "2.2.2", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.2.tgz", - "integrity": "sha512-46Tk9JiOL2z7ytNQWFLpj99RZkVgeHf87yGQKsIkaPz1qSH9UczKH1rO7K3wgRselo0tYMUNfecYpm/p1vC7tQ==", "license": "MIT", "bin": { "json5": "lib/cli.js" diff --git a/package.json b/package.json index 33caab290662..3fdfbf1b6c9f 100644 --- a/package.json +++ b/package.json @@ -143,7 +143,6 @@ "howler": "^2.2.4", "htmlparser2": "10.0.0", "idb-keyval": "^6.2.1", - "json5": "^2.2.2", "lodash-es": "4.17.21", "lottie-react-native": "6.5.1", "mapbox-gl": "^2.15.0", From 3abf02353f06cc3e25a9663ef4c3e3af0735f283 Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Fri, 22 May 2026 00:21:36 +0100 Subject: [PATCH 44/69] add json5 lock @ 2.2.2 --- package-lock.json | 3 +++ package.json | 1 + 2 files changed, 4 insertions(+) diff --git a/package-lock.json b/package-lock.json index 9cb31a77c5aa..6a22fd07ce28 100644 --- a/package-lock.json +++ b/package-lock.json @@ -79,6 +79,7 @@ "howler": "^2.2.4", "htmlparser2": "10.0.0", "idb-keyval": "^6.2.1", + "json5": "2.2.2", "lodash-es": "4.17.21", "lottie-react-native": "6.5.1", "mapbox-gl": "^2.15.0", @@ -30373,6 +30374,8 @@ }, "node_modules/json5": { "version": "2.2.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.2.tgz", + "integrity": "sha512-46Tk9JiOL2z7ytNQWFLpj99RZkVgeHf87yGQKsIkaPz1qSH9UczKH1rO7K3wgRselo0tYMUNfecYpm/p1vC7tQ==", "license": "MIT", "bin": { "json5": "lib/cli.js" diff --git a/package.json b/package.json index 3fdfbf1b6c9f..309154b72ab6 100644 --- a/package.json +++ b/package.json @@ -143,6 +143,7 @@ "howler": "^2.2.4", "htmlparser2": "10.0.0", "idb-keyval": "^6.2.1", + "json5": "2.2.2", "lodash-es": "4.17.21", "lottie-react-native": "6.5.1", "mapbox-gl": "^2.15.0", From 1159292069ee823433cc76c83b58caa14bb9eec1 Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Fri, 22 May 2026 00:41:39 +0100 Subject: [PATCH 45/69] lint still --- .storybook/webpack.config.ts | 1 + .../HTMLRenderers/VictoryChartRenderer/types.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.storybook/webpack.config.ts b/.storybook/webpack.config.ts index 391abe32b0ac..7e06c2034712 100644 --- a/.storybook/webpack.config.ts +++ b/.storybook/webpack.config.ts @@ -20,6 +20,7 @@ type CustomWebpackConfig = { resolve: { alias: Record; extensions: string[]; + fallback: Record; }; module: { rules: RuleSetRule[]; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/types.ts b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/types.ts index c8b08e339d5f..a860a72ec903 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/types.ts +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/types.ts @@ -1,6 +1,6 @@ import type {Color} from '@shopify/react-native-skia'; import type {CustomRendererProps, TBlock} from 'react-native-render-html'; -import {X_KEY, Y_KEY_PREFIX} from './constants'; +import type {X_KEY, Y_KEY_PREFIX} from './constants'; type VictoryChartRendererProps = CustomRendererProps; From 5cec82d2e34b59b269ec9cef21e3d37009018af9 Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Fri, 22 May 2026 16:22:58 +0100 Subject: [PATCH 46/69] apply refactor --- .../BaseVictoryChartRenderer.tsx | 461 +----------------- .../VictoryChartRenderer/VictoryChart.ts | 22 + .../components/VictoryChartBar.tsx | 32 ++ .../components/VictoryChartCartesian.tsx | 45 ++ .../components/VictoryChartContainer.tsx | 18 + .../components/VictoryChartLabels.tsx | 33 ++ .../components/VictoryChartLegend.tsx | 51 ++ .../components/VictoryChartLine.tsx | 28 ++ .../components/VictoryChartSeries.tsx | 29 ++ .../context/VictoryChartContext.tsx | 91 ++++ .../parsers/parserRegistry.ts | 19 + .../parsers/processVictoryChartTree.ts | 61 +++ .../parsers/victoryAxisParser.ts | 59 +++ .../parsers/victoryLabelParser.ts | 20 + .../parsers/victoryLegendParser.ts | 27 + .../parsers/victorySeriesParser.ts | 24 + .../VictoryChartRenderer/types.ts | 51 +- .../utils/getHierarchyID.ts | 16 + .../VictoryChartRenderer/utils/getYKey.ts | 13 + .../utils/parseAttribute.ts | 21 + .../utils/parseCornerRadius.ts | 48 ++ .../utils/parseDomainPadding.ts | 45 ++ .../VictoryChartRenderer/utils/parseStyles.ts | 36 ++ 23 files changed, 806 insertions(+), 444 deletions(-) create mode 100644 src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/VictoryChart.ts create mode 100644 src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartBar.tsx create mode 100644 src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartCartesian.tsx create mode 100644 src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartContainer.tsx create mode 100644 src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartLabels.tsx create mode 100644 src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartLegend.tsx create mode 100644 src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartLine.tsx create mode 100644 src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartSeries.tsx create mode 100644 src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartContext.tsx create mode 100644 src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/parsers/parserRegistry.ts create mode 100644 src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/parsers/processVictoryChartTree.ts create mode 100644 src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/parsers/victoryAxisParser.ts create mode 100644 src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/parsers/victoryLabelParser.ts create mode 100644 src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/parsers/victoryLegendParser.ts create mode 100644 src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/parsers/victorySeriesParser.ts create mode 100644 src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/getHierarchyID.ts create mode 100644 src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/getYKey.ts create mode 100644 src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/parseAttribute.ts create mode 100644 src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/parseCornerRadius.ts create mode 100644 src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/parseDomainPadding.ts create mode 100644 src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/parseStyles.ts diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/BaseVictoryChartRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/BaseVictoryChartRenderer.tsx index e6081f93d651..efbe4abf966a 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/BaseVictoryChartRenderer.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/BaseVictoryChartRenderer.tsx @@ -1,447 +1,26 @@ -import {Circle, Skia, Text as SkText} from '@shopify/react-native-skia'; -import type {SkTypeface} from '@shopify/react-native-skia'; -import JSON5 from 'json5'; -import lodashIsObject from 'lodash/isObject'; -import lodashMerge from 'lodash/merge'; -import React, {Fragment} from 'react'; -import type {ComponentProps} from 'react'; -import {View} from 'react-native'; -import type {ViewStyle} from 'react-native'; -import type {TNode} from 'react-native-render-html'; -import type {CartesianChartRenderArg, RoundedCorners} from 'victory-native'; -import {Bar, CartesianChart, Line} from 'victory-native'; -import {BAR_INNER_PADDING} from '@components/Charts/BarChart/BarChartContent'; -import {useChartDefaultTypeface} from '@components/Charts/hooks'; -import {DEFAULT_CHART_COLOR} from '@components/Charts/utils'; -import useThemeStyles from '@hooks/useThemeStyles'; -import {X_KEY, Y_KEY_PREFIX} from './constants'; -import type { - CartesianChartData, - LabelItem, - LegendItem, - LegendItemEntry, - RawAxisStyle, - RawChartData, - RawLabelStyle, - RawLegendData, - RawLegendStyle, - VictoryChartRendererProps, - YKey, -} from './types'; - -/** - * Get node unique ID based on hierarchy - */ -function getHierarchyID(tnode: TNode): string { - let id = String(tnode.nodeIndex); - let parent = tnode.parent; - while (parent) { - id = `${parent.nodeIndex}-${id}`; - parent = parent.parent; - } - return id; -} - -/** - * Get the Y-axis key for a given node - */ -function getYKey(tnode: TNode): YKey { - return `${Y_KEY_PREFIX}${getHierarchyID(tnode)}`; -} - -/** - * Traverse all nodes to extract points from `data` attributes and other config e.g. axis configuration - */ -function processNode(tnode: TNode, typeface: SkTypeface | null) { - const data: Record = {}; - const xKey = X_KEY; - const yKeys: YKey[] = []; - let xAxis: ComponentProps>['xAxis']; - let yAxis: ComponentProps>['yAxis']; - const labelItems: LabelItem[] = []; - const legendItems: LegendItem[] = []; - - if (tnode.tagName === 'victorybar' || tnode.tagName === 'victoryline') { - const points = parseAttribute(tnode.attributes.data) ?? []; - const yKey = getYKey(tnode); - yKeys.push(yKey); - for (const point of points) { - data[point.x] = { - [xKey]: point.x, - [yKey]: point.y, - } as CartesianChartData; - } - } else if (tnode.tagName === 'victoryaxis') { - const isDependentAxis = 'dependentaxis' in tnode.attributes && tnode.attributes.dependentaxis !== 'false'; - const orientation = parseAttribute(tnode.attributes.orientation); - const tickCount = parseAttribute(tnode.attributes.tickcount) ?? 0; - const tickValues = parseAttribute(tnode.attributes.tickvalues); - const tickFormat = parseAttribute(tnode.attributes.tickformat); - const formatLabel = (label: string | number) => tickFormat?.[tickValues?.indexOf(Number(label)) ?? -1] ?? String(label); - const style = parseAttribute(tnode.attributes.style); - const lineColor = style?.grid?.stroke; - const lineWidth = style?.grid?.strokeWidth !== undefined ? Number(style.grid.strokeWidth) : 0; // 0 Not to draw the lines for compatibility with VictoryChart - const labelColor = style?.tickLabels?.fill !== undefined ? String(style.tickLabels.fill) : undefined; - const labelOffset = style?.tickLabels?.padding !== undefined ? Number(style.tickLabels.padding) : undefined; - const fontSize = style?.tickLabels?.fontSize !== undefined ? Number(style.tickLabels.fontSize) : undefined; - const font = typeface ? Skia.Font(typeface, fontSize) : null; - if (isDependentAxis) { - yAxis = [ - { - tickCount, - tickValues, - formatYLabel: formatLabel, - axisSide: orientation === 'right' ? 'right' : 'left', - lineColor, - lineWidth, - labelColor, - labelOffset, - font, - }, - ]; - } else { - xAxis = { - tickCount, - tickValues, - formatXLabel: formatLabel, - axisSide: orientation === 'top' ? 'top' : 'bottom', - lineColor, - lineWidth, - labelColor, - labelOffset, - font, - }; - } - } else if (tnode.tagName === 'victorylabel') { - const x = parseAttribute(tnode.attributes.x) ?? 0; - const y = parseAttribute(tnode.attributes.y) ?? 0; - const text = parseAttribute(tnode.attributes.text) ?? ''; - const style = parseAttribute(tnode.attributes.style); - const color = style?.fill; - const fontSize = style?.fontSize !== undefined ? Number(style.fontSize) : undefined; - const fontWeight = Number(style?.fontWeight) === 700 ? 'bold' : undefined; - labelItems.push({ - x, - y, - text, - color, - fontSize, - fontWeight, - }); - } else if (tnode.tagName === 'victorylegend') { - const x = parseAttribute(tnode.attributes.x) ?? 0; - const y = parseAttribute(tnode.attributes.y) ?? 0; - const gutter = parseAttribute(tnode.attributes.gutter) ?? undefined; - const symbolSpacer = parseAttribute(tnode.attributes.symbolspacer) ?? undefined; - 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 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, - }; - }); - legendItems.push({ - x, - y, - entries, - gutter, - symbolSpacer, - }); - } - - for (const child of tnode.children) { - const childData = processNode(child, typeface); - yKeys.push(...childData.yKeys); - if (childData.xAxis) { - // It's safe to replace because there should be at most one xAxis - xAxis = childData.xAxis; - } - if (childData.yAxis) { - if (!yAxis) { - yAxis = []; - } - yAxis.push(...childData.yAxis); - } - labelItems.push(...childData.labelItems); - legendItems.push(...childData.legendItems); - lodashMerge(data, childData.data); - } - - return { - data, - xKey, - yKeys, - xAxis, - yAxis, - labelItems, - legendItems, - } as const; -} - -/** - * Parse attribute as JSON or fallback to input as is - * Example: "20" -> 20 - * : "[ {x: 'Jan', y: 3} ]" -> `[{"x": "Jan", "y": 3}]` (Valid RFC 8259) - * : "Green" -> "Green" - */ -function parseAttribute(attribute: string): T | undefined { - if (!attribute) { - return undefined; - } - try { - // Using JSON5 instead of JSON because the former is not as strict as the later e.g. can parse objects with non-stringified fields `'{x: 100}'` - return JSON5.parse(attribute); - } catch { - return attribute as T; - } -} - -/** - * Helper to parse VC's `cornerRadius` into VN's `roundedCorners` - */ -function parseCornerRadius(attribute: string): RoundedCorners | undefined { - const cornerRadius = parseAttribute(attribute); - if (typeof cornerRadius === 'number') { - return { - topLeft: cornerRadius, - topRight: cornerRadius, - bottomLeft: cornerRadius, - bottomRight: cornerRadius, - }; - } - if (lodashIsObject(cornerRadius)) { - let topLeft: number | undefined; - let topRight: number | undefined; - let bottomLeft: number | undefined; - let bottomRight: number | undefined; - if ('topLeft' in cornerRadius) { - topLeft = Number(cornerRadius.topLeft); - } else if ('top' in cornerRadius) { - topLeft = Number(cornerRadius.top); - } - if ('topRight' in cornerRadius) { - topLeft = Number(cornerRadius.topRight); - } else if ('top' in cornerRadius) { - topRight = Number(cornerRadius.top); - } - if ('bottomLeft' in cornerRadius) { - bottomLeft = Number(cornerRadius.bottomLeft); - } else if ('bottom' in cornerRadius) { - bottomLeft = Number(cornerRadius.bottom); - } - if ('bottomRight' in cornerRadius) { - bottomRight = Number(cornerRadius.bottomRight); - } else if ('bottom' in cornerRadius) { - bottomRight = Number(cornerRadius.bottom); - } - return { - topLeft, - topRight, - bottomLeft, - bottomRight, - }; - } - return undefined; -} - -/** - * Helper to parse VC's `domainPadding` into VN's `domainPadding` - */ -function parseDomainPadding(attribute: string): ComponentProps>['domainPadding'] | undefined { - const domainPadding = parseAttribute(attribute); - if (typeof domainPadding === 'number') { - return domainPadding; - } - if (Array.isArray(domainPadding)) { - return { - left: Number(domainPadding.at(0)), - right: Number(domainPadding.at(1)), - }; - } - if (lodashIsObject(domainPadding)) { - let left: number | undefined; - let right: number | undefined; - let top: number | undefined; - let bottom: number | undefined; - if ('x' in domainPadding && typeof domainPadding.x === 'number') { - left = domainPadding.x; - right = domainPadding.x; - } else if ('x' in domainPadding && Array.isArray(domainPadding.x)) { - left = Number(domainPadding.x.at(0)); - right = Number(domainPadding.x.at(1)); - } - if ('y' in domainPadding && typeof domainPadding.y === 'number') { - top = domainPadding.y; - bottom = domainPadding.y; - } else if ('y' in domainPadding && Array.isArray(domainPadding.y)) { - top = Number(domainPadding.y.at(1)); - bottom = Number(domainPadding.y.at(0)); - } - return { - left, - right, - top, - bottom, - }; - } - return undefined; -} - -/** - * Helper to parse common styles - */ -function parseStyles(tnode: TNode) { - const nodeStyles: ViewStyle = {}; - const parentNodeStyles: ViewStyle = {}; - - const parsedHeight = parseAttribute(tnode.attributes.height); - if (typeof parsedHeight === 'number') { - nodeStyles.height = parsedHeight; - } - const parsedWidth = parseAttribute(tnode.attributes.width); - if (typeof parsedWidth === 'number') { - nodeStyles.width = parsedWidth; - } - - const parsedStyle = parseAttribute(tnode.attributes.style); - if (lodashIsObject(parsedStyle)) { - if ('parent' in parsedStyle && lodashIsObject(parsedStyle.parent)) { - Object.assign(parentNodeStyles, parsedStyle.parent); - } - if ('data' in parsedStyle && lodashIsObject(parsedStyle.data)) { - Object.assign(nodeStyles, parsedStyle.data); - } - } - - return {nodeStyles, parentNodeStyles}; -} +import React from 'react'; +import type {VictoryChartRendererProps} from './types'; +import getYKey from './utils/getYKey'; +import VictoryChart from './VictoryChart'; function BaseVictoryChartRenderer({tnode}: VictoryChartRendererProps) { - const styles = useThemeStyles(); - const {regular: regularTypeface, bold: boldTypeface} = useChartDefaultTypeface(); - const {data, xKey, yKeys, xAxis, yAxis, labelItems, legendItems} = processNode(tnode, regularTypeface); - const {nodeStyles: chartContentStyles, parentNodeStyles: chartContainerStyles} = parseStyles(tnode); - const [isCartesianChart, isPolarChart] = [Object.keys(data).length > 0, false]; - - const renderCartesianChartChild = (tnodeChild: TNode, index: number, renderArgs: CartesianChartRenderArg) => { - const key = `${tnodeChild.tagName ?? 'node'}-${index}`; - const yKey = getYKey(tnodeChild); - const {points, chartBounds} = renderArgs; - const {nodeStyles} = parseStyles(tnodeChild); - switch (tnodeChild.tagName) { - case 'victorybar': - return ( - - ); - case 'victoryline': - return ( - - ); - default: - return null; - } - }; - - const renderCartesianChartOutside = () => { - return ( - <> - {labelItems.map(({x, y, text, color, fontSize, fontWeight}) => { - const typeface = fontWeight === 'bold' ? boldTypeface : regularTypeface; - const font = typeface ? Skia.Font(typeface, fontSize) : null; - return ( - - ); - })} - {legendItems.map(({x: startX, y, entries, gutter, symbolSpacer}) => { - let x = startX; - return entries.map(({text, color, fontSize, fontWeight, symbolColor, symbolSize}) => { - const typeface = fontWeight === 'bold' ? boldTypeface : regularTypeface; - const font = typeface ? Skia.Font(typeface, fontSize) : null; - const fontMetrics = font?.getMetrics(); - const lineHeight = fontMetrics ? fontMetrics.ascent + fontMetrics.descent + fontMetrics.leading : 0; - const symbolX = x; - x += (symbolSize ?? 0) + (symbolSpacer ?? 0); - const textX = x; - x += (font?.getGlyphWidths(font.getGlyphIDs(text)).reduce((acc, width) => acc + width, 0) ?? 0) + (gutter ?? 0); - return ( - - {!!symbolSize && ( - - )} - - - ); - }); - })} - - ); - }; - - // Invalid chart (no charts or mixed charts) - if (isCartesianChart === isPolarChart) { - return null; - } - return ( - - - - {(renderArgs) => tnode.children.map((child, childIndex) => renderCartesianChartChild(child, childIndex, renderArgs))} - - - + + + + {(renderArgs) => ( + + {tnode.children.map((child) => ( + + ))} + + )} + + + ); } diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/VictoryChart.ts b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/VictoryChart.ts new file mode 100644 index 000000000000..92e7fdb50790 --- /dev/null +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/VictoryChart.ts @@ -0,0 +1,22 @@ +import VictoryChartBar from './components/VictoryChartBar'; +import VictoryChartCartesian from './components/VictoryChartCartesian'; +import VictoryChartContainer from './components/VictoryChartContainer'; +import VictoryChartLabels from './components/VictoryChartLabels'; +import VictoryChartLegend from './components/VictoryChartLegend'; +import VictoryChartLine from './components/VictoryChartLine'; +import VictoryChartSeries from './components/VictoryChartSeries'; +import {VictoryChartProvider, VictoryChartRenderArgsProvider} from './context/VictoryChartContext'; + +const VictoryChart = { + Provider: VictoryChartProvider, + RenderArgsProvider: VictoryChartRenderArgsProvider, + Container: VictoryChartContainer, + Cartesian: VictoryChartCartesian, + Series: VictoryChartSeries, + Bar: VictoryChartBar, + Line: VictoryChartLine, + Labels: VictoryChartLabels, + Legend: VictoryChartLegend, +}; + +export default VictoryChart; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartBar.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartBar.tsx new file mode 100644 index 000000000000..415f2b60905e --- /dev/null +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartBar.tsx @@ -0,0 +1,32 @@ +import React from 'react'; +import type {TNode} from 'react-native-render-html'; +import {Bar} from 'victory-native'; +import {BAR_INNER_PADDING} from '@components/Charts/BarChart/BarChartContent'; +import {DEFAULT_CHART_COLOR} from '@components/Charts/utils'; +import {useVictoryChartRenderArgs} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartContext'; +import getYKey from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/getYKey'; +import parseAttribute from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/parseAttribute'; +import parseCornerRadius from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/parseCornerRadius'; +import parseStyles from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/parseStyles'; + +type VictoryChartBarProps = {tnode: TNode}; + +function VictoryChartBar({tnode}: VictoryChartBarProps) { + const {points, chartBounds} = useVictoryChartRenderArgs(); + const yKey = getYKey(tnode); + const {nodeStyles} = parseStyles(tnode); + return ( + + ); +} + +VictoryChartBar.displayName = 'VictoryChartBar'; + +export default VictoryChartBar; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartCartesian.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartCartesian.tsx new file mode 100644 index 000000000000..d4d7ef6e210b --- /dev/null +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartCartesian.tsx @@ -0,0 +1,45 @@ +import React from 'react'; +import type {CartesianChartRenderArg} from 'victory-native'; +import {CartesianChart} from 'victory-native'; +import {useVictoryChartContext} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartContext'; +import type {CartesianChartData, YKey} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/types'; +import parseAttribute from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/parseAttribute'; +import parseDomainPadding from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/parseDomainPadding'; +import VictoryChartLabels from './VictoryChartLabels'; +import VictoryChartLegend from './VictoryChartLegend'; + +type VictoryChartCartesianProps = { + children: (renderArgs: CartesianChartRenderArg) => React.ReactNode; +}; + +/** + * Renders the CartesianChart with data, axes, and domain config drawn from context. + * Labels and legend overlays are handled internally via `renderOutside`. + */ +function VictoryChartCartesian({children}: VictoryChartCartesianProps) { + const {data, xKey, yKeys, xAxis, yAxis, tnode} = useVictoryChartContext(); + return ( + ( + <> + + + + )} + > + {children} + + ); +} + +VictoryChartCartesian.displayName = 'VictoryChartCartesian'; + +export default VictoryChartCartesian; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartContainer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartContainer.tsx new file mode 100644 index 000000000000..350f3ec6e60b --- /dev/null +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartContainer.tsx @@ -0,0 +1,18 @@ +import React from 'react'; +import {View} from 'react-native'; +import {useVictoryChartContext} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartContext'; +import useThemeStyles from '@hooks/useThemeStyles'; + +function VictoryChartContainer({children}: {children: React.ReactNode}) { + const styles = useThemeStyles(); + const {nodeStyles: chartContentStyles, parentNodeStyles: chartContainerStyles} = useVictoryChartContext(); + return ( + + {children} + + ); +} + +VictoryChartContainer.displayName = 'VictoryChartContainer'; + +export default VictoryChartContainer; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartLabels.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartLabels.tsx new file mode 100644 index 000000000000..ec972cc76c14 --- /dev/null +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartLabels.tsx @@ -0,0 +1,33 @@ +import {Skia, Text as SkText} from '@shopify/react-native-skia'; +import React from 'react'; +import {useVictoryChartContext} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartContext'; + +/** + * Renders floating Skia text labels (from `` nodes) over the chart canvas. + * Intended for use inside CartesianChart's `renderOutside` callback. + */ +function VictoryChartLabels() { + const {labelItems, regularTypeface, boldTypeface} = useVictoryChartContext(); + return ( + <> + {labelItems.map(({x, y, text, color, fontSize, fontWeight}) => { + const typeface = fontWeight === 'bold' ? boldTypeface : regularTypeface; + const font = typeface ? Skia.Font(typeface, fontSize) : null; + return ( + + ); + })} + + ); +} + +VictoryChartLabels.displayName = 'VictoryChartLabels'; + +export default VictoryChartLabels; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartLegend.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartLegend.tsx new file mode 100644 index 000000000000..7e7f194bbd78 --- /dev/null +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartLegend.tsx @@ -0,0 +1,51 @@ +import {Circle, Skia, Text as SkText} from '@shopify/react-native-skia'; +import React, {Fragment} from 'react'; +import {useVictoryChartContext} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartContext'; + +/** + * Renders Skia legend symbols and labels (from `` nodes) over the chart canvas. + * Intended for use inside CartesianChart's `renderOutside` callback. + */ +function VictoryChartLegend() { + const {legendItems, regularTypeface, boldTypeface} = useVictoryChartContext(); + return ( + <> + {legendItems.map(({x: startX, y, entries, gutter, symbolSpacer}) => { + let x = startX; + return entries.map(({text, color, fontSize, fontWeight, symbolColor, symbolSize}) => { + const typeface = fontWeight === 'bold' ? boldTypeface : regularTypeface; + const font = typeface ? Skia.Font(typeface, fontSize) : null; + const fontMetrics = font?.getMetrics(); + const lineHeight = fontMetrics ? fontMetrics.ascent + fontMetrics.descent + fontMetrics.leading : 0; + const symbolX = x; + x += (symbolSize ?? 0) + (symbolSpacer ?? 0); + const textX = x; + x += (font?.getGlyphWidths(font.getGlyphIDs(text)).reduce((acc, width) => acc + width, 0) ?? 0) + (gutter ?? 0); + return ( + + {!!symbolSize && ( + + )} + + + ); + }); + })} + + ); +} + +VictoryChartLegend.displayName = 'VictoryChartLegend'; + +export default VictoryChartLegend; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartLine.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartLine.tsx new file mode 100644 index 000000000000..a846416089ba --- /dev/null +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartLine.tsx @@ -0,0 +1,28 @@ +import React from 'react'; +import type {TNode} from 'react-native-render-html'; +import {Line} from 'victory-native'; +import {DEFAULT_CHART_COLOR} from '@components/Charts/utils'; +import {useVictoryChartRenderArgs} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartContext'; +import getYKey from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/getYKey'; +import parseAttribute from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/parseAttribute'; +import parseStyles from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/parseStyles'; + +type VictoryChartLineProps = {tnode: TNode}; + +function VictoryChartLine({tnode}: VictoryChartLineProps) { + const {points} = useVictoryChartRenderArgs(); + const yKey = getYKey(tnode); + const {nodeStyles} = parseStyles(tnode); + return ( + + ); +} + +VictoryChartLine.displayName = 'VictoryChartLine'; + +export default VictoryChartLine; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartSeries.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartSeries.tsx new file mode 100644 index 000000000000..8768dfbfc33f --- /dev/null +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartSeries.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import type {TNode} from 'react-native-render-html'; +import VictoryChartBar from './VictoryChartBar'; +import VictoryChartLine from './VictoryChartLine'; + +type VictoryChartSeriesProps = {tnode: TNode}; + +type SeriesComponent = (props: VictoryChartSeriesProps) => React.ReactElement | null; + +/** + * Dispatches a chart child node to its series renderer based on the HTML tag name. + * To support a new series type, add its tag name here and create the renderer component. + */ +const SERIES_RENDERERS: Partial> = { + victorybar: VictoryChartBar, + victoryline: VictoryChartLine, +}; + +function VictoryChartSeries({tnode}: VictoryChartSeriesProps) { + const SeriesRenderer = SERIES_RENDERERS[tnode.tagName ?? '']; + if (!SeriesRenderer) { + return null; + } + return ; +} + +VictoryChartSeries.displayName = 'VictoryChartSeries'; + +export default VictoryChartSeries; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartContext.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartContext.tsx new file mode 100644 index 000000000000..de8058c9fb01 --- /dev/null +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartContext.tsx @@ -0,0 +1,91 @@ +import type {SkTypeface} from '@shopify/react-native-skia'; +import React, {createContext, useContext} from 'react'; +import type {TNode} from 'react-native-render-html'; +import type {CartesianChartRenderArg} from 'victory-native'; +import {useChartDefaultTypeface} from '@components/Charts/hooks'; +import processVictoryChartTree from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/parsers/processVictoryChartTree'; +import type {CartesianChartData, ProcessNodeResult, YKey} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/types'; +import parseStyles from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/parseStyles'; + +type VictoryChartContextValue = { + tnode: TNode; + data: ProcessNodeResult['data']; + xKey: ProcessNodeResult['xKey']; + yKeys: ProcessNodeResult['yKeys']; + xAxis: ProcessNodeResult['xAxis']; + yAxis: ProcessNodeResult['yAxis']; + labelItems: ProcessNodeResult['labelItems']; + legendItems: ProcessNodeResult['legendItems']; + nodeStyles: ReturnType['nodeStyles']; + parentNodeStyles: ReturnType['parentNodeStyles']; + regularTypeface: SkTypeface | null; + boldTypeface: SkTypeface | null; + isValidCartesian: boolean; +}; + +const VictoryChartContext = createContext(null); +const VictoryChartRenderArgsContext = createContext | null>(null); + +/** + * Parses the HTML tnode tree into chart config and makes it available to all chart sub-components. + * 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, bold: boldTypeface} = useChartDefaultTypeface(); + const {data, xKey, yKeys, xAxis, yAxis, labelItems, legendItems} = processVictoryChartTree(tnode, regularTypeface); + const {nodeStyles, parentNodeStyles} = parseStyles(tnode); + const isValidCartesian = Object.keys(data).length > 0; + + if (!isValidCartesian) { + return null; + } + + const contextValue: VictoryChartContextValue = { + tnode, + data, + xKey, + yKeys, + xAxis, + yAxis, + labelItems, + legendItems, + nodeStyles, + parentNodeStyles, + regularTypeface, + boldTypeface, + isValidCartesian, + }; + + return {children}; +} + +VictoryChartProvider.displayName = 'VictoryChartProvider'; + +/** + * Makes the CartesianChart render-prop arguments available to series sub-components + * (VictoryChartBar, VictoryChartLine) rendered inside the chart's children callback. + */ +function VictoryChartRenderArgsProvider({value, children}: {value: CartesianChartRenderArg; children: React.ReactNode}) { + return {children}; +} + +VictoryChartRenderArgsProvider.displayName = 'VictoryChartRenderArgsProvider'; + +function useVictoryChartContext(): VictoryChartContextValue { + const context = useContext(VictoryChartContext); + if (!context) { + throw new Error('useVictoryChartContext must be used within VictoryChartProvider'); + } + return context; +} + +function useVictoryChartRenderArgs(): CartesianChartRenderArg { + const context = useContext(VictoryChartRenderArgsContext); + if (!context) { + throw new Error('useVictoryChartRenderArgs must be used within VictoryChartRenderArgsProvider'); + } + return context; +} + +export {VictoryChartProvider, VictoryChartRenderArgsProvider, useVictoryChartContext, useVictoryChartRenderArgs}; +export type {VictoryChartContextValue}; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/parsers/parserRegistry.ts b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/parsers/parserRegistry.ts new file mode 100644 index 000000000000..d20acff2a2cc --- /dev/null +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/parsers/parserRegistry.ts @@ -0,0 +1,19 @@ +import type {NodeParser} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/types'; +import parseVictoryAxisNode from './victoryAxisParser'; +import parseVictoryLabelNode from './victoryLabelParser'; +import parseVictoryLegendNode from './victoryLegendParser'; +import parseVictorySeriesNode from './victorySeriesParser'; + +/** + * Maps HTML tag names to their corresponding parser functions. + * To support a new VictoryChart tag, add a new entry here and create the parser file. + */ +const PARSER_REGISTRY: Partial> = { + victorybar: parseVictorySeriesNode, + victoryline: parseVictorySeriesNode, + victoryaxis: parseVictoryAxisNode, + victorylabel: parseVictoryLabelNode, + victorylegend: parseVictoryLegendNode, +}; + +export default PARSER_REGISTRY; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/parsers/processVictoryChartTree.ts b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/parsers/processVictoryChartTree.ts new file mode 100644 index 000000000000..d1f52fbd880f --- /dev/null +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/parsers/processVictoryChartTree.ts @@ -0,0 +1,61 @@ +import type {SkTypeface} from '@shopify/react-native-skia'; +import lodashMerge from 'lodash/merge'; +import type {TNode} from 'react-native-render-html'; +import {X_KEY} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/constants'; +import type {ProcessNodeResult} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/types'; +import PARSER_REGISTRY from './parserRegistry'; + +/** + * Recursively walk the HTML tnode tree, dispatching each node to its registered parser + * and merging the results into a single chart config. + */ +function processVictoryChartTree(tnode: TNode, typeface: SkTypeface | null): ProcessNodeResult { + const data: ProcessNodeResult['data'] = {}; + const yKeys: ProcessNodeResult['yKeys'] = []; + let xAxis: ProcessNodeResult['xAxis']; + let yAxis: ProcessNodeResult['yAxis']; + const labelItems: ProcessNodeResult['labelItems'] = []; + const legendItems: ProcessNodeResult['legendItems'] = []; + + const parser = PARSER_REGISTRY[tnode.tagName ?? '']; + if (parser) { + const result = parser(tnode, typeface); + if (result.data) { + lodashMerge(data, result.data); + } + if (result.yKeys) { + yKeys.push(...result.yKeys); + } + if (result.xAxis) { + xAxis = result.xAxis; + } + if (result.yAxis?.length) { + yAxis = [...(yAxis ?? []), ...result.yAxis]; + } + if (result.labelItems) { + labelItems.push(...result.labelItems); + } + if (result.legendItems) { + legendItems.push(...result.legendItems); + } + } + + for (const child of tnode.children) { + const childResult = processVictoryChartTree(child, typeface); + yKeys.push(...childResult.yKeys); + if (childResult.xAxis) { + // Safe to replace — there should be at most one xAxis per chart + xAxis = childResult.xAxis; + } + if (childResult.yAxis?.length) { + yAxis = [...(yAxis ?? []), ...childResult.yAxis]; + } + labelItems.push(...childResult.labelItems); + legendItems.push(...childResult.legendItems); + lodashMerge(data, childResult.data); + } + + return {data, xKey: X_KEY, yKeys, xAxis, yAxis, labelItems, legendItems}; +} + +export default processVictoryChartTree; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/parsers/victoryAxisParser.ts b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/parsers/victoryAxisParser.ts new file mode 100644 index 000000000000..6087fc96eae9 --- /dev/null +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/parsers/victoryAxisParser.ts @@ -0,0 +1,59 @@ +import {Skia} from '@shopify/react-native-skia'; +import type {SkTypeface} from '@shopify/react-native-skia'; +import type {TNode} from 'react-native-render-html'; +import type {PartialProcessNodeResult, RawAxisStyle} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/types'; +import parseAttribute from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/parseAttribute'; + +/** + * Parse axis config from a `` node. + * Dependent axes become yAxis entries; independent axes become the xAxis. + */ +function parseVictoryAxisNode(tnode: TNode, typeface: SkTypeface | null): PartialProcessNodeResult { + const isDependentAxis = 'dependentaxis' in tnode.attributes && tnode.attributes.dependentaxis !== 'false'; + const orientation = parseAttribute(tnode.attributes.orientation); + const tickCount = parseAttribute(tnode.attributes.tickcount) ?? 0; + const tickValues = parseAttribute(tnode.attributes.tickvalues); + const tickFormat = parseAttribute(tnode.attributes.tickformat); + const formatLabel = (label: string | number) => tickFormat?.[tickValues?.indexOf(Number(label)) ?? -1] ?? String(label); + const style = parseAttribute(tnode.attributes.style); + const lineColor = style?.grid?.stroke; + // 0 width intentionally avoids drawing grid lines, preserving VictoryChart compatibility + const lineWidth = style?.grid?.strokeWidth !== undefined ? Number(style.grid.strokeWidth) : 0; + const labelColor = style?.tickLabels?.fill !== undefined ? String(style.tickLabels.fill) : undefined; + const labelOffset = style?.tickLabels?.padding !== undefined ? Number(style.tickLabels.padding) : undefined; + const fontSize = style?.tickLabels?.fontSize !== undefined ? Number(style.tickLabels.fontSize) : undefined; + const font = typeface ? Skia.Font(typeface, fontSize) : null; + + if (isDependentAxis) { + return { + yAxis: [ + { + tickCount, + tickValues, + formatYLabel: formatLabel, + axisSide: orientation === 'right' ? 'right' : 'left', + lineColor, + lineWidth, + labelColor, + labelOffset, + font, + }, + ], + }; + } + return { + xAxis: { + tickCount, + tickValues, + formatXLabel: formatLabel, + axisSide: orientation === 'top' ? 'top' : 'bottom', + lineColor, + lineWidth, + labelColor, + labelOffset, + font, + }, + }; +} + +export default parseVictoryAxisNode; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/parsers/victoryLabelParser.ts b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/parsers/victoryLabelParser.ts new file mode 100644 index 000000000000..37420261b889 --- /dev/null +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/parsers/victoryLabelParser.ts @@ -0,0 +1,20 @@ +import type {TNode} from 'react-native-render-html'; +import type {LabelItem, PartialProcessNodeResult, RawLabelStyle} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/types'; +import parseAttribute from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/parseAttribute'; + +/** + * Parse label config from a `` node. + */ +function parseVictoryLabelNode(tnode: TNode): PartialProcessNodeResult { + const x = parseAttribute(tnode.attributes.x) ?? 0; + const y = parseAttribute(tnode.attributes.y) ?? 0; + const text = parseAttribute(tnode.attributes.text) ?? ''; + const style = parseAttribute(tnode.attributes.style); + const color = style?.fill; + const fontSize = style?.fontSize !== undefined ? Number(style.fontSize) : undefined; + const fontWeight = Number(style?.fontWeight) === 700 ? 'bold' : undefined; + const labelItem: LabelItem = {x, y, text, color, fontSize, fontWeight}; + return {labelItems: [labelItem]}; +} + +export default parseVictoryLabelNode; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/parsers/victoryLegendParser.ts b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/parsers/victoryLegendParser.ts new file mode 100644 index 000000000000..ebc83a9ac9fe --- /dev/null +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/parsers/victoryLegendParser.ts @@ -0,0 +1,27 @@ +import type {TNode} from 'react-native-render-html'; +import type {LegendItem, LegendItemEntry, PartialProcessNodeResult, RawLegendData, RawLegendStyle} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/types'; +import parseAttribute from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/parseAttribute'; + +/** + * Parse legend config from a `` node. + */ +function parseVictoryLegendNode(tnode: TNode): PartialProcessNodeResult { + const x = parseAttribute(tnode.attributes.x) ?? 0; + const y = parseAttribute(tnode.attributes.y) ?? 0; + const gutter = parseAttribute(tnode.attributes.gutter) ?? undefined; + const symbolSpacer = parseAttribute(tnode.attributes.symbolspacer) ?? undefined; + 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 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}; + }); + const legendItem: LegendItem = {x, y, entries, gutter, symbolSpacer}; + return {legendItems: [legendItem]}; +} + +export default parseVictoryLegendNode; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/parsers/victorySeriesParser.ts b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/parsers/victorySeriesParser.ts new file mode 100644 index 000000000000..d9930fce5926 --- /dev/null +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/parsers/victorySeriesParser.ts @@ -0,0 +1,24 @@ +import type {TNode} from 'react-native-render-html'; +import {X_KEY} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/constants'; +import type {CartesianChartData, PartialProcessNodeResult, RawChartData} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/types'; +import getYKey from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/getYKey'; +import parseAttribute from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/parseAttribute'; + +/** + * Parse data points from a `` or `` node. + * Both series types share the same data structure: an array of {x, y} points. + */ +function parseVictorySeriesNode(tnode: TNode): PartialProcessNodeResult { + const points = parseAttribute(tnode.attributes.data) ?? []; + const yKey = getYKey(tnode); + const data: Record = {}; + for (const point of points) { + data[point.x] = { + [X_KEY]: point.x, + [yKey]: point.y, + } as CartesianChartData; + } + return {data, yKeys: [yKey]}; +} + +export default parseVictorySeriesNode; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/types.ts b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/types.ts index a860a72ec903..4f7258bad1fd 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/types.ts +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/types.ts @@ -1,5 +1,7 @@ -import type {Color} from '@shopify/react-native-skia'; -import type {CustomRendererProps, TBlock} from 'react-native-render-html'; +import type {Color, SkTypeface} from '@shopify/react-native-skia'; +import type {ComponentProps} from 'react'; +import type {CustomRendererProps, TBlock, TNode} from 'react-native-render-html'; +import type {CartesianChart} from 'victory-native'; import type {X_KEY, Y_KEY_PREFIX} from './constants'; type VictoryChartRendererProps = CustomRendererProps; @@ -108,4 +110,47 @@ type LegendItem = { symbolSpacer?: number; }; -export type {VictoryChartRendererProps, RawChartData, RawLegendData, RawAxisStyle, RawLabelStyle, RawLegendStyle, XKey, YKey, CartesianChartData, LabelItem, LegendItemEntry, LegendItem}; +/** Shared CartesianChart prop type used by the orchestrator, parsers, and Cartesian sub-component. */ +type CartesianChartProps = ComponentProps>; + +/** Fully merged result of walking the HTML tnode tree. */ +type ProcessNodeResult = { + data: Record; + xKey: XKey; + yKeys: YKey[]; + xAxis: CartesianChartProps['xAxis']; + yAxis: CartesianChartProps['yAxis']; + labelItems: LabelItem[]; + legendItems: LegendItem[]; +}; + +/** Partial slice produced by a single per-tag parser before merging. */ +type PartialProcessNodeResult = { + data?: Record; + yKeys?: YKey[]; + xAxis?: CartesianChartProps['xAxis']; + yAxis?: NonNullable; + labelItems?: LabelItem[]; + legendItems?: LegendItem[]; +}; + +type NodeParser = (tnode: TNode, typeface: SkTypeface | null) => PartialProcessNodeResult; + +export type { + VictoryChartRendererProps, + RawChartData, + RawLegendData, + RawAxisStyle, + RawLabelStyle, + RawLegendStyle, + XKey, + YKey, + CartesianChartData, + CartesianChartProps, + LabelItem, + LegendItemEntry, + LegendItem, + ProcessNodeResult, + PartialProcessNodeResult, + NodeParser, +}; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/getHierarchyID.ts b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/getHierarchyID.ts new file mode 100644 index 000000000000..ce46889eac9f --- /dev/null +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/getHierarchyID.ts @@ -0,0 +1,16 @@ +import type {TNode} from 'react-native-render-html'; + +/** + * Get a node's unique ID based on its position in the HTML hierarchy. + */ +function getHierarchyID(tnode: TNode): string { + let id = String(tnode.nodeIndex); + let parent = tnode.parent; + while (parent) { + id = `${parent.nodeIndex}-${id}`; + parent = parent.parent; + } + return id; +} + +export default getHierarchyID; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/getYKey.ts b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/getYKey.ts new file mode 100644 index 000000000000..e35660a35a78 --- /dev/null +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/getYKey.ts @@ -0,0 +1,13 @@ +import type {TNode} from 'react-native-render-html'; +import {Y_KEY_PREFIX} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/constants'; +import type {YKey} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/types'; +import getHierarchyID from './getHierarchyID'; + +/** + * Get the Y-axis key for a given node. + */ +function getYKey(tnode: TNode): YKey { + return `${Y_KEY_PREFIX}${getHierarchyID(tnode)}`; +} + +export default getYKey; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/parseAttribute.ts b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/parseAttribute.ts new file mode 100644 index 000000000000..e3c617e03c88 --- /dev/null +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/parseAttribute.ts @@ -0,0 +1,21 @@ +import JSON5 from 'json5'; + +/** + * Parse attribute as JSON or fallback to input as is. + * Example: "20" -> 20 + * : "[ {x: 'Jan', y: 3} ]" -> `[{"x": "Jan", "y": 3}]` (Valid RFC 8259) + * : "Green" -> "Green" + */ +function parseAttribute(attribute: string): T | undefined { + if (!attribute) { + return undefined; + } + try { + // Using JSON5 instead of JSON because the former is not as strict as the later e.g. can parse objects with non-stringified fields `'{x: 100}'` + return JSON5.parse(attribute); + } catch { + return attribute as T; + } +} + +export default parseAttribute; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/parseCornerRadius.ts b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/parseCornerRadius.ts new file mode 100644 index 000000000000..26627e21b8d9 --- /dev/null +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/parseCornerRadius.ts @@ -0,0 +1,48 @@ +import lodashIsObject from 'lodash/isObject'; +import type {RoundedCorners} from 'victory-native'; +import parseAttribute from './parseAttribute'; + +/** + * Translate VictoryChart's `cornerRadius` attribute into victory-native's `roundedCorners` shape. + */ +function parseCornerRadius(attribute: string): RoundedCorners | undefined { + const cornerRadius = parseAttribute(attribute); + if (typeof cornerRadius === 'number') { + return { + topLeft: cornerRadius, + topRight: cornerRadius, + bottomLeft: cornerRadius, + bottomRight: cornerRadius, + }; + } + if (lodashIsObject(cornerRadius)) { + let topLeft: number | undefined; + let topRight: number | undefined; + let bottomLeft: number | undefined; + let bottomRight: number | undefined; + if ('topLeft' in cornerRadius) { + topLeft = Number(cornerRadius.topLeft); + } else if ('top' in cornerRadius) { + topLeft = Number(cornerRadius.top); + } + if ('topRight' in cornerRadius) { + topRight = Number(cornerRadius.topRight); + } else if ('top' in cornerRadius) { + topRight = Number(cornerRadius.top); + } + if ('bottomLeft' in cornerRadius) { + bottomLeft = Number(cornerRadius.bottomLeft); + } else if ('bottom' in cornerRadius) { + bottomLeft = Number(cornerRadius.bottom); + } + if ('bottomRight' in cornerRadius) { + bottomRight = Number(cornerRadius.bottomRight); + } else if ('bottom' in cornerRadius) { + bottomRight = Number(cornerRadius.bottom); + } + return {topLeft, topRight, bottomLeft, bottomRight}; + } + return undefined; +} + +export default parseCornerRadius; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/parseDomainPadding.ts b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/parseDomainPadding.ts new file mode 100644 index 000000000000..8c439af02a03 --- /dev/null +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/parseDomainPadding.ts @@ -0,0 +1,45 @@ +import lodashIsObject from 'lodash/isObject'; +import type {CartesianChartProps} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/types'; +import parseAttribute from './parseAttribute'; + +type DomainPadding = CartesianChartProps['domainPadding']; + +/** + * Translate VictoryChart's `domainPadding` attribute into victory-native's `domainPadding` shape. + */ +function parseDomainPadding(attribute: string): DomainPadding { + const domainPadding = parseAttribute(attribute); + if (typeof domainPadding === 'number') { + return domainPadding; + } + if (Array.isArray(domainPadding)) { + return { + left: Number(domainPadding.at(0)), + right: Number(domainPadding.at(1)), + }; + } + if (lodashIsObject(domainPadding)) { + let left: number | undefined; + let right: number | undefined; + let top: number | undefined; + let bottom: number | undefined; + if ('x' in domainPadding && typeof domainPadding.x === 'number') { + left = domainPadding.x; + right = domainPadding.x; + } else if ('x' in domainPadding && Array.isArray(domainPadding.x)) { + left = Number(domainPadding.x.at(0)); + right = Number(domainPadding.x.at(1)); + } + if ('y' in domainPadding && typeof domainPadding.y === 'number') { + top = domainPadding.y; + bottom = domainPadding.y; + } else if ('y' in domainPadding && Array.isArray(domainPadding.y)) { + top = Number(domainPadding.y.at(1)); + bottom = Number(domainPadding.y.at(0)); + } + return {left, right, top, bottom}; + } + return undefined; +} + +export default parseDomainPadding; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/parseStyles.ts b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/parseStyles.ts new file mode 100644 index 000000000000..54cc9430ed94 --- /dev/null +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/parseStyles.ts @@ -0,0 +1,36 @@ +import lodashIsObject from 'lodash/isObject'; +import type {ViewStyle} from 'react-native'; +import type {TNode} from 'react-native-render-html'; +import parseAttribute from './parseAttribute'; + +/** + * Extract width, height, and style overrides from a tnode's HTML attributes. + * Returns separate style objects for the node itself and its parent container. + */ +function parseStyles(tnode: TNode): {nodeStyles: ViewStyle; parentNodeStyles: ViewStyle} { + const nodeStyles: ViewStyle = {}; + const parentNodeStyles: ViewStyle = {}; + + const parsedHeight = parseAttribute(tnode.attributes.height); + if (typeof parsedHeight === 'number') { + nodeStyles.height = parsedHeight; + } + const parsedWidth = parseAttribute(tnode.attributes.width); + if (typeof parsedWidth === 'number') { + nodeStyles.width = parsedWidth; + } + + const parsedStyle = parseAttribute(tnode.attributes.style); + if (lodashIsObject(parsedStyle)) { + if ('parent' in parsedStyle && lodashIsObject(parsedStyle.parent)) { + Object.assign(parentNodeStyles, parsedStyle.parent); + } + if ('data' in parsedStyle && lodashIsObject(parsedStyle.data)) { + Object.assign(nodeStyles, parsedStyle.data); + } + } + + return {nodeStyles, parentNodeStyles}; +} + +export default parseStyles; From be6d67d55e78d315f332e3fa6a9395a00003a38b Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Fri, 22 May 2026 17:57:09 +0100 Subject: [PATCH 47/69] clean up VictoryChartCartesian and add prepare for VictoryChartPolar --- .../BaseVictoryChartRenderer.tsx | 15 ++------ .../VictoryChartRenderer/VictoryChart.ts | 2 ++ .../components/VictoryChartCartesian.tsx | 36 ++++++++++++------- .../components/VictoryChartPolar.tsx | 19 ++++++++++ .../context/VictoryChartContext.tsx | 6 +++- 5 files changed, 51 insertions(+), 27 deletions(-) create mode 100644 src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartPolar.tsx diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/BaseVictoryChartRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/BaseVictoryChartRenderer.tsx index efbe4abf966a..d51acf810ac1 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/BaseVictoryChartRenderer.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/BaseVictoryChartRenderer.tsx @@ -1,24 +1,13 @@ import React from 'react'; import type {VictoryChartRendererProps} from './types'; -import getYKey from './utils/getYKey'; import VictoryChart from './VictoryChart'; function BaseVictoryChartRenderer({tnode}: VictoryChartRendererProps) { return ( - - {(renderArgs) => ( - - {tnode.children.map((child) => ( - - ))} - - )} - + + ); diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/VictoryChart.ts b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/VictoryChart.ts index 92e7fdb50790..30e4d8027615 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/VictoryChart.ts +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/VictoryChart.ts @@ -4,6 +4,7 @@ import VictoryChartContainer from './components/VictoryChartContainer'; import VictoryChartLabels from './components/VictoryChartLabels'; import VictoryChartLegend from './components/VictoryChartLegend'; import VictoryChartLine from './components/VictoryChartLine'; +import VictoryChartPolar from './components/VictoryChartPolar'; import VictoryChartSeries from './components/VictoryChartSeries'; import {VictoryChartProvider, VictoryChartRenderArgsProvider} from './context/VictoryChartContext'; @@ -12,6 +13,7 @@ const VictoryChart = { RenderArgsProvider: VictoryChartRenderArgsProvider, Container: VictoryChartContainer, Cartesian: VictoryChartCartesian, + Polar: VictoryChartPolar, Series: VictoryChartSeries, Bar: VictoryChartBar, Line: VictoryChartLine, diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartCartesian.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartCartesian.tsx index d4d7ef6e210b..58ecbaaa9cec 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartCartesian.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartCartesian.tsx @@ -1,23 +1,24 @@ import React from 'react'; -import type {CartesianChartRenderArg} from 'victory-native'; import {CartesianChart} from 'victory-native'; -import {useVictoryChartContext} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartContext'; -import type {CartesianChartData, YKey} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/types'; +import {useVictoryChartContext, VictoryChartRenderArgsProvider} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartContext'; import parseAttribute from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/parseAttribute'; import parseDomainPadding from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/parseDomainPadding'; +import getYKey from '../utils/getYKey'; import VictoryChartLabels from './VictoryChartLabels'; import VictoryChartLegend from './VictoryChartLegend'; - -type VictoryChartCartesianProps = { - children: (renderArgs: CartesianChartRenderArg) => React.ReactNode; -}; +import VictoryChartSeries from './VictoryChartSeries'; /** * Renders the CartesianChart with data, axes, and domain config drawn from context. * Labels and legend overlays are handled internally via `renderOutside`. */ -function VictoryChartCartesian({children}: VictoryChartCartesianProps) { - const {data, xKey, yKeys, xAxis, yAxis, tnode} = useVictoryChartContext(); +function VictoryChartCartesian() { + const {data, xKey, yKeys, xAxis, yAxis, tnode, isValidCartesian} = useVictoryChartContext(); + + if (!isValidCartesian) { + return; + } + return ( ( - <> + renderOutside={(renderArgs) => ( + - + )} > - {children} + {(renderArgs) => ( + + {tnode.children.map((child) => ( + + ))} + + )} ); } diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartPolar.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartPolar.tsx new file mode 100644 index 000000000000..c639b761fe89 --- /dev/null +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartPolar.tsx @@ -0,0 +1,19 @@ +import {useVictoryChartContext} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartContext'; + +/** + * Renders the PolarChart with data drawn from context. + */ +function VictoryChartPolar() { + const {isValidPolar} = useVictoryChartContext(); + + if (!isValidPolar) { + return; + } + + // Support for polar chars will be added in a follow up https://github.com/Expensify/App/issues/90546 + return null; +} + +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 de8058c9fb01..2adb4e897277 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartContext.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartContext.tsx @@ -21,6 +21,7 @@ type VictoryChartContextValue = { regularTypeface: SkTypeface | null; boldTypeface: SkTypeface | null; isValidCartesian: boolean; + isValidPolar: boolean; }; const VictoryChartContext = createContext(null); @@ -35,8 +36,10 @@ function VictoryChartProvider({tnode, children}: {tnode: TNode; children: React. const {data, xKey, yKeys, xAxis, yAxis, labelItems, legendItems} = processVictoryChartTree(tnode, regularTypeface); const {nodeStyles, parentNodeStyles} = parseStyles(tnode); const isValidCartesian = Object.keys(data).length > 0; + const isValidPolar = false; - if (!isValidCartesian) { + // XNOR Check. There must one and only one valid chart + if (isValidCartesian === isValidPolar) { return null; } @@ -54,6 +57,7 @@ function VictoryChartProvider({tnode, children}: {tnode: TNode; children: React. regularTypeface, boldTypeface, isValidCartesian, + isValidPolar, }; return {children}; From b03c6156e4757ec78af659a91a1533d85f1c7031 Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Fri, 22 May 2026 18:22:16 +0100 Subject: [PATCH 48/69] remove regularTypeface and boldTypeface from VictoryChartContext and fix renderOutside children --- .../components/VictoryChartCartesian.tsx | 6 +++--- .../components/VictoryChartContainer.tsx | 2 +- .../components/VictoryChartLabels.tsx | 11 ++++++++--- .../components/VictoryChartLegend.tsx | 11 ++++++++--- .../context/VictoryChartContext.tsx | 16 ++++++---------- 5 files changed, 26 insertions(+), 20 deletions(-) diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartCartesian.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartCartesian.tsx index 58ecbaaa9cec..0ad2de6ba1c3 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartCartesian.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartCartesian.tsx @@ -13,7 +13,7 @@ import VictoryChartSeries from './VictoryChartSeries'; * Labels and legend overlays are handled internally via `renderOutside`. */ function VictoryChartCartesian() { - const {data, xKey, yKeys, xAxis, yAxis, tnode, isValidCartesian} = useVictoryChartContext(); + const {data, xKey, yKeys, xAxis, yAxis, tnode, labelItems, legendItems, isValidCartesian} = useVictoryChartContext(); if (!isValidCartesian) { return; @@ -31,8 +31,8 @@ function VictoryChartCartesian() { padding={parseAttribute(tnode.attributes.padding)} renderOutside={(renderArgs) => ( - - + + )} > diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartContainer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartContainer.tsx index 350f3ec6e60b..fd00dc668be4 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartContainer.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartContainer.tsx @@ -5,7 +5,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; function VictoryChartContainer({children}: {children: React.ReactNode}) { const styles = useThemeStyles(); - const {nodeStyles: chartContentStyles, parentNodeStyles: chartContainerStyles} = useVictoryChartContext(); + const {chartContentStyles, chartContainerStyles} = useVictoryChartContext(); return ( {children} diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartLabels.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartLabels.tsx index ec972cc76c14..225858ec0af8 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartLabels.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartLabels.tsx @@ -1,13 +1,18 @@ import {Skia, Text as SkText} from '@shopify/react-native-skia'; import React from 'react'; -import {useVictoryChartContext} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartContext'; +import {useChartDefaultTypeface} from '@components/Charts/hooks'; +import type {LabelItem} from '../types'; + +type VictoryChartLabelsProps = { + labelItems: LabelItem[]; +}; /** * Renders floating Skia text labels (from `` nodes) over the chart canvas. * Intended for use inside CartesianChart's `renderOutside` callback. */ -function VictoryChartLabels() { - const {labelItems, regularTypeface, boldTypeface} = useVictoryChartContext(); +function VictoryChartLabels({labelItems}: VictoryChartLabelsProps) { + const {regular: regularTypeface, bold: boldTypeface} = useChartDefaultTypeface(); return ( <> {labelItems.map(({x, y, text, color, fontSize, fontWeight}) => { diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartLegend.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartLegend.tsx index 7e7f194bbd78..191a36468f37 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartLegend.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartLegend.tsx @@ -1,13 +1,18 @@ import {Circle, Skia, Text as SkText} from '@shopify/react-native-skia'; import React, {Fragment} from 'react'; -import {useVictoryChartContext} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartContext'; +import {useChartDefaultTypeface} from '@components/Charts/hooks'; +import type {LegendItem} from '../types'; + +type VictoryChartLegendProps = { + legendItems: LegendItem[]; +}; /** * Renders Skia legend symbols and labels (from `` nodes) over the chart canvas. * Intended for use inside CartesianChart's `renderOutside` callback. */ -function VictoryChartLegend() { - const {legendItems, regularTypeface, boldTypeface} = useVictoryChartContext(); +function VictoryChartLegend({legendItems}: VictoryChartLegendProps) { + const {regular: regularTypeface, bold: boldTypeface} = useChartDefaultTypeface(); return ( <> {legendItems.map(({x: startX, y, entries, gutter, symbolSpacer}) => { diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartContext.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartContext.tsx index 2adb4e897277..2a027002bae9 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartContext.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartContext.tsx @@ -16,10 +16,8 @@ type VictoryChartContextValue = { yAxis: ProcessNodeResult['yAxis']; labelItems: ProcessNodeResult['labelItems']; legendItems: ProcessNodeResult['legendItems']; - nodeStyles: ReturnType['nodeStyles']; - parentNodeStyles: ReturnType['parentNodeStyles']; - regularTypeface: SkTypeface | null; - boldTypeface: SkTypeface | null; + chartContentStyles: ReturnType['nodeStyles']; + chartContainerStyles: ReturnType['parentNodeStyles']; isValidCartesian: boolean; isValidPolar: boolean; }; @@ -32,9 +30,9 @@ const VictoryChartRenderArgsContext = createContext 0; const isValidPolar = false; @@ -52,10 +50,8 @@ function VictoryChartProvider({tnode, children}: {tnode: TNode; children: React. yAxis, labelItems, legendItems, - nodeStyles, - parentNodeStyles, - regularTypeface, - boldTypeface, + chartContentStyles, + chartContainerStyles, isValidCartesian, isValidPolar, }; From a0d6f9173e6f3d5c251c25948e9d6fd4fa2e9913 Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Fri, 22 May 2026 18:28:01 +0100 Subject: [PATCH 49/69] spellcheck --- cspell.json | 1 + 1 file changed, 1 insertion(+) diff --git a/cspell.json b/cspell.json index 5ed2e087392a..6a92c4d068ee 100644 --- a/cspell.json +++ b/cspell.json @@ -894,6 +894,7 @@ "Xours", "Xtheirs", "XYWH", + "XNOR", "yalc", "Yapl", "YAPL", From a29a7703fbccbce55e6bbcdf3326327b71d66cef Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Fri, 22 May 2026 18:44:05 +0100 Subject: [PATCH 50/69] lint --- .../VictoryChartRenderer/components/VictoryChartCartesian.tsx | 2 +- .../VictoryChartRenderer/components/VictoryChartContainer.tsx | 4 ++-- .../VictoryChartRenderer/components/VictoryChartLabels.tsx | 2 +- .../VictoryChartRenderer/components/VictoryChartLegend.tsx | 2 +- .../VictoryChartRenderer/context/VictoryChartContext.tsx | 1 - 5 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartCartesian.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartCartesian.tsx index 0ad2de6ba1c3..bdbe24a50af2 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartCartesian.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartCartesian.tsx @@ -1,9 +1,9 @@ import React from 'react'; import {CartesianChart} from 'victory-native'; import {useVictoryChartContext, VictoryChartRenderArgsProvider} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartContext'; +import getYKey from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/getYKey'; import parseAttribute from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/parseAttribute'; import parseDomainPadding from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/parseDomainPadding'; -import getYKey from '../utils/getYKey'; import VictoryChartLabels from './VictoryChartLabels'; import VictoryChartLegend from './VictoryChartLegend'; import VictoryChartSeries from './VictoryChartSeries'; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartContainer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartContainer.tsx index fd00dc668be4..88979e163d65 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartContainer.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartContainer.tsx @@ -7,8 +7,8 @@ function VictoryChartContainer({children}: {children: React.ReactNode}) { const styles = useThemeStyles(); const {chartContentStyles, chartContainerStyles} = useVictoryChartContext(); return ( - - {children} + + {children} ); } diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartLabels.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartLabels.tsx index 225858ec0af8..6e4aaf831c0a 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartLabels.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartLabels.tsx @@ -1,7 +1,7 @@ import {Skia, Text as SkText} from '@shopify/react-native-skia'; import React from 'react'; import {useChartDefaultTypeface} from '@components/Charts/hooks'; -import type {LabelItem} from '../types'; +import type {LabelItem} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/types'; type VictoryChartLabelsProps = { labelItems: LabelItem[]; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartLegend.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartLegend.tsx index 191a36468f37..5ee16361b177 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 React, {Fragment} from 'react'; import {useChartDefaultTypeface} from '@components/Charts/hooks'; -import type {LegendItem} from '../types'; +import type {LegendItem} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/types'; type VictoryChartLegendProps = { legendItems: LegendItem[]; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartContext.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartContext.tsx index 2a027002bae9..9dad4feb6f5a 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartContext.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartContext.tsx @@ -1,4 +1,3 @@ -import type {SkTypeface} from '@shopify/react-native-skia'; import React, {createContext, useContext} from 'react'; import type {TNode} from 'react-native-render-html'; import type {CartesianChartRenderArg} from 'victory-native'; From 8304deb44592745295d2514b783f30a98e07cf45 Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Fri, 22 May 2026 19:42:19 +0100 Subject: [PATCH 51/69] revert unintended change --- .../VictoryChartRenderer/components/VictoryChartContainer.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartContainer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartContainer.tsx index 88979e163d65..fd00dc668be4 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartContainer.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartContainer.tsx @@ -7,8 +7,8 @@ function VictoryChartContainer({children}: {children: React.ReactNode}) { const styles = useThemeStyles(); const {chartContentStyles, chartContainerStyles} = useVictoryChartContext(); return ( - - {children} + + {children} ); } From 1ecda27a29eec84085f92ce6622b1cd6222a9110 Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Fri, 22 May 2026 20:11:46 +0100 Subject: [PATCH 52/69] log warn when rendering non supported series chart --- .../VictoryChartRenderer/components/VictoryChartSeries.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartSeries.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartSeries.tsx index 8768dfbfc33f..2720c6322a00 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartSeries.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartSeries.tsx @@ -1,5 +1,6 @@ import React from 'react'; import type {TNode} from 'react-native-render-html'; +import Log from '@libs/Log'; import VictoryChartBar from './VictoryChartBar'; import VictoryChartLine from './VictoryChartLine'; @@ -19,6 +20,7 @@ const SERIES_RENDERERS: Partial> = { function VictoryChartSeries({tnode}: VictoryChartSeriesProps) { const SeriesRenderer = SERIES_RENDERERS[tnode.tagName ?? '']; if (!SeriesRenderer) { + Log.warn('Unsupported series chart', {tagName: tnode.tagName}); return null; } return ; From fddafa4cab002667d490b593169faa20ad7e1a41 Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Fri, 22 May 2026 20:15:54 +0100 Subject: [PATCH 53/69] better log --- .../VictoryChartRenderer/components/VictoryChartSeries.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartSeries.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartSeries.tsx index 2720c6322a00..a383ac040c52 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartSeries.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartSeries.tsx @@ -20,7 +20,7 @@ const SERIES_RENDERERS: Partial> = { function VictoryChartSeries({tnode}: VictoryChartSeriesProps) { const SeriesRenderer = SERIES_RENDERERS[tnode.tagName ?? '']; if (!SeriesRenderer) { - Log.warn('Unsupported series chart', {tagName: tnode.tagName}); + Log.warn('Trying to render an unsupported series chart', {tagName: tnode.tagName}); return null; } return ; From b85d2e87fe35a42b45b45cc04d1dac2dc194f9f1 Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Fri, 22 May 2026 20:16:18 +0100 Subject: [PATCH 54/69] log warn when trying to render polar charts --- .../VictoryChartRenderer/components/VictoryChartPolar.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartPolar.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartPolar.tsx index c639b761fe89..e46b2f43957d 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartPolar.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartPolar.tsx @@ -1,4 +1,5 @@ import {useVictoryChartContext} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartContext'; +import Log from '@libs/Log'; /** * Renders the PolarChart with data drawn from context. @@ -11,6 +12,7 @@ function VictoryChartPolar() { } // Support for polar chars will be added in a follow up https://github.com/Expensify/App/issues/90546 + Log.warn('Trying to render unsupported polar charts'); return null; } From a21ac585380be33720bf541fb9d1cfb206eb9f40 Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Fri, 22 May 2026 20:42:12 +0100 Subject: [PATCH 55/69] add type to VictoryChartContext --- .../components/VictoryChartCartesian.tsx | 7 +++--- .../components/VictoryChartPolar.tsx | 7 +++--- .../VictoryChartRenderer/constants.ts | 7 +++++- .../context/VictoryChartContext.tsx | 25 +++++++++++++------ .../VictoryChartRenderer/types.ts | 6 ++++- 5 files changed, 36 insertions(+), 16 deletions(-) diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartCartesian.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartCartesian.tsx index bdbe24a50af2..d90cb769dc27 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 {CHART_TYPE} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/constants'; import {useVictoryChartContext, VictoryChartRenderArgsProvider} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartContext'; import getYKey from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/getYKey'; import parseAttribute from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/parseAttribute'; @@ -13,10 +14,10 @@ import VictoryChartSeries from './VictoryChartSeries'; * Labels and legend overlays are handled internally via `renderOutside`. */ function VictoryChartCartesian() { - const {data, xKey, yKeys, xAxis, yAxis, tnode, labelItems, legendItems, isValidCartesian} = useVictoryChartContext(); + const {data, xKey, yKeys, xAxis, yAxis, tnode, labelItems, legendItems, type} = useVictoryChartContext(); - if (!isValidCartesian) { - return; + if (type !== CHART_TYPE.CARTESIAN) { + return null; } return ( diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartPolar.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartPolar.tsx index e46b2f43957d..533cadbebf10 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartPolar.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartPolar.tsx @@ -1,3 +1,4 @@ +import {CHART_TYPE} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/constants'; import {useVictoryChartContext} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartContext'; import Log from '@libs/Log'; @@ -5,10 +6,10 @@ import Log from '@libs/Log'; * Renders the PolarChart with data drawn from context. */ function VictoryChartPolar() { - const {isValidPolar} = useVictoryChartContext(); + const {type} = useVictoryChartContext(); - if (!isValidPolar) { - return; + if (type !== CHART_TYPE.POLAR) { + return null; } // Support for polar chars will be added in a follow up https://github.com/Expensify/App/issues/90546 diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/constants.ts b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/constants.ts index 4424f7f1ff5e..67cd247e8c6f 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/constants.ts +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/constants.ts @@ -1,4 +1,9 @@ const X_KEY = 'x'; const Y_KEY_PREFIX = 'y'; -export {X_KEY, Y_KEY_PREFIX}; +const CHART_TYPE = { + CARTESIAN: 'cartesian', + POLAR: 'polar', +} as const; + +export {X_KEY, Y_KEY_PREFIX, CHART_TYPE}; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartContext.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartContext.tsx index 9dad4feb6f5a..405091303b84 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartContext.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartContext.tsx @@ -3,8 +3,9 @@ import type {TNode} from 'react-native-render-html'; import type {CartesianChartRenderArg} from 'victory-native'; import {useChartDefaultTypeface} from '@components/Charts/hooks'; import processVictoryChartTree from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/parsers/processVictoryChartTree'; -import type {CartesianChartData, ProcessNodeResult, YKey} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/types'; +import type {CartesianChartData, ChartType, ProcessNodeResult, YKey} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/types'; import parseStyles from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/parseStyles'; +import {CHART_TYPE} from '../constants'; type VictoryChartContextValue = { tnode: TNode; @@ -17,8 +18,7 @@ type VictoryChartContextValue = { legendItems: ProcessNodeResult['legendItems']; chartContentStyles: ReturnType['nodeStyles']; chartContainerStyles: ReturnType['parentNodeStyles']; - isValidCartesian: boolean; - isValidPolar: boolean; + type: ChartType; }; const VictoryChartContext = createContext(null); @@ -32,11 +32,21 @@ function VictoryChartProvider({tnode, children}: {tnode: TNode; children: React. const {regular: regularTypeface} = useChartDefaultTypeface(); const {data, xKey, yKeys, xAxis, yAxis, labelItems, legendItems} = processVictoryChartTree(tnode, regularTypeface); const {nodeStyles: chartContentStyles, parentNodeStyles: chartContainerStyles} = parseStyles(tnode); - const isValidCartesian = Object.keys(data).length > 0; - const isValidPolar = false; + + const hasCartesianData = Object.keys(data).length > 0; + const hasPolarData = false; + let type: ChartType = null; // XNOR Check. There must one and only one valid chart - if (isValidCartesian === isValidPolar) { + if (hasCartesianData === hasPolarData) { + type = null; + } else if (hasCartesianData) { + type = CHART_TYPE.CARTESIAN; + } else if (hasPolarData) { + type = CHART_TYPE.POLAR; + } + + if (!type) { return null; } @@ -51,8 +61,7 @@ function VictoryChartProvider({tnode, children}: {tnode: TNode; children: React. legendItems, chartContentStyles, chartContainerStyles, - isValidCartesian, - isValidPolar, + type, }; return {children}; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/types.ts b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/types.ts index 4f7258bad1fd..046284999db0 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/types.ts +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/types.ts @@ -1,8 +1,9 @@ import type {Color, SkTypeface} from '@shopify/react-native-skia'; import type {ComponentProps} from 'react'; import type {CustomRendererProps, TBlock, TNode} from 'react-native-render-html'; +import type {ValueOf} from 'type-fest'; import type {CartesianChart} from 'victory-native'; -import type {X_KEY, Y_KEY_PREFIX} from './constants'; +import type {CHART_TYPE, X_KEY, Y_KEY_PREFIX} from './constants'; type VictoryChartRendererProps = CustomRendererProps; @@ -136,6 +137,8 @@ type PartialProcessNodeResult = { type NodeParser = (tnode: TNode, typeface: SkTypeface | null) => PartialProcessNodeResult; +type ChartType = ValueOf | null; + export type { VictoryChartRendererProps, RawChartData, @@ -153,4 +156,5 @@ export type { ProcessNodeResult, PartialProcessNodeResult, NodeParser, + ChartType, }; From 94e83a099f4d30dfce839a6f9751f1316c12d1ac Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Fri, 22 May 2026 20:54:26 +0100 Subject: [PATCH 56/69] move null out of ChartType --- .../VictoryChartRenderer/context/VictoryChartContext.tsx | 4 ++-- .../HTMLRenderers/VictoryChartRenderer/types.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartContext.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartContext.tsx index 405091303b84..447f8fcb91bd 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartContext.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartContext.tsx @@ -18,7 +18,7 @@ type VictoryChartContextValue = { legendItems: ProcessNodeResult['legendItems']; chartContentStyles: ReturnType['nodeStyles']; chartContainerStyles: ReturnType['parentNodeStyles']; - type: ChartType; + type: ChartType | null; }; const VictoryChartContext = createContext(null); @@ -35,7 +35,7 @@ function VictoryChartProvider({tnode, children}: {tnode: TNode; children: React. const hasCartesianData = Object.keys(data).length > 0; const hasPolarData = false; - let type: ChartType = null; + let type: ChartType | null = null; // XNOR Check. There must one and only one valid chart if (hasCartesianData === hasPolarData) { diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/types.ts b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/types.ts index 046284999db0..eebc036acfac 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/types.ts +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/types.ts @@ -137,7 +137,7 @@ type PartialProcessNodeResult = { type NodeParser = (tnode: TNode, typeface: SkTypeface | null) => PartialProcessNodeResult; -type ChartType = ValueOf | null; +type ChartType = ValueOf; export type { VictoryChartRendererProps, From 38aee59ee7b58a46c70801eed84773af2780fa64 Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Fri, 22 May 2026 21:06:59 +0100 Subject: [PATCH 57/69] add VictoryChartContent --- .../BaseVictoryChartRenderer.tsx | 3 +-- .../VictoryChartRenderer/VictoryChart.ts | 2 ++ .../components/VictoryChartContent.tsx | 22 +++++++++++++++++++ 3 files changed, 25 insertions(+), 2 deletions(-) create mode 100644 src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartContent.tsx diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/BaseVictoryChartRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/BaseVictoryChartRenderer.tsx index d51acf810ac1..e6cbe6fb6461 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/BaseVictoryChartRenderer.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/BaseVictoryChartRenderer.tsx @@ -6,8 +6,7 @@ function BaseVictoryChartRenderer({tnode}: VictoryChartRendererProps) { return ( - - + ); diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/VictoryChart.ts b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/VictoryChart.ts index 30e4d8027615..94f7268e5165 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/VictoryChart.ts +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/VictoryChart.ts @@ -1,6 +1,7 @@ import VictoryChartBar from './components/VictoryChartBar'; import VictoryChartCartesian from './components/VictoryChartCartesian'; import VictoryChartContainer from './components/VictoryChartContainer'; +import VictoryChartContent from './components/VictoryChartContent'; import VictoryChartLabels from './components/VictoryChartLabels'; import VictoryChartLegend from './components/VictoryChartLegend'; import VictoryChartLine from './components/VictoryChartLine'; @@ -12,6 +13,7 @@ const VictoryChart = { Provider: VictoryChartProvider, RenderArgsProvider: VictoryChartRenderArgsProvider, Container: VictoryChartContainer, + Content: VictoryChartContent, Cartesian: VictoryChartCartesian, Polar: VictoryChartPolar, Series: VictoryChartSeries, diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartContent.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartContent.tsx new file mode 100644 index 000000000000..4ddba201698c --- /dev/null +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartContent.tsx @@ -0,0 +1,22 @@ +import React from 'react'; +import VictoryChartCartesian from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartCartesian'; +import VictoryChartPolar from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartPolar'; +import {CHART_TYPE} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/constants'; +import {useVictoryChartContext} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartContext'; + +function VictoryChartContent() { + const {type} = useVictoryChartContext(); + + switch (type) { + case CHART_TYPE.CARTESIAN: + return ; + case CHART_TYPE.POLAR: + return ; + } + + return null; +} + +VictoryChartContent.displayName = 'VictoryChartContent'; + +export default VictoryChartContent; From dba8852544fcf3bf4f6f3e3e7aa0447caba91383 Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Fri, 22 May 2026 21:18:48 +0100 Subject: [PATCH 58/69] move VictoryChartRenderArgsContext to its own file --- .../VictoryChartRenderer/VictoryChart.ts | 3 ++- .../components/VictoryChartBar.tsx | 2 +- .../components/VictoryChartCartesian.tsx | 3 ++- .../components/VictoryChartLine.tsx | 2 +- .../context/VictoryChartContext.tsx | 26 +++---------------- .../context/VictoryChartRenderArgsContext.tsx | 25 ++++++++++++++++++ 6 files changed, 34 insertions(+), 27 deletions(-) create mode 100644 src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartRenderArgsContext.tsx diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/VictoryChart.ts b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/VictoryChart.ts index 94f7268e5165..2d02281bfbf4 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/VictoryChart.ts +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/VictoryChart.ts @@ -7,7 +7,8 @@ import VictoryChartLegend from './components/VictoryChartLegend'; import VictoryChartLine from './components/VictoryChartLine'; import VictoryChartPolar from './components/VictoryChartPolar'; import VictoryChartSeries from './components/VictoryChartSeries'; -import {VictoryChartProvider, VictoryChartRenderArgsProvider} from './context/VictoryChartContext'; +import {VictoryChartProvider} from './context/VictoryChartContext'; +import {VictoryChartRenderArgsProvider} from './context/VictoryChartRenderArgsContext'; const VictoryChart = { Provider: VictoryChartProvider, diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartBar.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartBar.tsx index 415f2b60905e..f6afefd75ee3 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartBar.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartBar.tsx @@ -3,7 +3,7 @@ import type {TNode} from 'react-native-render-html'; import {Bar} from 'victory-native'; import {BAR_INNER_PADDING} from '@components/Charts/BarChart/BarChartContent'; import {DEFAULT_CHART_COLOR} from '@components/Charts/utils'; -import {useVictoryChartRenderArgs} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartContext'; +import {useVictoryChartRenderArgs} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartRenderArgsContext'; import getYKey from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/getYKey'; import parseAttribute from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/parseAttribute'; import parseCornerRadius from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/parseCornerRadius'; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartCartesian.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartCartesian.tsx index d90cb769dc27..7e426e33cbc6 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartCartesian.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartCartesian.tsx @@ -1,7 +1,8 @@ import React from 'react'; import {CartesianChart} from 'victory-native'; import {CHART_TYPE} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/constants'; -import {useVictoryChartContext, VictoryChartRenderArgsProvider} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartContext'; +import {useVictoryChartContext} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartContext'; +import {VictoryChartRenderArgsProvider} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartRenderArgsContext'; import getYKey from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/getYKey'; import parseAttribute from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/parseAttribute'; import parseDomainPadding from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/parseDomainPadding'; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartLine.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartLine.tsx index a846416089ba..50af0cf4db99 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartLine.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartLine.tsx @@ -2,7 +2,7 @@ import React from 'react'; import type {TNode} from 'react-native-render-html'; import {Line} from 'victory-native'; import {DEFAULT_CHART_COLOR} from '@components/Charts/utils'; -import {useVictoryChartRenderArgs} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartContext'; +import {useVictoryChartRenderArgs} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartRenderArgsContext'; import getYKey from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/getYKey'; import parseAttribute from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/parseAttribute'; import parseStyles from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/parseStyles'; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartContext.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartContext.tsx index 447f8fcb91bd..671b9900e1cb 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartContext.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartContext.tsx @@ -1,11 +1,10 @@ import React, {createContext, useContext} from 'react'; import type {TNode} from 'react-native-render-html'; -import type {CartesianChartRenderArg} from 'victory-native'; import {useChartDefaultTypeface} from '@components/Charts/hooks'; +import {CHART_TYPE} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/constants'; import processVictoryChartTree from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/parsers/processVictoryChartTree'; -import type {CartesianChartData, ChartType, ProcessNodeResult, YKey} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/types'; +import type {ChartType, ProcessNodeResult} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/types'; import parseStyles from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/parseStyles'; -import {CHART_TYPE} from '../constants'; type VictoryChartContextValue = { tnode: TNode; @@ -22,7 +21,6 @@ type VictoryChartContextValue = { }; const VictoryChartContext = createContext(null); -const VictoryChartRenderArgsContext = createContext | null>(null); /** * Parses the HTML tnode tree into chart config and makes it available to all chart sub-components. @@ -69,16 +67,6 @@ function VictoryChartProvider({tnode, children}: {tnode: TNode; children: React. VictoryChartProvider.displayName = 'VictoryChartProvider'; -/** - * Makes the CartesianChart render-prop arguments available to series sub-components - * (VictoryChartBar, VictoryChartLine) rendered inside the chart's children callback. - */ -function VictoryChartRenderArgsProvider({value, children}: {value: CartesianChartRenderArg; children: React.ReactNode}) { - return {children}; -} - -VictoryChartRenderArgsProvider.displayName = 'VictoryChartRenderArgsProvider'; - function useVictoryChartContext(): VictoryChartContextValue { const context = useContext(VictoryChartContext); if (!context) { @@ -87,13 +75,5 @@ function useVictoryChartContext(): VictoryChartContextValue { return context; } -function useVictoryChartRenderArgs(): CartesianChartRenderArg { - const context = useContext(VictoryChartRenderArgsContext); - if (!context) { - throw new Error('useVictoryChartRenderArgs must be used within VictoryChartRenderArgsProvider'); - } - return context; -} - -export {VictoryChartProvider, VictoryChartRenderArgsProvider, useVictoryChartContext, useVictoryChartRenderArgs}; +export {VictoryChartProvider, useVictoryChartContext}; export type {VictoryChartContextValue}; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartRenderArgsContext.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartRenderArgsContext.tsx new file mode 100644 index 000000000000..d5e4a06ecf30 --- /dev/null +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartRenderArgsContext.tsx @@ -0,0 +1,25 @@ +import React, {createContext, useContext} from 'react'; +import type {CartesianChartRenderArg} from 'victory-native'; +import type {CartesianChartData, YKey} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/types'; + +const VictoryChartRenderArgsContext = createContext | null>(null); + +/** + * Makes the CartesianChart render-prop arguments available to series sub-components + * (VictoryChartBar, VictoryChartLine) rendered inside the chart's children callback. + */ +function VictoryChartRenderArgsProvider({value, children}: {value: CartesianChartRenderArg; children: React.ReactNode}) { + return {children}; +} + +VictoryChartRenderArgsProvider.displayName = 'VictoryChartRenderArgsProvider'; + +function useVictoryChartRenderArgs(): CartesianChartRenderArg { + const context = useContext(VictoryChartRenderArgsContext); + if (!context) { + throw new Error('useVictoryChartRenderArgs must be used within VictoryChartRenderArgsProvider'); + } + return context; +} + +export {VictoryChartRenderArgsProvider, useVictoryChartRenderArgs}; From dd28402da8886f1c1af44459cb0bc978c361fc7a Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Fri, 22 May 2026 21:26:02 +0100 Subject: [PATCH 59/69] Use single VictoryChart import --- .../components/VictoryChartCartesian.tsx | 19 ++++++++----------- .../components/VictoryChartContent.tsx | 7 +++---- 2 files changed, 11 insertions(+), 15 deletions(-) diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartCartesian.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartCartesian.tsx index 7e426e33cbc6..7b4da0ae3688 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartCartesian.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartCartesian.tsx @@ -2,13 +2,10 @@ import React from 'react'; import {CartesianChart} from 'victory-native'; import {CHART_TYPE} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/constants'; import {useVictoryChartContext} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartContext'; -import {VictoryChartRenderArgsProvider} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartRenderArgsContext'; import getYKey from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/getYKey'; import parseAttribute from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/parseAttribute'; import parseDomainPadding from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/parseDomainPadding'; -import VictoryChartLabels from './VictoryChartLabels'; -import VictoryChartLegend from './VictoryChartLegend'; -import VictoryChartSeries from './VictoryChartSeries'; +import VictoryChart from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/VictoryChart'; /** * Renders the CartesianChart with data, axes, and domain config drawn from context. @@ -32,21 +29,21 @@ function VictoryChartCartesian() { domainPadding={parseDomainPadding(tnode.attributes.domainpadding)} padding={parseAttribute(tnode.attributes.padding)} renderOutside={(renderArgs) => ( - - - - + + + + )} > {(renderArgs) => ( - + {tnode.children.map((child) => ( - ))} - + )} ); diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartContent.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartContent.tsx index 4ddba201698c..3c86449e7e3a 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartContent.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartContent.tsx @@ -1,17 +1,16 @@ import React from 'react'; -import VictoryChartCartesian from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartCartesian'; -import VictoryChartPolar from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartPolar'; import {CHART_TYPE} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/constants'; import {useVictoryChartContext} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartContext'; +import VictoryChart from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/VictoryChart'; function VictoryChartContent() { const {type} = useVictoryChartContext(); switch (type) { case CHART_TYPE.CARTESIAN: - return ; + return ; case CHART_TYPE.POLAR: - return ; + return ; } return null; From 8ad11eae7e033965b1633b9ac40a5162558598a3 Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Fri, 22 May 2026 21:30:04 +0100 Subject: [PATCH 60/69] use Partial<> and avoid type re-def --- .../HTMLRenderers/VictoryChartRenderer/types.ts | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/types.ts b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/types.ts index eebc036acfac..7fb426a6ef1c 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/types.ts +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/types.ts @@ -126,14 +126,7 @@ type ProcessNodeResult = { }; /** Partial slice produced by a single per-tag parser before merging. */ -type PartialProcessNodeResult = { - data?: Record; - yKeys?: YKey[]; - xAxis?: CartesianChartProps['xAxis']; - yAxis?: NonNullable; - labelItems?: LabelItem[]; - legendItems?: LegendItem[]; -}; +type PartialProcessNodeResult = Partial; type NodeParser = (tnode: TNode, typeface: SkTypeface | null) => PartialProcessNodeResult; From 9f1c4722f3d487f0b64b2550e592f0a7e3f9e21f Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Fri, 22 May 2026 21:31:18 +0100 Subject: [PATCH 61/69] clean up --- .../VictoryChartRenderer/parsers/processVictoryChartTree.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/parsers/processVictoryChartTree.ts b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/parsers/processVictoryChartTree.ts index d1f52fbd880f..a5f1762d94e8 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/parsers/processVictoryChartTree.ts +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/parsers/processVictoryChartTree.ts @@ -42,9 +42,9 @@ function processVictoryChartTree(tnode: TNode, typeface: SkTypeface | null): Pro for (const child of tnode.children) { const childResult = processVictoryChartTree(child, typeface); + lodashMerge(data, childResult.data); yKeys.push(...childResult.yKeys); if (childResult.xAxis) { - // Safe to replace — there should be at most one xAxis per chart xAxis = childResult.xAxis; } if (childResult.yAxis?.length) { @@ -52,7 +52,6 @@ function processVictoryChartTree(tnode: TNode, typeface: SkTypeface | null): Pro } labelItems.push(...childResult.labelItems); legendItems.push(...childResult.legendItems); - lodashMerge(data, childResult.data); } return {data, xKey: X_KEY, yKeys, xAxis, yAxis, labelItems, legendItems}; From 1697d462d86167ef00e0407ff8be20be595b83c0 Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Fri, 22 May 2026 21:33:20 +0100 Subject: [PATCH 62/69] typo --- .../VictoryChartRenderer/context/VictoryChartContext.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartContext.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartContext.tsx index 671b9900e1cb..329e8f690e12 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartContext.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartContext.tsx @@ -35,7 +35,7 @@ function VictoryChartProvider({tnode, children}: {tnode: TNode; children: React. const hasPolarData = false; let type: ChartType | null = null; - // XNOR Check. There must one and only one valid chart + // XNOR Check. There must be one and only one valid chart if (hasCartesianData === hasPolarData) { type = null; } else if (hasCartesianData) { From 977e2eda566cb7c7354961e479e075afde8f92f2 Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Fri, 22 May 2026 21:33:58 +0100 Subject: [PATCH 63/69] alpha order --- cspell.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cspell.json b/cspell.json index 6a92c4d068ee..380b8311ea7b 100644 --- a/cspell.json +++ b/cspell.json @@ -891,10 +891,10 @@ "xlarge", "xlink", "xmlgateway", + "XNOR", "Xours", "Xtheirs", "XYWH", - "XNOR", "yalc", "Yapl", "YAPL", From ab3bd7a360a819b17a905b44668ceaa7f3c82ee4 Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Fri, 22 May 2026 21:36:08 +0100 Subject: [PATCH 64/69] sort words in cspall --- cspell.json | 842 ++++++++++++++++++++++++++-------------------------- 1 file changed, 421 insertions(+), 421 deletions(-) diff --git a/cspell.json b/cspell.json index 380b8311ea7b..78db58479e96 100644 --- a/cspell.json +++ b/cspell.json @@ -7,34 +7,419 @@ }, "words": [ "--longpress", + "ADDCOMMENT", + "ADFS", + "AMRO", + "APCA", + "APPL", + "ARGB", + "ARNK", + "ARROWDOWN", + "ARROWLEFT", + "ARROWRIGHT", + "ARROWUP", + "ASPAC", + "AUTOAPPROVE", + "AUTOREIMBURSED", + "AUTOREPORTING", + "AVURL", + "Accelo", + "Addendums", + "Aeroplan", + "Aircall", + "Airplus", + "Airwallex", + "Amal", + "Amal's", + "Amina", + "Areport", + "Authy", + "BBVA", + "BILLCOM", + "BMO", + "BNDCCAMM", + "BNDL", + "BOFMCAM2", + "BROWSABLE", + "BYOC", + "BambooHr", + "Bancorporation", + "Banque", + "Bartek", + "Batchinator", + "Belfius", + "Billpay", + "Bobbeth", + "Borderless", + "Botify", + "Broadwoven", + "Bronn", + "Buildscript", + "Bunq", + "Bushwick", + "CARDFROZEN", + "CARDUNFROZEN", + "CAROOT", + "CCDQCAMM", + "CFPB", + "CIBC", + "CIBCCATT", + "CLIA", + "CLIENTID", + "CPPFLAGS", + "CREDS", + "CROSSLINK", + "Caixa", + "Carta", + "Certinia", + "Certinia's", + "Charleson", + "Checkmark", + "Chronos", + "Cliqbook", + "Codat", + "Codice", + "Combustors", + "Corpay", + "Countertop", + "Crédit", + "DFOLLY", + "DSYM", + "DYNAMICEXTERNAL", + "Danske", + "Deel", + "Depósitos", + "Desjardins", + "Deutsch", + "Dishoom", + "DocuSign", + "Drycleaners", + "Drycleaning", + "Dtype", + "Dumpty", + "EDDSA", + "EDIFACT", + "EMEA", + "ENVFILE", + "ERECEIPT", + "ERECEIPTS", + "ESTA", + "EUVAT", + "EXPENSIDEV", + "EXPENSIFYAPI", + "EXPENSIFYPDBUSINESS", + "EXPENSIFYPDTEAM", + "EXPENSIFYWEB", + "Egencia", + "Electromedical", + "Electrotherapeutic", + "Emphemeral", + "Entra", + "Ephermeral", + "Erste", + "Español", + "Expatriot", + "Expensable", + "Expensi", + "Expensicon", + "Expensicons", + "Expensidev", + "Expensifier", + "Expensiworks", + "FLJZ", + "FWTV", + "FXHF", + "Fbclid", + "Ferroalloy", + "FinancialForce", + "Fiscale", + "Français", + "Frederico", + "Fábio", + "GBRRBR", + "GDSO", + "GEOLOCATION", + "Gaber", + "Gclid", + "Geral", + "Grantmaking", + "Gsuite", + "Générale", + "HKBCCATT", + "HRMS", + "HSBCSGS", + "Handelsbanken", + "Handtool", + "Heathrow", + "HiBob", + "Highfive", + "Highlightable", + "Hoverable", + "Humpty", + "Hydronics", + "IBTA", + "IDEIN", + "IHDR", + "INTECOMS", + "IPHONEOS", + "ITSM", + "Idology", + "Inactives", + "Inclusivity", + "Intacct", + "Invoicify", + "Italiano", + "Jakub", + "KHTML", + "Kantonalbank", + "Kearny", + "Kolkata", + "Kort", + "Kowalski", + "Krasoń", + "LDFLAGS", + "LHNGBR", + "LIBCPP", + "LIGHTBOXES", + "LLDB", + "Lagertha", + "Limpich", + "Lothbrok", + "Luhn", + "MARKASRESOLVED", + "MCTEST", + "MMYY", + "MVCP", + "MYOB", + "Maat", + "Mahal", + "Mapbox", + "Marcin", + "Marqeta", + "McAfee", + "Menlo", + "Microtransaction", + "Miniwarehouses", + "Mobasher", + "Monzo", + "Multifactor", + "NAICS", + "NBSA", + "NETSUITE", + "NEWDOT", + "NEWEXPENSIFY", + "NGROK", + "NLRA", + "NMLS", + "NOSCCATT", + "NSQS", + "NSQSOAuth", + "NSURL", + "NSUTF8", + "NTSB", + "Nacha", + "Namecheap", + "Nanobiotechnology", + "Navan", + "Nederlands", + "Nesw", + "Neue", + "Nonchocolate", + "Noncitrus", + "Nondepository", + "Nonfinancial", + "Nonmortgage", + "Nonnull", + "Nonstore", + "Nonupholstered", + "Noto", + "Novobanco", + "Nuevo", + "OCBC", + "OLDDOT", + "ONYXKEY", + "ONYXKEYS", + "Oncorp", + "PINATM", + "PINATM", + "PINGPONG", + "POLICYCHANGELOG_ADD_EMPLOYEE", + "POWERFORM", + "Parcelable", + "Passwordless", + "Payoneer", + "Pekao", + "Perfetto", + "Pettinella", + "Picklist", + "Playroll", + "Pleo", + "Pluginfile", + "Podfile", + "Pokdumss", + "Polska", + "Polski", + "Português", + "Postale", + "Postharvest", + "Postproduction", + "Powerform", + "Precheck", + "Pressable", + "Pressables", + "Proofpoint", + "Protip", + "Précédent", + "QAPR", + "QUICKBOOKS", + "Qonto", + "RAAS", + "RBC", + "RCTI18nUtil", + "RCTIs", + "RCTURL", + "REBOOKED", + "REDIRECTURI", + "REIMBURSER", + "REJECTEDTOSUBMITTER", + "REJECTEDTRANSACTION", + "REPORTPREVIEW", + "RNCORE", + "RNFS", + "RNLinksdk", + "RNTL", + "RNVP", + "ROYCCAT2", + "RPID", + "RRGGBB", + "RTER", + "Ragnar", + "Raiffeisen", + "Rankable", + "Reauthenticator", + "Rebooked", + "Reimb", + "Reimbursability", + "Reimbursables", + "Reimbursments", + "Renderable", + "Resawing", + "Reupholstery", + "Revolut", + "Roni", + "Rosiclair", + "SAASPASS", + "SBFJ", + "SCANREADY", + "SCIM", + "SMARTREPORT", + "SONIFICATION", + "SSAE", + "STORYLANE", + "SVFG", + "SVGID", + "Salagatan", + "Saqbd", + "Scaleway", + "Scaleway's", + "Schengen", + "Schiffli", + "Scotiabank", + "Segoe", + "Selec", + "Sepa", + "Sharees", + "Sharons", + "Signup", + "Skydo", + "Slurper", + "Smartscan", + "Société", + "Speedscope", + "Spendesk", + "Splittable", + "Spotnana", + "Strikethrough", + "Subprocessors", + "Subviews", + "Supercenters", + "Svenska", + "Svmy", + "Swedbank", + "Swipeable", + "Symbolicates", + "Synovus", + "TBUM", + "TDOMCATTTOR", + "TIMATIC", + "TOTP", + "TQBQW", + "Talkspace", + "Tele", + "Teleproduction", + "Timothée", + "Touchless", + "Trainline", + "Transpiles", + "Typeform", + "UATP", + "UBOI", + "UBOS", + "UDID", + "UDIDS", + "UIBG", + "UKEU", + "UNSWIPEABLE", + "UPWORK", + "USAA", + "USCA", + "USDVBBA", + "Unassigning", + "Uncapitalize", + "Undelete", + "Unlaminated", + "Unmigrated", + "Unsharing", + "Unvalidated", + "VBBA", + "VMPD", + "Valuska", + "Venmo", + "WDYR", + "Wallester", + "Warchoł", + "Wintrust", + "Woohoo", + "Wooo", + "Wordmark", + "XNOR", + "XYWH", + "Xfermode", + "Xours", + "Xtheirs", + "YAPL", + "YYMM", + "Yapl", + "Yema", + "Zenefit", + "Zenefits", + "Zipaligning", + "Zürcher", "abytes", - "Accelo", "accountid", "achreimburse", "actool", "adbd", - "ADDCOMMENT", - "Addendums", - "ADFS", "aeiou", - "Aeroplan", - "águero", - "Aircall", - "Airplus", "airshipconfig", "airside", "alrt", - "Amal", - "Amal's", "americanexpress", "americanexpressfdx", - "Amina", "androiddebugkey", "androidx", - "APCA", "apksigner", "apktool", - "APPL", "applauseauto", "applauseleads", "appleauth", @@ -45,125 +430,75 @@ "approvalstatus", "appversion", "archivado", - "Areport", - "ARGB", "armeabi", "armv7", - "ARNK", - "ARROWDOWN", - "ARROWLEFT", - "ARROWRIGHT", - "ARROWUP", "artículo", "artículos", "as_siteseach", "asar", - "ASPAC", "assetlinks", "attributes.accountid", "attributes.reportid", "authorised", - "Authy", - "AUTOAPPROVE", "autocompletions", + "autocorrection", "autodocs", "autofilled", "automations", "autoplay", - "AUTOREIMBURSED", "autoreleasepool", - "AUTOREPORTING", "autoresizesSubviews", "autoresizing", "autosync", "avds", - "AVURL", - "barwidth", + "backgrounded", "bamboohr", - "Bartek", + "barwidth", "basehead", "baselined", - "Batchinator", "behaviour", "bigdecimal", - "BILLCOM", "billpay", - "Billpay", "blahblahblah", "blakeembrey", "blankrows", - "BMO", - "BNDCCAMM", - "BNDL", - "Bobbeth", "bofa", - "BOFMCAM2", "bolditalic", "bootsplash", - "Borderless", - "Botify", "brex", "bridgeless", - "Broadwoven", - "Bronn", - "BROWSABLE", "buildscript", - "Buildscript", - "Bushwick", - "BYOC", "cacerts", "canvaskit", "capitalone", "cardreader", - "CAROOT", - "Carta", "ccache", - "CCDQCAMM", "ccupload", "cdfbmo", - "Certinia", - "Certinia's", - "CFPB", "changeit", "chargeback", - "Charleson", - "Checkmark", "checkmarked", "chien", - "Chronos", - "CIBC", - "CIBCCATT", "citi", "clawback", "cleartext", - "CLIA", - "CLIENTID", "clippath", - "Cliqbook", "cloudflarestream", "cmaps", "cmjs", "cocoapods", - "Codat", "codegen", "codeshare", "codesign", - "Codice", - "Combustors", "commentbubbles", "contenteditable", "copiloted", "copiloting", "copyable", "cornerradius", - "Corpay", - "Countertop", - "CPPFLAGS", "cpuprofile", "creditamount", "creditcards", - "CREDS", - "CROSSLINK", "crios", "csvg", "customairshipextender", @@ -176,71 +511,46 @@ "deburr", "deburred", "dedupe", - "Deel", - "dependentaxis", - "domainpadding", "deeplink", "deeplinked", "deeplinking", "deeplinks", "delegators", "delish", + "dependentaxis", "deployers", + "deprioritizes", "describedby", - "Desjardins", - "Deutsch", "devportal", - "DFOLLY", "diems", "dimen", "directfeeds", - "Dishoom", "displaystatus", - "DocuSign", + "domainpadding", "domelementtype", "domhandler", "domparser", "dont", "dotlottie", - "Drycleaners", - "Drycleaning", - "DSYM", "dsyms", - "Dtype", - "Dumpty", "durationMillis", - "DYNAMICEXTERNAL", "e2edelta", "ecash", "ecconnrefused", "econn", - "EDDSA", - "EDIFACT", - "Egencia", - "Electromedical", + "effectful", "electronmon", - "Electrotherapeutic", "ellipsize", - "EMEA", "emojibase", - "Emphemeral", "endcapture", "enddate", "endfor", "endgroup", "enroute", "entityid", - "Entra", - "ENVFILE", - "Ephermeral", "eraa", - "ERECEIPT", - "ERECEIPTS", - "Español", - "ESTA", "ethnicities", "eticket", - "EUVAT", "evenodd", "eventmachine", "evictable", @@ -248,42 +558,24 @@ "exchrate", "exfy", "exitstatus", - "Expatriot", - "Expensable", "expensescount", - "Expensi", - "Expensicon", - "Expensicons", "expensicorp", - "Expensidev", - "EXPENSIDEV", - "Expensifier", - "EXPENSIFYAPI", "expensifyhelp", "expensifylite", "expensifymono", "expensifyneue", "expensifynewkansas", - "EXPENSIFYPDBUSINESS", - "EXPENSIFYPDTEAM", "expensifyreactnative", - "EXPENSIFYWEB", - "Expensiworks", - "Fábio", "fabs", "falso", "favicons", - "Ferroalloy", - "FinancialForce", "feedcountry", "firebaselogging", "fireroom", "firstname", - "Fiscale", "flac", "flatlist", "flexsearch", - "FLJZ", "fname", "fnames", "focusability", @@ -291,97 +583,62 @@ "fontawesome", "foreignamount", "formatjs", - "Français", - "Frederico", "freetext", "frontpart", "fullstory", - "FWTV", - "FXHF", - "Gaber", "gastos", - "GBRRBR", "gcsc", "gcse", - "GDSO", "genkey", - "GEOLOCATION", "getenv", "getprop", "gibsdk", "glcode", - "gödecke", "googleusercontent", "gorhom", "gpgsign", "gradlew", - "Grantmaking", "groupmonth", "groupweek", "gscb", "gsib", "gsst", - "Gsuite", - "Handtool", + "gödecke", "hanno", "hanno_gödecke", "headshot", "healthcheck", - "Heathrow", "helpdot", "helpsite", "hexcode", "hibob", - "thumbsup", - "Highfive", - "Highlightable", - "HKBCCATT", - "Hoverable", "hrefs", "hris", - "HRMS", - "HSBCSGS", - "Humpty", "hybridapp", - "Hydronics", + "iOSQRCode", "iaco", - "IBTA", - "IDEIN", "idempotently", "idfa", - "Idology", "ifdef", - "IHDR", "imagebutton", - "Inactives", "inbetweenCompo", - "Inclusivity", "initialises", "inputmethod", "instancetype", - "Intacct", - "INTECOMS", "intenthandler", - "Invoicify", "ionatan", - "iOSQRCode", - "IPHONEOS", "iphonesimulator", "isemojisonly", "islarge", "ismedium", "isnonreimbursable", "issmall", - "Italiano", - "ITSM", - "Jakub", "jank", "janky", "jarsigner", "johndoe", - "jsbundle", "jsSrcsDir", - "Kearny", + "jsbundle", "keyalg", "keycap", "keycommand", @@ -391,69 +648,44 @@ "keytool", "keyval", "keyvaluepairs", - "KHTML", "killall", "kilometre", "kilometres", - "Kort", - "Kowalski", - "Krasoń", "labelledby", - "Lagertha", "laggy", "lastiPhoneLogin", "lastname", - "LDFLAGS", - "LHNGBR", - "LIBCPP", "libexec", "licence", - "LIGHTBOXES", "lightningcss", - "Limpich", "linecap", "linejoin", "lintable", + "lintrk", "listformat", "liveupdate", - "LLDB", "locationbias", "logcat", "logomark", - "Lothbrok", "lucene", - "Luhn", - "Maat", - "Mahal", "maildrop", "manualreimburse", - "Mapbox", "mapboxgl", "marcaaron", - "Marcin", "margelo", - "MARKASRESOLVED", - "Marqeta", "mateusz", - "McAfee", "mchmod", - "MCTEST", - "Menlo", "mechler", "mediumitalic", "memberof", "metainfo", "metatags", "microtime", - "Microtransaction", "microtransactions", "midoffice", "mimecast", - "Miniwarehouses", "miterlimit", "mkcert", - "MMYY", - "Mobasher", "mobiexpensifyg", "mobileprovision", "moveElemsAttrsToGroup", @@ -463,77 +695,40 @@ "msword", "mtrl", "multidex", - "Multifactor", - "MVCP", - "MYOB", "mysubdomain", - "Nacha", - "NAICS", - "Namecheap", - "Nanobiotechnology", - "Navan", "navattic", "navigations", - "NBSA", "nbta", "ndkversion", - "Nederlands", "negsign", "nesw", - "Nesw", "netinfo", "netrc", - "NETSUITE", - "Neue", "newarch", - "NEWDOT", "newdotreport", - "NEWEXPENSIFY", "newhelp", "ngneat", - "NGROK", - "NLRA", "nmanager", - "NMLS", "nocreeps", "nodownload", - "Nonchocolate", - "Noncitrus", - "Nondepository", - "Nonfinancial", - "Nonmortgage", - "Nonnull", "nonreimbursable", - "Nonstore", - "Nonupholstered", "noopener", "noprompt", "noreferer", "noreferrer", - "NOSCCATT", "nosymbol", - "Noto", - "NSQS", - "NSQSOAuth", - "NSURL", - "NSUTF8", "ntag", "ntdiary", - "NTSB", - "Nuevo", "nullptr", - "nums", "numberformat", + "nums", "objc", "objdump", "oblador", - "OCBC", "octocat", "officedocument", "oldarch", - "OLDDOT", "onclosetag", - "Oncorp", "oneline", "oneteam", "oneui", @@ -542,8 +737,6 @@ "onopentag", "onplayerror", "onxy", - "ONYXKEY", - "ONYXKEYS", "openxmlformats", "ordinality", "organisation", @@ -553,320 +746,184 @@ "otpauth", "outplant", "parasharrajat", - "Parcelable", "passcodes", "passplus", "passwordless", - "Passwordless", "pathspec", - "Payoneer", "payrollcode", "payrollid", "pbxproj", "pdfreport", "pdfs", - "Perfetto", "persistable", "peterparker", - "Pettinella", "pgrep", "phonenumber", - "Picklist", "picklists", - "PINATM", - "PINGPONG", - "PINATM", "pkill", - "Pluginfile", "pluralrules", "pnrs", - "Podfile", "podspec", "podspecs", - "Pokdumss", - "POLICYCHANGELOG_ADD_EMPLOYEE", - "Polski", "popen3", - "Português", "positionMillis", - "Postharvest", - "Postproduction", - "Powerform", - "POWERFORM", "preauthorization", - "Précédent", "precheck", - "Precheck", "prescribers", "presentationml", - "Pressable", - "Pressables", "prettierrc", "progname", "proguard", - "Proofpoint", - "Protip", "purchaseamount", "purchasecurrency", - "QAPR", "qrcode", - "QUICKBOOKS", - "RAAS", "rach", - "Ragnar", - "Rankable", - "RBC", - "RCTI18nUtil", - "RCTIs", - "RCTURL", "reactnative", "reactnativebackgroundtask", "reactnativehybridapp", "reactnativekeycommand", "reannounce", "reauthentication", - "Reauthenticator", - "Rebooked", - "REBOOKED", "rebooking", "recategorize", "recents", - "REDIRECTURI", + "recyclerview", "regexpu", "reimagination", - "Reimb", "reimbursability", - "Reimbursability", - "Reimbursables", "reimbursementid", - "REIMBURSER", "reimbursible", - "Reimbursments", - "REJECTEDTRANSACTION", - "REJECTEDTOSUBMITTER", "remotedesktop", "remotesync", "removeHiddenElems", - "Renderable", - "REPORTPREVIEW", "requestee", - "Resawing", "resizeable", "resultsbox", "retryable", - "Reupholstery", "rideshare", - "RNCORE", - "RNFS", - "RNLinksdk", "rnmapbox", - "RNTL", - "RNVP", "rock", - "Roni", - "Rosiclair", - "ROYCCAT2", "rpartition", - "RPID", - "RRGGBB", "rstrip", - "recyclerview", - "RTER", "s3uqn2oe4m85tufi6mqflbfbuajrm2i3", - "SAASPASS", "safesearch", - "Salagatan", "samltool", - "Saqbd", "sbaiahmed", - "SBFJ", - "Scaleway", - "Scaleway's", - "SCANREADY", "schedulable", - "Schengen", - "Schiffli", - "SCIM", - "Scotiabank", "scriptname", "sdkmanager", "seamless", - "Segoe", "seguiemj", - "Selec", - "Sepa", "serveo", "setuptools", "sharee", "shareeEmail", "sharees", - "Sharees", - "Sharons", "shellcheck", "shellenv", "shipit", "shouldshowellipsis", "signingkey", "signup", - "Signup", "simctl", "skia", "skip_codesigning", - "Skydo", - "Slurper", - "SMARTREPORT", - "Smartscan", "soloader", - "SONIFICATION", - "Speedscope", - "Splittable", - "Spotnana", "spreadsheetml", "srgb", - "SSAE", "stackoverflow", "startdate", "stdev", "stdlib", "storepass", - "STORYLANE", "strikethrough", - "Strikethrough", "subcomponents", "subfolders", "sublicensees", "sublocality", + "subpages", "subpremise", "subprocessors", - "Subprocessors", "subrates", "substep", "substeps", - "subpages", "subtab", "subtabs", "subview", - "Subviews", "superapp", - "Supercenters", "superpowered", "supportal", - "SVFG", - "SVGID", "svgs", - "Svmy", - "Swipeable", "symbolicate", "symbolicated", - "Symbolicates", "symbolication", "symbolspacer", "systempreferences", "tabindex", - "Talkspace", - "TBUM", - "TDOMCATTTOR", "teachersunite", - "Tele", - "Teleproduction", "testflight", "threadsafe", - "TIMATIC", - "Timothée", + "thumbsup", + "tickcount", + "tickformat", + "tickvalues", "tnode", "tobe", "togglefullscreen", "tosorted", - "TOTP", "touchables", - "Touchless", - "TQBQW", - "Trainline", "tranid", - "Transpiles", "trivago", "trustcacerts", "tsgo", "twocards", - "Typeform", - "tickcount", - "tickvalues", - "tickformat", "uatp", - "UATP", - "UBOI", - "UBOS", - "UDID", - "UDIDS", - "UIBG", "uimanager", - "UKEU", "ukkonen", "unapprove", "unapproves", "unassigning", - "Unassigning", "unassignment", "unassigns", - "Uncapitalize", "uncategorized", - "Undelete", "unflushed", "unheld", "unhold", "uninstallations", - "Unlaminated", - "Unmigrated", "unmuting", "unredacted", "unregisters", "unscrollable", "unsharing", - "Unsharing", "unsubmitted", - "UNSWIPEABLE", - "Unvalidated", - "UPWORK", "urbanairship", "urlset", - "USAA", - "USCA", - "USDVBBA", "useAutolayout", "useCallback", "useMemo", "usernotifications", "utilise", - "Valuska", - "VBBA", - "Venmo", - "victorychart", - "victorybar", - "victoryline", "victoryaxis", + "victorybar", + "victorychart", "victorylabel", "victorylegend", + "victoryline", "viewability", "viewport", "viewports", - "VMPD", "voidings", "vorbis", "vvcf", "vypj", "waitlist", "waitlisted", - "Warchoł", - "WDYR", "webapps", "webauthn", "webcredentials", "webrtc", "welldone", "widgetkit", - "Woohoo", - "Wooo", - "Wordmark", "wordprocessingml", "worklet", "workletization", @@ -887,76 +944,19 @@ "xcworkspace", "xdescribe", "xero", - "Xfermode", "xlarge", "xlink", "xmlgateway", - "XNOR", - "Xours", - "Xtheirs", - "XYWH", "yalc", - "Yapl", - "YAPL", - "Yema", "yourcompany", "yourname", - "YYMM", "zencdn", - "Zenefit", - "Zenefits", "zipalign", - "Zipaligning", "zoneinfo", "zxcv", "zxldvw", - "مثال", - "Airwallex", - "deprioritizes", - "AMRO", - "Bancorporation", - "Banque", - "BBVA", - "Belfius", - "Bunq", - "Caixa", - "Crédit", - "Danske", - "Depósitos", - "effectful", - "Erste", - "Générale", - "Geral", - "Handelsbanken", - "Kantonalbank", - "Monzo", - "Novobanco", - "Pekao", - "Playroll", - "Pleo", - "Polska", - "Postale", - "Qonto", - "Raiffeisen", - "Revolut", - "Société", - "Spendesk", - "Svenska", - "Swedbank", - "Synovus", - "Wallester", - "Wintrust", - "Zürcher", - "CARDFROZEN", - "CARDUNFROZEN", - "backgrounded", - "Kolkata", - "lintrk", - "Fbclid", - "Gclid", - "autocorrection", - "BambooHr", - "HiBob" + "águero", + "مثال" ], "ignorePaths": [ ".gitignore", From 2067d14f6d784380658b2b0cb3135a7ad09ed5bf Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Fri, 22 May 2026 21:39:04 +0100 Subject: [PATCH 65/69] Revert "Use single VictoryChart import" This reverts commit dd28402da8886f1c1af44459cb0bc978c361fc7a. --- .../components/VictoryChartCartesian.tsx | 19 +++++++++++-------- .../components/VictoryChartContent.tsx | 7 ++++--- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartCartesian.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartCartesian.tsx index 7b4da0ae3688..7e426e33cbc6 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartCartesian.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartCartesian.tsx @@ -2,10 +2,13 @@ import React from 'react'; import {CartesianChart} from 'victory-native'; import {CHART_TYPE} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/constants'; import {useVictoryChartContext} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartContext'; +import {VictoryChartRenderArgsProvider} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartRenderArgsContext'; import getYKey from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/getYKey'; import parseAttribute from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/parseAttribute'; import parseDomainPadding from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/parseDomainPadding'; -import VictoryChart from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/VictoryChart'; +import VictoryChartLabels from './VictoryChartLabels'; +import VictoryChartLegend from './VictoryChartLegend'; +import VictoryChartSeries from './VictoryChartSeries'; /** * Renders the CartesianChart with data, axes, and domain config drawn from context. @@ -29,21 +32,21 @@ function VictoryChartCartesian() { domainPadding={parseDomainPadding(tnode.attributes.domainpadding)} padding={parseAttribute(tnode.attributes.padding)} renderOutside={(renderArgs) => ( - - - - + + + + )} > {(renderArgs) => ( - + {tnode.children.map((child) => ( - ))} - + )} ); diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartContent.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartContent.tsx index 3c86449e7e3a..4ddba201698c 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartContent.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartContent.tsx @@ -1,16 +1,17 @@ import React from 'react'; +import VictoryChartCartesian from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartCartesian'; +import VictoryChartPolar from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartPolar'; import {CHART_TYPE} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/constants'; import {useVictoryChartContext} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartContext'; -import VictoryChart from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/VictoryChart'; function VictoryChartContent() { const {type} = useVictoryChartContext(); switch (type) { case CHART_TYPE.CARTESIAN: - return ; + return ; case CHART_TYPE.POLAR: - return ; + return ; } return null; From 3183d12118eee81bd9abf35503dd6198bde1a81b Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Fri, 22 May 2026 21:45:02 +0100 Subject: [PATCH 66/69] better imports --- .../components/VictoryChartCartesian.tsx | 6 +++--- .../VictoryChartRenderer/components/VictoryChartSeries.tsx | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartCartesian.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartCartesian.tsx index 7e426e33cbc6..032f3e03a9f0 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartCartesian.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartCartesian.tsx @@ -1,14 +1,14 @@ import React from 'react'; import {CartesianChart} from 'victory-native'; +import VictoryChartLabels from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartLabels'; +import VictoryChartLegend from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartLegend'; +import VictoryChartSeries from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartSeries'; import {CHART_TYPE} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/constants'; import {useVictoryChartContext} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartContext'; import {VictoryChartRenderArgsProvider} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartRenderArgsContext'; import getYKey from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/getYKey'; import parseAttribute from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/parseAttribute'; import parseDomainPadding from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/parseDomainPadding'; -import VictoryChartLabels from './VictoryChartLabels'; -import VictoryChartLegend from './VictoryChartLegend'; -import VictoryChartSeries from './VictoryChartSeries'; /** * Renders the CartesianChart with data, axes, and domain config drawn from context. diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartSeries.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartSeries.tsx index a383ac040c52..d6dc3152152e 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartSeries.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartSeries.tsx @@ -1,8 +1,8 @@ import React from 'react'; import type {TNode} from 'react-native-render-html'; +import VictoryChartBar from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartBar'; +import VictoryChartLine from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartLine'; import Log from '@libs/Log'; -import VictoryChartBar from './VictoryChartBar'; -import VictoryChartLine from './VictoryChartLine'; type VictoryChartSeriesProps = {tnode: TNode}; From 05b58b7b66f43320301b5323e72aef09c6a6d1fd Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Fri, 22 May 2026 21:51:22 +0100 Subject: [PATCH 67/69] remove VictoryChart.ts --- .../BaseVictoryChartRenderer.tsx | 14 +++++----- .../VictoryChartRenderer/VictoryChart.ts | 27 ------------------- 2 files changed, 8 insertions(+), 33 deletions(-) delete mode 100644 src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/VictoryChart.ts diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/BaseVictoryChartRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/BaseVictoryChartRenderer.tsx index e6cbe6fb6461..7f951f2c9b96 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/BaseVictoryChartRenderer.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/BaseVictoryChartRenderer.tsx @@ -1,14 +1,16 @@ import React from 'react'; +import VictoryChartContainer from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartContainer'; +import VictoryChartContent from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartContent'; +import {VictoryChartProvider} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartContext'; import type {VictoryChartRendererProps} from './types'; -import VictoryChart from './VictoryChart'; function BaseVictoryChartRenderer({tnode}: VictoryChartRendererProps) { return ( - - - - - + + + + + ); } diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/VictoryChart.ts b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/VictoryChart.ts deleted file mode 100644 index 2d02281bfbf4..000000000000 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/VictoryChart.ts +++ /dev/null @@ -1,27 +0,0 @@ -import VictoryChartBar from './components/VictoryChartBar'; -import VictoryChartCartesian from './components/VictoryChartCartesian'; -import VictoryChartContainer from './components/VictoryChartContainer'; -import VictoryChartContent from './components/VictoryChartContent'; -import VictoryChartLabels from './components/VictoryChartLabels'; -import VictoryChartLegend from './components/VictoryChartLegend'; -import VictoryChartLine from './components/VictoryChartLine'; -import VictoryChartPolar from './components/VictoryChartPolar'; -import VictoryChartSeries from './components/VictoryChartSeries'; -import {VictoryChartProvider} from './context/VictoryChartContext'; -import {VictoryChartRenderArgsProvider} from './context/VictoryChartRenderArgsContext'; - -const VictoryChart = { - Provider: VictoryChartProvider, - RenderArgsProvider: VictoryChartRenderArgsProvider, - Container: VictoryChartContainer, - Content: VictoryChartContent, - Cartesian: VictoryChartCartesian, - Polar: VictoryChartPolar, - Series: VictoryChartSeries, - Bar: VictoryChartBar, - Line: VictoryChartLine, - Labels: VictoryChartLabels, - Legend: VictoryChartLegend, -}; - -export default VictoryChart; From 5de25c2ac7376bdd76e0abc46c11cf54f8f5ea66 Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Fri, 22 May 2026 22:17:19 +0100 Subject: [PATCH 68/69] lint --- .../VictoryChartRenderer/BaseVictoryChartRenderer.tsx | 6 +++--- .../components/VictoryChartCartesian.tsx | 6 +++--- .../components/VictoryChartContent.tsx | 8 ++++---- .../components/VictoryChartSeries.tsx | 4 ++-- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/BaseVictoryChartRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/BaseVictoryChartRenderer.tsx index 7f951f2c9b96..da522351285b 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/BaseVictoryChartRenderer.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/BaseVictoryChartRenderer.tsx @@ -1,7 +1,7 @@ import React from 'react'; -import VictoryChartContainer from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartContainer'; -import VictoryChartContent from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartContent'; -import {VictoryChartProvider} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartContext'; +import VictoryChartContainer from './components/VictoryChartContainer'; +import VictoryChartContent from './components/VictoryChartContent'; +import {VictoryChartProvider} from './context/VictoryChartContext'; import type {VictoryChartRendererProps} from './types'; function BaseVictoryChartRenderer({tnode}: VictoryChartRendererProps) { diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartCartesian.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartCartesian.tsx index 032f3e03a9f0..7e426e33cbc6 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartCartesian.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartCartesian.tsx @@ -1,14 +1,14 @@ import React from 'react'; import {CartesianChart} from 'victory-native'; -import VictoryChartLabels from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartLabels'; -import VictoryChartLegend from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartLegend'; -import VictoryChartSeries from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartSeries'; import {CHART_TYPE} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/constants'; import {useVictoryChartContext} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartContext'; import {VictoryChartRenderArgsProvider} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartRenderArgsContext'; import getYKey from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/getYKey'; import parseAttribute from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/parseAttribute'; import parseDomainPadding from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/parseDomainPadding'; +import VictoryChartLabels from './VictoryChartLabels'; +import VictoryChartLegend from './VictoryChartLegend'; +import VictoryChartSeries from './VictoryChartSeries'; /** * Renders the CartesianChart with data, axes, and domain config drawn from context. diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartContent.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartContent.tsx index 4ddba201698c..0fda15f2c4d5 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartContent.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartContent.tsx @@ -1,8 +1,8 @@ import React from 'react'; -import VictoryChartCartesian from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartCartesian'; -import VictoryChartPolar from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartPolar'; import {CHART_TYPE} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/constants'; import {useVictoryChartContext} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartContext'; +import VictoryChartCartesian from './VictoryChartCartesian'; +import VictoryChartPolar from './VictoryChartPolar'; function VictoryChartContent() { const {type} = useVictoryChartContext(); @@ -12,9 +12,9 @@ function VictoryChartContent() { return ; case CHART_TYPE.POLAR: return ; + default: + return null; } - - return null; } VictoryChartContent.displayName = 'VictoryChartContent'; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartSeries.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartSeries.tsx index d6dc3152152e..a383ac040c52 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartSeries.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartSeries.tsx @@ -1,8 +1,8 @@ import React from 'react'; import type {TNode} from 'react-native-render-html'; -import VictoryChartBar from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartBar'; -import VictoryChartLine from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartLine'; import Log from '@libs/Log'; +import VictoryChartBar from './VictoryChartBar'; +import VictoryChartLine from './VictoryChartLine'; type VictoryChartSeriesProps = {tnode: TNode}; From c92d914ae70078ab7e2cbecc3551d7977c2f68c6 Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Fri, 22 May 2026 23:09:04 +0100 Subject: [PATCH 69/69] log once --- .../components/VictoryChartCartesian.tsx | 7 +------ .../components/VictoryChartContent.tsx | 10 +++++++++- .../components/VictoryChartPolar.tsx | 10 ++-------- .../components/VictoryChartSeries.tsx | 12 ++++++++++-- 4 files changed, 22 insertions(+), 17 deletions(-) diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartCartesian.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartCartesian.tsx index 7e426e33cbc6..753ffb2314f6 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 {CHART_TYPE} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/constants'; import {useVictoryChartContext} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartContext'; import {VictoryChartRenderArgsProvider} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartRenderArgsContext'; import getYKey from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/utils/getYKey'; @@ -15,11 +14,7 @@ import VictoryChartSeries from './VictoryChartSeries'; * Labels and legend overlays are handled internally via `renderOutside`. */ function VictoryChartCartesian() { - const {data, xKey, yKeys, xAxis, yAxis, tnode, labelItems, legendItems, type} = useVictoryChartContext(); - - if (type !== CHART_TYPE.CARTESIAN) { - return null; - } + const {data, xKey, yKeys, xAxis, yAxis, tnode, labelItems, legendItems} = useVictoryChartContext(); return ( { + if (type) { + return; + } + Log.warn('Trying to render an invalid chart (empty or mixed chart types).'); + }, [type]); + switch (type) { case CHART_TYPE.CARTESIAN: return ; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartPolar.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartPolar.tsx index 533cadbebf10..72a8edd94690 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartPolar.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartPolar.tsx @@ -1,19 +1,13 @@ -import {CHART_TYPE} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/constants'; -import {useVictoryChartContext} from '@components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/context/VictoryChartContext'; +import {useEffect} from 'react'; import Log from '@libs/Log'; /** * Renders the PolarChart with data drawn from context. */ function VictoryChartPolar() { - const {type} = useVictoryChartContext(); - - if (type !== CHART_TYPE.POLAR) { - return null; - } + useEffect(() => Log.warn('Trying to render unsupported polar charts'), []); // Support for polar chars will be added in a follow up https://github.com/Expensify/App/issues/90546 - Log.warn('Trying to render unsupported polar charts'); return null; } diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartSeries.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartSeries.tsx index a383ac040c52..dc4999a94a98 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartSeries.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/VictoryChartRenderer/components/VictoryChartSeries.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, {useEffect} from 'react'; import type {TNode} from 'react-native-render-html'; import Log from '@libs/Log'; import VictoryChartBar from './VictoryChartBar'; @@ -19,10 +19,18 @@ const SERIES_RENDERERS: Partial> = { function VictoryChartSeries({tnode}: VictoryChartSeriesProps) { const SeriesRenderer = SERIES_RENDERERS[tnode.tagName ?? '']; - if (!SeriesRenderer) { + + useEffect(() => { + if (SeriesRenderer) { + return; + } Log.warn('Trying to render an unsupported series chart', {tagName: tnode.tagName}); + }, [SeriesRenderer, tnode.tagName]); + + if (!SeriesRenderer) { return null; } + return ; }