Update d3 data react hooks - javascript

Got this piece of code:
export default function Chart({ data, changeData}) {
console.log(data);
const ref = useRef();
const createGraph = (data) => {
var margin = { top: 20, right: 20, bottom: 30, left: 40 },
width = 1360 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
var x = d3.scaleBand().range([0, width]).padding(0.1);
var y = d3.scaleLinear().range([height, 0]);
var svg = d3
.select(ref.current)
.append("svg")
.attr("id", "chart")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
x.domain(
data.map(function (d) {
return d.name;
})
);
y.domain([
0,
d3.max(data, function (d) {
return d.number;
}),
]);
// append the rectangles for the bar chart
const bars = svg.selectAll().data(data).enter().append("rect");
bars
.attr("class", "bar")
.attr("x", function (d) {
return x(d.name);
})
.attr("width", x.bandwidth())
.attr("y", function (d) {
return y(d.number);
})
.attr("fill", "pink")
.attr("height", function (d) {
return height - y(d.number);
})
.on("mouseenter", function (actual, i) {
d3.select(this).attr("opacity", 0.5);
d3.select(this)
.transition()
.duration(300)
.attr("opacity", 0.6)
.attr("x", (a) => x(a.name) - 5)
.attr("width", x.bandwidth() + 10);
})
.on("mouseleave", function (actual, i) {
d3.select(this).attr("opacity", 1);
d3.select(this)
.transition()
.duration(300)
.attr("opacity", 1)
.attr("x", (a) => x(a.name))
.attr("width", x.bandwidth());
});
bars
.append("text")
.attr("class", "value")
.attr("x", (a) => x(a.name) + x.bandwidth() / 2)
.attr("y", (a) => y(a.number) + 30)
.attr("fill", "blue")
.attr("text-anchor", "middle")
.text((a) => `${a.number}%`);
d3.selectAll("bars").append("text").attr("class", "divergence");
// add the x Axis
svg
.append("g")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x));
// add the y Axis
svg.append("g").call(d3.axisLeft(y));
};
useEffect(() => {
createGraph(data);
}, [data]);
return (
<React.Fragment>
<Filter>
<h2>By</h2>
<span>Popularity</span>
<span onClick={() => changeData()}>Following</span>
</Filter>
<div style={{ marginLeft: "100px" }} ref={ref}></div>
</React.Fragment>
);
}
How does one update the data with every render?
I tried creating a function that removes the SVG and adds a new one but the positions are messed up after first render, also I tried using exit() and remove() in useEffect but no results.
The result that I'm trying to achieve is that I have a graph and the data is fetched and passed into this component and whenever I press a button, the data will change. I'm not looking at any animations right now, I just want to see how I can change the data .

It's a bit late but in case anyone happens on this question while searching.
useEffect(() => {
*// your d3 code here*
instead of using .enter().append('rect')
use .join('rect')
}, [data]) *// runs the d3 code every time the data changes*
more about .join() https://observablehq.com/#d3/selection-join
In the return statement specify the svg and g tags.
Instead of ref on the div, do useRef on the svg tag
Instead of .append(g), do .select('.x-axis') etc
This is so that you do not append new g every time the useEffect is run
<svg ref={d3Container}>
<g className="chart">
<g className="x-axis" />
<g className="y-axis" />
</g>
</svg>
I learnt the above after watching the 1st and 3rd video in this series https://muratorium.com/using-react-hooks-with-d3

Related

D3 Text Slice based on the width of rect

I am building a tree map with D3 v4 and all good so far. However, some of the text within their respective rectangles goes out over the edge of the rectangle. I want to use text slice to cut off the text if it does this, and instead put in three dots.
As a test, I have been able to get the slice function to truncate text that goes beyond let's say 5 characters, but when I try to specify that I want the slice function to truncate based on the width of the corresponding rectangle, it doesn't work on all except one (which I think is because it goes out over the edge of the whole tree map.
I can't seem to find a way to pull in the width of the rectangles to the slice function in order to compare it to the width of the text.
// set the dimensions and margins of the graph
var margin = {top: 10, right: 10, bottom: 10, left: 10},
width = 945 - margin.left - margin.right,
height = 1145 - margin.top - margin.bottom;
// append the svg object to the body of the page
var svg = d3.select("#my_dataviz")
.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform",
"translate(" + margin.left + "," + margin.top + ")");
// Read data
d3.csv('https://raw.githubusercontent.com/rootseire/survey/main/treemap-data.csv', function(data) {
// stratify the data: reformatting for d3.js
var root = d3.stratify()
.id(function(d) {
return d.name; }) // Name of the entity (column name is name in csv)
.parentId(function(d) { return d.parent; }) // Name of the parent (column name is parent in csv)
(data);
root.sum(function(d) { return +d.value }) // Compute the numeric value for each entity
// Then d3.treemap computes the position of each element of the hierarchy
// The coordinates are added to the root object above
d3.treemap()
.size([width, height])
.padding(4)
(root)
// use this information to add rectangles:
svg
.selectAll("rect")
.data(root.leaves())
.enter()
.append("rect")
.attr('x', function (d) { return d.x0; })
.attr('y', function (d) { return d.y0; })
.attr('width', function (d) { return d.x1 - d.x0; })
.attr('height', function (d) { return d.y1 - d.y0; })
.style("stroke", "black")
.style("fill", "#94C162")
.attr("class", "label")
.on("mouseover", function(d) {
tip.style("opacity", 1)
.html("Genre: " + d.data.name + "<br/> Number: " + d.value + "<br/>")
.style("left", (d3.event.pageX-25) + "px")
.style("top", (d3.event.pageY-25) + "px")
})
.on("mouseout", function(d) {
tip.style("opacity", 0)
});
svg
.selectAll("text")
.data(root.leaves())
.enter()
.append("text")
.attr("x", function(d){ return d.x0+6}) // +10 to adjust position (more right)
.attr("y", function(d){ return d.y0+15}) // +20 to adjust position (lower)
.attr('dy', 0) // here
.text(function(d){ return d.data.name + ' (' + d.data.value +')' })
.attr("font-size", "15px")
.attr("fill", "black")
.each(slice);
})
// Define the div for the tooltip
var tip = d3.select("#my_dataviz").append("div")
.attr("class", "tooltip")
.style("opacity", 0)
// Add events to circles
d3.selectAll('.label')
.attr("x", function(t) {
return Math.max(0, 100-this.textLength.baseVal.value);
});
function slice(d) {
var self = d3.select(this),
textLength = self.node().getComputedTextLength(),
text = self.text();
while (textLength > text.getBoundingClientRect().width && text.length > 0) {
text = text.slice(0, 5);
self.text(text + '...');
textLength = self.node().getComputedTextLength();
}
}
.tooltip {
position: absolute;
pointer-events: none;
background: #000;
color: #fff;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>
<!DOCTYPE html>
<head>
<script type="text/javascript" src="https://raw.githubusercontent.com/rootseire/survey/main/word_wrap.js"></script>
</head>
<meta charset="utf-8">
<body>
<div id="my_dataviz"></div>
</body>
</html>
Any help greatly appreciated.

text labels are wrong in grouped bar chart d3js

I am newbie in d3js, I do not know why all labels in the bar are wrong.
My code and captures are shown as below, then you can see that all labels are different from my data.
Anyone know what's going on in my text label section?
// set the dimensions and margins of the graph
var margin = { top: 10, right: 30, bottom: 40, left: 50 },
width = 700 - margin.left - margin.right,
height = 400 - margin.top - margin.bottom;
const dataUrl = "https://raw.githubusercontent.com/yushinglui/IV/main/time_distance_status_v2.csv"
//fetch the data
d3.csv(dataUrl)
.then((data) => {
// append the svg object to the body of the page
var svg = d3.select("#graph-2")
.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform",
"translate(" + margin.left + "," + margin.top + ")")
// List of subgroups = header of the csv files = soil condition here
var subgroups = data.columns.slice(1)
// List of groups = species here = value of the first column called group -> I show them on the X axis
var groups = d3.map(data, function (d) { return (d.startTime) })
// Add X axis
var x = d3.scaleBand()
.domain(groups)
.range([0, width])
.padding([0.2])
svg.append("g")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x).tickSize(0));
// Add Y axis
var y = d3.scaleLinear()
.domain([0, 20])
.range([height, 0]);
svg.append("g")
.call(d3.axisLeft(y));
// Another scale for subgroup position?
var xSubgroup = d3.scaleBand()
.domain(subgroups)
.range([0, x.bandwidth()])
.padding([0.05])
// color palette = one color per subgroup
var color = d3.scaleOrdinal()
.domain(subgroups)
.range(['#98abc5', '#8a89a6'])
// Show the bars
svg.append("g")
.selectAll("g")
// Enter in data = loop group per group
.data(data)
.enter()
.append("g")
.attr("transform", function (d) { return "translate(" + x(d.startTime) + ",0)"; })
.selectAll("rect")
.data(function (d) { return subgroups.map(function (key) { return { key: key, value: d[key] }; }); })
.enter()
.append("rect")
.attr("x", function (d) { return xSubgroup(d.key); })
.attr("y", function (d) { return y(d.value); })
.attr("width", xSubgroup.bandwidth())
.attr("height", function (d) { return height - y(d.value); })
.attr("fill", function (d) { return color(d.key); })
// mouseover and mouseout animation
.on("mouseover", function (d) {
d3.select(this).style("fill", d3.rgb(color(d.key)).darker(2))
})
.on("mouseout", function (d) {
d3.select(this).style("fill", function (d) { return color(d.key); })
})
//axis labels
svg.append('text')
.attr('x', - (height / 2))
.attr('y', width - 650)
.attr('transform', 'rotate(-90)')
.attr('text-anchor', 'middle')
.style("font-size", "17px")
.text('Average Distance');
svg.append('text')
.attr('x', 300)
.attr('y', width - 240)
.attr('transform', 'rotate()')
.attr('text-anchor', 'middle')
.style("font-size", "17px")
.text('Start Time');
// legend
svg.append("circle").attr("cx", 200).attr("cy", 20).attr("r", 6).style("fill", "#98abc5")
svg.append("circle").attr("cx", 300).attr("cy", 20).attr("r", 6).style("fill", "#8a89a6")
svg.append("text").attr("x", 220).attr("y", 20).text("Present").style("font-size", "15px").attr("alignment-baseline", "middle")
svg.append("text").attr("x", 320).attr("y", 20).text("Absent").style("font-size", "15px").attr("alignment-baseline", "middle")
//text labels on bars -- all labels wrong!!
svg.append("g")
.selectAll("g")
// Enter in data = loop group per group
.data(data)
.enter()
.append("g")
.attr("transform", function (d) { return "translate(" + x(d.startTime) + ",0)"; })
.selectAll("text")
.data(function (d) { return subgroups.map(function (key) { return { key: key, value: d[key] }; }); })
.enter()
.append("text")
.text(function (d) { return y(d.value); })
.attr("font-family", "sans-serif")
.attr("font-size", "12px")
.attr("fill", "black")
.attr("text-anchor", "middle")
.attr("x", function (d) { return xSubgroup(d.key); })
.attr("y", function (d) { return y(d.value) + 10; })
});
My reference website:
http://plnkr.co/edit/9lAiAXwet1bCOYL58lWN?p=preview&preview
https://bl.ocks.org/bricedev/0d95074b6d83a77dc3ad
Your issue is that when you're appending the text, you inadvertently called the y function, which is used to get the y-location on where to insert the text. The numbers you're getting are actually y-location values, which seems completely random.
.text(function (d) { return y(d.value); }) // here is the issue
Change it to
.text(function (d) { return d.value; })
and it should work!

D3.js treemap cells overlap with each other

When I draw my treemap in the local server, the treemap cells overlap with each other. Ive tried using other tiling algorithms but that did not work. I have also tried messing round with my linear scales but I can seem to get the correct scaling. Would this be a scaling problem or something else. I have also messed with the transform attributes but that just made it even worse. The current one I have is the best one that I find works.
createTreemap() {
//***VARS */
var margin = { left: 100, right: 10, top: 10, bottom:100}
var svg = d3.select("svg")
//.attr("transform", function(d) { return "translate(" + margin.left + "," + margin.top+ ")"; });
var width = +svg.attr("width") - margin.left - margin.right //width = 960
var height = +svg.attr("height") - margin.top - margin.bottom //height = 570
// var width = +svg.attr("width")
// var height = +svg.attr("height")
//linear scales
var y = d3.scaleLinear()
.domain([0,height])
.range([0,height/2])
var x = d3.scaleLinear()
.domain([0,width])
.range([0,width/2])
var scale = d3.scaleLinear()
.domain([0,width])
.range([0,width/2])
//creating a treemap variable
var treemapLayout = d3.treemap()
.tile(d3.treemapBinary) //type of squares
.size([width/2,height/2]) //size
.round(true) //if number are decimal, round to int. when true
.paddingInner(1) //padding between rectangles (1px)
//****loading data into function****
d3.json("../static/warehouses.json").then(function(data){
var root = d3.hierarchy(data, d => d.warehouses)
.sum(function(d){return d.itemCount}) //formating data to a more complex hierarchy form
treemapLayout(root)//passing data stuct to treemap variable
console.log(treemapLayout(root))//logging to console
//setting canvas sizes
var cells = svg.selectAll("g")
.data(root.leaves())
.enter()
.append("g")
//.attr("transform", function(d) { return "translate(" + scale(d.x0) + "," + scale(d.y0)+ ")"; });
cells.append("rect")
.attr("x", function(d) { return d.x0 })
.attr("y", function(d) { return d.y0 })
.attr("width", function(d) { return d.x1 })
.attr("height", function(d) { return d.y1 })
.attr("fill", "#ccc")
cells.append("text")
.attr("x", function(d) { return d.x0 + 5 })
.attr("y", function(d) { return d.y0 + 15 })
.style("font", "15px monospace")
.text(function(d){ return d.data.name})
.attr("fill", "black")
cells.append("text")
.attr("x", function(d) { return d.x0 +5 })
.attr("y", function(d) { return d.y0 +27 })
.style("font", "10px monospace")
.text(function(d) { return "Item count: " + d.data.itemCount })
.attr("fill", "black")
})
},
To be specific the warehouse "PROVEEDOR" is overlaying or laid over the warehouse "Wec"
image of treemap what would be the cause of this? Because I thought d3 automatically figures out where each x0, y0, x1, y1 should go so they dont overlap?
Thank you for any help
Just learning d3 but ran into the same problem that brought me to this post. I solved it by debugging this code:
cells.append("rect")
.attr("x", function(d) { return d.x0 })
.attr("y", function(d) { return d.y0 })
.attr("width", function(d) { return d.x1 })
.attr("height", function(d) { return d.y1 })
.attr("fill", "#ccc")
with this code...
cell.append("rect")
.attr("width", d=> d.x1 - d.x0)
.attr("height", d => d.y1 - d.y0)

D3 components do not update in React

I am trying to update some bar graphs when a button is clicked.
Here's the button
<button className="login_buttons" onClick={this.setRecommendations}>
Click to See Top Tracks and Recommendations
</button>
it calls this function, which does successfully update all the states, and right now the graph displays dummy data just fine.
setRecommendations(){
getAlbums().then(albums =>{
this.setState({topAlbums: albums[0]});
this.setState({album_count: albums[1]});
});
getArtists().then(artists =>{
this.setState({topArtist: artists[0]});
this.setState({artist_count: artists[1]});
});
getGenres().then(genres =>{
this.setState({topGenre: genres[0]});
this.setState({genre_count: genres[1]});
});
popularityData().then(popData =>{
this.setState({popRange: popData[0]});
this.setState({pop_count: popData[1]});
});
recommendations().then(recs => {
this.setState({recommendations: recs});
});
}
and here's my Graph component
import React from 'react';
import * as d3 from "d3";
class Graphs extends React.Component {
componentDidMount() {
this.drawChart();
}
componentDidUpdate() {
d3.select(`#${this.props.graphID}`).remove()
this.drawChart();
}
drawChart() {
const data = this.props.data;
const labels = this.props.axisLabels;
const title = this.props.title;
const margins = {top:50, right:50, bottom:50, left:50}
const h = 600 - margins.top - margins.bottom;
const w = 600 - margins.right - margins.left;
var x = d3.scaleBand()
.range([0, w])
.domain(labels);
var y = d3.scaleLinear()
.range([h, 0])
.domain([0, d3.max(data)]);
const svg = d3.select(`#${this.props.graphID}`)
.append("svg")
.attr("width", w + margins.right + margins.left)
.attr("height", h + margins.top + margins.bottom)
.append("g")
.attr("transform", `translate(${margins.left}, ${margins.top})`);
svg.selectAll("rect")
.data(data)
.enter()
.append("rect")
.attr("y", (d) => y(d))
.attr("x", (d, i) => (x.bandwidth()*i + 10))
.attr("height", (d) => h - y(d))
.attr("width", x.bandwidth() - 10)
.attr("fill", "blue");
svg.append("g")
.attr("transform", `translate(0, ${h})`)
.call(d3.axisBottom(x))
svg.append("g")
.call(d3.axisLeft(y));
svg.append("text")
.attr("x", (w/2))
.attr("y", 0 - (margins.top / 2))
.attr("text-anchor", "middle")
.style("font-size", "16px")
.style("fill", "white")
.style("text-decoration", "underline")
.text(title);
}
render(){
return <div id={this.props.graphID} />
}
}
export default Graphs
And when I click the button, the Graphs that have dummy data in them now do actually disappear so componentsWillUpdate is called but it is not redrawing the graphs, and I do not understand why because compnentsDoMount calls this.drawChart() alright.
Don't remove the container:
d3.select(`#${this.props.graphID}`).remove()
Remove what's in it:
d3.select(`#${this.props.graphID}`).selectAll('*').remove()

Updating d3 scatterplot, new data points are not in the correct positions

Spent hours on this and still not really sure whats going wrong.
My plot is supposed to update based on a bunch of parameters the user selects. When the plot needs to add new data points the new points are not displayed correctly on the plot.
Check out the new plot:
With these parameters all the circles should be in a line. While the original "line" is in the correct location the new "line" does not match up with the grid.
Here is the function to make a new plot. This works fine, all the data points are where they should be.
export const newPlot = (Params) => {
d3.selectAll("svg").remove();
let margin = {top: 50, right: 20, bottom: 30, left: 40},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
let x = d3.scaleLinear().range([0, width]);
let y = d3.scaleLinear().range([height, 0]);
let svg = d3.select('.plot').append("svg")
.attr('class', 'svgPlot')
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform","translate(" + margin.left + "," + margin.top + ")");
d3.json(`../assets/data/${Params.type}${Params.Year}.json`, (error, data) => {
if (error) throw error;
const refinedData = parametrize(data, Params);
refinedData.forEach((d) => {
d[Params.xSelect] = Number(d[Params.xSelect]);
d[Params.ySelect] = Number(d[Params.ySelect]);
});
let min = d3.min(refinedData,(d) => d[Params.xSelect]);
x.domain([(min - 2 <= 0 ? 0 : min - 2),
d3.max(refinedData,(d) => d[Params.xSelect])]);
y.domain([0, d3.max(refinedData,(d) => d[Params.ySelect])]);
svg.selectAll("circles")
.data(refinedData)
.enter().append("circle")
.attr('id', (d) => `${d.Player}`)
.attr("r", 5)
.attr("cx", (d) => x((d[Params.xSelect])) )
.attr("cy", (d) => y((d[Params.ySelect])) );
svg.append("g")
.attr("class", "x-axis")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x));
svg.append("g")
.attr("class", "y-axis")
.call(d3.axisLeft(y));
svg.append('text')
.attr("class", "label")
.attr('id', 'xlabel')
.attr("transform","translate(" + (width - 20) + " ," + (height-5) + ")")
.style("fill", "white")
.style("text-anchor", "middle")
.text(`${Params.xSelect}`);
svg.append('text')
.attr("class", "label")
.attr('id', 'ylabel')
.attr("transform", "rotate(-90)")
.attr("y", 1)
.attr("x", (height/2 - 250))
.attr("dy", "1em")
.style("font-family", "sans-serif")
.style("fill", "white")
.style("text-anchor", "middle")
.text(`${Params.ySelect}`);
});
};
Here is the update function. Circles that are added are not in the correct location and are all offset by the same amount.
export const rePlot = (Params) => {
let margin = {top: 50, right: 20, bottom: 30, left: 40},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
let xUp = d3.scaleLinear().range([0, width]);
let yUp = d3.scaleLinear().range([height, 0]);
let tooltip = d3.select("body").append("div")
.attr("class", "toolTip")
.style("display", "none");
let svg = d3.select('.svgPlot');
d3.json(`../assets/data/${Params.type}${Params.Year}.json`, (error, data) => {
if (error) throw error;
const refinedData = parametrize(data, Params);
refinedData.forEach((d) => {
d[Params.xSelect] = Number(d[Params.xSelect]);
d[Params.ySelect] = Number(d[Params.ySelect]);
});
let min = d3.min(refinedData,(d) => d[Params.xSelect]);
xUp.domain([(min - 2 <= 0 ? 0 : min - 2),
d3.max(refinedData,(d) => d[Params.xSelect])]);
yUp.domain([0, d3.max(refinedData,(d) => d[Params.ySelect])]);
svg.select('.x-axis')
.transition()
.duration(1000)
.call(d3.axisBottom(xUp));
svg.select('.y-axis')
.transition()
.duration(1000)
.call(d3.axisLeft(yUp));
svg.select('#xlabel')
.text(`${Params.xSelect}`);
svg.select('#ylabel')
.text(`${Params.ySelect}`);
let circle = svg.selectAll("circle")
.data(refinedData);
circle.exit()
.transition()
.remove();
circle.transition()
.duration(1000)
.attr("r", 5)
.attr("cx", (d) => xUp((d[Params.xSelect])) )
.attr("cy", (d) => yUp((d[Params.ySelect])) );
circle.enter().append("circle")
.attr('id', (d) => `${d.Player}`)
.attr("r", 5)
.attr("cx", (d) => xUp((d[Params.xSelect])) )
.attr("cy", (d) => yUp((d[Params.ySelect])) );
});
}
Your first set of circles gets appended to a group that is translated:
let svg = d3.select('.plot').append("svg")
.attr('class', 'svgPlot')
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform","translate(" + margin.left + "," + margin.top + ")");
In this case, the svg variable refers to a translated group. However, when you later reselect, you actually append to the root SVG element:
let svg = d3.select('.svgPlot');
This is the origin of the difference.

Categories

Resources