I'm trying to update the bar chart in d3 based on the input selected by the user. The updated data is being displayed but it is being displayed on the old SVG elements. I tried using exit().remove() but it did not work.
Can anyone edit the code attached below so that the old SVG elements are removed.
<html>
<head>
<script src="https://d3js.org/d3.v4.min.js" charset="utf-8"></script>
<style>
.rect {
fill: steelblue;
}
.text {
fill: white;
font: 10px sans-serif;
text-anchor: middle;
}
</style>
</head>
<body>
<select id = "variable">
<option >select</option>
<option value="AZ">Arizona</option>
<option value="IL">Illinois</option>
<option value="NV">NV</option>
</select>
<script>
var margin = {top: 20, right: 20, bottom: 70, left: 40},
width = 500 - margin.left - margin.right,
height = 400 - margin.top - margin.bottom;
var y = d3.scaleLinear()
.domain([0,5])
.range([height, 0]);
var yAxis = d3.axisLeft(y)
.ticks(10);
var svg = d3.select("body")
.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 + ")");
var bar;
function update(state)
{
d3.csv("test3.csv", function(error, data)
{
data = data.filter(function(d, i)
{
if (d['b_state'] == state)
{
return d;
}
});
data = data.filter(function(d, i)
{
if (i<10)
{
return d;
}
});
var barWidth = width / data.length;
svg.append("g")
.attr("class", "y axis")
.call(yAxis)
.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", ".71em")
.style("text-anchor", "end")
.text("Stars");
bar = svg.selectAll("bar")
.data(data)
.enter()
.append("g")
.attr("transform", function(d, i) { return "translate(" + i * barWidth + ",0)"; });
bar.append("rect")
.attr("y", function(d) { return y(d.b_stars); })
.attr("height", function(d) { return height - y(d.b_stars); })
.attr("width", barWidth - 1)
.attr("fill", "steelblue");
bar.append("text")
.attr("x", function(d) { return height - y(d.b_stars); })
.attr("y", -40)
.attr("dy", ".75em")
.text(function(d) { return d.b_name; })
.attr("transform", "rotate(90)" );
});
svg.exit().remove();
bar.exit().remove();
}
d3.select("#variable")// selects the variable
.on("change", function() {// function that is called on changing
var variableName = document.getElementById("variable").value;// reads the variable value selected into another variable
update(variableName);});
</script>
</body>
Your problem here is about your bar selection. You can have a look to this part of the d3 documentation: Joining data.
By writing
bar = svg.selectAll("bar")
.data(data)
.enter()
You are selecting all bar elements, joining them data, and with this enter(), you are getting all the data items not linked to a bar element (enter() documentation).
But, your bar selector matches nothing. The parameter of the select()/selectAll() has to be a selector (element, class with ., id with #...). That is why the enter() your enter() selection is creating always new elements above the old ones instead of updating them.
So the first step is to rewrite this selection and creating the DOM elements that will match later this selection:
bar = svg.selectAll(".bar")
.data(data)
.enter()
.append("g")
.attr('class', 'bar')
.attr("transform", function(d, i) { return "translate(" + i * barWidth + ",0)"; });
Here, we are selecting all elements with the bar class. If there is no DOM element linked to an item from data, so we are creating it (in the enter() selection), as a new g with the bar class.
With the selection written like this, on the next call of your update, the selectAll('.bar') will match all the g previously created and not apply the enter() selection for existing elements.
To update or remove your existing bars, you can write your code like this:
var barData = svg.selectAll(".bar")
.data(data)
// Bars creation
var barEnter = barData.enter()
.append("g")
.attr('class', 'bar')
.attr("transform", function(d, i) { return "translate(" + i * barWidth + ",0)"; });
barEnter.append("rect")
.attr("y", function(d) { return y(d.b_stars); })
.attr("height", function(d) { return height - y(d.b_stars); })
.attr("width", barWidth - 1)
.attr("fill", "steelblue");
barEnter.append("text")
.attr("x", function(d) { return height - y(d.b_stars); })
.attr("y", -40)
.attr("dy", ".75em")
.text(function(d) { return d.b_name; })
.attr("transform", "rotate(90)" );
// Update the bar if the item in data is modified and already linked to a .bar element
barData.select('rect')
.attr("height", function(d) { return height - y(d.b_stars); })
// Remove the DOM elements linked to data items which are not anymore in the data array
barData.exit().remove()
Related
I'm trying to get data labels to show inside of each bar of my stacked bar chart. When I view source, I can see the <text> elements in each bar with the correct number, but they aren't visible in the bar itself
<!DOCTYPE html>
<meta charset="utf-8">
<!-- Load d3.js -->
<script src="https://d3js.org/d3.v4.js"></script>
<!-- Create a div where the graph will take place -->
<div id="my_dataviz"></div>
<div id="legend"></div>
<style>
</style>
<script>
// set the dimensions and margins of the graph
var margin = { top: 10, right: 30, bottom: 20, left: 50 },
width = 460 - margin.left - margin.right,
height = 400 - 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 + ")");
// Parse the Data
d3.csv("https://raw.githubusercontent.com/JakeRatliff/dh-valve-data/main/Valves%20Data%20-%20Sheet1.csv", function(data) {
// List of subgroups = header of the csv files = soil condition here
var subgroups = data.columns.slice(1)
subgroups.pop()
console.log(subgroups)
// 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.Year) }).keys()
console.log(groups)
// 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).tickSizeOuter(0));
// Add Y axis
var y = d3.scaleLinear()
//.domain([0, 60])
.domain([0, d3.max(data, function(d) { return +d.Total; })])
.range([height, 0]);
svg.append("g")
.call(d3.axisLeft(y));
// color palette = one color per subgroup
var color = d3.scaleOrdinal()
.domain(subgroups)
.range(['#00539B', '#E0750B'])
//stack the data? --> stack per subgroup
var stackedData = d3.stack()
.keys(subgroups)
(data)
// Show the bars
svg.append("g")
.selectAll("g")
// Enter in the stack data = loop key per key = group per group
.data(stackedData)
.enter().append("g")
.attr("fill", function(d) { return color(d.key); })
.selectAll("rect")
// enter a second time = loop subgroup per subgroup to add all rectangles
.data(function(d) { return d; })
.enter().append("rect")
.attr("x", function(d) { return x(d.data.Year); })
.attr("y", function(d) { return y(d[1]); })
.attr("height", function(d) { return y(d[0]) - y(d[1]); })
.attr("width", x.bandwidth())
.attr("class", "bar")
.append("text")
.attr("x", function(d) { return x(d.data.Year); })
.attr("y", function(d) { return y(d[1]); })
.text(function(d) { return d[1] })
})
</script>
Here is a codepen if that is preferred:
https://codepen.io/jake2134/pen/QWMQJOB
Thanks in advance for any help. I've Google around but the results seem to be outdated.
Use a variable to create a group, then append twice to it.
var bar_groups = svg.append("g")
.selectAll("g")
// Enter in the stack data = loop key per key = group per group
.data(stackedData)
.enter().append("g")
.attr("fill", function(d) { return color(d.key); })
var bars = bar_groups.selectAll("g")
// enter a second time = loop subgroup per subgroup to add all rectangles
.data(function(d) { return d; })
.enter().append("g")
bars.append('rect')
.attr("x", function(d) { return x(d.data.Year); })
.attr("y", function(d) { return y(d[1]); })
.attr("height", function(d) { return y(d[0]) - y(d[1]); })
.attr("width", x.bandwidth())
.attr("class", "bar")
bars.append("text")
.attr("x", function(d) { return x(d.data.Year); })
.attr("y", function(d) { return y(d[1]); })
.text(function(d) { return d[1] })
I am trying to insert some text inside each bar of the given chart with it's values and I just can't figure it out. I'm still new to d3 and struggle with most of the things.
I also need to have different text inside each bar, dependent on the data.
I've looked up around and found that I need to use the following structure:
<g class="barGroup">
<rect />
<text />
</g>
This is how the chart looks:
This is how I want to achieve:
This is the function which creates the whole chart, the part where I attempt to insert the text is at the end:
function createBarChart(divChartId, receivedData, chartDimensions) {
// Set dimensions
var margin = chartDimensions["margin"];
var width = chartDimensions["width"];
var height = chartDimensions["height"];
// Create Svg and group
var svg = d3.select(divChartId)
.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 + ")");
// Create and append X axis
var xAxis = d3.scaleBand()
.range([0, width])
.domain(receivedData.map(function (d) { return d.answerText; }))
.padding(0.2);
svg.append("g")
.attr("class", "x-axis")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(xAxis))
.selectAll("text")
.attr("class", "font-weight-bold")
.attr("transform", "translate(-10,0)rotate(-45)")
.style("text-anchor", "end")
.style("font-size", "1rem");
// Create and append Y axis
var yAxis = d3.scaleLinear()
.domain([0, Math.max.apply(Math, receivedData.map(function (d) { return d.answerCount; }))])
.range([height, 0]);
svg.append("g")
.attr("class", "y-axis")
.call(d3.axisLeft(yAxis)
.ticks(5)
.tickFormat(d3.format("d")));
// create a tooltip
var tooltip = d3.select("body")
.append("div")
.attr("class", "font-weight-bold text-dark px-2 py-2")
.style("position", "absolute")
.style("z-index", "10")
.style("visibility", "hidden")
.style("background", "lavender")
.style("border", "1px solid gray")
.style("border-radius", "12px")
.style("text-align", "center")
.style("visibility", "hidden")
.style("visibility", "hidden")
.style("visibility", "hidden")
// Generate random color
var colorObject = generateRandomColorObject();
// Create and append bars, set values to 0 for animation start
svg.selectAll("rect").data(receivedData)
.enter()
.append("g")
.attr("class", "barGroup")
.append("rect")
.attr("x", function (d) { return xAxis(d.answerText); })
.attr("y", function (d) { return yAxis(0); })
.attr("width", xAxis.bandwidth())
.attr("height", function (d) { return height - yAxis(0); })
.attr("fill", colorObject.color)
.style("stroke", colorObject.border)
.style("stroke-width", 1)
.on("mouseover", function (d) { return tooltip.style("visibility", "visible").html(d.answerText + ": " + d.answerCount); })
.on("mousemove", function () { return tooltip.style("top", (event.pageY - 45) + "px").style("left", (event.pageX) + 5 + "px"); })
.on("mouseout", function (d) { return tooltip.style("visibility", "hidden").html(""); });
// Set correct values for animation end
svg.selectAll("rect")
.transition().duration(1500)
.attr("y", function (d) { return yAxis(d.answerCount); })
.attr("height", function (d) { return height - yAxis(d.answerCount); })
.delay(function (d, i) { return (i * 100) });
// Append text to each bar
svg.selectAll("barGroup")
.append("text")
.attr("font-size", "2em")
.attr("color", "black")
.text("Test");
}
In this line you want your selector to look for a class: svg.selectAll("barGroup") should be svg.selectAll(".barGroup"). Then, once you see your text show up, you'll need to position them.
Consider positioning your g.barGroups's first, using .attr('transform', function(d){ return 'translate(...)' }, before you .append('rect') and .append('text'). That way the entire group will be in approximately the right position, and you can make small adjustments to the rect and text.
Everything seems to be working as expected except for the text element. I can't seem to get text elements to append to my g element. Here is my code so far. I've inspected the DOM in a chrome browser, but I don't see any text elements and I'm not sure why. I was using this site as a sort of guide: https://www.dashingd3js.com/svg-text-element.
Also, I know the elements should stack on each other since they all share the same x and y position, I'm just trying to get the elements to appear first.
var svg = d3.select('svg'),
margin = {top: 60, right: 20, bottom: 45, left: 60},
width = +svg.attr("width") - margin.left - margin.right,
height = +svg.attr("height") - margin.top - margin.bottom;
var x = d3.scaleBand().rangeRound([0, width]).padding(.1);
var y = d3.scaleLinear().range([height, 0]);
var g = svg.append("g").attr("transform", "translate(" + margin.left + "," + margin.top + ")");
x.domain(test.map(function(d) { return d.level; }));
y.domain([0, d3.max(test, function(d) { return d.time; })]);
g.append("g")
.attr("class", "axis axis--x")
.attr("transform", "translate(0," + height + ")")
.attr("writing-mode", "tb-rl")
.call(d3.axisBottom(x).tickSize(0).tickPadding(10));
g.append("g")
.attr("class", "axis axis--y")
.call(d3.axisLeft(y).ticks(10))
.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 9)
.attr("dy", "0.71em")
.attr("text-anchor", "end")
.text("Frequency")
g.selectAll(".bar")
.data(test)
.enter()
.append("rect")
.attr("class", "bar")
.attr("x", function(d) { return x(d.level); })
.attr("y", function(d) { return y(d.time); })
.attr("width", x.bandwidth())
.attr("height", function(d) { return height - y(d.time); })
var text = g.selectAll("text")
.data(test).enter()
.append("text");
var textLabels = text.attr("x", 100)
.attr("y", 100)
.text("testing")
.attr("fill", "blue")
your selection g.selectAll("text") will include text from the axes you appended to the g element earlier in the code, so your "enter" won't have anything in it. D3 compares the incoming to data to the items in the selection, and if you don't specify a key, will do a simple comparison on the number of elements in each, and then add (enter) and remove (exit) accordingly.
If you change your selection to something that you know won't be on in DOM yet (ie an empty selection), for example g.selectAll(".label"), then when you append data, the enter selection will contain your new text labels.
I want to update data on a click but the bars that are changing are not the right ones. There is something I cant quite fix with the select. On click the grey bars, which should be bar2 are updating. It should be bar.
Example: https://jsfiddle.net/Monduiz/kaqv37gu/
D3 chart:
var values = feature.properties;
var data = [
{name:"Employment rate",value:values["ERate15P"]},
{name:"Participation rate",value:values["PR15P"]},
{name:"Unemployment rate",value:values["URate15P"]}
];
var margin = {top: 70, right: 50, bottom: 20, left: 50},
width = 400 - margin.left - margin.right,
height = 270 - margin.top - margin.bottom,
barHeight = height / data.length;
// Scale for X axis
var x = d3.scale.linear()
.domain([0, 100]) //set input to a scale of 0 - 1. The index has a score scale of 0 to 1. makes the bars more accurate for comparison.
.range([0, width]);
var y = d3.scale.ordinal()
.domain(["Employment rate", "Participation rate", "Unemployment rate"])
.rangeRoundBands([0, height], 0.2);
var svg = d3.select(div).select("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
.classed("chartInd", true);
var bar2 = svg.selectAll("g.bar")
.data(data)
.enter()
.append("g")
.attr("transform", function(d, i) { return "translate(0," + i * barHeight + ")"; });
var bar = svg.selectAll("g.bar")
.data(data)
.enter()
.append("g")
.attr("transform", function(d, i) { return "translate(0," + i * barHeight + ")"; });
bar2.append("rect")
.attr("height", y.rangeBand()-15)
.attr("fill", "#EDEDED")
.attr("width", 300);
bar.append("rect")
.attr("height", y.rangeBand()-15)
.attr("fill", "#B44978")
.attr("width", function(d){return x(d.value);});
bar.append("text")
.attr("class", "text")
.attr("x", 298)
.attr("y", y.rangeBand() - 50)
.text(function(d) { return d.value + " %"; })
.attr("fill", "black")
.attr("text-anchor", "end");
bar.append("text")
.attr("class", "text")
.attr("x", function(d) { return x(d.name) -5 ; })
.attr("y", y.rangeBand()-50)
//.attr("dy", ".35em")
.text(function(d) { return d.name; });
d3.select("p")
.on("click", function() {
//New values for dataset
var values = feature.properties;
var dataset = [
{name:"Employment rate",value:values["ERate15_24"]},
{name:"Participation rate",value:values["PR15_24"]},
{name:"Unemployment rate",value:values["URate15_24"]}
];
//Update all rects
var bar = svg.selectAll("rect")
.data(dataset)
.attr("x", function(d){return x(d.value);})
.attr("width", function(d){return x(d.value);})
});
}
var bar2 = svg.selectAll("g.bar")
.data(data)
.enter()
.append("g")
.attr("transform", function(d, i) { return "translate(0," + i * barHeight + ")"; });
var bar = svg.selectAll("g.bar")
.data(data)
.enter()
.append("g")
.attr("transform", function(d, i) { return "translate(0," + i * barHeight + ")"; });
'bar2' above generates 3 new g elements (one for each datum)
Since you don't set attr("class","bar") for these then 'bar' will also generate 3 new g elements - (if you had set the class attribute bar would return empty as no new elements would be generated and you'd see missing stuff)
Further on you add rects to all these g elements for six rectangles in total and in the click function you select all these rectangles and re-attach 3 fresh bits of data
Since bar2 was added first the rectangles in its g elements are hoovering up the new data
You need to select and set different classes on the g elements, .selectAll("g.bar") and .attr("class", "bar") for bar, and .selectAll("g.bar2") and .attr("class", "bar2") for bar2 (use the same name to keep it simple)
then in the new data you need select only the rects belonging to g elements of the bar class: svg.selectAll(".bar rect")
Another way would be to have only one set of g elements and add two types of rectangle (differentiated by class attribute)
I'm trying to get a second bar into my graph. The elements are correctly getting appended but not in the correct location and not the correct height. What I want from the data to be at the 1 position in the x-axis to have 2 bars one with a height of 2 and the other height of 3 and so on.
http://jsfiddle.net/626uesbh/4/
var svg2 = d3.select("#histogram").append("svg2")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
svg2.selectAll(".rect")
.data(data)
.enter().append("g")
.attr("transform", "translate(0, 100)")
.append("rect")
.attr("class", "rect")
.attr("width", 10)
.attr("height", function(d) { return height - y2Map(d); })
.attr("x", xMap)
.attr("y", yMap)
.style("fill", "blue");
I suspect svg2 transform is the problem but after trying fiddling with it for an hour I can seem to get what I want. I looked at this question and tried to implement it into my problem. D3.js grouped bar chart
Since each element in your data contains the values for both bars, you have to add them as a group. That is, add a 'g' element to the chart for each element in the array, then add a bar for inner_array[1] and inner_array[2].
Hopefully this gets you on the right path, essentially all I changed was the stuff after your //bar comment.
http://jsfiddle.net/626uesbh/6/
// bar
var bar_groups = svg.selectAll('.bar-group')
.data(data)
.enter().append('g')
.attr('class', 'bar-group');
bar_groups.append('rect')
.attr("class", "rect")
.attr("width", 10)
.attr("height", function(d) { return height - yScale(d[1]); })
.attr("x", function(d) {
return xScale(d[0]) - 5;
})
.attr("y", function(d) {
return yScale(d[1]);
})
.style("fill", "green");
bar_groups.append('rect')
.attr("class", "rect")
.attr("width", 10)
.attr("height", function(d) { return height - yScale(d[2]); })
.attr("x", function(d) {
return xScale(d[0]) + 5;
})
.attr("y", function(d) {
return yScale(d[2]);
})
.style("fill", "blue");
Note: there are much more elegant ways to do this. I am only showing you how to add the bars to your existing code. Please take a look at http://bl.ocks.org/mbostock/3887051 for further guidance.