How to animate react-native-svg Polygon element? - reactjs

Is there a simple way to animate the Polygon element from the react-native-svg library?
I need to animate his shape by animating the points.
I found few examples on how to animate Path element or Circle, but couldn't find anything regarding the Polygon. Thanks in advance.

Bit late to the party, but I've found a solution if you're still interested. It's not exactly 'simple', but it works. There's a library called React Native Reanimated, and it extends the functionality of Animated components
substantially. Here's what I was able to achieve:
The reason animating Polygons isn't available out of the box is because the standard Animated API only handles simple values, namely individual numbers. The Polygon component in react-native-svg takes props of points, which is an array of each of the points, themselves array of x and y. For example:
<Polygon
strokeWidth={1}
stroke={strokeColour}
fill={fillColour}
fillOpacity={1}
points={[[firstPointX, firstPointY],[secondPointX, secondPointY]}
/>
React Native Reanimated allows you to animate even complex data types. In this case, there is useSharedValue, which functions almost identical to new Animated.value(), and a function called useAnimatedProps, where you can create your points (or whatever else you want to animate) and pass them to the component.
// import from the library
import Animated, {
useSharedValue,
useAnimatedProps,
} from 'react-native-reanimated';
// creates the animated component
const AnimatedPolygon = Animated.createAnimatedComponent(Polygon);
const animatedPointsValues = [
{x: useSharedValue(firstXValue), y: useSharedValue(firstYValue)},
{x: useSharedValue(secondXValue), y: useSharedValue(secondYValue)},
];
const animatedProps = useAnimatedProps(() => ({
points: data.map((_, i) => {
return [
animatedPointValues[i].x.value,
animatedPointValues[i].y.value,
];
}),
})),
Then in your render/return:
<AnimatedPolygon
strokeWidth={1}
stroke={strokeColour}
fill={fillColour}
fillOpacity={1}
animatedProps={animatedProps}
/>
Then whenever you update one of those shared values, the component will animate.
I'd recommend reading their docs and becoming familiar with the library, as it will open up a whole world of possibilities:
https://docs.swmansion.com/react-native-reanimated/
Also, the animations are handled in the native UI thread, and easily hit 60fps, yet you can write them in JS.
Good luck!

react-native-reanimated also supports flat arrays for the Polygon points prop, so we can simplify the animation setup even more.
Full example which will animate the react-native-svg's Polygon when the points prop changes looks like this:
import React, { useEffect } from 'react'
import Animated, { useAnimatedProps, useSharedValue, withTiming } from 'react-native-reanimated'
import { Polygon } from 'react-native-svg'
interface Props {
points: number[]
}
const AnimatedPolygonInternal = Animated.createAnimatedComponent(Polygon)
export const AnimatedPolygon: React.FC<Props> = ({ points }: Props) => {
const sharedPoints = useSharedValue(points)
useEffect(() => {
sharedPoints.value = withTiming(points)
}, [points, sharedPoints])
const animatedProps = useAnimatedProps(() => ({
points: sharedPoints.value,
}))
return <AnimatedPolygonInternal fill="lime" animatedProps={animatedProps} />
}

Related

After first load of Highcharts, how to properly swap different series in/out using React and properly render Highchart?

Currently, I am using useState to swap out the series with a different kind of data set. The goal is to have the user click a button to show/swap the kind of data they want to see. I have attached a codesandbox for demo purposes (and it also shows one of the bugs). When I do it this way (as shown in this sandbox), 2 bugs occur.
Firstly, after swapping the data set, the legend shows series names properly (e.g. per the sandbox, will swap to a single series name as there is technically only 1 series to show along with its corresponding arearange). But after swapping back to the original, the legend only shows one name (of the two available series). However, the line series on the graph itself still shows the 2 lines properly despite the legend only have 1 of the line's names. This behavior shows with more than two series as well.
Secondly, I can't seem to reproduce it in the code sandbox. But essentially with a similar behavior as the first bug I explained earlier, the line series/arearange series will sometimes change. Meaning that when I flip between the data, the line series sometimes becomes an arearange (not intended) and vice versa.
The actual goal of my code is that the chart will get the data as a prop and then I would manipulate this data and set it to state (e.g. filtering an array for specific series sets). I'm not sure why this is not working as intended and any help is much appreciated :).
Here's an example code (same as the sandbox):
import Highcharts, { Options } from "highcharts";
import HighchartsReact from "highcharts-react-official";
import HighchartsExport from "highcharts/modules/exporting";
import HighchartsMore from "highcharts/highcharts-more";
import * as React from "react";
import { LineData, LineDataWithRange } from "./mocks/LineChartMockData";
if (typeof Highcharts === "object") {
HighchartsExport(Highcharts);
HighchartsMore(Highcharts);
}
export const Example: React.FC = () => {
const [chartData, setChartData] = React.useState(LineData);
const handleChangeToDataRange = () => {
setChartData(LineDataWithRange);
};
const handleChangeToJustData = () => {
setChartData(LineData);
};
const chartOptions: Options = {
chart: {
type: "line"
},
title: {
text: "Highcharts/React/Typescript"
},
series: chartData
};
return (
<>
<HighchartsReact highcharts={Highcharts} options={chartOptions} />
<button onClick={handleChangeToDataRange}>
Change to Line Data with Range
</button>
<button onClick={handleChangeToJustData}>Change to Just Line Data</button>
</>
);
};
That's because the wrapper uses chart.update to react to a component update. Series are updated and their options are merged. Example in pure JS: http://jsfiddle.net/gk06rxqw/
As a solution enable immutable property:
<HighchartsReact immutable={true} highcharts={Highcharts} options={chartOptions} />
Or overwrite all of the used series options:
export const LineData = [
{...},
{
...,
linkedTo: null
}
];
export const LineDataWithRange = [
{...},
{
...,
linkedTo: "series3"
}
];
Live example: https://codesandbox.io/s/sample-highchart-forke-o2byno
API Reference: https://api.highcharts.com/class-reference/Highcharts.Chart#update
Docs: https://www.npmjs.com/package/highcharts-react-official#options-details

React & Deck.GL: Add default props to each child component

I'm working on a configurable set of map layers with Deck.GL & React. I have a BaseMap component that I'll pass layers of data to as react children.
Currently, I have this:
BaseMap:
export const BaseMap = ({ latitude = 0, longitude = 0, zoom = 4, children }) => {
const deckProps = {
initialViewState: { latitude, longitude, zoom },
controller: true
};
return (
<DeckGL {...deckProps}>
{children}
<StaticMap />
</DeckGL>
);
};
And it's used like this:
<BaseMap>
<ScatterplotLayer
data={scatterData}
getPosition={getPositionFn}
getRadius={1}
radiusUnits={'pixels'}
radiusMinPixels={1}
radiusMaxPixels={100}
filled={true}
getFillColor={[255, 255, 255]}
/>
<TextLayer
data={textData}
getPosition={getPositionFn}
getColor={[255, 0, 0]}
getText={getTextFn}
/>
</BaseMap>
This is okay, but I want to add default props to each child.
Attempt 1
I've tried this in BaseMap, but get the error cannot assign to read only property props of object #<Object>:
...
return (
<DeckGL {...deckProps}>
{React.Children.map(children, (c) => {
const defaultProps = {
loaders: [CSVLoader]
}
c.props = { ...defaultProps, ...c.props };
return c;
})}
</DeckGL>
);
Attempt 2
I've also tried creating a wrapper component for each type of layer, but get the error Cannot call a class as a function:
wrapper:
export const ScatterplotLayerWrapper = (props) => {
const defaultScatterProps = {
loaders: [CSVLoader]
};
const scatterLayerProps = {
...defaultScatterProps,
...props
};
return <ScatterplotLayer {...scatterLayerProps} />;
};
used like this:
<BaseMap>
<ScatterplotLayerWrapper
data={scatterData}
getPosition={getPositionFn}
/>
</BaseMap>
I suspect the problem with this second attempt has something to do with the caveat here.
Solution?
I can imagine two types of solutions (and obviously, there may be others!):
correct method for checking the layer type & modifying child props depending on the type, or something similar - is this possible?
or
Some way to convince react/deck.gl that ScatterplotLayer will be a child of Deck.GL, even if it isn't in ScatterplotLayerWrapper. (This one seems less likely)
The confusion came from a mis-understanding of how deck.gl's React component works & what those <ScatterplotLayer> components really are (they're not react components).
Deck.gl's react component, DeckGL, intercepts all children and determines if they are in fact "layers masquerading as react elements" (see code). It then builds layers from each of those "elements" and passes them back to DeckGL's layers property.
They look like react components, but really aren't. They can't be rendered on their own in a React context. They can't be rendered outside of the DeckGL component at all, because they're still just plain deck.gl layers.
The solution here is to create a new map layer class just like you might in any other context (not a React component wrapping a layer). Docs for that are here.
class WrappedTextLayer extends CompositeLayer {
renderLayers() { // a method of `Layer` classes
// special logic here
return [new TextLayer(this.props)];
}
}
WrappedTextLayer.layerName = 'WrappedTextLayer';
WrappedTextLayer.defaultProps = {
getText: (): string => 'x',
getSize: (): number => 32,
getColor: [255, 255, 255]
};
export { WrappedTextLayer };
This new layer can then be used in the BaseMap component (or the un-wrapped DeckGL component`) like this:
<BaseMap>
<WrappedTextLayer
data={dataUrl}
getPosition={(d) => [d.longitude, d.latitude]}
/>
</BaseMap>
In addition, the exact same layer can be passed to DeckGL as a layer prop:
<DeckGL
layers={[
new WrappedTextLayer({
data: dataUrl,
getPosition: (d) => [d.longitude, d.latitude]
})
]}
></DeckGL>
Modifying the BaseMap component a little will allow it to accept layers either as JSX-like children, or via the layers prop as well:
export const BaseMap = ({ latitude = 0, longitude = 0, zoom = 4, children, layers }) => {
const deckProps = {
initialViewState: { latitude, longitude, zoom },
controller: true,
layers
};
return (
<DeckGL {...deckProps}>
{children && !layers ? children : null}
<StaticMap />
</DeckGL>
);
};

How do I turn off Framer Motion animations in mobile view?

I am trying to create a React website using Framer Motion, the problem is that my animation looks good in desktop view, but not really in mobile. Now I want to disable the animation. How do I do this?
Without knowing further details, I would recommend using Variants for this.
Inside your functional component, create a variable that checks for mobile devices. Then, only fill the variants if this variable is false.
Note that it doesn't work when you resize the page. The component must get rerendered.
I've created a codesandbox for you to try it out!
For further information on Variants, check this course
Another simple way I just did myself when this exact question came up. Below we are using a ternary operatory to generate a object which we then spread using the spread syntax
const attributes = isMobile ? {
drag: "x",
dragConstraints: { left: 0, right: 0 },
animate: { x: myVariable },
onDragEnd: myFunction
} : { onMouseOver, onMouseLeave };
<motion.div {...attributes}> {/* stuff */} </motion.div>
As you can see I want onMouseEnter & onMouseLeave on desktop with no animations. On mobile I want the opposite. This is working of me perfectly.
Hope this helps too.
Daniel
This is how we've done it:
import {
type ForwardRefComponent,
type HTMLMotionProps,
motion as Motion,
} from 'framer-motion';
import { forwardRef } from 'react';
const ReducedMotionDiv: ForwardRefComponent<
HTMLDivElement,
HTMLMotionProps<'div'>
> = forwardRef((props, ref) => {
const newProps = {
...props,
animate: undefined,
initial: undefined,
transition: undefined,
variants: undefined,
whileDrag: undefined,
whileFocus: undefined,
whileHover: undefined,
whileInView: undefined,
whileTap: undefined,
};
return <Motion.div {...newProps} ref={ref} />;
});
export const motion = new Proxy(Motion, {
get: (target, key) => {
if (key === 'div') {
return ReducedMotionDiv;
}
// #ts-expect-error - This is a proxy, so we can't be sure what the key is.
return target[key];
},
});
export {
AnimatePresence,
type Variants,
type HTMLMotionProps,
type MotionProps,
type TargetAndTransition,
type Transition,
type Spring,
} from 'framer-motion';
Same principle as other answers, just complete example.

Moving slider with Cypress

I've got a Slider component from rc-slider and I need Cypress to set the value of it.
<Slider
min={5000}
max={40000}
step={500}
value={this.state.input.amount}
defaultValue={this.state.input.amount}
className="sliderBorrow"
onChange={(value) => this.updateInput("amount",value)}
data-cy={"input-slider"}
/>
This is my Cypress code:
it.only("Changing slider", () => {
cy.visit("/");
cy.get(".sliderBorrow")
.invoke("val", 23000)
.trigger("change")
.click({ force: true })
});
What I've tried so far does not work.
Starting point of slider is 20000, and after test runs it goes to 22000, no matter what value I pass, any number range.
Looks like it used to work before, How do interact correctly with a range input (slider) in Cypress? but not anymore.
The answer is very and very simple. I found the solution coincidentally pressing enter key for my another test(date picker) and realized that pressing left or right arrow keys works for slider.
You can achieve the same result using props as well. The only thing you need to do is to add this dependency: cypress-react-selector and following instructions here: cypress-react-selector
Example of using {rightarrow}
it("using arrow keys", () => {
cy.visit("localhost:3000");
const currentValue = 20000;
const targetValue = 35000;
const increment = 500;
const steps = (targetValue - currentValue) / increment;
const arrows = '{rightarrow}'.repeat(steps);
cy.get('.rc-slider-handle')
.should('have.attr', 'aria-valuenow', 20000)
.type(arrows)
cy.get('.rc-slider-handle')
.should('have.attr', 'aria-valuenow', 35000)
})
#darkseid's answer helped guide me reach an optimal solution.
There are two steps
Click the slider's circle, to move the current focus on the slider.
Press the keyboard arrow buttons to reach your desired value.
My slider jumps between values on the sliders, therefore this method would work. (I am using Ion range slider)
This method doesn't require any additional depedency.
// Move the focus to slider, by clicking on the slider's circle element
cy.get(".irs-handle.single").click({ multiple: true, force: true });
// Press right arrow two times
cy.get(".irs-handle.single").type(
"{rightarrow}{rightarrow}"
);
You might be able to tackle this using Application actions, provided you are able to modify the app source code slightly.
Application actions give the test a hook into the app that can be used to modify the internal state of the app.
I tested it with a Function component exposing setValue from the useState() hook.
You have used a Class component, so I guess you would expose this.updateInput() instead, something like
if (window.Cypress) {
window.app = { updateInput: this.updateInput };
}
App: index.js
import React, { useState } from 'react';
import { render } from 'react-dom';
import './style.css';
import Slider from 'rc-slider';
import 'rc-slider/assets/index.css';
function App() {
const [value, setValue] = useState(20000);
// Expose the setValue() method so that Cypress can set the app state
if (window.Cypress) {
window.app = { setValue };
}
return (
<div className="App">
<Slider
min={5000}
max={40000}
step={500}
value={value}
defaultValue={value}
className="sliderBorrow"
onChange={val => setValue(val)}
data-cy={"input-slider"}
/>
<div style={{ marginTop: 40 }}><b>Selected Value: </b>{value}</div>
</div>
);
}
render(<App />, document.getElementById('root'));
Test: slider.spec.js
The easiest way I found assert the value in the test is to use the aria-valuenow attribute of the slider handle, but you may have another way of testing that the value has visibly changed on the page.
describe('Slider', () => {
it("Changing slider", () => {
cy.visit("localhost:3000");
cy.get('.rc-slider-handle')
.should('have.attr', 'aria-valuenow', 20000)
cy.window().then(win => {
win.app.setValue(35000);
})
cy.get('.rc-slider-handle')
.should('have.attr', 'aria-valuenow', 35000)
})
})
For whoever comes across this with Material UI/MUI 5+ Sliders:
First off, this github issue and comment might be useful: https://github.com/cypress-io/cypress/issues/1570#issuecomment-606445818.
I tried changing the value by accessing the input with type range that is used underneath in the slider, but for me that did not do the trick.
My solution with MUI 5+ Slider:
<Slider
disabled={false}
step={5}
marks
data-cy="control-percentage"
name="control-percentage"
defaultValue={0}
onChange={(event, newValue) =>
//Handle change
}
/>
What is important here is the enabled marks property. This allowed me to just click straight on the marks in the cypress test, which of course can also be abstracted to a support function.
cy.get('[data-cy=control-percentage]').within(() => {
// index 11 represents 55 in this case, depending on your step setting.
cy.get('span[data-index=11]').click();
});
I got this to work with the popular react-easy-swipe:
cy.get('[data-cy=week-picker-swipe-container]')
.trigger('touchstart', {
touches: [{ pageY: 0, pageX: 0 }]
})
.trigger('touchmove', {
touches: [{ pageY: 0, pageX: -30 }]
})

How to add an SVG/d3 map to React Native?

Use Case
I want to create a React Native app that displays coordinates stored in a PostGIS database on a world map with a special map projection.
What I have done so far
I successfully loaded the map to a React environment for my web version of the app. This is the code I implemented (only including the component that holds the map):
import React, { useEffect, useRef } from 'react';
import Aux from '../../../../hoc/Auxiliary';
import styles from './Backmap.module.css';
import * as d3 from "d3";
import { feature } from "topojson-client";
import landmass from "./../../../../land-50m";
const Backmap = (props) => {
const mapContainer = useRef(null);
let width = props.windowSize.windowWidth;
let height = props.windowSize.windowHeight;
useEffect(() => {
const svg = d3.select(mapContainer.current)
.attr("width", width)
.attr("height", height)
const g = svg.append("g")
let features = feature(landmass, landmass.objects.land).features
let projection = d3.geoAzimuthalEqualArea()
.center([180, -180])
.rotate([0, -90])
.fitSize([width, height], { type: "FeatureCollection", features: features })
.clipAngle(150)
let path = d3.geoPath().projection(projection)
g.selectAll("#landmass")
.data(features)
.enter().append("path")
.attr("id", "landmass")
.attr("d", path);
}, [])
return (
<Aux>
<svg
className={styles.Backmap}
width={props.windowSize.windowWidth}
height={props.windowSize.windowHeight}
ref={mapContainer}
>
</svg>
</Aux>
)
}
export default Backmap;
This is the resulting map from above code:
I also researched how to implement SVG shapes and even maps to React Native and came across several ways of doing so:
react-native-svg(SVG)
React Native ART(SVG)
react-native-simple-maps(map)
Problem
However, I could not implement any map using these solutions. The former two in the above list don't mention any map implementation and the latter one seems to be a very early beta version. So I am wondering if this is even currently possible in React Native. Does anybody know an approach to inserting an SVG map in React native?
It is possible to create d3 maps in react native. The thing is you cant use Dom selectors like d3.select().
You need to use react-native-svg.
Read the documentation here learn how to install and use it. Its implementaion is really close to the browser SVG API.
You import the Svg, G, and Path components
import Svg, { G, Path } from "react-native-svg";
You create a projection and path in the usual d3 manner.
let projection = d3.geoAzimuthalEqualArea()
.center([180, -180]).rotate([0, -90])
.fitSize([width, height], { type: "FeatureCollection", features: features })
.clipAngle(150)
let path = d3.geoPath().projection(projection)
The features are also extracted the same way.
let features = feature(landmass, landmass.objects.land).features
But we dont append components using d3. We append the Svg and G components directly.
return (
<Svg width='100%' height='100%'>
<G>
</G>
</Svg> )
The width and height can be styled as required.
Next the paths are appended but mapping the featurers Array to create path components.
return (
<Svg width='100%' height='100%'>
<G>
{features.map((feature, index) => {
return (<Path d={path(feature)} key={index} stroke="black" fill="grey"></Path>)
})}
</G>
</Svg> )
If you need help in rotating the svg ask in the comments to this answer.

Resources