how to update d3 graph in react using redux actions? - reactjs

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.

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 [].

Chart in d3js not showing up in react app

I am trying to follow the tutorial here but using react app. The chart is not showing up and I am not sure what I am doing wrong, please help. I am using d3 v7 and react 17. I get no errors but the buttons show up so the component is called correctly.
This is my component:
import * as d3 from 'd3'
import * as React from 'react'
import { useState, useEffect} from 'react'
const PieChart = (props) => {
const ref = React.createRef()
useEffect(() => {
draw()
});
// set the dimensions and margins of the graph
const width = 450, height = 450, margin = 40;
// The radius of the pieplot is half the width or half the height (smallest one). I subtract a bit of margin.
const radius = Math.min(width, height) / 2 - margin;
// set the color scale
const color = d3.scaleOrdinal()
.domain(["a", "b", "c", "d", "e", "f"])
.range(d3.schemeDark2);
// create 2 data_set
const data1 = {a: 9, b: 20, c:30, d:8, e:12}
const data2 = {a: 6, b: 16, c:20, d:14, e:19, f:12}
const svg = d3.select(".PieChart")
// A function that create / update the plot for a given variable:
const update = (data) => {
if (data == "data1"){
data = data1;
}else{
data = data2;
}
// Compute the position of each group on the pie:
const pie = d3.pie()
.value(function(d) {return d[1]; })
.sort(function(a, b) { return d3.ascending(a.key, b.key);} ) // This make sure that group order remains the same in the pie chart
const data_ready = pie(Object.entries(data))
// map to data
const u = svg.selectAll("path")
.data(data_ready)
// Build the pie chart: Basically, each part of the pie is a path that we build using the arc function.
u
.join('path')
.transition()
.duration(1000)
.attr('d', d3.arc()
.innerRadius(0)
.outerRadius(radius)
)
.attr('fill', function(d){ return(color(d.data[0])) })
.attr("stroke", "white")
.style("stroke-width", "2px")
.style("opacity", 1)
}
const draw = () => {
svg.append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", `translate(${width/2}, ${height/2})`);
// Initialize the plot with the first dataset
update("data1")
};
return <div ref={ref} >
<button onClick={update("data1")}>Data 1</button>
<button onClick={update("data2")}>Data 2</button>
<div className="PieChart" />
</div>
}
export default PieChart
This is what I get, no graph
I think there are quite a few things that need to be fixed.
First & foremost is the usage of ref. You have to do const ref = React.useRef() i.e use useRef instead of createRef.
Also, the ref has to be set to the svg element and not the div.
The 'almost' working code is here. https://codesandbox.io/s/flamboyant-heisenberg-vppny?file=/src/pie.js
I say almost because the chart does not render on page load! I do not know why (I am not very familiar with react). To render the chart, change the value of width or height from 450 to 451. I do not know why this works but it does.
One the page is loaded with the chart, the buttons & the chart transition work fine.
If you figure how to render the chart on page load, please let me know.

React-hooks and d3.forceSimulation

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.

How to show the labels in a pie chart using d3 and reactjs

I'm new to both reactjs and d3js and was trying to create a pie chart using d3 in a reactjs website. I wanted to create a pie chart that shows the labels in the pie chart as tooltips and also an arrow pointing to the slices with their corresponding labels exactly like this example.
I have created a codesandbox to further demonstrate where I'm stuck right now. As you can see the values are being shown in the slices but I want the labels as well just like in the example above. I would be grateful if someone can please help me with this one.
This is the code that I have created so far.
import React, { useEffect, useRef } from "react";
import * as d3 from "d3";
const Pie = props => {
const ref = useRef(null);
const createPie = d3
.pie()
.value(d => d.value)
.sort(null);
const createArc = d3
.arc()
.innerRadius(props.innerRadius)
.outerRadius(props.outerRadius);
const colors = d3.scaleOrdinal(d3.schemeCategory10);
const format = d3.format(".2f");
useEffect(() => {
const data = createPie(props.data);
const group = d3.select(ref.current);
const groupWithData = group.selectAll("g.arc").data(data);
groupWithData.exit().remove();
const groupWithUpdate = groupWithData
.enter()
.append("g")
.attr("class", "arc");
const path = groupWithUpdate
.append("path")
.merge(groupWithData.select("path.arc"));
path
.attr("class", "arc")
.attr("d", createArc)
.attr("fill", (d, i) => colors(i));
const text = groupWithUpdate
.append("text")
.merge(groupWithData.select("text"));
text
.attr("text-anchor", "middle")
.attr("alignment-baseline", "middle")
.attr("transform", d => `translate(${createArc.centroid(d)})`)
.style("fill", "white")
.style("font-size", 10)
.text(d => format(d.value));
}, [props.data]);
return (
<svg width={props.width} height={props.height}>
<g
ref={ref}
transform={`translate(${props.outerRadius} ${props.outerRadius})`}
/>
</svg>
);
};
export default Pie;

D3 Chart does not redraw on new data

Using react I draw a D3 scatter chart. When I try to add new lines on the chart through the svg i initialize at the start, it works i.e. when using the button rendered in the svg and the onlick function.
When I try to do this using the parent's props, the function to add the lines gets triggered but the lines are not added. I have read other posts about using enter() but this does not work. What I think is wrong is the way I setup the div structure to mount the original SVG, since I have a div with class chart-container and inside it a div with id chart, which I add the SVG inside. Does anyone thing the way I am selecting the svg when adding the lines is wrong? I do not understand why the lines are not rendered on the D3 chart.
import React, { useEffect, useLayoutEffect } from 'react';
import * as d3 from 'd3';
import './Chart.css';
const Chart = (props) => {
useEffect(() => {
drawChart(props.data)
}, [props.columnType]);
useLayoutEffect(() => {
if (showLines) {
addLines(props.data)
return
} else {
removeLines()
}
}, [props.showLines]);
const addLines = (data) => {
const x = d3.scaleLinear().range([0, width]);
const y = d3.scaleLinear().range([height, 0]);
const parsedData = parseData(data, columnType);
parsedData.forEach((dataSet, index) => {
const { points } = dataSet;
const line = d3.line()
.x((d) => x(d.x))
.y((d) => y(d.yhat));
// If I use this selection, I cannot add the lines
d3.select("svg").enter().append("path")
.datum(points)
.attr("class", `line`)
.attr("d", line);
});
}
const drawChart = (data) => {
..... // Other code not added (related to configuring the graph)
// If I use this SVG I can add the lines
const svg = d3.select("#chart").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.attr("id", "chart-svg")
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
..... // other code not added (related to rendering the points using the data)
}
return (
<div className="chart-container">
<div id="chart"></div>
</div>
);
}
Figured it out. I need to reset the x and y ranges again, as it was rendering the lines off screen.

Resources