Skip to content

Commit eac9533

Browse files
committed
readme, add queue for functions in case chart not created in time
1 parent 7d36d02 commit eac9533

File tree

2 files changed

+138
-78
lines changed

2 files changed

+138
-78
lines changed

README.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,13 @@ React native wrapper around [uPlot](https://github.com/leeoniya/uPlot). Works on
88
<img src="https://github.com/user-attachments/assets/f9e2e65c-bfe2-40a4-87ef-4d68faa11e77" height="400" />
99
</p>
1010

11-
**Note**: This library is a work in progress. It may not fully support all uPlot features yet, so feel free to open issues or pull requests to help improve it.
11+
### Caveats
12+
13+
1. The library is not a simple drop-in replacement for uPlot. It requires a decent amount of setup and understanding how this wrapper works.
14+
15+
2. Interactions like zooming and panning are not supported on iOS and Android. This is due to the limitations of the WebView component. I suggest using `react-native-gesture-handler`, `react-native-reanimated`, and `react-native-animateable-text` to roll your own interactions and legend display.
16+
17+
3. The library is still in active development, so things may change. I've not tested all possible features of uPlot. Feel free to open an issue / PR to improve it.
1218

1319
## Why?
1420

@@ -156,6 +162,8 @@ eas build --profile development --platform ios --local
156162

157163
2. The functions you pass to the `injectedJavaScript` can be tricky to debug, so be sure to test them in a web environment first.
158164

165+
3. Clear the ref you pass to `ChartUPlot` when the component unmounts.
166+
159167
## Contributing
160168

161169
If you would like to contribute, please open an issue or a pull request. Contributions are welcome!

src/components/ChartUPlot.tsx

Lines changed: 129 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,21 @@ const MARGIN_LEGEND = 50; // Height of the legend
2727
// they are also here since in some cases the <script>
2828
// tag in the HTML file is sometimes not executed in time
2929
const UTIL_FUNCTIONS = `
30+
/* queue helpers: available early via injectedJavaScriptBeforeContentLoaded */
31+
window.__uplot_queue__ = window.__uplot_queue__ || [];
32+
window.__uplot_enqueue__ = function(fn) {
33+
window.__uplot_queue__ = window.__uplot_queue__ || [];
34+
window.__uplot_queue__.push(fn);
35+
};
36+
window.__uplot_flush__ = function() {
37+
if (!window.__uplot_queue__) return;
38+
var q = window.__uplot_queue__;
39+
window.__uplot_queue__ = [];
40+
for (var i = 0; i < q.length; i++) {
41+
try { q[i](); } catch (e) { console.error('uplot queue fn error', e); }
42+
}
43+
};
44+
3045
function parseOptions(options) {
3146
var parsed = JSON.parse(options, (k, v) => {
3247
if (typeof v === 'string' && v.startsWith('__UPLOT_FUNC__')) {
@@ -201,28 +216,84 @@ function getCreateChartString(
201216
return `
202217
(function() {
203218
${chartCreatedCheck}
204-
window.__CHART_CREATED__ = true;
205219
206-
// inject custom functions if provided
220+
// ensure helper functions are available
207221
${injectedJavaScript}
208222
209223
document.body.style.backgroundColor='${bgColor}';
210224
211225
// stash your data on window if provided
212226
${dataAssignment}
213227
214-
// stash options on window
215-
window._opts = parseOptions('${stringify(options)}');
228+
// helper that actually builds the chart (keeps errors visible)
229+
function __createUPlotChart() {
230+
try {
231+
window._opts = parseOptions('${stringify(options)}');
216232
217-
if (window._chart) window._chart.destroy();
233+
if (window._chart) {
234+
try { window._chart.destroy(); } catch (e) {}
235+
}
218236
219-
// now actually construct uPlot
220-
window._chart = new uPlot(window._opts, window._data, document.getElementById('chart'));
237+
window._chart = new uPlot(window._opts, window._data, document.getElementById('chart'));
238+
239+
// mark created and flush queued commands
240+
window.__CHART_CREATED__ = true;
241+
if (window.__uplot_flush__) {
242+
try { window.__uplot_flush__(); } catch (e) { console.error('flush error', e); }
243+
}
244+
245+
console.log('uPlot chart created');
246+
} catch (err) {
247+
console.error('createUPlotChart error', err && err.message ? err.message : err);
248+
window.__CHART_CREATED__ = false;
249+
}
250+
}
251+
252+
// If uPlot is already loaded, create immediately; otherwise poll until available (timeout)
253+
if (typeof window.uPlot !== 'undefined') {
254+
__createUPlotChart();
255+
} else {
256+
var __waitCount = 0;
257+
var __waitMax = 20; // ~1s with 50ms interval
258+
var __iv = setInterval(function() {
259+
if (typeof window.uPlot !== 'undefined') {
260+
clearInterval(__iv);
261+
__createUPlotChart();
262+
return;
263+
}
264+
__waitCount++;
265+
if (__waitCount >= __waitMax) {
266+
clearInterval(__iv);
267+
console.error('uPlot not found after timeout; ensure uPlot script is included in the HTML');
268+
}
269+
}, 50);
270+
}
221271
})();
222272
true;
223273
`;
224274
}
225275

276+
// helper used on the RN side to wrap injected snippets so they either run now or enqueue
277+
const runWhenReady = (body: string): string => {
278+
return `
279+
(function(){
280+
function __run(){ ${body} }
281+
if (typeof window !== 'undefined' && window._chart) {
282+
__run();
283+
} else {
284+
// prefer enqueue helper if present
285+
if (window.__uplot_enqueue__) {
286+
window.__uplot_enqueue__(__run);
287+
} else {
288+
window.__uplot_queue__ = window.__uplot_queue__ || [];
289+
window.__uplot_queue__.push(__run);
290+
}
291+
}
292+
})();
293+
true;
294+
`;
295+
};
296+
226297
export interface UPlotProps {
227298
/** uPlot data array: [xValues[], series1[], series2[], ...] */
228299
data: [number[], ...number[][]];
@@ -396,25 +467,6 @@ const ChartUPlot = forwardRef<any, UPlotProps>(
396467
}
397468
}, [data]);
398469

399-
// var { optionsFinal, containerWidth, containerHeight } = useMemo(() => {
400-
401-
// const { containerWidth, containerHeight } = dimensionsRef.current.containerWidth
402-
// ? dimensionsRef.current
403-
// : { containerWidth: width, containerHeight: height };
404-
405-
// return getDimensions(options, style, containerWidth, containerHeight, margin);
406-
// }, [style, width, height]);
407-
408-
// var optsFinal = useMemo(() => {
409-
// return options
410-
// // return getDimensions(
411-
// // options,
412-
// // dimensionsRef.current.containerWidth,
413-
// // dimensionsRef.current.containerHeight,
414-
// // margin,
415-
// // );
416-
// }, [style, margin]);
417-
418470
useEffect(() => {
419471
// update uplot height and width if options change
420472

@@ -429,14 +481,14 @@ const ChartUPlot = forwardRef<any, UPlotProps>(
429481
return;
430482
}
431483

432-
webref.current.injectJavaScript(`
484+
const body = `
433485
if (window._chart) {
434486
window._chart.setSize(${JSON.stringify(dimensionsRef.current.containerWidth)}, ${JSON.stringify(dimensionsRef.current.containerHeight)});
435487
} else {
436488
console.error('useEffect - dim | Chart not initialized');
437489
}
438-
true;
439-
`);
490+
`;
491+
webref.current.injectJavaScript(runWhenReady(body));
440492
}
441493
}, [
442494
dimensionsRef.current.containerHeight,
@@ -475,9 +527,16 @@ const ChartUPlot = forwardRef<any, UPlotProps>(
475527

476528
// Ensure dataRef is the source of truth for the created chart in the WebView
477529
dataRef.current = toPlainArrays(chartData || []) as number[][];
478-
webref.current.injectJavaScript(
479-
getCreateChartString(dataRef.current, optsFinal, bgColor),
530+
const createChartStr = getCreateChartString(
531+
dataRef.current,
532+
optsFinal,
533+
bgColor,
534+
UTIL_FUNCTIONS,
480535
);
536+
537+
console.log('createChartStr', createChartStr);
538+
539+
webref.current.injectJavaScript(createChartStr);
481540
}
482541
initialized.current = true;
483542
},
@@ -491,8 +550,6 @@ const ChartUPlot = forwardRef<any, UPlotProps>(
491550
* @param {Object} newOptions - The new options to set for the chart.
492551
*/
493552
const updateOptions = useCallback((newOptions: any): void => {
494-
// call getDimensions to update optionsFinal
495-
496553
destroy(true); // keep data
497554
createChart(newOptions);
498555
}, []);
@@ -518,16 +575,15 @@ const ChartUPlot = forwardRef<any, UPlotProps>(
518575
return;
519576
}
520577

521-
// Mirror full data into window._data and set chart
522-
webref.current.injectJavaScript(`
578+
const body = `
523579
window._data = ${JSON.stringify(dataRef.current)};
524580
if (window._chart) {
525581
window._chart.setData(window._data);
526582
} else {
527583
console.error('setData | Chart not initialized');
528584
}
529-
true;
530-
`);
585+
`;
586+
webref.current.injectJavaScript(runWhenReady(body));
531587
}, []);
532588

533589
/**
@@ -553,22 +609,20 @@ const ChartUPlot = forwardRef<any, UPlotProps>(
553609

554610
// For native: inject only the new item and append to window._data in the WebView,
555611
// then call setData there. Avoid sending the entire dataRef.
556-
webref.current.injectJavaScript(`
557-
(function() {
558-
var item = ${JSON.stringify(item)};
559-
if (!window._data || window._data.length !== item.length) {
560-
window._data = [];
561-
for (var i = 0; i < item.length; i++) window._data.push([]);
562-
}
563-
for (var j = 0; j < item.length; j++) window._data[j].push(item[j]);
564-
if (window._chart) {
565-
window._chart.setData(window._data);
566-
} else {
567-
console.error('pushData | Chart not initialized');
568-
}
569-
})();
570-
true;
571-
`);
612+
const body = `
613+
var item = ${JSON.stringify(item)};
614+
if (!window._data || window._data.length !== item.length) {
615+
window._data = [];
616+
for (var i = 0; i < item.length; i++) window._data.push([]);
617+
}
618+
for (var j = 0; j < item.length; j++) window._data[j].push(item[j]);
619+
if (window._chart) {
620+
window._chart.setData(window._data);
621+
} else {
622+
console.error('pushData | Chart not initialized');
623+
}
624+
`;
625+
webref.current.injectJavaScript(runWhenReady(body));
572626
}, []);
573627

574628
/**
@@ -601,16 +655,15 @@ const ChartUPlot = forwardRef<any, UPlotProps>(
601655
}
602656

603657
// Mirror sliced data into window._data and update the chart in the WebView
604-
webref.current.injectJavaScript(`
658+
const body = `
605659
window._data = ${JSON.stringify(dataRef.current)};
606660
if (window._chart) {
607661
window._chart.setData(window._data);
608662
} else {
609663
console.error('sliceSeries | Chart not initialized or data not available');
610664
}
611-
true;
612-
`);
613-
return;
665+
`;
666+
webref.current.injectJavaScript(runWhenReady(body));
614667
},
615668
[],
616669
);
@@ -625,14 +678,14 @@ const ChartUPlot = forwardRef<any, UPlotProps>(
625678
return;
626679
}
627680

628-
webref.current.injectJavaScript(`
681+
const body = `
629682
if (window._chart) {
630-
window._chart.setScale(${JSON.stringify(axis)}, ${JSON.stringify(options)});true;
683+
window._chart.setScale(${JSON.stringify(axis)}, ${JSON.stringify(options)});
631684
} else {
632685
console.error('setScale | Chart not initialized');
633686
}
634-
true;
635-
`);
687+
`;
688+
webref.current.injectJavaScript(runWhenReady(body));
636689
}
637690
}, []);
638691

@@ -671,14 +724,14 @@ const ChartUPlot = forwardRef<any, UPlotProps>(
671724
return;
672725
}
673726

674-
webref.current.injectJavaScript(`
675-
if (!window._chart) {
676-
window._chart.setSize(${JSON.stringify(width)}, ${JSON.stringify(height)});true;
727+
const body = `
728+
if (window._chart) {
729+
window._chart.setSize(${JSON.stringify(width)}, ${JSON.stringify(height)});
677730
} else {
678731
console.error('setSize | Chart not initialized');
679732
}
680-
true;
681-
`);
733+
`;
734+
webref.current.injectJavaScript(runWhenReady(body));
682735
}
683736
}, []);
684737

@@ -697,15 +750,17 @@ const ChartUPlot = forwardRef<any, UPlotProps>(
697750

698751
var keepDataStr = keepData ? '' : `window._data = [];`;
699752

700-
webref.current.injectJavaScript(`
753+
const body = `
701754
${keepDataStr}
702-
703755
if (window._chart) {
704-
window._chart.destroy();
756+
try { window._chart.destroy(); } catch (e) {}
705757
}
706758
window.__CHART_CREATED__ = false;
707-
true;
708-
`);
759+
// clear queued ops to avoid stale calls after destroy
760+
window.__uplot_queue__ = [];
761+
`;
762+
// run immediately (no need to queue)
763+
webref.current.injectJavaScript(`${body} true;`);
709764
}
710765
initialized.current = false;
711766
}, []);
@@ -730,11 +785,6 @@ const ChartUPlot = forwardRef<any, UPlotProps>(
730785
`;
731786
}, [injectedJavaScript]);
732787

733-
console.log(
734-
'injectedJavaScriptWithFunctions',
735-
injectedJavaScriptWithFunctions,
736-
);
737-
738788
useImperativeHandle(ref, () => ({
739789
createChart,
740790
updateOptions,
@@ -768,7 +818,9 @@ const ChartUPlot = forwardRef<any, UPlotProps>(
768818
ref={setWebRef}
769819
onLayout={handleLayout}
770820
javaScriptEnabled={true}
771-
injectedJavaScript={injectedJavaScriptWithFunctions}
821+
injectedJavaScriptBeforeContentLoaded={
822+
injectedJavaScriptWithFunctions
823+
}
772824
onMessage={handleMessage}
773825
/>
774826
);

0 commit comments

Comments
 (0)