Tradingview charting library avoid rerendering when changing state (React) - reactjs

Task: create a custom replay mode using tradingview's charting library. Bars data does not come from a websocket but instead an uploaded/selected CSV.
I am trying to conditionally enable/disable subscribe bars using a state variable. When enabled it loops through the CSV and imitates a websocket to add bars every x seconds using a timeout, when paused, don't do anything.
The issue I am facing right now is, if I change the state, it re-renders the chart however I do not want to do that.
const TVChartContainer = (props) => {
const { allBars, replayBars, playing, speed } = props;
useEffect(() => {
const widgetOptions = {
debug: false,
symbol: defaultProps.symbol,
datafeed: Datafeed(allBars, replayBars, playing, speed),
interval: defaultProps.interval,
container_id: defaultProps.containerId,
library_path: defaultProps.libraryPath,
locale: getLanguageFromURL() || "en",
// disabled_features: ["use_localstorage_for_settings"],
enabled_features: ["study_templates"],
charts_storage_url: defaultProps.chartsStorageUrl,
charts_storage_api_version: defaultProps.chartsStorageApiVersion,
client_id: defaultProps.clientId,
user_id: defaultProps.userId,
fullscreen: defaultProps.fullscreen,
autosize: defaultProps.autosize,
studies_overrides: defaultProps.studiesOverrides,
data_status: "streaming",
overrides: {
"mainSeriesProperties.showCountdown": true,
"paneProperties.background": "#fff",
"paneProperties.vertGridProperties.color": "#fff",
"paneProperties.horzGridProperties.color": "#fff",
"scalesProperties.textColor": "#000",
"mainSeriesProperties.candleStyle.wickUpColor": "#2196f3",
"mainSeriesProperties.candleStyle.upColor": "#2196f3",
"mainSeriesProperties.candleStyle.borderUpColor": "#2196f3",
"mainSeriesProperties.candleStyle.wickDownColor": "#000",
"mainSeriesProperties.candleStyle.downColor": "#000",
"mainSeriesProperties.candleStyle.borderDownColor": "#000",
},
};
new widget(widgetOptions);
}, [allBars, playing, replayBars, speed]);
return <div id={defaultProps.containerId} className={"TVChartContainer"} />;
};
The playing variable is a state coming from its parent component, when changed it rerenders the chart as it initializes a new widget. Is there a workaround that? Maybe change the datafeed after it has been initialized?
Note: I do not intend to have a backend for this React app.

Related

Is it bad practice to access HTML elements by ID in React TypeScript?

I was told at a previous job that I should never access HTML elements directly through means like getElementById in React TypeScript. I'm currently implementing Chart.js. For setting up the chart, I was initially using a useRef hook instead of accessing context, but now it seems like I need to grab the canvas by ID in order to instantiate it properly. I want to know if this is kosher.
I suspect something is wrong with me not using a context, because my chart data doesn't load and throws a console error: "Failed to create chart: can't acquire context from the given item"
useEffect(() => {
chart = new Chart(chartRef.current, {
type: "bar",
data: {
labels: labelsArray.map((label) => {
const date = new Date(label);
// add one because month is 0-indexed
return date.getUTCMonth() + 1 + "/" + date.getUTCDate();
}),
datasets: [
{
data: valuesArray,
backgroundColor: "#1565C0",
borderRadius: 6,
},
],
},
options: {
interaction: { mode: "index" },
onHover: (event, chartElement) => {
const target = event.native.target;
(target as HTMLInputElement).style.cursor = chartElement[0]
? "pointer"
: "default";
},
plugins: {
tooltip: {
mode: "index",
enabled: true,
},
title: {
display: true,
text: "Daily Usage Past 30 Days",
align: "start",
fullSize: true,
font: {
size: 24,
},
padding: {
bottom: 36,
},
},
},
scales: {
x: {
display: false,
},
},
elements: {
line: {
borderJoinStyle: "round",
},
},
},
});
return () => {
chart.destroy();
};
}, [labelsArray, valuesArray]);
and HTML:
<div className="mt-80 ml-12 p-8 shadow-lg border-2 border-slate-100 rounded-xl items-center">
<canvas id="chart" ref={chartRef}></canvas>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
</div>
Also, per the Chart.js documentation: "To create a chart, we need to instantiate the Chart class. To do this, we need to pass in the node, jQuery instance, or 2d context of the canvas of where we want to draw the chart." Not sure how we would do this with a useRef
Yes,
It is not good practice to access dom elements directly through document API.
Because in react
virtual dom is responsible for painting/ re-rendering the UI.
State updation is the proper way to tell react to trigger re-render.
The flow is state updation -> calculate differences -> find who over is using that state -> grab those components -> re-render only those components.
virtual dom is the source of truth for react to render and update actual DOM.
Now, If you directly access some dom elements and do some operation on it, like updating, react will never know that some change has happened and it will basically break the flow of the react, in which case there will be no reason to use react.js
The flow would be accessing some dom element -> updating -> displaying.
The problem with this approach if react encounters that later what i have in virtual dom is not actual presentation in the actual dom, which will create mess.
That is the reason there is useRef hook to manipulate dom.

Re-rendering a component with useSpring resets animation chain and over time makes other animations staggery

I have this spring defining a looped animation:
const style = useSpring({
from: { marginBottom: 0 },
to: [
{ marginBottom: 0 },
{ marginBottom: 80, config: { duration: 600 }, delay: 3000 },
{ marginBottom: -80, config: { duration: 0 } },
{ marginBottom: 0, config: { duration: 600, delay: 1000 } },
],
loop: true,
})
My problem is that whenever I re-render the component containing the spring, the animation 'resets' and starts moving towards the first to position (in this case { martinBottom: 0 })
It can be seen in this sandbox - click the 'reset' button just as the arrow is traveling up and it will reverse course instantly
The documentation also hints that stabilizing the references of from and to may solve the issue... and it kind of does, but it introduces a new one: if the components re-renders shortly before the animation starts, it skips the 'travel up' portion: sandbox here.
The bigger problem is that the more times I 'interrupt' the animation cycle by re-rendering, the more it affects some of my other, more demanding animations, making them more and more staggered.
Refreshing whole app instantly resets this decay and makes all animations fluid again. When I remove this useSpring from my code, this decay disappears
So what is going on there? Is there something I'm missing?
From this docs example, It makes sense to me that the animation resets on re-render ... but then why does this example instruct me to compose staged animations this way, am I missing some key step?
And what's up with this slow 'decay' of other animations' performance, is that a known cause to that?

Duplicate image shown - useEffect issue?

I have some code to display my board as shown below:
<Grid item xs={6} justify="center">
<Paper id="board">
</Paper>
</Grid>
But sometimes when I save my code, it displays a duplicate board on the screen on localhost:3000, although not always. Example below:
I am suspecting it it might have something to do with my useEffect:
useEffect(() => {
// componentDidMount() {
const board = new Gameboard(document.getElementById('board'), {
// position: 'r4r1k/p1P1qppp/1p6/8/5B2/2Q1P3/P4PPP/R3KB1R b KQ - 0 21',
position: game.fen(),
sprite: {
url: './gameboard-sprite.svg', // pieces and markers are stored as svg in the sprite
grid: 40, // the sprite is tiled with one piece every 40px
cache: true,
},
orientation: COLOR.white,
moveInputMode: MOVE_INPUT_MODE.dragPiece,
responsive: true,
});
board.enableMoveInput(inputHandler);
}, []);
Any idea why it could be displaying twice, but only sometimes? It always removes the duplicate board when I click the browser refresh button if that helps. But it is often there when I first save my code.
Save your Gameboard instance in useRef, this way only one instance is created:
const board = useRef();
useEffect(() => {
if(!board.current) {
board.current = new Gameboard(document.getElementById('board'), {
// position: 'r4r1k/p1P1qppp/1p6/8/5B2/2Q1P3/P4PPP/R3KB1R b KQ - 0 21',
position: game.fen(),
sprite: {
url: './gameboard-sprite.svg', // pieces and markers are stored as svg in the sprite
grid: 40, // the sprite is tiled with one piece every 40px
cache: true,
},
orientation: COLOR.white,
moveInputMode: MOVE_INPUT_MODE.dragPiece,
responsive: true,
});
board.current.enableMoveInput(inputHandler);
}
}, []);
You need to return cleanup function from effect, so when fast refresh will rerun it, there will be no duplicates. It works properly sometimes because sometimes fast refresh sometimes don't try to preserve state and renders everything as new. This depends on changes you made, so when you change one file it will render clean, but on another files it will try to preserve state and only rerun effects (this is where second board added).
Also, it is better not to interact with DOM directly and use ref instead. This is better way to work with DOM in react, since it's react main purpose to update DOM.

How to animate a google material bar chart

I'm drawing a react google bar chart (the material one) in a react project and I'm trying to make an animation. I've read that this kind of chart doesn't support animation but I need to do it, there has to be any way to do it. It's hard for me to think that a newer thing is worse than the old one. Anyone knows how can I do it? I've tried many different ways but nothing worked. This is my code:
import React from 'react';
import './App.css';
import Chart from 'react-google-charts'
function App() {
return (
<div className="App">
<Chart
width={'500px'}
height={'300px'}
// Note here we use Bar instead of BarChart to load the material design version
chartType="Bar"
loader={<div>Loading Chart</div>}
data={[
['City', '2010 Population', '2000 Population'],
['New York City, NY', 8175000, 8008000],
['Los Angeles, CA', 3792000, 3694000],
['Chicago, IL', 2695000, 2896000],
['Houston, TX', 2099000, 1953000],
['Philadelphia, PA', 1526000, 1517000],
]}
options={{
// Material chart options
chart: {
title: 'Population of Largest U.S. Cities',
subtitle: 'Based on most recent and previous census data',
},
hAxis: {
title: 'Total Population',
minValue: 0,
},
animation: {
duration: 1000,
easing: 'out',
startup: true,
},
vAxis: {
title: 'City',
},
bars: 'horizontal',
axes: {
y: {
0: { side: 'right' },
},
},
}}
/>
</div>
);
}
export default App;
Demo using react | Demo Using vanilla javascript
Animation is not supported on google material charts.
If you want to add animation to material google charts, you can do it manually with css animations. let's do it (Demo):
First we should get a selector for actual bars. it seems the third svg group (g tag) is the actual bars in chart (other groups are for labels / titles / etc.):
.animated-chart g:nth-of-type(3) {...}
Then we should add a css transition to it:
.animated-chart g:nth-of-type(3) {
transition: 1s;
}
Then we can create a class (.animated-chart-start) for toggling between transform: scaleX(1); and transform: scaleX(0); , like this:
.animated-chart g:nth-of-type(3) {
transition: 1s;
transform: scaleX(1);
}
.animated-chart.animated-chart-start g:nth-of-type(3) {
transform: scaleX(0);
}
So far we added css, and now we should add these classes to our chart and also toggle the .animated-chart-start class after a short delay. we can do it on componentDidMount, but it's more clean to do it on chart ready:
<Chart
...
className={`animated-chart animated-chart-start`}
chartEvents={[
{
eventName: "ready",
callback: ({ chartWrapper, google }) => {
const chartEl = chartWrapper.getChart().container;
setTimeout(() => {
chartEl.classList.remove('animated-chart-start')
}, 100)
},
}
]}
/>
It adds the .animated-chart-start class to the chart, and it removes it after 100 ms. (100ms is optional, you can toggle it instantly also) .
Also note that google charts doesn't seem to support binding a data to the className (like className={this.state.dynamicClass}), that's why we can't use a state variable for toggling the animation class.
At the end, we can wrap this animated chart to a separate component like AnimatedChart to make it more reusable. (you can see it on stackblitz code).
Run it live
Known Limitations:
Setting the state during the chart animation will cause a re-render and it ruins the css transition.
We supposed that the third svg group is the chart. but it may vary based on the chart type or even chart properties.
Update: for vertical charts, you can use scaleY for animation, and also you may want to set transform origin like: transform-origin: 0 calc(100% - 50px); to make it look better. (Run vertical version On Stackblitz)
Update 2: For vanilla javascript version (without any framework), see here.
You can try to simulate animation just by swap the chart data after some short amount of time. Here is my proposition in 3 steps.
Initially load chart with chart values as "0".
Then load the partial values of data.
In the end set the real data values.
function ChartBox() {
let initialData = [
['City', '2010 Population', '2000 Population'],
['New York City, NY', 0, 0],
['Los Angeles, CA', 0, 0],
['Chicago, IL', 0, 0],
['Houston, TX', 0, 0],
['Philadelphia, PA', 0, 0],
];
let n = 250; // divider
let dataLoading = [
['City', '2010 Population', '2000 Population'],
['New York City, NY', 8175000/n, 8008000/n],
['Los Angeles, CA', 3792000/n, 3694000/n],
['Chicago, IL', 2695000/n, 2896000/n],
['Houston, TX', 2099000/n, 1953000/n],
['Philadelphia, PA', 1526000/n, 1517000/n],
];
let finalData = [
['City', '2010 Population', '2000 Population'],
['New York City, NY', 8175000, 8008000],
['Los Angeles, CA', 3792000, 3694000],
['Chicago, IL', 2695000, 2896000],
['Houston, TX', 2099000, 1953000],
['Philadelphia, PA', 1526000, 1517000],
];
const [chartData, setChartData] = useState(initialData);
useEffect(() => {
const timer = setTimeout(() => {
setChartData(dataLoading)
}, 100);
const timer2 = setTimeout(() => {
setChartData(finalData)
}, 300);
return () => {clearTimeout(timer); clearTimeout(timer2)}
}, []);
return (
<div className="App">
<Chart
{...}
data={chartData}
{...}
Using the State Hook along with useEffect help to manipulate data which we want to present. In <Chart/> component I pass the chartData, which value will change after 100ms and 300ms. Of course you can add more steps with fraction of values (like dataLoading), so your "animation" will look more smoothly.
Just updated the code and tried to re-implement it in a better way but Can't find a better solution to it.
You need to paly along with CSS a bit
For Y-axis animation
g:nth-of-type(3) transition: 2s; transform: scaleX(1);
OR
For X-axis animation
g:nth-of-type(3) transform: scaleX(0);
https://codesandbox.io/s/google-react-chart-do602?file=/src/styles.css

How to animate dynamic updates in CanvasJS using React functional components?

Using React functional components, I have not been able to find a way to animate my chart with dynamic data received asynchronously. The sandbox below illustrates the problem with a timer simulating the asynchronous read.
https://codesandbox.io/s/basic-column-chart-in-react-canvasjs-0gfv6?fontsize=14&hidenavigation=1&theme=dark
When running the example code, you should see 5 vertical bars of increasing heights animate. Then, after 5 seconds, it switches immediately to 4 bars of descending heights. I am looking to have that update animate.
Here is some reference information I've reviewed:
CanvasJS React Demos: many of which animate on initial draw, but I couldn't find one that animates with dynamic data loaded after the initial render.
Chart using JSON Data is an demo that has dynamic data, but doesn't animate.
Reviewing the CanvasJS forum, I found a couple links, but none that address React functional components
Vishwas from Team Canvas said:
To update dataPoints dynamically and to animate chart, you can instantiate the chart, update dataPoints via chart-options and then call chart.render as shown in this updated JSFiddle.
var chart = new CanvasJS.Chart("chartContainer", {
title: {
text: "Animation test"
},
animationEnabled: true,
data: [{
type: "column",
dataPoints: []
}]
});
chart.options.data[0].dataPoints = [{ label: "Apple", y: 658 },
{ label: "Orange", y: 200 },
{ label: "Banana", y: 900 }];
chart.render();
This sample is pure JS, but I tried to adapt the principle to my React functional component. To better comport with React best practices, I incorporated the useState hook for storing the data and the useEffect hook to handle the fetch. But, alas, I couldn't get my sandbox to animate with the dynamic data.
I think the problem is that CanvasJS expects to animate only on the first render, as stated by Sanjoy in the CanvasJS forum on 7/19/2016.
I found this SO question from Jan 2015 that suggests:
My current ugly workaround is to reinstantiate the chart every time I
update just to achieve that animation effect.
I'm hopeful that the situation has improved in the last four years, but if this hack is still the best/only way to go, I need some guidance on how to reinstantiate the chart every time using a React functional component.
To force a remount of a component pass a different key when you want to remount the component
<CanvasJSChart
key={dataPoints.toString()} // force a remount when `dataPoints` change
containerProps={containerProps}
options={options}
onRef={ref => (chart.current = ref)}
/>
Working example
I found a partial answer. Full executing code is in this code sandbox, but the critical bit is to delay the initial render of the chart until a state variable indicates that the data is available:
return (
<div className="App">
{!initialized ? (
<h1> Loading...</h1>
) : (
<CanvasJSChart containerProps={containerProps} options={options} />
)}
</div>
);
This is only a partial solution because subsequent data updates still do not animate.
Both examples works fine.
You can always animate chars with some kind of calling. I use in this case setInterval.
<script src="https://canvasjs.com/assets/script/canvasjs.min.js"></script>
<script>
var chart;
window.onload = function () {
chart = new CanvasJS.Chart("chartContainer", {
title: {
text: "Animation test"
},
animationEnabled: true,
data: [{
type: "column",
dataPoints: []
}]
});
chart.options.data[0].dataPoints = [{ label: "Apple", y: 0 },
{ label: "Orange", y: 0 },
{ label: "Banana", y: 0 }];
chart.render();
}
var max = 0;
var s = {c: 0, i: 0};
function ANIMATE() {
if (typeof chart === 'undefined') return;
chart.options.data[0].dataPoints.forEach(function(item, index, array) {
if (index == s.i) {
array[index].y += 3;
s.c++;
}
if (s.c > 12) {
s.i++;
s.c = 0;
if (s.i == 15) { s.i = 0}
}
});
if (max < 12) {
chart.options.data[0].dataPoints.push({label: "apple" + Math.random(), y: 1 + Math.random() * 10});
max++;
}
chart.render()
}
setInterval(function(){
ANIMATE()
}, 1)
</script>
<div id="chartContainer" style="height: 370px; width: 100%;"></div>

Resources