I have a flat JSON array and try to format it for a tree representation in D3.js v7 using the below code. I grouped the data and then used hierarchy to make the link and nodes as is described in the documentation but when I make the graph it produces an empty root and last children.
see: https://codepen.io/nvelden/pen/LYmveWz?editors=1111
//Load data
const data = [
{"root":"project","project_nr":"project 1","department":"1","devision":"A"},
{"root":"project","project_nr":"project 1","department":"1","devision":"B"},
{"root":"project","project_nr":"project 1","department":"2","devision":"A"},
{"root":"project","project_nr":"project 2","department":"3","devision":"A"}
]
var margin = {top: 10, right: 10, bottom: 10, left: 50},
width = 500 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
var tree = d3.cluster()
.size([height, width])
.size([height-margin.top-margin.bottom,width-margin.left-margin.right]);
var groupedData = d3.group(data,
d => d.root,
d => d.project_nr,
d => d.department,
d => d.devision)
//Create root
var root = d3.hierarchy(groupedData)
//Attach canvas element
var svg = d3.select("body")
.append("svg")
.attr("width", 1000)
.attr("height", 1000);
var g = svg
.append("g")
.attr('transform','translate('+ margin.left +','+ margin.right +')');
var link = g.selectAll(".link")
.data(tree(root).links())
.enter()
.append("path")
.attr("class", "link")
.attr("d", d3.linkHorizontal()
.x(function(d) {return d.y;})
.y(function(d) {return d.x;}));
var node = g.selectAll(".node")
.data(root.descendants())
.enter()
.append("g")
.attr("class", "link")
.attr("class", d =>
{ return "node" + (d.children ? " node--internal" : " node--leaf")})
.attr("transform", d =>
{ return "translate(" + d.y + ","+ d.x + ")" ; })
var text = g.selectAll("text")
.data(root.descendants())
.enter().append("text")
.text(d => d.data[0])
.attr('dy', "0.32em")
.attr("class", "label glow")
.attr('text-anchor', "center")
.attr("x", d => d.y)
.attr("y", d => d.x);
node.append("circle")
.attr("r", 2.5)
How should I format the data to get the tree in the below graph?
If we assume that there is only one project at the top, then it would be better not to group by project. Similarly, since the divisions are leaf-nodes, it would be better not to group by division.
groupedData = d3.group(
data,
(d) => d.project_nr,
(d) => d.department
);
However now we're missing the "project" name in the root node, and the division name in the leaf nodes. These can be retrieved by using:
({ data }) => Array.isArray(data) ? data[0] || "project" : data.devision
Here's a notebook with the complete code:
https://observablehq.com/#recifs/data-to-tree--support
I am trying to graph this set of data (showing a snippet of a larger set) link to photo of chart shown below
month_started Member_casual count_of_rides
7 casual 435,289
8 casual 407,009
9 member 385,843
8 member 385,305
7 member 373,710
As you can see I have a month_started that maps to a member or casual attribute. I want to make a staked bar chart of the month_started as an independent variable (x) and the count_of_rides as a dependent (y) with the casual value stacked on top of the member per month.
I currently have a d3 bar chart that overlays both month_started = 7 on top of each other instead of stacked. I'm not sure how I can get d3 to recognize that it needs to read off of the member_casual variable to separate the two values.
I have additional code but here is the relevant sections I believe
Also, the .data(bike_trips) I believe should be .data(stackedData) but for some reason doesn't show up any bars, which makes me think my error could be with the stakedData variable.
Could anyone point me in the right direction please
bike_trip_chart = {
const svg = d3.create("svg")
.attr("viewBox", [0, 0, width, height]);
svg.append("g")
.attr("fill-opacity", .8)
.selectAll("rect")
.data(bike_trips)
.join("rect")
.attr("fill", (d) => colorScale(d.key))
.attr("x", d => x(d.month_started))
.attr("width", x.bandwidth())
.attr("y", d => y1(d.count_of_rides))
.attr("height", d => y1(0) - y1(d.count_of_rides));
svg.append("path")
.attr("fill", "none")
.attr("stroke", "#274e13")
.attr("stroke-miterlimit", 4)
.attr("stroke-width", 4)
.attr("d", line(chicago_weather));
svg.append("g")
.attr("fill", "none")
.attr("pointer-events", "all")
.selectAll("rect")
.data(bike_trips)
.join("rect")
.attr("x", d => x(d.month_started))
.attr("width", x.bandwidth())
.attr("y", 0)
.attr("height", height);
svg.append("g")
.call(xAxis);
svg.append("g")
.call(y1Axis);
svg.append("g")
.call(y2Axis);
return svg.node();
}
stackedData = d3.stack()
.keys(["member","casual"])
(bike_trips)
colorScale = d3.scaleOrdinal()
.domain(["member","casual"])
.range(["#E4BA14","#45818e"]);
Before you pass your data to d3.stack(), you want it to look like this:
[
{month: 7, casual: 435289, member: 373710},
{month: 8, casual: 407009, member: 385305},
{month: 9, member: 385843}
]
Here's a full example:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<script src="https://d3js.org/d3.v7.js"></script>
</head>
<body>
<div id="chart"></div>
<script>
// set up
const margin = { top: 25, right: 25, bottom: 50, left: 50 };
const width = 500 - margin.left - margin.right;
const height = 500 - margin.top - margin.bottom;
const svg = d3.select('#chart')
.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})`);
// data
const data = [
{ month_started: 7, member_casual: "casual", count_of_rides: 435289 },
{ month_started: 8, member_casual: "casual", count_of_rides: 407009 },
{ month_started: 9, member_casual: "member", count_of_rides: 385843 },
{ month_started: 8, member_casual: "member", count_of_rides: 385305 },
{ month_started: 7, member_casual: "member", count_of_rides: 373710 },
];
const keys = ["member", "casual"];
const months = Array.from(new Set(data.map(d => d.month_started))).sort(d3.ascending);
// get a map from the month_started to the member_casual to the count_of_rides
const monthToTypeToCount = d3.rollup(
data,
// g is an array that contains a single element
// get the count for this element
g => g[0].count_of_rides,
// group by month first
d => d.month_started,
// then group by member of casual
d => d.member_casual
);
// put the data in the format mentioned above
const countsByMonth = Array.from(monthToTypeToCount, ([month, counts]) => {
// counts is a map from member_casual to count_of_rides
counts.set("month", month);
counts.set("total", d3.sum(counts.values()));
// turn the map into an object
return Object.fromEntries(counts);
});
const stackedData = d3.stack()
.keys(keys)
// return 0 if a month doesn't have a count for member/casual
.value((d, key) => d[key] ?? 0)
(countsByMonth);
// scales
const x = d3.scaleBand()
.domain(months)
.range([0, width])
.padding(0.25);
const y = d3.scaleLinear()
.domain([0, d3.max(countsByMonth, d => d.total)])
.range([height, 0]);
const color = d3.scaleOrdinal()
.domain(keys)
.range(["#E4BA14","#45818e"]);
// axes
const xAxis = d3.axisBottom(x);
svg.append('g')
.attr('transform', `translate(0,${height})`)
.call(xAxis)
const yAxis = d3.axisLeft(y);
svg.append('g')
.call(yAxis);
// draw bars
const groups = svg.append('g')
.selectAll('g')
.data(stackedData)
.join('g')
.attr('fill', d => color(d.key));
groups.selectAll('rect')
.data(d => d)
.join('rect')
.attr('x', d => x(d.data.month))
.attr('y', d => y(d[1]))
.attr('width', x.bandwidth())
.attr('height', d => y(d[0]) - y(d[1]));
// title
svg.append('g')
.attr('transform', `translate(${width / 2},${-10})`)
.attr('font-family', 'sans-serif')
.append('text')
.attr('text-anchor', 'middle')
.call(
text => text.append('tspan')
.attr('fill', color('member'))
.text('member')
)
.call(
text => text.append('tspan')
.attr('fill', 'black')
.text(' vs. ')
)
.call(
text => text.append('tspan')
.attr('fill', color('casual'))
.text('casual')
)
</script>
</body>
</html>
I would like to draw rectangle on a barchart just like a animation. I only need to draw one rectangle one time (maybe keep it for 0.5 secs) then remove it and draw another rectangle.
Currently all rectangles will be draw on screen! I try to use exit pattern but not work!
barchart_hist()
function barchart_hist() {
var m = [10, 80, 25, 80]
var w = 600 - m[1] - m[3]
var h = 400 - m[0] - m[2]
var y_data = [2,1,5,6,2,3,0]
var graph = d3.select('body').append("svg")
.attr("width", w + m[1] + m[3])
.attr("height", h + m[0] + m[2])
.append("svg:g")
.attr("transform", "translate(" + m[3] + "," + m[0] + ")")
var x = d3.scaleBand()
.domain(d3.range(y_data.length))
.range([0, w])
.padding(0.05);
var y = d3.scaleLinear()
.domain([0, d3.max(y_data)])
.range([h,0])
var xAxis = d3.axisBottom(x)
.ticks(y_data.length)
var yAxisLeft = d3.axisLeft(y)
.ticks(d3.max(y_data))
graph.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + (h) + ")")
.call(xAxis);
graph.append("g")
.attr("class", "y axis")
.attr("transform", "translate(-25,0)")
.call(yAxisLeft);
graph.selectAll("bar")
.data(y_data)
.enter().append("rect")
.style("fill", "none")
.attr('stroke','black')
.attr("x", (d,i) => x(i))
.attr("y", (d,i) => y(d))
.attr("width", x.bandwidth())
.attr("height", (d,i) => h - y(d));
var rdata = [[0,1],[3,4],[2,4],[5,6],[4,6],[1,6]]
var box = graph.selectAll('box')
.data(rdata)
var boxenter = box.enter()
.append('rect')
.attr('stroke','red')
.attr('stroke-width',2)
.attr('fill','none')
.attr('x',d => {
return x(d[0])})
.attr('y',d => y(y_data[d[0]]))
.attr('width',d => {
return x(d[1])-x(d[0])})
.attr('height',d => {
return y(0) - y(y_data[d[0]])
})
box.exit().transition().duration(500).remove()
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.7.0/d3.min.js"></script>
There appear to be two key issues:
Enter/Update/Exit
The exit selection only contains elements when there are selected elements in the DOM which do not have corresponding items in the data array. In your case we have an empty selection with bound data:
// an empty selection (no elements with tag box exist):
var box = graph.selectAll('box')
.data(rdata) // bind data to the selection
Then we use the enter selection box.enter() to create elements so that every item in the data array as a corresponding element in the DOM. Since there are no matching elements in the DOM, we create a new element for every item in the DOM:
var boxenter = box.enter()
.append('rect')
...
The box exit selection, box.exit() contains any elements for which a corresponding item in the data array does not exist. selection.exit() does nothing other than return a selection. Since there was no pre-existing elements, no elements can be in the exit selection. So this:
box.exit().transition().duration(500).remove()
Does nothing as the exit selection is empty.
For reference, any existing elements that are not included in the exit selection are in the update selection (box).
If we want to do something with the newly entered boxes, we can use the boxenter selection.
Without using a key function with .data(), only one of the enter/exit selections can contain elements: either we have too many elements (exit selection contains elements) or not enough (enter selection contains elements).
Transitions
Your use of selection.transition().duration(500).remove() will remove a selection, but it will not transition anything. Transitions are used to change an attribute/style/property over time. You haven't specified what property you want to transition. selection.transition().duration(500).remove() will simply remove the selection from the DOM after 500 ms.
Instead, you need to transition an attribute/style/property, eg:
selection.transition() // return a transition (not a selection).
.attr('y',d => y(0))
.attr('height',0)
.duration(10000)
.remove();
Note, transitions have similar methods as selections - but there are methods for each which do not apply to the other. Here .attr(), .duration(), and .remove() are common to both transitions and selections, but .duration(), for example is only for transitions.
We can just chain the transition to the enter selection, since we want to remove it right after we add it.
Here's the above in action:
barchart_hist()
function barchart_hist() {
var m = [10, 80, 25, 80]
var w = 600 - m[1] - m[3]
var h = 400 - m[0] - m[2]
var y_data = [2,1,5,6,2,3,0]
var graph = d3.select('body').append("svg")
.attr("width", w + m[1] + m[3])
.attr("height", h + m[0] + m[2])
.append("svg:g")
.attr("transform", "translate(" + m[3] + "," + m[0] + ")")
var x = d3.scaleBand()
.domain(d3.range(y_data.length))
.range([0, w])
.padding(0.05);
var y = d3.scaleLinear()
.domain([0, d3.max(y_data)])
.range([h,0])
var xAxis = d3.axisBottom(x)
.ticks(y_data.length)
var yAxisLeft = d3.axisLeft(y)
.ticks(d3.max(y_data))
graph.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + (h) + ")")
.call(xAxis);
graph.append("g")
.attr("class", "y axis")
.attr("transform", "translate(-25,0)")
.call(yAxisLeft);
graph.selectAll("bar")
.data(y_data)
.enter().append("rect")
.style("fill", "none")
.attr('stroke','black')
.attr("x", (d,i) => x(i))
.attr("y", (d,i) => y(d))
.attr("width", x.bandwidth())
.attr("height", (d,i) => h - y(d));
var rdata = [[0,1],[3,4],[2,4],[5,6],[4,6],[1,6]]
var box = graph.selectAll('box')
.data(rdata)
var boxenter = box.enter()
.append('rect')
.attr('stroke','red')
.attr('stroke-width',2)
.attr('fill','none')
.attr('x',d => {
return x(d[0])})
.attr('y',d => y(y_data[d[0]]))
.attr('width',d => {
return x(d[1])-x(d[0])})
.attr('height',d => {
return y(0) - y(y_data[d[0]])
})
.transition()
.attr('y',d => y(0))
.attr('height',0)
.duration(10000)
.remove();
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.7.0/d3.min.js"></script>
Consecutive Transitions
Ok, now we can look at putting the above together for consecutive transitions. This would be easiest if you had the same number of elements in each series: we could just use one data array that has properties representing the values in both arrays and transition from one to the other while also entering items only once. If desired, I can also expand on that approach.
But, as you have different sized arrays (or you might have dynamic data), we'll need a different approach. The below uses a standardized data structure for both arrays so that we can use the same update function for each.
I modified the last element in the first data array so it is visible in the chart for demonstration of the code (so you know it isn't errantly missing)
// The data:
var data1 = [[0,2],[1,1],[2,5],[3,6],[4,2],[5,3],[6,1]];
var data2 = [[0,1],[3,4],[2,4],[5,6],[4,6],[1,6]]
// The set up:
var m = [10, 80, 25, 80]
var w = 600 - m[1] - m[3]
var h = 300 - m[0] - m[2]
var graph = d3.select('body').append("svg")
.attr("width", w + m[1] + m[3])
.attr("height", h + m[0] + m[2])
.append("svg:g")
.attr("transform", "translate(" + m[3] + "," + m[0] + ")")
var xAxis = graph.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + (h) + ")")
var yAxis = graph.append("g")
.attr("class", "y axis")
.attr("transform", "translate(-25,0)")
// (For demonstration):
var i = 0;
var datasets = [data1, data2];
// Update function:
function update(data) {
// set up scales:
var x = d3.scaleBand()
.domain(d3.range(data.length))
.range([0, w])
.padding(0.05);
var y = d3.scaleLinear()
.domain([0, d3.max(data, d=>d[1])])
.range([h,0])
// set up axis:
xAxis.transition().call(d3.axisBottom(x));
yAxis.transition().call(d3.axisLeft(y));
// Enter then remove:
graph.selectAll(".bar")
.data(data)
.enter()
.append("rect")
.style("fill", "none")
.attr('stroke','black')
.attr("x", d=>x(d[0]))
.attr("width", x.bandwidth())
// Start new rects with zero height:
.attr("y", y(0))
.attr("height", 0)
// Transition up:
.transition()
.attr("y", d=>y(d[1]))
.attr("height", d=>y(0)-y(d[1]))
// Transition down:
.transition()
.delay(1000) // wait a bit first
.attr("y", y(0))
.attr("height", 0)
.remove()
.end()
// get next dataset, and repeat:
.then(()=>update(datasets[++i%2]));
// example to get one more data set and stop:
//.then(function() {
// if (data === data1) update(data2)
// else console.log("end");
//});
}
update(data1);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.7.0/d3.min.js"></script>
Note the use of transition.end() returns a promise once all transitions end whereas the use of transition.on("end", fn) will trigger on each element's transition end.
You've stated you want to remove the existing elements before updating with the new data. There are many questions on SO that ask how to remove all existing elements and then create new elements, this pattern omits update and exit selections and is frequently non-canonical. It may be that your second data array items don't represent any of the entities represented in the first data array - in which case this approach is fine. But, often this approach limits your functionality. Should you want to update the existing items with new data, we can alter the approach to make full use of updating existing data points (which may tell an interesting story, depending on your data and what it represents):
// The data:
var data1 = [[0,2],[1,1],[2,5],[3,6],[4,2],[5,3],[6,1]];
var data2 = [[0,1],[3,4],[2,4],[5,6],[4,6],[1,6]]
// The set up:
var m = [10, 80, 25, 80]
var w = 600 - m[1] - m[3]
var h = 300 - m[0] - m[2]
var graph = d3.select('body').append("svg")
.attr("width", w + m[1] + m[3])
.attr("height", h + m[0] + m[2])
.append("svg:g")
.attr("transform", "translate(" + m[3] + "," + m[0] + ")")
var xAxis = graph.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + (h) + ")")
var yAxis = graph.append("g")
.attr("class", "y axis")
.attr("transform", "translate(-25,0)")
// (For demonstration):
var i = 0;
var datasets = [data1, data2];
// Update function:
function update(data) {
// set up scales:
var x = d3.scaleBand()
.domain(d3.range(data.length))
.range([0, w])
.padding(0.05);
var y = d3.scaleLinear()
.domain([0, d3.max(data, d=>d[1])])
.range([h,0])
// set up axis:
xAxis.transition().delay(500).call(d3.axisBottom(x));
yAxis.transition().delay(500).call(d3.axisLeft(y));
// Enter then remove:
var selection = graph.selectAll(".bar")
.data(data);
var enterTransition = selection.enter()
.append("rect")
.attr("class","bar")
.style("fill", "steelblue")
.attr("opacity",0)
.attr('stroke','black')
.attr('stroke-width',1)
.attr("x", d=>x(d[0]))
.attr("width", x.bandwidth())
// Start new rects with zero height:
.attr("y", y(0))
.attr("height", 0)
// Transition up and remove color:
.transition()
.delay(500)
.duration(1000)
.style("fill","white")
.attr("opacity",1)
.attr("y", d=>y(d[1]))
.attr("height", d=>y(0)-y(d[1]))
.end()
var updateTransition = selection.transition()
.delay(500)
.duration(1000)
.attr("y", d=>y(d[1]))
.attr("height", d=>y(0)-y(d[1]))
.attr("x", d=>x(d[0]))
.attr("width", x.bandwidth())
.end()
var exitTransition = selection.exit()
.transition()
.duration(500)
.attr("opacity",0)
.style("fill","crimson")
.remove()
.end()
Promise.allSettled([enterTransition,updateTransition,exitTransition])
.then(()=>update(datasets[++i%2]));
}
update(data1);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.7.0/d3.min.js"></script>
I've added red for exit selection, blue for enter, with some opacity changes to highlight the changes better
You could consider this approach: when you create the box selection, append the rect but no other attribute thereby creating as many invisible rects as there are pairs in the rdata array.
You can then use selection.each() to iterate box. Since you know the index argument of each you can use that with selection.filter()
to identify the box to set styles and coordinates on; otherwise remove the stroke attribute to render the box invisible. Doing this within a setTimeout gives you the animated process.
I'd say the comment from #AndrewReid re omission of update/ exit being non-canonical applies to the approach below, but I feel it meets the brief.
// prep all the boxes (rect only - will be invisible)
var box = graph.selectAll('box')
.data(rdata)
.enter()
.append('rect');
// iterate boxes
box.each((d, i) => {
setTimeout(() => {
// otherBoxes are boxes not matching i argument in each
var otherBoxes = boxes.filter((d, i2) => i2 !== i);
otherBoxes.attr('stroke', 'none');
// thisbox is the currentbox per the i argument in each
var thisBox = boxes.filter((d, i2) => i2 === i);
// your original rendering code
thisBox
.attr('stroke','red')
.attr('stroke-width',2)
.attr('fill','none')
.attr('x',d => {
return x(d[0])})
.attr('y',d => y(y_data[d[0]]))
.attr('width',d => {
return x(d[1])-x(d[0])})
.attr('height',d => {
return y(0) - y(y_data[d[0]])
});
}, i * 1000); // 1s delay
});
Working example:
barchart_hist()
function barchart_hist() {
var m = [10, 80, 25, 80]
var w = 600 - m[1] - m[3]
var h = 200 - m[0] - m[2]
var y_data = [2,1,5,6,2,3,0]
var graph = d3.select('body').append("svg")
.attr("width", w + m[1] + m[3])
.attr("height", h + m[0] + m[2])
.append("svg:g")
.attr("transform", "translate(" + m[3] + "," + m[0] + ")")
var x = d3.scaleBand()
.domain(d3.range(y_data.length))
.range([0, w])
.padding(0.05);
var y = d3.scaleLinear()
.domain([0, d3.max(y_data)])
.range([h,0])
var xAxis = d3.axisBottom(x)
.ticks(y_data.length)
var yAxisLeft = d3.axisLeft(y)
.ticks(d3.max(y_data))
graph.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + (h) + ")")
.call(xAxis);
graph.append("g")
.attr("class", "y axis")
.attr("transform", "translate(-25,0)")
.call(yAxisLeft);
graph.selectAll("bar")
.data(y_data)
.enter().append("rect")
.style("fill", "none")
.attr('stroke','black')
.attr("x", (d,i) => x(i))
.attr("y", (d,i) => y(d))
.attr("width", x.bandwidth())
.attr("height", (d,i) => h - y(d));
var rdata = [[0,1],[3,4],[2,4],[5,6],[4,6],[1,6]]
// prep all the boxes (id only)
var boxes = graph.selectAll('box')
.data(rdata)
.enter()
.append('rect');
// iterate boxes
boxes.each((d, i) => {
setTimeout(() => {
var otherBoxes = boxes.filter((d, i2) => i2 !== i);
otherBoxes.attr('stroke', 'none');
var thisBox = boxes.filter((d, i2) => i2 === i);
thisBox
.attr('stroke','red')
.attr('stroke-width',2)
.attr('fill','none')
.attr('x',d => {
return x(d[0])})
.attr('y',d => y(y_data[d[0]]))
.attr('width',d => {
return x(d[1])-x(d[0])})
.attr('height',d => {
return y(0) - y(y_data[d[0]])
});
}, i * 1000);
});
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.7.0/d3.min.js"></script>
I'm using d3 to build a horizontal bar chart.
When it is first rendered the values are displayed within the bar:
However, when further data is added something is preventing it and the new bars from showing:
This problem is related to my code to display the values within the bars.
When I remove this chunk of code, the new bars show (just without the values in them):
Where am I going wrong?
function renderBarChart(data, metric, countryID) {
data = sortByHighestValues(data, metric)
const width = 0.9 * screen.width
const height = 0.8 * screen.height
const margin = { top: 0, right: 0, bottom: 20, left: 30 }
const innerHeight = height - margin.top - margin.bottom
const xScale = d3
.scaleLinear()
.domain([0, d3.max(data, (d) => d[metric])])
.range([margin.left, width]);
const yScale = d3
.scaleBand()
.domain(data.map((d) => d[countryID]))
.range([0, innerHeight])
.padding(0.2);
const yAxis = d3.axisLeft(yScale);
const xAxis = d3.axisBottom(xScale).ticks(10);
if (!barChartAxisRendered) {
renderYAxis(width, height, margin, yAxis)
renderXAxis(width, height, margin, xAxis, innerHeight)
} else {
updateXAxis(width, height, xAxis)
updateYAxis(width, height, yAxis)
}
barChartAxisRendered = true
renderBars(data, yScale, xScale, margin, metric, countryID)
};
function renderBars(data, yScale, xScale, margin, metric, countryID) {
let selectDataForBarCharts = d3.select("svg")
.selectAll("rect")
.data(data)
selectDataForBarCharts
.enter()
.append("rect")
.merge(selectDataForBarCharts)
.attr("fill", d => setBarColor(d))
.attr("y", (d) => yScale(d[countryID]))
.attr("width", (d) => xScale(d[metric]))
.attr("height", yScale.bandwidth())
.attr("transform", `translate(${margin.left}, ${margin.top})`)
selectDataForBarCharts
.enter()
.append("text")
.merge(selectDataForBarCharts)
.attr("class", "casesPerCapitaValues")
.attr('text-anchor', 'middle')
.attr("x", d => xScale(d[metric])-10)
.attr("y", d => yScale(d[countryID]) + 13)
.attr("fill", "white")
.style("font-size", "10px")
.text(d => d.casesPerCapita)
}
Your code breaks because you overwrite the rectangles with text. Make a different selections for texts and rects, or make a group containing corresponding rect+text of one row.
First, sorry that the title may not be clear since I am still new to D3 and Javascript.
I have a heat map visualisation. And I tried appending the country name as title for each square on which mouse cursor stays.
I've got the correct value of year and income from data in appendix.
However, I am stuck in retrieving the correct country value.
About the data structure in appendix: The income values of each country by years and the country name are stored in one array. And the value of country name is in the last index of array.
svg.append("g")
.selectAll("g")
.data(data.values)
.join("g")
.attr("transform", (d,i) => `translate(0, ${yScale(data.countries[i])})`)
.selectAll("rect")
.data(d => d)
.join("rect")
.attr("x", (d,i) => xScale(data.years[i]))
.attr("width", (d,i) => xScale(data.years[i] + 1) - xScale(data.years[i])-1)
.attr("height", yScale.bandwidth()-1)
.attr("fill", d => color(d))
.append("title")
.text((d,i) => `${d} + ${data.years[i]}`);
Please find the link https://observablehq.com/d/287af954b6aaf349 of the project, hope it would help to explain the question.
Thank you all for reading my question!
I've turned your observable into a snippet. I've changed the parsing of the data a little, because there were some errors there. You compared strings, not numbers, to find the max and min. Changing the order fixed that. I also removed the country name as the last value, because it's bad practice to paste different data types with different meanings together like that.
Instead, I used .each() to access both the index j of the country group and the data d from the individual bars. That way, I could find the name of the bar and append it as a title.
Ah, and I limited the data to 20 countries for simplicity.
const margin = {
top: 20,
right: 15,
bottom: 40,
left: 145
},
areaHeight = 16,
width = 800,
translateLabel = -40,
color = d3.scaleThreshold([2890, 8000, 25000], d3.schemeBlues[4])
d3.csv('https://static.observableusercontent.com/files/c5ae708547e6de9f679bd6a843bfed1b294c1c5b9a4c4621891f6961eac0509e6c2fc382e04957ef920239ae1351e6a86d60c4e7ac5cf981ce91b61bda44b555?response-content-disposition=attachment%3Bfilename*%3DUTF-8%27%27income_per_person_gdppercapita_ppp_inflation_adjusted-2.csv')
.catch(console.warn)
.then(originData => {
const countries = d3.map(originData, d => d.country).slice(0, 20);
const yearStart = d3.min(originData.columns, s => +s);
const yearEnd = d3.max(originData.columns, s => +s);
const years = d3.range(yearStart, yearEnd + 1);
//map: iterate over the each object(countries) and return an array
//arrow function: grabbing all the values from each country and return an array
const values = originData.map(country => {
const result = Object.values(country);
result.pop(); // Remove the last value because it's the country name
return result.map(v => +v);
}).slice(0, 20);
// Make sure we have first parsed all values, otherwise we're comparing strings, not numbers
let maxIncome = d3.max(values.map(country => d3.max(country)));
let minIncome = d3.min(values.map(country => d3.min(country)));
return {
years,
countries,
maxIncome,
minIncome,
values,
};
})
.then(data => {
const innerHeight = areaHeight * (data.countries.length),
xScale = d3.scaleLinear()
.domain(d3.extent(data.years))
.rangeRound([margin.left, width - margin.right]),
xAxis = g => g
.attr("transform", `translate(0,${margin.top})`)
.call(d3.axisTop(xScale))
.select(".domain").remove(),
yScale = d3.scaleBand()
.domain(data.countries)
.rangeRound([margin.top, margin.top + innerHeight]),
yAxis = g => g.attr("transform", `translate(${margin.left},0)`)
.call(d3.axisLeft(yScale).tickSize(0))
.call(g => g.select(".domain").remove())
.selectAll("text")
.attr("transform", `translate(${translateLabel},10)rotate(-45)`);
const svg = d3.select("svg")
.attr("viewBox", [0, 0, width, innerHeight + margin.top + margin.bottom])
.attr("font-family", "sans-serif")
.attr("font-size", 10)
.style("background-color", "#bdbdbd");
svg.append("g")
.call(xAxis);
svg.append("g")
.call(yAxis);
let country;
svg.append("g")
.selectAll("g")
.data(data.values)
.join("g")
.attr("transform", (d, i) => `translate(0, ${yScale(data.countries[i])})`)
.each(function(e, j) {
d3.select(this)
.selectAll("rect")
.data(e)
.join("rect")
.attr("x", (d, i) => xScale(data.years[i]))
.attr("width", (d, i) => xScale(data.years[i] + 1) - xScale(data.years[i]) - 1)
.attr("height", yScale.bandwidth() - 1)
.attr("fill", d => color(d))
.append("title")
.text(d => `${d} (${data.countries[j]})`);
});
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.2.0/d3.min.js"></script>
<svg></svg>