I’ve run into a long running stumper. Have a chart component in react native I want to render a series of chaerts from some api data. Heres fragment of the containing element, this is fine data is collected and rendered as you’d expect, until it’s feed into the chart.
<View className={'w-full'}>
{chartWidgets &&
chartWidgets.length > 0 &&
chartWidgets?.map((w: PageWidget<IQuickGraphWidget>, i) => {
return (
<View className={'w-full mb-4'} key={i}>
{w?.widget?.settings?.graph?.split(':')[1] && (
<ChartWidget
enabled={true}
widgetData={w}
key={i}
onSelectControl={() => {}}
pointData={{}}
graphId={w?.widget?.settings?.graph?.split(':')[1]}
pos={i}
/>
)}
</View>
);
})}
</View>
below is the chart component. In my test scenario I want to render 3 charts each charts has an array of on average 1000 – 2000 points to render. when I get over 2 charts the render of the 3rd or more charts is always broken.
/** Vendor */
import React, {useEffect, useRef, useState} from 'react';
import {
Pressable,
View,
Text,
ActivityIndicator,
TextInput,
} from 'react-native';
import {useQueries, useQuery} from '@tanstack/react-query';
import Modal from 'react-native-modal';
import {RadioGroup} from 'react-native-radio-buttons-group';
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome';
import {
faEllipsisVertical,
faLineChart,
} from '@fortawesome/free-solid-svg-icons';
import {useNavigation} from '@react-navigation/native';
import {StackNavigationProp} from '@react-navigation/stack';
/** Lib */
import {
someApiEndpoint
} from 'someApiEndpoint';
import {cardShadow} from '../../utils/nativeStyles';
/** State */
import {useStateContext} from '../../stateManager';
import {VictoryLineChart} from './VictoryLineChart';
import {VictoryBarChart} from './VictoryBarChart';
export const ChartWidget: React.FC<any> = ({
widgetData,
pointData,
handleOnSelectControl,
enabled = true,
graphId = '',
inView = false,
triggerModal = false,
pos = 0,
}) => {
/** Variables */
const navigation = useNavigation<StackNavigationProp<any>>();
const {appState} = useStateContext();
const defaultEndDate = new Date();
const defaultStartDate = new Date();
defaultStartDate.setDate(defaultEndDate.getDate() - 1);
const [startDate, setStartDate] = useState<Date | null>(null);
const [endDate, setEndDate] = useState(defaultEndDate);
const timeRangeOptions: any[] = [
{id: '1', label: '1 Hour', value: 1},
{id: '2', label: '12 Hours', value: 12},
{id: '3', label: '1 Day', value: 24},
{id: '4', label: '1 Week', value: 168},
{id: '5', label: '1 Month', value: 720},
{id: '6', label: '1 Year', value: 8760},
];
const [selectedTimeRangeOption, setSelectedTimeRangeOption] = useState('3');
const [scaleMinValue, setScaleMinValue] = useState('');
const [scaleMaxValue, setScaleMaxValue] = useState('');
const [showModal, setShowModal] = useState(false);
const [modalSettingType, setModalSettingType] = useState('');
const [initDateRange, setInitDateRange] = useState(false);
const [chartSettings, setChartSettings] = useState<any | null>(null);
const [chartData, setChartData] = useState<any | null>(null);
const [isVisible, setIsVisible] = useState(enabled);
const [shouldRefetch, setShouldRefetch] = useState(false);
const [isLoading, setIsLoading] = useState(true);
/** Utils */
const parseDate = (dt): string => {
const padL = (nr, len = 2, chr = '0') => `${nr}`.padStart(2, chr);
const returnString = `${padL(dt.getFullYear())}-${padL(
dt.getMonth() + 1,
)}-${dt.getDate()} ${padL(dt.getHours())}:${padL(dt.getMinutes())}:${padL(
dt.getSeconds(),
)}`;
return returnString.trim();
};
const getSelectedRangeLabel = () => {
const option = timeRangeOptions.find(i => i.id === selectedTimeRangeOption);
return option?.label || '';
};
const setDefaultStartDateRangeBySetting = defaultTimePeriod => {
const defaultStartfromSettings = new Date();
if (!startDate) {
if (defaultTimePeriod === 604800) {
setSelectedTimeRangeOption('4');
defaultStartfromSettings.setDate(defaultEndDate.getDate() - 7);
} else {
defaultStartfromSettings.setDate(defaultEndDate.getDate() - 1);
}
setStartDate(defaultStartfromSettings);
setInitDateRange(true);
return defaultStartfromSettings;
}
setInitDateRange(true);
return startDate;
};
const checkDisableForward = (): boolean => {
const now = new Date();
now.setMinutes(0);
now.setSeconds(0);
now.setMilliseconds(0);
return endDate > now;
};
const getSettings = async (gId: number) => {
setIsLoading(true);
const settings = await someApiEndpoint(
appState?.siteId,
gId,
);
setChartSettings(settings);
getData(graphId, settings);
};
const getData = async (gId, settings: any | null = null) => {
setIsLoading(true);
let queryStartDate: Date | null = startDate;
if (isNaN(parseInt(gId)) || !settings?.web_quick_graph) {
return null;
}
if (settings?.web_quick_graph && !queryStartDate) {
queryStartDate = setDefaultStartDateRangeBySetting(
settings.web_quick_graph?.settings?.default_time_period,
);
}
const res: any = await someApiEndpoint();
setChartData(res);
};
/** Hooks */
useEffect(() => {
if (graphId) {
getSettings(graphId);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [graphId]);
useEffect(() => {
if (initDateRange) {
getData(graphId, chartSettings);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [startDate, endDate]);
// refetch when a user preference is changed
useEffect(() => {
if (shouldRefetch) {
setShouldRefetch(false);
getSettings(graphId);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [shouldRefetch]);
useEffect(() => {
if (showModal) {
return;
}
if (chartSettings) {
const listSetting =
chartSettings?.web_quick_graph?.settings?.auto_display || false;
const visible = inView ? true : listSetting;
setIsVisible(visible);
if (!visible) {
return;
}
}
if (!!chartSettings && !!chartData) {
if (chartSettings?.web_quick_graph?.user_scale) {
setScaleMinValue(
chartSettings?.web_quick_graph?.user_scale?.minimum.toString(),
);
setScaleMaxValue(
chartSettings?.web_quick_graph?.user_scale?.maximum.toString(),
);
} else if (chartSettings?.web_quick_graph?.value_axes) {
setScaleMinValue(
chartSettings?.web_quick_graph?.value_axes[0].minimum.toString(),
);
setScaleMaxValue(
chartSettings?.web_quick_graph?.value_axes[0].maximum.toString(),
);
} else {
let largest = -Infinity;
let smallest = Infinity;
for (const item of chartData?.data) {
if (Array.isArray(item?.g) && item?.g?.length > 0) {
for (const value of item.g) {
if (value > largest) {
largest = value;
}
if (value < smallest) {
smallest = value;
}
}
}
}
setScaleMinValue(smallest.toString());
setScaleMaxValue(largest.toString());
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [chartSettings, chartData]);
/** Handlers */
const handleOnCancel = () => {};
consthandleOnOpenModal = () => {};
const handleOnSelectOption = val => {};
const handleOnSetScale = () => {};
const handleOnTimeline = () => {};
const handleOnBack = () => {};
const handleOnForward = () => {};
const handleOnNavigateToChart = () => {};
if (!isVisible || !chartSettings || !chartData) {
return <></>;
}
return (
<View
className="w-full flex flex-1 bg-white"
style={!inView ? cardShadow : {}}>
{!inView && (
<View className="flex flex-row justify-between w-full border-b border-ricado-gray mb-2 p-4">
<Text className="text-ricado-green text-sm text-left pt-1">
{widgetData?.widget?.settings?.title}
</Text>
<View className="flex flex-row">
<Pressable onPress={handleOnNavigateToChart} className="mt-2 mr-4">
<Text>
<FontAwesomeIcon icon={faLineChart} />
</Text>
</Pressable>
<Pressable onPress={handleOnOpenModal} className="mt-2">
<Text>
<FontAwesomeIcon icon={faEllipsisVertical} />
</Text>
</Pressable>
</View>
</View>
)}
<View className={'w-full relative'} style={{minHeight: 300}}>
{chartData && chartSettings?.web_quick_graph?.type === 'line' && (
<VictoryLineChart
chartData={chartData}
chartSettings={chartSettings}
/>
)}
{chartData && chartSettings?.web_quick_graph?.type === 'bar' && (
<VictoryBarChart
chartData={chartData}
chartSettings={chartSettings}
/>
)}
</View>
<View className="flex flex-row justify-between w-full border-y border-ricado-gray mt-2 p-4">
<Pressable onPress={handleOnBack}>
<Text className="text-black uppercase text-sm">
{'u25C0'} Back {getSelectedRangeLabel()}
</Text>
</Pressable>
<Pressable onPress={handleOnForward} disabled={checkDisableForward()}>
<Text
className={`${
checkDisableForward() ? 'text-gray-400' : 'text-black'
} text-sm uppercase`}>
Forward {getSelectedRangeLabel()} {'u25B6'}
</Text>
</Pressable>
</View>
</View>
);
};
so far I’ve tried: victory charts native, victory XL, amcharts5 in a webview, echarts, gifted chartds, multiple component and data fetching refactors (too many to list), useMemo, useRef, useCallback, Staggering renders, reducing data points, etc.
All have the same bug, data rendered fine when printed out there only and issue when rendering more than 2 charts.
Research tells me and issue with reactSVG but in the latest iteration I’ve swapped to victoryXL which replaces reactSVG with Skia and the problem remains. Have I just missing something simple?
up to 2 instances of the charts will render but more thn that consistently fails. Not allowed to screenshot the failing render but I mean it draws a couple of gridlines of the chat component is broken including the header and footer which are just simple reactNative elements.
Sometimes they all just bunch up with the mostly rendered charts stacked and the container element just missing.
Often rotating the screen can fix it on a re render. sometimes (but ofc it must rerender first time)
Remove the chart and the rest of the component and it’s data comes out as expected so convinced this is all chart related. no error message is produced in the console.