D3 Chart does not redraw on new data - reactjs

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.

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.

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 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;

World Map Zoom with D3 + ReactJS

I found following code for zoom in a simple D3 solution which works well with Javascript:
var zoom = d3.behavior.zoom()
.on("zoom", function() {
projection.translate(d3.event.translate).scale(d3.event.scale);
feature.attr("d", path);
circle.attr("transform", ctr);
})
;
zoom.translate(projection.translate())
.scale(projection.scale())
.scaleExtent([h / 6, h])
;
When I try to convert this code in D3 + ReactJS, I get issues. There have been many since I've tried so many different solutions. The latest one zooms my world map once but it's all a mess.
This is my code:
import React, { Component } from 'react';
import 'd3';
import * as d3Z from 'd3-zoom'
import * as d3 from 'd3-selection';
//import d3Scale from 'd3-scale'
import { geoMercator, geoPath } from 'd3-geo';
import './WorldMap.css';
import countries from '../resources/world-countries.json';
//
export default class WorldMap extends Component {
componentDidUpdate() {
const width = 1300, //document.body.clientWidth,
height = 600;//document.body.clientHeight;
const circleRadius = 2;
const lstRadius = [7, circleRadius];
const lstColor = ["green", "white"];
const duration = 500;
const lstShots = [];
const projection = geoMercator();
const path = geoPath()
.projection(projection);
const d3Zoom = d3Z.zoom();
for (var i = 0; i < this.props.data.length; i++) {
lstShots.push(JSON.parse(JSON.stringify(this.props.data[i])));
}
const getCirclePos = (d) => {
return "translate(" + projection([d.long, d.lat]) + ")";
}
const svgObj = d3.select(this.refs.svgMap);
const feature = svgObj
.selectAll("path.feature")
.data(countries.features)
.enter().append("path")
.attr("class", "feature");
projection
.scale(width / 6.5)
.translate([width / 2, height / 1.6]);
const zoom = d3Zoom
.on("zoom", function() {
console.log(d3.event);
projection.translate([d3.event.transform.x, d3.event.transform.y]).scale(d3.event.scale);
feature.attr("d", path);
// circle.attr("transform", getCirclePos);
})
;
svgObj.call(zoom);
// console.log(zoom);
// zoom.translateTo(projection.translate())
// .scale(projection.scale())
// .scaleExtent([height / 6, height])
// ;
feature.attr("d", path);
// eslint-disable-next-line
const circle = svgObj.selectAll("circle")
.data(lstShots)
.enter()
.append("circle")
.attr("r", circleRadius)
.attr("fill", 'white')
.attr("transform", getCirclePos)
.attr("node", function(d) { return JSON.stringify(d); })
.style("cursor", "pointer");
}
render() {
return (
<svg ref="svgMap"></svg>
);
}
}
The problem is that d3.event does not contain fields of translate or scale which I have seen people use in other example of d3-zoom. While I have replaced translate with transform in zoom function, I don't know what to replace d3.event.scale with.
If you port code from d3v3 to d3v4 you need to do it all, d3.event.scale does not exist in d3v4
projection.translate([d3.event.transform.x, d3.event.transform.y]).scale(d3.event.transform.k);
You need to initialise the zoom too
svgObj.call(zoom);
svgObj.call(zoom.transform, d3.zoomIdentity.translate(projection.translate()[0], projection.translate()[1]).scale(projection.scale()));
Don't use ref strings but Callback Refs
I needed to use componentDidMount() to see anything.

Resources