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
Related
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.
I am currently using Viro-React (React-Viro) for a AR project in which, if a certain pictures gets seen by the camera a video is played infront of it. I had it working perfectly, but somehow and a few days later, without changing the code, the video and everything inside the ViroARImageMarker is always positioned inside the camera, when the target gets seen.
This behavior only seems to happen in my own projects and not in the samples provided by Viro Media.
I have tried to:
Reinstalling node modules
Compared the package.json's and reinstalled.
Changed the position of the elements inside the ViroARImageMarker
And reorganised the elements.
But nothing seems to work.
As I said the code itself shows and hides the video, but does not position the video (every inside the ViroARImageMarker correctly, but positions them in the position of the camera when the targets gets seen and then keeps them there.
Here is the code. (Snippet at the end)
I pass this function to the ViroARSceneNavigator in another script.
There are a few animations, which just scale up/down the video depending if the target is in view or not.
(I removed the whitespaces to fit more onto one screen)
Main Code
Viro Animations and Material
"use strict";
import React, { useState } from "react";
import { ViroARScene, ViroNode, ViroARImageMarker, ViroVideo, ViroARTrackingTargets, ViroAnimations, ViroMaterials } from "react-viro";
const MainScene = (props) => {
const videoPath = require("./res/Test_Video.mp4");
const [playVideoAnimation, setPlayVideoAnimation] = useState(false);
const [videoAnimationName, setVideoAnimationString] = useState("showVideo");
const [shouldPlayVideo, setShouldPlayVideo] = useState(false);
function onAnchorFound() {
setPlayVideoAnimation(true);
setVideoAnimationString("showVideo");
setShouldPlayVideo(true);
}
function onAnchorRemoved() {
setShouldPlayVideo(false);
setVideoAnimationString("closeVideo");
setPlayVideoAnimation(true);
}
function onVideoAnimationFinish() {
setPlayVideoAnimation(false);
}
function onVideoFinish() {
setShouldPlayVideo(false);
setVideoAnimationString("closeVideo");
setPlayVideoAnimation(true);
}
return (
<ViroARScene>
<ViroARImageMarker target={"targetOne"} onAnchorFound={onAnchorFound} onAnchorRemoved={onAnchorRemoved}>
<ViroNode rotation={[-90, 0, 0]}>
<ViroVideo
position={[0, 0, 0]}
scale={[0, 0, 0]}
dragType="FixedToWorld"
animation={{ name: videoAnimationName, run: playVideoAnimation, onFinish: onVideoAnimationFinish }}
source={videoPath}
materials={["chromaKeyFilteredVideo"]}
height={0.2 * (9 / 16)}
width={0.2}
paused={!shouldPlayVideo}
onFinish={onVideoFinish}
/>
</ViroNode>
</ViroARImageMarker>
</ViroARScene>
);
};
ViroAnimations.registerAnimations({
showVideo: {
properties: { scaleX: 0.9, scaleY: 0.9, scaleZ: 0.9 },
duration: 1,
easing: "bounce",
},
closeVideo: {
properties: { scaleX: 0, scaleY: 0, scaleZ: 0 },
duration: 1,
},
});
ViroMaterials.createMaterials({
chromaKeyFilteredVideo: {
chromaKeyFilteringColor: "#00FF00",
},
});
ViroARTrackingTargets.createTargets({
targetOne: {
source: require("./res/Test_Bild.png"),
orientation: "Up",
physicalWidth: 0.01, // real world width in meters
},
});
export default MainScene;
I was able to resolve the issue by copying (downgrading) my dependencies in my package.json from the React-Viro codesamples and decreasing the width/height (inside the element) and scale (in the animation) of the video.
Note that if the sub element of the ViroARImageMarker is too big (in scale and size), the issue comes back.
I'm trying to align the title to the left instead of the center (shown in the image).
I know this is possible on chartJS v3.0.0-alpha.2, but reactchartjs only goes up to 2.9.30 as far as I know.
Does anyone know how to achieve this in reactchartjs?
I thought it would be something like:
options={{
title: {
position: 'top',
align: 'left',
},
}}
Any help would be appreciated.
You should use the align parameter. This sets the alignment of the title. Your options are:
start
center
end
Your align: 'left' isn't one of the above and will not have any effect. Setting align: 'start' however will give you exactly what you want:
The full code looks like this:
<Chart
type='line'
data={dat}
options={{
plugins: {
title: {
display: true,
align: 'start',
text: 'Bitcoin Mining Difficulty'
}
}
}} />
Let me also mention that you should not confuse that with position: 'left'. position moves the whole title into one of top, left, bottom, right area of the chart. An example with a combination of position: 'left' and align: start:
The Chart.js Plugin Core API offers a range of hooks that may be used for performing custom code. You can use the afterDraw hook to draw the title yourself directly on the canvas using CanvasRenderingContext2D.fillText().
In React, you can register the plugin as follows:
componentWillMount() {
Chart.pluginService.register({
afterDraw: chart => {
let ctx = chart.chart.ctx;
ctx.save();
let xAxis = chart.scales['x-axis-0'];
ctx.textAlign = "left";
ctx.font = "14px Arial";
ctx.fillStyle = "black";
ctx.fillText("Title", xAxis.left, 10);
ctx.restore();
}
});
}
You'll also have to define some extra padding at the top of the chart to make sure, the title appears on the canvas.
options: {
layout: {
padding: {
top: 20
}
},
...
Please take a look at this StackBlitz and see how it works.
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>
I have started playing around with react-spring and I am loving the concept but I am struggling to work out how to build the animation that I want.
I would like to make a div move to the right, then back to start, then to the right, but not as far, back to start, to the right, but not as far again and finally back to start. Just like a spring, that is pulled and when released goes boingoingoing back to it's resting place.
I can see from the documentation how to adjust the feel of the spring and how to trigger the animation, but I have never made an animation before so knowing which properties to change and how to make it loop properly are what I am looking for help on.
Edit: I have this animation so far, and it works, but it feels very disjointed.
const shake = useSpring({
from: { "margin-left": 0 },
to: [
{ "margin-left": 30 },
{ "margin-left": 0 },
{ "margin-left": 20 },
{ "margin-left": 0 },
{ "margin-left": 10 },
{ "margin-left": 0 }
],
config: {
mass: 1,
tension: 500,
friction: 10
}
});
Currently it is clearly three movements, can I decrease the delay between the movements so that it looks like one movement?
Is margin left the best CSS property to use?
UPDATE START
I just came across a simple solution for this problem. It I way better than the one I tried to achieve with configuration. It uses interpolation with range. It is using transform now but can easily adopted to margin.
export default function App() {
const { x } = useSpring({
from: { x: 0 },
to: { x: 1 }
});
return (
<div className="App">
<animated.div
style={{
transform: x
.interpolate({
range: [0, 0.25, 0.35, 0.45, 0.55, 0.65, 0.75, 1],
output: [180, 220, 180, 220, 180, 220, 180, 200]
})
.interpolate(x => `translate3d(${x}px, 0px, 0px)`)
}}
>
Shake
</animated.div>
</div>
);
}
https://codesandbox.io/s/awesome-framework-go5oh?file=/src/App.js:103-609
UPDATE END*
You can achieve the bouncy effect. There are 3 variable controlling the spring based animation:
mass
friction
tension
You can play with the pink square here: https://www.react-spring.io/docs/hooks/api
I recommend low mass and friction and high tension. You can set these variable in every animation type. For example:
useSpring({ ..., config: {mass: 1, tension: 500, friction: 10} });