I am trying to update a barchart using React and D3. The functionality I am trying to implement is basically the one shown in this d3 example: https://d3-graph-gallery.com/graph/barplot_button_data_hard.html. Thus I was trying to adapt the code to my react app.
Thus, I want to update the data when my state changes with data from the state. The problem I am facing is that the y-axis does not show any ticks at all, and the x-axis does not update the ticks correctly but rather adds the new tick values to the axis.
My code is as follows:
const BarChart = () => {
let statePlots = useSelector((state) => state.plots);
const data = useSelector((state) => state.plots.data);
const ref = useRef();
useEffect(() => {
const svg = d3.select(ref.current);
// set the dimensions and margins of the graph
const margin = { top: 30, right: 30, bottom: 70, left: 60 },
width = 460 - margin.left - margin.right,
height = 400 - margin.top - margin.bottom;
// add to the svg object of the page
svg
.attr("width", width margin.left margin.right)
.attr("height", height margin.top margin.bottom)
.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`);
// Initialize the X axis
const x = d3.scaleBand().range([0, width]).padding(0.2);
const xAxis = svg.append("g").attr("transform", `translate(0,${height})`);
// Initialize the Y axis
const y = d3.scaleLinear().range([height, 0]);
const yAxis = svg.append("g").attr("class", "myYaxis");
// Update the X axis
x.domain(data.map((d) => d.group));
xAxis.call(d3.axisBottom(x));
// Update the Y axis
y.domain([0, d3.max(data, (d) => d.value)]);
yAxis.transition().duration(1000).call(d3.axisLeft(y));
// Create the u variable
var u = svg.selectAll("rect").data(data);
u.join("rect") // Add a new rect for each new elements
.transition()
.duration(1000)
.attr("x", (d) => x(d.group))
.attr("y", (d) => y(d.value))
.attr("width", x.bandwidth())
.attr("height", (d) => height - y(d.value))
.attr("fill", "#69b3a2");
}, [statePlots]);
return (
<div>
<div>D3 Plot</div>
<svg ref={ref}></svg>
</div>
);
};
The code above does switch the data, but as I mentioned, it doesn't correctly update the ticks. I am very new to d3.js and also not an expert on React. Any help is appreciated. Thanks in advance
CodePudding user response:
The first issue I identified with your adaptation is that you're not separating initialization from the update. Notice there are two different blocks of code in the d3 example you linked, one for initializing which is in the main body of the script, and the other for the update, which is inside the update
function. You can separate them using two useEffect hooks and an additional state to check the graph has been initialized.
const [loading, setLoading] = useState(true);
const ref = useRef();
const chartConfig = useRef();
useEffect(() => {
// set up d3 objects (chartConfig)
setLoading(false);
}, []);
useEffect(() => {
if (loading || !chartConfig.current) return;
const { svg, x, xAxis, y, yAxis } = chartConfig.current;
// update chart UI
}, [data, loading]);
Additionally, you mistakenly assigned the SVG container by not chaining the "g" element selection, where you should draw all other elements since it has the margins. This was causing the ticks to be too far to the left.
Instead of this
const svg = d3.select(ref.current);
// add to the svg object of the page
svg
.attr("width", width margin.left margin.right)
.attr("height", height margin.top margin.bottom)
.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`);
You need to leave as in the example
const svg = d3
.select(ref.current)
.attr("width", width margin.left margin.right)
.attr("height", height margin.top margin.bottom)
.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`)
And lastly, you also need to remove the old elements when updating the chart
u.exit().remove();
You can check my adaptation of the example here and change it to your needs https://codesandbox.io/s/d3-charts-initializing-and-updating-leke3m?file=/src/App.js
CodePudding user response:
I think you are not calling enter method and instead of join you should call append
So can you try this and check please
u
.enter()
.append("rect")
.transition()
.duration(1000)
.attr("x", d => x(d.group))
.attr("y", d => y(d.value))
.attr("width", x.bandwidth())
.attr("height", d => height - y(d.value))
.attr("fill", "#69b3a2")