I'm following the given tutorial on D3
bar chart -2
I've setup my code in two functions one is init and one is update
var xScale = null;
var chart = null;
function init(w, c) {
xScale = d3.scale.linear()
.range([0, w]);
chart = d3.select(c)
.append('svg')
.attr('width', w);
function update(data) {
xScale.domain([0, d3.max(data, function(d) { return +d.value; })]);
chart.attr('height', 20 * data.length);
var bars = chart.selectAll('g')
.data(data);
bars.exit().remove();
bars.enter().append('g')
.attr('transform', function(d, i) { return 'translate(0,' + i * 20 + ')'; });
bars.append('rect')
.attr('width', function(d) { return xScale(+d.value); })
.attr('height', 18);
bars.append('text')
.attr('x', function(d) { return xScale(+d.value); })
.attr('y', 10)
.attr('dy', '.45em')
.text(function (d) { return d.name; });
}
When I call update first time, the bar chart is created correctly, on subsequenet update calls, it creates rect and text elements under tags instead of updating
My data is a dict {'name': a, 'value': 12, .....} The number of elements per update can be different. There might be same keys(names) with different values in each update
bars = chart.selectAll('g')
You are selecting all of the g elements (both new and existing).
bars.append('rect');
bars.append('text');
As a result, when you call append on bars, you are appending rect and text elements to both the new and existing g elements.
/* Enter */
enter = bars.enter().append('g');
enter.append('rect');
enter.append('text');
/* Update */
bars.attr('transform', function(d, i) {
return 'translate(0,' + i * 20 + ')';
});
bars.select('rect')
.attr('width', function(d) { return xScale(+d.value); })
.attr('height', 18);
bars.select('text')
.attr('x', function(d) { return xScale(+d.value); })
.attr('y', 10)
.attr('dy', '.45em')
.text(function (d) { return d.name; });
This allows you to append rect and text elements only to the enter selection, yet still allows you to update all the elements using your new data.
Note:
The enter selection merges into the update selection when you append or insert. Rather than applying the same operators to the enter and update selections separately, you can now apply them only once to the update selection after entering the nodes.
See: https://github.com/mbostock/d3/wiki/Selections#data
Related
I'm stuck on a small problem regarding force simulation in D3.
I have data representing poverty rates for each country, from 1998 to 2008. It's a bubble chart that's split into three clusters, representing poor countries, not-poor countries, and countries with no information.
When the app is initially loaded, it's loaded with the 1998 data. However, I have some buttons at the top, that, when clicked, will change the year, and subsequently the bubbles should rearrange themselves. All I've been able to do, is when the button is clicked, I change a variable year. However, there are functions and variables that use year throughout the code. When year changes, I want to recalculate all the node properties and force parameters that are depending on year
Here's my code. I've included all of it in case you want to try it out. The data file is at the end of this post.
async function init() {
// Set up the canvas
var height = 1000, width = 2000;
var svg = d3.select("#panel1").append("svg")
.attr("height", height)
.attr("width", width)
.attr("class", "bubblePanel");
var canvas = svg.append("g")
.attr("transform", "translate(0,0)");
// Choose what year to look at, based on button clicks.
var year = "X1998"
d3.select("#b1998").on("click", function() {
year = "X1998"
console.log(year)
// NOTIFY SIMULATION OF CHANGE //
})
d3.select("#b1999").on("click", function() {
year = "X1999"
console.log(year)
// NOTIFY SIMULATION OF CHANGE //
})
d3.select("#b2000").on("click", function() {
year = "X2000"
console.log(year)
// NOTIFY SIMULATION OF CHANGE //
})
// Implement the physics of the elements. Three forces act according to the poverty level (poor, not poor, and no info)
var simulation = d3.forceSimulation()
.force("x", d3.forceX(function(d) {
if (parseFloat(d[year]) >= 10) {
return 1700
} else if (parseFloat(d[year]) === 0) {
return 1000
} else {
return 300
}
}).strength(0.05))
.force("y", d3.forceY(300).strength(0.05))
.force("collide", d3.forceCollide(function(d) {
return radiusScale(d[year])
}));
// Function to pick colour of circles according to region
function pickColor(d) {
if (d === "East Asia & Pacific") {
return "red"
} else if (d === "Europe & Central Asia") {
return "orange"
} else if (d === "Latin America & Caribbean") {
return "yellow"
} else if (d === "Middle East & North Africa") {
return "green"
} else if (d === "North America") {
return "blue"
} else if (d === "South Asia") {
return "indigo"
} else {
return "violet"
}
}
// Set the scales for bubble radius, and text size.
var radiusScale = d3.scaleSqrt().domain([0, 50]).range([20,80]);
var labelScale = d3.scaleSqrt().domain([0,50]).range([10,40]);
// Read the data
await d3.csv("wd3.csv").then(function(data) {
// Assign each data point to a circle that is colored according to region and has radius according to its poverty level
var bubbles = svg.selectAll("circle")
.data(data)
.enter().append("circle")
.attr("cx", 100)
.attr("cy", 100)
.attr("fill", function(d) {
return pickColor(d.Region)
})
.attr("r", function(d) {
return radiusScale(d[year])
});
// Assign each ddata point to a text element that shows the counry code of the data point. The text is scaled according to the poverty level
var labels = svg.selectAll("text")
.data(data)
.enter().append("text")
.attr("x", 100)
.attr("y", 100)
.attr("dominant-baseline", "central")
.text(function(d) { return d.XCountryCode })
.style("stroke", "black")
.style("text-anchor", "middle")
.style("font-size", function(d) { return labelScale(d[year]); });
// Code to handle the physics of the bubble and the text
simulation.nodes(data)
.on("tick", ticked)
function ticked() {
bubbles.attr("transform", function(d) {
var k = "translate(" + d.x + "," + d.y + ")";
return k;
})
labels.attr("transform", function(d) {
var k = "translate(" + d.x + "," + d.y + ")";
return k;
})
}
});
}
When year changes, the data values will change for each country. I want the following parts of my code to be updated.
The x forces on the nodes: Countries can go from poor in one year to not-poor in another year, so their cluster will change
The radius of the circles: The radius represents poverty level. These change from year to year, so the size of the circles will change when a button is clicked
The coordinates of the country labels: These labels are attached to the data as well. So when the x forces on the circles causes the circles to move, the labels should move as well.
I'd greatly appreciate the help.
The data file can be found here. I accidentally named it povertyCSV, but in the code, it's referenced as "wd3.csv"
If I understand the question correctly:
Re-initializing Forces
The functions provided to set parameters of d3 forces such as forceX or forceCollision are executed once per node at initialization of the simulation (when nodes are originally assigned to the layout). This saves a lot of time once the simulation starts: we aren't recalculating force parameters every tick.
However, if you have an existing force layout and want to modify forceX with a new x value or new strength, or forceCollision with a new radius, for example, we can re-initialize the force to perform the recalculation:
// assign a force to the force diagram:
simulation.force("someForce", d3.forceSomeForce().someProperty(function(d) { ... }) )
// re-initialize the force
simulation.force("someForce").initialize(nodes);
This means if we have a force such as:
simulation.force("x",d3.forceX().x(function(d) { return fn(d["year"]); }))
And we update the variable year, all we need to do is:
year = "newValue";
simulation.force("x").initialize(nodes);
Positioning
If the forces are re-initialized (or re-assigned), there is no need to touch the tick function: it'll update the nodes as needed. Labels and circles will continue to be updated correctly.
Also, non-positional things such as color need to be updated in the event handler that also re-initializes the forces. Other than radius, most things should either be updated via the force or via modifying the elements directly, not both.
Radius is a special case:
With d3.forceCollide, radius affects positioning
Radius, however, does not need to be updated every tick.
Therefore, when updating the radius, we need to update the collision force and modify the r attribute of each circle.
If looking for a smooth transition of radius that is reflected graphically and in the collision force, this should be a separate question.
Implementation
I've borrowed from your code to make a fairly generic example. The below code contains the following event listener for some buttons where each button's datum is a year:
buttons.on("click", function(d) {
// d is the year:
year = d;
// reheat the simulation:
simulation
.alpha(0.5)
.alphaTarget(0.3)
.restart();
// (re)initialize the forces
simulation.force("x").initialize(data);
simulation.force("collide").initialize(data);
// update altered visual properties:
bubbles.attr("r", function(d) {
return radiusScale(d[year]);
}).attr("fill", function(d) {
return colorScale(d[year]);
})
})
The following snippet uses arbitrary data and due to its size may not allow for nodes to re-organize perfectly every time. For simplicity, position, color, and radius are all based off the same variable. Ultimately, it should address the key part of the question: When year changes, I want to update everything that uses year to set node and force properties.
var data = [
{year1:2,year2:1,year3:3,label:"a"},
{year1:3,year2:4,year3:5,label:"b"},
{year1:5,year2:9,year3:7,label:"c"},
{year1:8,year2:16,year3:11,label:"d"},
{year1:13,year2:25,year3:13,label:"e"},
{year1:21,year2:36,year3:17,label:"f"},
{year1:34,year2:1,year3:19,label:"g"},
{year1:2,year2:4,year3:23,label:"h"},
{year1:3,year2:9,year3:29,label:"i"},
{year1:5,year2:16,year3:31,label:"j"},
{year1:8,year2:25,year3:37,label:"k"},
{year1:13,year2:36,year3:3,label:"l"},
{year1:21,year2:1,year3:5,label:"m"}
];
// Create some buttons:
var buttons = d3.select("body").selectAll("button")
.data(["year1","year2","year3"])
.enter()
.append("button")
.text(function(d) { return d; })
// Go about setting the force layout:
var svg = d3.select("body")
.append("svg")
.attr("width", 500)
.attr("height", 300);
var radiusScale = d3.scaleSqrt()
.domain([0, 40])
.range([5,30]);
var colorScale = d3.scaleLinear()
.domain([0,10,37])
.range(["#c7e9b4","#41b6c4","#253494"]);
var year = "year1";
var simulation = d3.forceSimulation()
.force("x", d3.forceX(function(d) {
if (parseFloat(d[year]) >= 15) {
return 100
} else if (parseFloat(d[year]) > 5) {
return 250
} else {
return 400
}
}).strength(0.05))
.force("y", d3.forceY(150).strength(0.05))
.force("collide", d3.forceCollide()
.radius(function(d) {
return radiusScale(d[year])
}));
var bubbles = svg.selectAll("circle")
.data(data)
.enter().append("circle")
.attr("r", function(d) {
return radiusScale(d[year])
})
.attr("fill", function(d) {
return colorScale(d[year]);
});
var labels = svg.selectAll("text")
.data(data)
.enter()
.append("text")
.text(function(d) {
return d.label;
})
.style("text-anchor","middle");
simulation.nodes(data)
.on("tick", ticked)
function ticked() {
bubbles.attr("cx", function(d) {
return d.x;
}).attr("cy", function(d) {
return d.y;
})
labels.attr("x", function(d) {
return d.x;
})
.attr("y", function(d) {
return d.y +5;
})
}
buttons.on("click", function(d) {
// d is the year:
year = d;
simulation
.alpha(0.5)
.alphaTarget(0.3)
.restart();
simulation.force("x").initialize(data);
simulation.force("collide").initialize(data);
bubbles.attr("r", function(d) {
return radiusScale(d[year]);
}).attr("fill", function(d) {
return colorScale(d[year]);
})
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
I am trying to add color options for my heat-map visualization. I have a predefined colors array at the beginning, and I draw rectangles like this:
plotChart.selectAll(".cell")
.data(data)
.enter().append("rect")
.attr("class", "cell")
.attr("x", function (d) { return x(d.timestamp); })
.attr("y", function (d) { return y(d.hour); })
.attr("width", function (d) { return x(d3.timeWeek.offset(d.timestamp, 1)) - x(d.timestamp); })
.attr("height", function (d) { return y(d.hour + 1) - y(d.hour); })
.attr("fill", function (d) { return colorScale(d.value); });
When I click a link in a dropdown menu, I do this:
$(".colorMenu").click(function (event) {
event.preventDefault();
// remove # from clicked link
var addressValue = $(this).attr("href").substring(1);
// get color scheme array
var newColorScheme = colorDict[addressValue];
// update color scale range
colorScale.range(newColorScheme);
// here I need to repaint with colors
});
My color scale is quantile scale, so I cannot use invert function to find values of each rectangle. I don't want to read the data again because it would be a burden, so how can I change fill colors of my rectangles?
I don't want to read the data again...
Well, you don't need to read the data again. Once the data was bound to the element, the datum remains there, unless you change/overwrite it.
So, this can simply be done with:
.attr("fill", d => colorScale(d.value));
Check this demo:
var width = 500,
height = 100;
var ranges = {};
ranges.range1 = ['#f7fbff','#deebf7','#c6dbef','#9ecae1','#6baed6','#4292c6','#2171b5','#08519c','#08306b'];
ranges.range2 = ['#fff5eb','#fee6ce','#fdd0a2','#fdae6b','#fd8d3c','#f16913','#d94801','#a63603','#7f2704'];
ranges.range3 = ['#f7fcf5','#e5f5e0','#c7e9c0','#a1d99b','#74c476','#41ab5d','#238b45','#006d2c','#00441b'];
ranges.range4 = ['#fff5f0','#fee0d2','#fcbba1','#fc9272','#fb6a4a','#ef3b2c','#cb181d','#a50f15','#67000d'];
var color = d3.scaleQuantile()
.domain([0, 15])
.range(ranges.range1);
var svg = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", height);
var data = d3.range(15);
var rects = svg.selectAll(".rects")
.data(data)
.enter()
.append("rect");
rects.attr("y", 40)
.attr("x", d => d * 25)
.attr("height", 20)
.attr("width", 20)
.attr("stroke", "gray")
.attr("fill", d => color(d));
d3.selectAll("button").on("click", function() {
color.range(ranges[this.value]);
rects.attr("fill", d => color(d))
})
<script src="https://d3js.org/d3.v4.min.js"></script>
<button value="range1">Range1</button>
<button value="range2">Range2</button>
<button value="range3">Range3</button>
<button value="range4">Range4</button>
I have a D3 barchart which has 5 bars. When I update it I can see it transitioning to the correct 3 bars but some of the original bars are left visible - how do I make them exit?
This is what it initially looks like:
This is what it ends up looking like:
The dark blue bars are correct. The current code for updating the "rect" objects is the following:
var plot = d3.select("#barChartPlot")
.datum(currentDatasetBarChart);
/* Note that here we only have to select the elements - no more appending! */
plot.selectAll("rect")
.data(currentDatasetBarChart)
.transition()
.duration(750)
.attr("x", function (d, i) {
return xScale(i);
})
.attr("width", width / currentDatasetBarChart.length - barPadding)
.attr("y", function (d) {
return yScale(+d.measure);
})
.attr("height", function (d) {
return height - yScale(+d.measure);
})
.attr("fill", colorChosen);
You only have 3 new bars, so the number of elements on your data has changed.
You need to use the update pattern.
var rects = plot.selectAll("rect")
.data(currentDatasetBarChart);
rects.enter()
.append("rect")
//Code to style and define new rectangles.
//Update
rects.update()
.transition()
.duration(750)
.attr("x", function (d, i) {
return xScale(i);
})
.attr("width", width / currentDatasetBarChart.length - barPadding)
.attr("y", function (d) {
return yScale(+d.measure);
})
.attr("height", function (d) {
return height - yScale(+d.measure);
})
.attr("fill", colorChosen);
// Remove unused rects
rects.exit().remove();
I'm creating a bar chart as part of a bigger data visualization in d3. I want to be able to change the data in one part of the visualization and all the charts will be updated. A simplified version of the chart is as follows.
var dataset = [1, 3, 5, 3, 3];
...
var svg = d3.select("body #container").append("svg")
.attr("width", width)
.attr("height", height);
var g = svg.append("g");
...
I create other charts like a map, circle etc with this svg element. The bar chart is implemented like this.
function bars(dataset) {
var barChart = g.selectAll("rect.bar")
.data(dataset)
.enter();
barChart.append("rect")
.attr("class", "bar")
.attr("x", function(d, i) { return i * 30 + 100; })
.attr("y", function(d) { return (height - 130) - d * 4;})
.attr("width", 25)
.attr("height", function(d) { return d * 4; });
barChart.append("text")
.text(function(d) { return d; })
.attr("x", function(d, i) { return i * 30 + 103; })
.attr("y", function(d) { return (height - 130) - d/10 - 5;})
.attr("font-family", "sans-serif")
.attr("font-size", "10px")
.attr("fill", "darkgray");
}
Now this renders the bar chart fine but there is a function
...
.on("click", function() {
...
var newdata = [5, 2, 6, 2, 4]; // new values
g.selectAll("rect.bar").remove(); // This removes the bars
g.selectAll("text").remove(); // Problem here: All texts are removed
bars(newdata);
}
I have tried to transition the bar chart with new values with the .remove() function. This works for the bar rectangles because there are no othe bar charts but when I tried to remove the value labels like shown above all the other text elements were also removed. Is there a way to only update the text associated with the bars?
Have you tried applying a class to the text and only selecting those ones for removal?
e.g.
barChart.append("text")
.attr('class','label')
.text(function(d) { return d; })
then
g.selectAll(".label").remove();
Incidentally, if not all of the elements are being deleted between updates, then instead of removing all of the elements, have you considered using enter() and exit() to bind the new data to the existing elements and only remove the elements that are changing?
EDIT Like this:
function bars(dataset) {
var bar = g.selectAll(".bar").data(dataset);
bar.exit().remove();
bar.enter().append("rect").attr("class", "bar");
bar
.attr("x", function(d, i) { return i * 30 + 100; })
.attr("y", function(d) { return (height - 130) - d * 4;})
.attr("width", 25)
.attr("height", function(d) { return d * 4; });
var label = g.selectAll(".label").data(dataset);
label.exit().remove();
label.enter().append("text").attr("class", "label");
label
.text(function(d) { return d; })
.attr("x", function(d, i) { return i * 30 + 103; })
.attr("y", function(d) { return (height - 130) - d/10 - 5;})
.attr("font-family", "sans-serif")
.attr("font-size", "10px")
.attr("fill", "darkgray");
}
I'm new to D3, and spent already a few hours to find out anything about dealing with structured data, but without positive result.
I want to create a bar chart using data structure below.
Bars are drawn (horizontally), but only for user "jim".
var data = [{"user":"jim","scores":[40,20,30,24,18,40]},
{"user":"ray","scores":[24,20,30,41,12,34]}];
var chart = d3.select("div#charts").append("svg")
.data(data)
.attr("class","chart")
.attr("width",800)
.attr("height",350);
chart.selectAll("rect")
.data(function(d){return d3.values(d.scores);})
.enter().append("rect")
.attr("y", function(d,i){return i * 20;})
.attr("width",function(d){return d;})
.attr("height", 20);
Could anyone point what I did wrong?
When you join data to a selection via selection.data, the number of elements in your data array should match the number of elements in the selection. Your data array has two elements (for Jim and Ray), but the selection you are binding it to only has one SVG element. Are you trying to create multiple SVG elements, or put the score rects for both Jim and Ray in the same SVG element?
If you want to bind both data elements to the singular SVG element, you can wrap the data in another array:
var chart = d3.select("#charts").append("svg")
.data([data])
.attr("class", "chart")
…
Alternatively, use selection.datum, which binds data directly without computing a join:
var chart = d3.select("#charts").append("svg")
.datum(data)
.attr("class", "chart")
…
If you want to create multiple SVG elements for each person, then you'll need a data-join:
var chart = d3.select("#charts").selectAll("svg")
.data(data)
.enter().append("svg")
.attr("class", "chart")
…
A second problem is that you shouldn't use d3.values with an array; that function is for extracting the values of an object. Assuming you wanted one SVG element per person (so, two in this example), then the data for the rect is simply that person's associated scores:
var rect = chart.selectAll("rect")
.data(function(d) { return d.scores; })
.enter().append("rect")
…
If you haven't already, I recommend reading these tutorials:
Thinking with Joins
Nested Selections
This may clarify the nested aspect, in addition to mbostock's fine answer.
Your data has 2 degrees of nesting. You have an array of 2 objects, each has an array of ints. If you want your final image to reflect these differences, you need to do a join for each.
Here's one solution: Each user is represented by a group g element, with each score represented by a rect. You can do this a couple of ways: Either use datum on the svg, then an identity function on each g, or you can directly join the data on the g. Using data on the g is more typical, but here are both ways:
Using datum on the svg:
var chart = d3.select('body').append('svg')
.datum(data) // <---- datum
.attr('width',800)
.attr('height',350)
.selectAll('g')
.data(function(d){ return d; }) // <----- identity function
.enter().append('g')
.attr('class', function(d) { return d.user; })
.attr('transform', function(d, i) { return 'translate(0, ' + i * 140 + ')'; })
.selectAll('rect')
.data(function(d) { return d.scores; })
.enter().append('rect')
.attr('y', function(d, i) { return i * 20; })
.attr('width', function(d) { return d; })
.attr('height', 20);
Using data on the group (g) element:
var chart = d3.select('body').append('svg')
.attr('width',800)
.attr('height',350)
.selectAll('g')
.data(data) // <--- attach directly to the g
.enter().append('g')
.attr('class', function(d) { return d.user; })
.attr('transform', function(d, i) { return 'translate(0, ' + i * 140 + ')'; })
.selectAll('rect')
.data(function(d) { return d.scores; })
.enter().append('rect')
.attr('y', function(d, i) { return i * 20; })
.attr('width', function(d) { return d; })
.attr('height', 20);
Again, you don't have to create these g elements, but by doing so I can now represent the user scores differently (they have different y from the transform) and I can also give them different styles, like this:
.jim {
fill: red;
}
.ray {
fill: blue;
}