Currently been using react-spring to do our animations in our app; unfortunately, animation is not something I excel in and the design our designer gave us for our new logo is leaving me stumped on implementation. It is pretty easy with plain old JS but implementing it in react-spring has proved a challenge that I can not get past.
The end goal for the animation is to look like this:
https://codepen.io/darylginn/pen/GRqZxBZ
Currently, I am up to this stage:
import React from "react";
import { useTrail, animated } from "react-spring";
const Loader: React.FC = () => {
// Animations
const paths = [
{
id: 1,
color: "#466FB5",
d: "M90.6672 33H16L53.3336 96.4409L90.6672 33Z",
},
{
id: 2,
color: "#0093D3",
d: "M53.3347 96.4443H128.002L90.6683 33.0034L53.3347 96.4443Z",
},
{
id: 3,
color: "#53BFA2",
d: "M128.001 96.3701H53.3336L90.6672 159.811L128.001 96.3701Z",
},
{
id: 4,
color: "#93C83F",
d: "M90.6675 159.892H165.335L128.001 96.4417L90.6675 159.892Z",
},
{
id: 5,
color: "#58B647",
d: "M165.334 159.892H90.6664L128 223.333L165.334 159.892Z",
},
{
id: 6,
color: "#F2E90B",
d: "M202.667 96.4436H128L165.334 159.894L202.667 96.4436Z",
},
{
id: 7,
color: "#FBB12C",
d: "M128.001 96.4443H202.668L165.335 33.0034L128.001 96.4443Z",
},
{
id: 8,
color: "#FF5E8D",
d: "M240 33H165.333L202.666 96.4409L240 33Z",
},
];
const trail = useTrail(paths.length, {
from: {
scale: 1,
},
to: async (next: any) => {
while (1) {
await next({ scale: 1.5 });
await next({ scale: 1 });
}
},
});
return (
<div style={{ width: "200px", height: "200px" }}>
<svg
width="72"
height="72"
viewBox="0 0 256 256"
fill="none"
xmlns="http://www.w3.org/2000/svg"
style={{ overflow: "visible" }}>
{trail.map(({ scale }, index) => {
const path = paths[index];
return (
<animated.path
key={path.id}
fill={path.color}
d={path.d}
style={{
transformOrigin: "center",
transform: scale.interpolate((s: any) => `scale(${s})`),
}}
/>
);
})}
</svg>
</div>
);
};
The main issue I am at now is the scale of each triangle in the SVG needs to happen one after another, but nothing I do with the useTrail make this happen.
I did try adding a delay like this to the useTrail
delay: (key: any) => key * 200
But the delay doesn't even seem to make a difference. If someone could help make sure each triangle finish it sequences before the next one starts that would be great.
Bonus if you can also help me add the colour change as seen in the original design (see link).
I have tried posting in the react-spring community but got no replies
I would change the useTrail to one useSpring for all the triangles. If you change the x value from 0 to 8, then you can use interpolate and a range for each triangle to change. For example for the second triangle you can use range:[0,1,1.5,2,8],output:[1,1,1.5,1,1]. It means that when x is between 1 and 2 it will change the scale from 1 to 1.5 to 1 and all other places it will remain 1.
const props = useSpring({
from: {
x: 0
},
config: {
duration: 4000
},
to: async (next: any) => {
while (1) {
await next({ x: 8 });
await next({ reset: true });
}
}
});
I also added the color interpolation.
{paths.map((path, index) => {
const colors = [];
for (let i = 0; i < 8; i++) {
colors.push(paths[(i + index) % 8].color);
}
return (
<animated.path
key={path.id}
fill={path.color}
d={path.d}
style={{
transformOrigin: "center",
transform: props.x
.interpolate({
range: [0, index, index + 0.5, index + 1, 8],
output: [1, 1, 1.5, 1, 1]
})
.interpolate((x) => `scale(${x})`),
fill: props.x.interpolate({
range: [0, 1, 2, 3, 4, 5, 6, 7, 8],
output: colors
})
}}
/>
);
})}
I have an example. There is some glitch with the center triangle. But I think you get the idea.
Example: https://codesandbox.io/s/animate-triangles-sequentially-with-interpolating-and-range-oer84
Related
I think this question is best described by an example:
let's say I want to apply margin to an element like this:
const myView = () => <View style={styles.viewStyle}></View>
const styles = StyleSheet.create({
viewStyle: {
margin: "0 0 5 10",
},
})
Is it possible to do this without multiple margin statements?
Thanks for reading.
I don't think you can unless you write a function to do something like that.
Like this:
const CommonStyle = {
margin: (t, r, b, l) => {
marginTop: t,
marginRight: r,
marginBottom: b,
marginLeft: l,
}
}
Then in your style:
const styles = StyleSheet.create({
viewStyle: {
...CommonStyle.margin(0, 0, 5, 10),
},
})
But, in the most common case, we atmost only change margin for 2 directions. And there are serveral options to quickly styling your component when you get used to styling.
Example:
"5 10 5 10" is equal to
{
marginVertical: 5,
marginHorizontal: 10,
}
"0 0 0 5" is equal to
{
margin: 0,
marginLeft: 5,
}
I am using react native gesture handlers to create a bar that can be scrolled up and down. Currently I can scroll it as much as I want. I want to modify it such that it should stop scrolling when I certain limit has reached.
export const SwipeablePanel: React.FunctionalComponent = () => {
//set up animation variables
const dragY = useRef(new Animated.Value(0));
const onGestureEvent = Animated.event(
[{ nativeEvent: { translationY: dragY.current } }],
{
useNativeDriver: true,
},
);
const onHandleStateChange = (event: any) => {
if (event.nativeEvent.oldState === State.ACTIVE) {
dragY.current.extractOffset();
}
console.log('EVENT', event.nativeEvent)
};
const animateInterpolation = dragY.current.interpolate({
inputRange: [-(SCREEN_HEIGHT * 2) / 3, 0],
outputRange: [moderateScale(80) - (SCREEN_HEIGHT * 2) / 3, 0],
extrapolate: 'clamp',
});
const animatedStyles = {
transform: [
{
translateY: animateInterpolation,
},
],
};
const whitelistBarMarginInterpolation = dragY.current.interpolate({
inputRange: [-SCREEN_HEIGHT + SCREEN_HEIGHT / 3, 0],
outputRange: [0, moderateScale(150)],
extrapolate: 'clamp',
});
const whitelistBarStyles = {
transform: [
{
translateY: whitelistBarMarginInterpolation,
},
],
};
return (
<PanGestureHandler
onGestureEvent={onGestureEvent}
onHandlerStateChange={onHandleStateChange}
activeOffsetX={[-1000, 1000]}
activeOffsetY={[-10, 10]}
>
<Animated.View
style={[
styles.container,
animatedStyles
]}>
<ScrollBar />
);
};
In onHandleStateChange, I can get values of event.nativeEvent such as
absoluteX: 237
absoluteY: 348.5
handlerTag: 109
numberOfPointers: 0
oldState: 4
state: 5
target: 5235
translationX: 7
translationY: 200.5
velocityX: 0
velocityY: 0
x: 237
y: 84.84616088867188
I want to use an if else condition in the code such that I can set limits after which point the scrolling stops. But I am not sure how to do that since the scrolling happens from the onGestureEvent.
I thought of adding checks in here but if I change it like this, it no longer works:
const onGestureEvent = () => {
Animated.event([{ nativeEvent: { translationY: dragY.current } }], {
useNativeDriver: true,
});
};
Snack Expo : https://snack.expo.io/#nhammad/insane-pizza
I tried to reproduce it but here I can't scroll at it. I am using the same code in my app and it scrolls over there.
Try ScrollView, It has prop called scrollEnabled. By help of this prop you can turn to false when you don't want user to be able to scroll any more. When false, the view cannot be scrolled via touch interaction.
Here is documentation: https://facebook.github.io/react-native/docs/scrollview.html#scrollenabled
ScrollView also has onScroll so it should be easy to implement the logic here.
I am using Victory for data visualisation in my project. However, while implementing event handlers in my charts, I noticed that while they change the target properties, the charts are never re-rendered so nothing changes.
Below is an example from Victory's documentation, which does not work on my machine:
<div>
<h3>Click Me</h3>
<VictoryScatter
style={{ data: { fill: "#c43a31" } }}
size={9}
labels={() => null}
events={[{
target: "data",
eventHandlers: {
onClick: () => {
return [
{
target: "data",
mutation: (props) => {
const fill = props.style && props.style.fill;
return fill === "black" ? null : { style: { fill: "black" } };
}
}, {
target: "labels",
mutation: (props) => {
return props.text === "clicked" ?
null : { text: "clicked" };
}
}
];
}
}
}]}
data={[{ x: 1, y: 2 },
{ x: 2, y: 3 },
{ x: 3, y: 5 },
{ x: 4, y: 4 },
{ x: 5, y: 7 }]}
/>
</div>
After some debugging, I can confirm that the data and labels properties of the component are changed as a result of the onClick event, but these changes are not actually reflected in the chart. Any solutions?
It seems the issue is caused by having <React.StrictMode> on in the code inside index.js. Removing it solves the problem. I am not exactly sure why though!!
I'm using Nivo bar to represent a user's progress on a budget. I've normalized the data by dividing the category balance by the category goal. Example data.
[{
"category": "Gas",
"budget": 0.24,
"over_budget": 0.0
},
{
"category": "Groceries",
"budget": 1.0,
"over_budget": 0.26
}]
I don't want these values to be used as the label on the chart. I plan to use the actual balance value as the label. I have an endpoint that will return the balance for a category and have attempted the following to use that value:
<ResponsiveBar
...
label={d => this.getDollarAmount(d.value)}
...
>
With the function POC as:
getDollarAmount(value) {
console.log("hitting getDollarAmount")
return 1
};
The log message gets logged 500+ times. My expectation would be that the function would only be hit once for each bar in the chart.
I'm still learning react so this could be something obvious. Thanks in advance!
EDIT - Here's the entire BarChart component:
import axios from 'axios';
import React, { Component } from "react";
import { ResponsiveBar } from '#nivo/bar'
// Nivo theming
const theme = {
axis: {
ticks: {
line: {
stroke: "#e9ecee",
strokeWidth: 40
},
text: {
// fill: "#919eab",
fill: "black",
fontFamily: "BlinkMacSystemFont",
fontSize: 16
}
}
},
grid: {
line: {
stroke: "#e9ecee",
strokeWidth: 5
}
},
legends: {
text: {
fontFamily: "BlinkMacSystemFont"
}
}
};
let budgetStatusAPI = 'http://127.0.0.1:8000/api/budget_status/?auth_user=1&month=2020-02-01';
class BarChart extends Component {
constructor(props) {
super(props);
this.state = {
data: [],
}
this.getDollarAmount = this.getDollarAmount.bind(this);
}
componentDidMount() {
console.log("component did mount")
axios.get(budgetStatusAPI).then(response => {
this.setState({
data: response.data
}, function () {
console.log(this.state.data);
})
});
}
componentDidUpdate() {
console.log("component did update")
}
getDollarAmount(value) {
console.log("hitting getDollarAmount")
console.log(value)
return 1
};
render() {
const hard_data = [
{
"category": "Groceries",
"budget_status": 1.0,
"over_budget": .26,
},
{
"category": "Gas",
"budget_status": .24,
"over_budget": 0.0,
}]
return(
<ResponsiveBar
maxValue={1.5}
markers={[
{
axis: 'x',
value: 1,
lineStyle: { stroke: 'rgba(0, 0, 0, .35)', strokeWidth: 2 },
legend: 'Goal',
legendOrientation: 'horizontal',
legendPosition: 'top'
},
]}
enableGridX={false}
gridXValues={[1]}
enableGridY={false}
data={this.state.data}
// data={hard_data}
keys={['budget_status', 'over_budget']}
indexBy="category"
margin={{ top: 25, right: 130, bottom: 50, left: 125 }}
padding={0.3}
layout="horizontal"
colors={{ scheme: 'set2' }}
theme={theme}
defs={[
{
id: 'dots',
type: 'patternDots',
background: 'inherit',
color: '#38bcb2',
size: 4,
padding: 1,
stagger: true
},
{
id: 'lines',
type: 'patternLines',
background: 'inherit',
color: '#eed312',
rotation: -45,
lineWidth: 6,
spacing: 10
}
]}
borderColor={{ from: 'color', modifiers: [ [ 'darker', 1.6 ] ] }}
axisBottom={null}
label={d => this.getDollarAmount(d.value)}
isInteractive={false}
animate={true}
motionStiffness={90}
motionDamping={15}
/>
)
}
}
export default BarChart;
Reproduced here: https://codesandbox.io/s/nivo-bar-label-issue-k4qek
The multiple calling is happening because the bar chart is calling label function for each animation tick/frame render. If we setup a counter, we'll see with animate prop set to true it will render from 450+ to 550+ times, but if we set the prop animate to false, we'll it renders 6 times which is exactly how many price values are > 0.0.
If you want to avoid all these renders, you'll have to disable animation using animate={false} prop like this:
getDollarAmount(value) {
// Remove every console.log inside this function
return `$${value}`
}
render() {
return (
<ResponsiveBar
animate={false}
label={d => this.getDollarAmount(d.value)}
...
);
}
You can check it running to your cloned CodeSandbox. I have set animate to false and the counter log inside getDollarAmount is calling 6 times. Try to change animate to true and you'll see the 500+- renders.
Also, you don't have to create a function for each label call, you can just pass the getDollarAmount function and let it handle the whole d parameter like this:
getDollarAmount(d) {
// Remove every console.log inside this function
return `$${d.value}`
}
render() {
return (
<ResponsiveBar
animate={false}
label={this.getDollarAmount}
...
);
}
I am using react-plotly to generate a large timeline of data (10,000-100,000 points) and I animate across the data in another window. I need to get a scrubber (vertical line) that moves with a react-property representing time, but I need to update the scrubber without updating the rest of the timeline, since it would take so long to do so. How can I get just the vertical line to update?
Edit: Was asked for code
In the following code, the backtracks and thresholds objects are Uint32Arrays and represent the y-axis of traces, where the x-axes are the Uint32Arrays backtracksTime and thresholdsTime. What I am trying to get is a vertical line at the x-coordinate currentTime.
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import Plotly from 'plotly.js';
import Plot from 'react-plotly.js';
import styles from './style.scss';
export default class ThresholdWindow extends Component {
static propTypes = {
name: PropTypes.string,
backtracks: PropTypes.object,
backtracksTime: PropTypes.object,
thresholds: PropTypes.object,
thresholdsTime: PropTypes.object,
currentTime: PropTypes.number,
}
constructor(props) {
super(props);
this.state = {
plotRevision: 0,
width: 0,
height: 0,
};
}
componentDidMount() {
const resizeObserver = new ResizeObserver(entries => {
const oldPlotRevision = this.state.plotRevision;
const rect = entries[0].contentRect;
this.setState({
plotRevision: oldPlotRevision + 1,
height: rect.height,
width: rect.width,
});
});
resizeObserver.observe(this.container);
}
shouldComponentUpdate(nextProps, nextState) {
if (this.state.plotRevision !== nextState.plotRevision) {
return true;
} else if (this.props.currentTime !== nextProps.currentTime) {
return true;
}
return false;
}
render() {
const data = [
{
name: 'Threshold',
type: 'scattergl',
mode: 'lines',
x: this.props.thresholdsTime,
y: this.props.thresholds,
side: 'above',
},
{
name: 'Backtracks',
type: 'scattergl',
mode: 'lines',
x: this.props.backtracksTime,
y: this.props.backtracks,
},
{
name: 'Current Time',
type: 'scattergl',
mode: 'lines',
x: [this.props.currentTime, this.props.currentTime],
y: [0, 1],
yaxis: 'y2',
},
];
return (
<div className={styles['threshold-window']} ref={(el) => { this.container = el; }}>
<Plot
divId={`backtracks-${this.props.name}`}
className={styles['threshold-graph']}
ref={(el) => { this.plot = el; }}
layout={{
width: this.state.width,
height: this.state.height,
yaxis: {
fixedrange: true,
},
yaxis2: {
side: 'right',
range: [0, 1],
},
margin: {
l: 35,
r: 15,
b: 20,
t: 15,
},
legend: {
orientation: 'h',
y: 1,
},
}}
revision={this.state.plotRevision}
data={data}
/>
</div>
);
}
}
Edit2: I don't actually see the currentTime line anywhere, so I'm pretty sure there's a bug somewhere.
With react-plotly.js the performance should be decent, as it will only redraw what it needs to.