React-hooks and d3.forceSimulation - reactjs

I'm having some issues with my forceSimulation in React (using Hooks). The problem is that whenever data is changed the position of each bubble is translated into the middle of the screen, rather than considering its previous position (see gif), making the bubbles jump in an annoying way.
I do not have much experience with forceSimulation, d3 and react-hooks in conjunction, and its hard to find any similar examples; so I would really appreciate any guidance.
import React, { useEffect, useRef } from 'react'
import { forceSimulation, forceX, forceY, forceCollide, select } from 'd3'
export default function Bubbles({ data, width, height }) {
const ref = useRef()
const svg = select(ref.current)
useEffect(() => {
const simulation = forceSimulation(data)
.force('x', forceX().strength(0.02))
.force('y', forceY().strength(0.02))
.force(
'collide',
forceCollide((d) => {
return d.value * 20 + 3
}).strength(0.3)
)
simulation.on('tick', () =>
svg
.selectAll('circle')
.data(data)
.join('circle')
.style('fill', () => 'red')
.attr('cx', (d) => d.x)
.attr('cy', (d) => d.y)
.attr('r', (d) => d.value * 20)
)
}, [data])
return (
<svg
viewBox={`${-width / 2} ${-height / 2} ${height} ${width}`}
width={width}
height={height}
ref={ref}
style={{
marginRight: '0px',
marginLeft: '0px'
}}
></svg>
)
}

I think the problem here is that React renders the SVG, but D3 renders the circles. When React re-renders on update it creates the SVG and discards any children because it doesn't know about them.
Thus the data can't be bound to the DOM in the usual D3 way, but should be stored in your React component's state.

Related

ReactJS D3 chart not showing data mapped from Context, but works with static variable

New to D3 and React.
I'm trying to show data on a grid (x/y axis) for each record against a background image (logo), showing a small images.
I've got the D3 graph working if I use static data (gcs in my example code below). But, if I try to filter/map the data from CohortContext, the elements are not being shown on the graph.
I think it's a timing issue that the D3 is rendering before the .map returns with the data, but I may be wrong. I tried wrapping the useEffect in an async, but I'm grasping at straws now and it didn't resolve the issue anyway, based on all the code being within the async.
The code:
import React, { useEffect, useContext } from "react"
import * as d3 from "d3"
import logo from '../../assets/images//charts/360Chart.svg'
import CohortContext from "../../utility/context/cohort/cohort-context"
import Saboteur from '../../assets/images/charts/chartBubbleKey.svg'
function ChartData({ goal }) {
const gcs = [
{
chartAsset: Saboteur,
xCoordinates: 200,
yCoordinates: 300
},
{
chartAsset: Saboteur,
xCoordinates: 150,
yCoordinates: 150
}
]
console.log(gcs)
const width = 800
const height = 800
const { cohorts } = useContext(CohortContext)
useEffect(() => {
//Retrieve the Goal data from the useContext
const fetchData = async () => {
const goalCohorts = cohorts
.filter(records => records.goalId === goal.goalId)
.map((records) => {
return {
chartAsset: Saboteur,
xCoordinates: records.xcoordinates,
yCoordinates: records.ycoordinates
}
})
console.log('goal cohorts for Chart :', goalCohorts)
const svg = d3
.select("#svgcontainer")
.append("svg")
.attr("width", width)
.attr("height", height)
//background Chart
svg
.append("svg:image")
.attr("xlink:href", () => logo)
.attr("width", 580)
.attr("height", 580)
.attr("x", 110)
.attr("y", 20)
// Add in the data for the chart data
const g = svg
.selectAll("g")
.append("g")
.data(goalCohorts) // works if I use .data(gcs)
.enter()
g.append("svg:image")
.attr("xlink:href", (data) => data.chartAsset)
.attr("width", 50)
.attr("height", 50)
.attr("x", (data) => data.xCoordinates)
.attr("y", (data) => data.yCoordinates)
}
fetchData()
}, [])
return (
< div className="App" >
<div id="svgcontainer"></div>
</div >
)
}
export default ChartData
If I look at the console.logs, the data is the same - so assuming a timing issue.
Can anyone see anything obvious on why data(goalCohorts) is not showing the data?
I'm using React v18.1, D3 v7.8
useEffect(() => {
//Retrieve the Goal data from the useContext
const fetchData = async () => {
const goalCohorts = cohorts
.....
, [cohorts]);
Please replace [cohorts] instead of [].

Best way to make a custom smooth scroll and scrollto coexist (ReactJs - Framer motion)

Made a smoothscroll component using framer motion that's working well :
export default function SmoothScroll({ children }: Props) {
const { width } = useWindowSize();
const scrollContainer = useRef() as RefObject<HTMLDivElement>;
const [pageHeight, setPageHeight] = useState(0);
useEffect(() => {
setTimeout(() => {
// added a setTimeout so the page has the time to load and it still fits
const scrollContainerSize =
scrollContainer.current?.getBoundingClientRect();
scrollContainerSize && setPageHeight(scrollContainerSize.height);
}, 500);
}, [width]);
const { scrollY } = useScroll(); // measures how many pixels user has scrolled vertically
// as scrollY changes between 0px and the scrollable height, create a negative scroll value...
// ... based on current scroll position to translateY
const transform = useTransform(scrollY, [0, pageHeight], [0, -pageHeight]);
const physics = { damping: 15, mass: 0.17, stiffness: 55 }; // easing of smooth scroll
const spring = useSpring(transform, physics); // apply easing to the negative scroll value
return (
<>
<motion.div
ref={scrollContainer}
style={{ y: spring }} // translateY of scroll container using negative scroll value
className="app fixed overflow-hidden w-screen"
>
{children}
</motion.div>
<motion.div style={{ height: pageHeight }} />
</>
);
}
The thing is, I'd like to scrollTo sections of my page upon click on the navbar but don't really know how to implement it without removing the smoothScroll ...
Tried the following logic but obviously it did not work as the vanilla scroll has been hijacked :
const scrollToSection = (
e: React.MouseEvent<HTMLLIElement, globalThis.MouseEvent>,
anchor?: string
) => {
e.preventDefault();
if (!anchor) return;
const section = document.querySelector(anchor);
section?.scrollIntoView({ behavior: "smooth" });
};
Is it doable ?

D3 React - Cannot remove map of paths before drawing paths with new locations

I am trying to programatically draw a map of SVG paths using D3 in React. Every time I render a new map of paths, the previously drawn paths are not removed from the DOM.
I have tried all suggestions here, but none are working effectively.
When I click a button, a new node is added to an array and D3 draws a SVG path from its starting point. But at render-time previously drawn lines still appear.
Here is my code:
useEffect(() => {
const svg = d3
.select(svgRef.current)
.attr('viewBox', `0 0 ${avaliableHorizontalSpace} ${avaliableHorizontalSpace}`);
svg.select('path').remove(); // this doesn't work
svg.selectAll("*").remove(); // only one stays drawn using this
if (rects.length > 0) {
rects.forEach((_el, idx) => {
const line = d3
.line<Number>()
.x((value, index) => 200 * index)
.y((value) => Number(value));
svg
.selectAll(`.node[nodeid='${nodes[idx][0].id}]'`)
.data([rects])
.join('path')
.attr('d', (value: (Number[] | Iterable<Number>)[]) => {
return line(value[idx]);
});
});
}
}, [rects]);
Later in JSX:
const NodePathWithId = (props: NodePathWithId): ReactElement => {
return (<path {...props} />);
};
...
{rects.length > 0 && rects.map((el, idx) => (
<svg
fill="white"
fillOpacity="0"
stroke="#000"
strokeWidth="1.5"
ref={(ref): void => { svgRef.current = ref; }}
style={{ position: 'absolute' }}
key={`y${nodes[idx][0].id}`}
>
<NodePathWithId nodeid={nodes[idx][0].id} />
</svg>
))}
How do I update the SVG to remove old paths so I only have the number of paths in the rects array?
Update:
Some picture below to demonstrate the problem:
First click
Second click:

how to update d3 graph in react using redux actions?

I'm trying to store data from tooltip using redux action called in mouse events (mouseenter, mouseleave).
The problem: When mouse cursor is on element (g element) I expected only mouseenter event but mouseleave is also triggered. This cause updating redux store and re-rendering react component in a loop.
I have an svg element and then I append g element, which is wrapper for circle and text. I added event listeners on g element.
Here is my code:
const SomeReactComponent =() => {
const dispatch = useDispatch();
const createChart = () => {
const svg = d3
.select(svgNode)
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom)
.style('border', '2px solid black')
.append('g')
.attr(
'transform',
`translate(${width / 2 + margin.left / 2},${height / 2 + margin.top})`
);
const Tooltip = d3
.select('.polar-scatter-chart')
.append('div')
.attr('class', 'tooltip');
const wrapper = svg
.selectAll('Points')
.data(newData)
.enter()
.append('g')
.on('mouseenter', ({ id, name, themeName }) => {
Tooltip.html(name)
.style('visibility', 'visible')
.style('top', `${d3.event.pageY - 350}px`)
.style('left', `${d3.event.pageX}px`);
dispatch(handleTooltip({ id, themeName }));
}
})
.on('mouseleave', () => {
Tooltip.style('visibility', 'hidden');
dispatch(handleTooltip({ id: '', themeName: '' }));
});
wrapper
.append('circle')
.attr('class', 'point')
.attr('transform', (d, i) => {})
.attr('r',7)
wrapper
.append('text')
.attr('transform', (d) => {})
.text((d) => some_text);
};
}
createChart();
return (
<div className="polar-scatter-chart">
<svg ref={svgRef} />
</div>
);
}
How Can I properly update d3 graph using redux actions ?
Thanks for answer
You are calling createChart on every render, creating a whole new set of elements that are probably on top of the elements that are already existing.
So every time, you "leave" the old element because you "enter" the one that is drawn on top.
If you want to do it that way, you should wrap createChart in a useEffect call and only execute it once.
But the better idea would be to go with a D3 library for react.

How to execute two animations sequentially, using react-spring?

I tried chaining two springs (using useChain), so that one only starts after the other finishes, but they are being animated at the same time. What am I doing wrong?
import React, { useRef, useState } from 'react'
import { render } from 'react-dom'
import { useSpring, animated, useChain } from 'react-spring'
function App() {
const [counter, setCounter] = useState(0)
const topRef = useRef()
const leftRef = useRef()
const { top } = useSpring({ top: (window.innerHeight * counter) / 10, ref: topRef })
const { left } = useSpring({ left: (window.innerWidth * counter) / 10, ref: leftRef })
useChain([topRef, leftRef])
return (
<div id="main" onClick={() => setCounter((counter + 1) % 10)}>
Click me!
<animated.div id="movingDiv" style={{ top, left }} />
</div>
)
}
render(<App />, document.getElementById('root'))
Here's a codesandbox demonstrating the problem:
https://codesandbox.io/s/react-spring-usespring-hook-m4w4t
I just found out that there's a much simpler solution, using only useSpring:
function App() {
const [counter, setCounter] = useState(0)
const style = useSpring({
to: [
{ top: (window.innerHeight * counter) / 5 },
{ left: (window.innerWidth * counter) / 5 }
]
})
return (
<div id="main" onClick={() => setCounter((counter + 1) % 5)}>
Click me!
<animated.div id="movingDiv" style={style} />
</div>
)
}
Example: https://codesandbox.io/s/react-spring-chained-animations-8ibpi
I did some digging as this was puzzling me as well and came across this spectrum chat.
I'm not sure I totally understand what is going on but it seems the current value of the refs in your code is only read once, and so when the component mounts, the chain is completed instantly and never reset. Your code does work if you put in hardcoded values for the two springs and then control them with turnaries but obviously you are looking for a dynamic solution.
I've tested this and it seems to do the job:
const topCurrent = !topRef.current ? topRef : {current: topRef.current};
const leftCurrent = !leftRef.current ? leftRef : {current: leftRef.current};
useChain([topCurrent, leftCurrent]);
It forces the chain to reference the current value of the ref each time. The turnary is in there because the value of the ref on mount is undefined - there may be a more elegant way to account for this.

Resources