D3.js Enter, Update, Exit issue - javascript

I have a relatively simple barchart. I want to .transition() between datasets with an .on("click") event. What I'm getting is a complete redraw of an additional chart appended to the DOM id, instead of removing the original chart and transitioning or replacing it. I think I'm misunderstanding how to correctly .remove().
d3.json("data/cfilt-steps.json", function(d) {
d.forEach(function(d) {
parseDate = d3.time.format("%Y-%m-%d").parse;
d.date = parseDate(d.date); d.value = +d.value;
});
margin = {top:5, right:5, bottom: 40, left:5},
height = 150 - margin.top - margin.bottom,
width = 500 - margin.left - margin.right,
barPadding = 1;
steps = crossfilter(d),
monthdim = steps.dimension(function(d){ thisDate = new Date(d.date); return thisDate.getMonth(); }),
monthgrp = monthdim.group().reduceSum(function(d){ return d.value; });
daydim = steps.dimension(function(d){ thisDate = new Date(d.date); return thisDate.getDay(); }),
daygrp = daydim.group().reduceSum( function(d) { return d.value; });
stepColor = d3.scale.threshold()
.domain([100, 150, 200, 250, 300, 400, 500])
.range(["#E3E3E3", "#D0DFD2", "#C3DABC", "#BDCB87", "#CAB44E", "#E29517", "#FF6600"]);
d3.select("#monthly-steps-previous-selector")
.on("click", function(d) {reDraw(monthgrp.all()) })
d3.select("#monthly-steps-next-selector")
.on("click", function(d) {reDraw(daygrp.all()); })
function reDraw(data) {
xScale = d3.scale.ordinal().domain(monthdim).range(0, width);
yScale = d3.scale.linear().domain([0, d3.max(data, function(d){return d.value;})]).range([height, 5]);
var stepbars = d3.select("#steps-bar")
.append("svg:svg")
.attr("width", width)
.attr("height", height)
stepbars.selectAll("rect")
.data(data)
.enter().append("rect")
.attr("x", function(d,i){ return i * width/data.length; })
.attr("y", function(d){ return yScale(d.value); })
.attr("width", width/data.length - barPadding)
.attr("height", function(d) { return height-yScale(d.value); })
.attr("fill", function(d){ return stepColor(d.value/2000); })
}
reDraw(monthgrp.all());
});
Can someone show me what this is supposed to look like, or tell me what I'm doing wrong?

Your reDraw function appends the svg, this means every time you call redraw a new svg is appended, hence the double chart. I would suggest to put the lines
var stepbars = d3.select("#steps-bar")
.append("svg:svg")
.attr("width", width)
.attr("height", height)
above the reDraw function.
Furthermore, your redraw function does not call remove. I would do something like:
//Select and bind to data
var selection = stepbars.selectAll("rect")
.data(data);
//Enter and create new rectangles
selection.enter()
.append("rect");
//Update all rectangles
selection.attr("x", function(d,i){ return i * width/data.length; })
.attr("y", function(d){ return yScale(d.value); })
.attr("width", width/data.length - barPadding)
.attr("height", function(d) { return height-yScale(d.value); })
.attr("fill", function(d){ return stepColor(d.value/2000); });
//Remove unused rectangles
selection.exit().remove();

Related

I want to redraw dots every time I click update

Right now I have a bubble chart that plots bubbles at different point with different radius. I want to be able to click a button and the re-populate the chart with new random bubbles.
I created a function update(), where I have tried putting d3.selectall("circles").remove() before appending new circles to it. But it only removes the circles after I click it.
<body>
<button class="btn" onclick="update()">Update</button>
<!-- load the d3.js library -->
<script src="https://d3js.org/d3.v4.min.js"></script>
<script>
var margin = {top: 20, right: 20, bottom: 30, left: 50},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
var x = d3.scaleLinear().range([0, width]);
var y = d3.scaleLinear().range([height, 0]);
var r = d3.scaleLinear().range([10, 50]);
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 getdata = function() {
var dataset = []
for (var i = 0; i < 20; i++) {
var x = d3.randomUniform(-50,50)();
var y = d3.randomUniform(-50,50)();
var r = d3.randomUniform(-50,50)();
dataset.push({"x": x, "y": y,"r":r});
}
return dataset
}
var data = getdata()
data.forEach(function(d) {
d.x = +d.x;
d.y = +d.y;
d.r = +d.r;
});
x.domain([-50, 50]);
y.domain([-50, 50]);
r.domain([-50, 50]);
svg.selectAll("dot")
.data(data)
.enter().append("circle")
.attr("r", function(d) { return r(d.r); })
.attr("cx", function(d) { return x(d.x); })
.attr("cy", function(d) { return y(d.y); })
.style("stroke", "black")
.style("fill", "none")
svg.append("g")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x));
svg.append("g")
.call(d3.axisLeft(y));
function update(){
d3.selectAll("circle").remove()
svg.selectAll("dot")
.data(data)
.enter().append("circle")
.attr("r", function(d) { return r(d.r); })
.attr("cx", function(d) { return x(d.x); })
.attr("cy", function(d) { return y(d.y); })
.style("stroke", "black")
.style("fill", "none")
}
You're actually deleting the bubble and redrawing the same.
You need to update your chart.
function update(){
d3.selectAll("circle").remove()
svg.selectAll("dot")
.data(getdata())
.enter().append("circle")
.attr("r", function(d) { return r(d.r); })
.attr("cx", function(d) { return x(d.x); })
.attr("cy", function(d) { return y(d.y); })
.style("stroke", "black")
.style("fill", "none")
}
I just replaced .data(data) with .data(getdata())

Transitioning a bar chart with negative values for the width

I am creating a horizontal bar chart using d3. And I am using an animation to "grow" the chart at startup. Here is the code.
// Create the svg element
d3.select("#chart-area")
.append("svg")
.attr("height", 800)
.attr("width", 800);
.data(dataValues) // This data is previously prepared
.enter().append("rect")
.style("fill", "blue")
.attr("x", function () { return xScale(0); }) // xScale is defined earlier
.attr("y", function (d) { return yScale(d); }) // yScale is defined earlier
.attr("height", yScale.bandwidth()) // yScale is defined earlier
// Initial value of "width" (before animation)
.attr("width", 0)
// Start of animation transition
.transition()
.duration(5000) // 5 seconds
.ease (d3.easeLinear);
// Final value of "width" (after animation)
.attr("width", function(d) { return Math.abs(xScale(d) - xScale(0)); })
The above code would work without any problem, and the lines would grow as intended, from 0 to whichever width, within 5 seconds.
Now, if we change the easing line to the following
// This line changed
.ease (d3.easeElasticIn);
Then, the ease would try to take the width to a negative value before going to a final positive value. As you can see here, d3.easeElasticIn returns negative values as time goes by, then back to positive, resulting in width being negative at certain points in the animation. So the bars do not render properly (because SVG specs state that if width is negative, then use 0)
I tried every solution to allow the bars to grow negatively then back out. But could not find any. How can I fix this problem?
Thanks.
As you already know, the use of d3.easeElasticIn in your specific code will create negative values for the rectangles' width, which is not allowed.
This basic demo reproduces the issue, the console (your browser's console, not the snippet's console) is populated with error messages, like this:
Error: Invalid negative value for attribute width="-85.90933910798789"
Have a look:
const svg = d3.select("svg");
const margin = 50;
const line = svg.append("line")
.attr("x1", margin)
.attr("x2", margin)
.attr("y1", 0)
.attr("y2", 150)
.style("stroke", "black")
const data = d3.range(10).map(function(d) {
return {
y: "bar" + d,
x: Math.random()
}
});
const yScale = d3.scaleBand()
.domain(data.map(function(d) {
return d.y
}))
.range([0, 150])
.padding(0.2);
const xScale = d3.scaleLinear()
.range([margin, 300]);
const bars = svg.selectAll(null)
.data(data)
.enter()
.append("rect")
.attr("x", margin)
.attr("width", 0)
.style("fill", "steelblue")
.attr("y", function(d) {
return yScale(d.y)
})
.attr("height", yScale.bandwidth())
.transition()
.duration(2000)
.ease(d3.easeElasticIn)
.attr("width", function(d) {
return xScale(d.x) - margin
})
<script src="https://d3js.org/d3.v5.min.js"></script>
<svg></svg>
So, what's the solution?
One of them is catching those negative values as they are generated and, then, moving the rectangle to the left (using the x attribute) and converting those negative numbers to positive ones.
For that to work, we'll have to use attrTween instead of attr in the transition selection.
Like this:
.attrTween("width", function(d) {
return function(t){
return Math.abs(xScale(d.x) * t);
};
})
.attrTween("x", function(d) {
return function(t){
return xScale(d.x) * t < 0 ? margin + xScale(d.x) * t : margin;
};
})
In the snippet above, margin is just a margin that I created so you can see the bars going to the left of the axis.
And here is the demo:
const svg = d3.select("svg");
const margin = 100;
const line = svg.append("line")
.attr("x1", margin)
.attr("x2", margin)
.attr("y1", 0)
.attr("y2", 150)
.style("stroke", "black")
const data = d3.range(10).map(function(d) {
return {
y: "bar" + d,
x: Math.random()
}
});
const yScale = d3.scaleBand()
.domain(data.map(function(d) {
return d.y
}))
.range([0, 150])
.padding(0.2);
const xScale = d3.scaleLinear()
.range([0, 300 - margin]);
const bars = svg.selectAll(null)
.data(data)
.enter()
.append("rect")
.attr("x", margin)
.attr("width", 0)
.style("fill", "steelblue")
.attr("y", function(d) {
return yScale(d.y)
})
.attr("height", yScale.bandwidth())
.transition()
.duration(2000)
.ease(d3.easeElasticIn)
.attrTween("width", function(d) {
return function(t) {
return Math.abs(xScale(d.x) * t);
};
})
.attrTween("x", function(d) {
return function(t) {
return xScale(d.x) * t < 0 ? margin + xScale(d.x) * t : margin;
};
})
<script src="https://d3js.org/d3.v5.min.js"></script>
<svg></svg>

Conditional Bar Chart with D3

I have a D3 visualization with a map and a bar chart. I am trying to get the bar chart to change depending on which circle on the map is clicked. Not sure how to do this. I have a function in my bar_chart.js file named update(newData) and a few extra arrays for the different circles on the map. Here is the link to the bl.ocks for the map and bar char.
js code for map
var myData = [21, 3, 5, 21, 15];
//Width and height
var w = 200;
var h = 125;
var yScale = null;
function draw(initialData) {
var xScale = d3.scale.ordinal()
.domain(d3.range(initialData.length))
.rangeRoundBands([0, w], 0.05);
yScale = d3.scale.linear()
.domain([0, d3.max(initialData)])
.range([0, h]);
//Create SVG element
var svg = d3.select("#chart")
.append("svg")
.attr("width", w)
.attr("height", h);
svg.selectAll("rect")
.data(initialData)
.enter()
.append("rect")
.attr("x", function(d, i) {
return xScale(i);
})
.attr("y", function(d) {
return h - yScale(d);
})
.attr("width", xScale.rangeBand())
.attr("height", function(d) {
return yScale(d);
})
.attr("fill", "steelblue");
svg.selectAll("text")
.data(initialData)
.enter()
.append("text")
.text(function(d) {
return d;
})
.attr("text-anchor", "middle")
.attr("x", function(d, i) {
return xScale(i) + xScale.rangeBand() / 2;
})
.attr("y", function(d) {
return h - yScale(d) + 14;
})
.attr("font-family", "sans-serif")
.attr("font-size", "11px")
.attr("fill", "white");
}
draw(myData);
//update function
function update(newData) {
yScale.domain([0, d3.max(newData)]);
var rects = d3.select("#chart svg")
.selectAll("rect")
.data(newData);
// enter selection
rects
.enter().append("rect");
// update selection
rects
.transition()
.duration(300)
.attr("y", function(d) {
return h - yScale(d);
})
.attr("height", function(d) {
return yScale(d);
})
// exit selection
rects
.exit().remove();
var texts = d3.select("#chart svg")
.selectAll("text")
.data(newData);
// enter selection
texts
.enter().append("rect");
// update selection
texts
.transition()
.duration(300)
.attr("y", function(d) {
return h - yScale(d) + 14;
})
.text(function(d) {
return d;
})
// exit selection
texts
.exit().remove();
}
var mk = [10,17,20,14,8];
var cn = [18,4,9,20,15];
var nd = [5,12,7,15,21];
d3.select("#update").on("click", function() { update(newData); });
You have to incorporate the barchart data in your cities.csv file.
In the on-click handler of cities.csv where you show the tooltip you have to transform the data from the CSV into an array and call the bar chart update() method with this array.
One way of doing is to replace the , from the bar chart data with another char and split the string and convert the parts to numbers.
var cityData = d.barchart.split('#').map(Number);
update(cityData);
You also have to set the attributes of the new rects and texts of the bar chart. And the x-position will change if the number of bars change.

update d3 chart with new data

I want to update the bar chart with new data and I looked over this question:
How to update d3.js bar chart with new data
But it is not working for me. Probably because I don't know where to put the exit().remove() functions within my code. I tried putting this line
svg.selectAll("rect").exit().remove();
below the create bars section but it just removes all of the labels. Then, if I put it right after the create labels portion it removes the chart entirely. How can I get the update button change the chart with new data?
function draw(data) {
//Width and height
var w = 250;
var h = 250;
var xScale = d3.scale.ordinal()
.domain(d3.range(data.length))
.rangeRoundBands([0, w], 0.05);
var yScale = d3.scale.linear()
.domain([0, d3.max(data)])
.range([0, h]);
//Create SVG element
var svg = d3.select("#chart")
.append("svg")
.attr("width", w)
.attr("height", h);
//Create bars
svg.selectAll("rect")
.data(data)
.enter()
.append("rect")
.attr("x", function(d, i) {
return xScale(i);
})
.attr("y", function(d) {
return h - yScale(d);
})
.attr("width", xScale.rangeBand())
.attr("height", function(d) {
return yScale(d);
})
.attr("fill", "steelblue");
//Create labels
svg.selectAll("text")
.data(data)
.enter()
.append("text")
.text(function(d) {
return d;
})
.attr("text-anchor", "middle")
.attr("x", function(d, i) {
return xScale(i) + xScale.rangeBand() / 2;
})
.attr("y", function(d) {
return h - yScale(d) + 14;
})
.attr("font-family", "sans-serif")
.attr("font-size", "11px")
.attr("fill", "white");
};
function update() {
var data = [ 25, 22, 18, 15, 13 ];
draw(data);
}
var data = [ 21, 3, 5, 21, 15 ];
window.onload = draw(data);
With d3v3 (as I can see from your code you use this version) you should update your chart this way:
In update function set the new domain for yScale:
function update(newData) {
yScale.domain([0, d3.max(newData)]);
After that, apply new selection with selectAll("rect").data(newData), store selection in rects variable and set new value for appropriate attributes (if you do not want animation effect, remove .transition() .duration(300)):
var rects = d3.select("#chart svg")
.selectAll("rect")
.data(newData);
// enter selection
rects
.enter().append("rect");
// update selection
rects
.transition()
.duration(300)
.attr("y", function(d) {
return h - yScale(d);
})
.attr("height", function(d) {
return yScale(d);
})
Exit selection with exit method:
rects
.exit().remove();
Do the same way with text. I rewrite your code, look at the example in a hidden snippet below:
var myData = [21, 3, 5, 21, 15];
//Width and height
var w = 250;
var h = 250;
var yScale = null;
function draw(initialData) {
var xScale = d3.scale.ordinal()
.domain(d3.range(initialData.length))
.rangeRoundBands([0, w], 0.05);
yScale = d3.scale.linear()
.domain([0, d3.max(initialData)])
.range([0, h]);
//Create SVG element
var svg = d3.select("#chart")
.append("svg")
.attr("width", w)
.attr("height", h);
svg.selectAll("rect")
.data(initialData)
.enter()
.append("rect")
.attr("x", function(d, i) {
return xScale(i);
})
.attr("y", function(d) {
return h - yScale(d);
})
.attr("width", xScale.rangeBand())
.attr("height", function(d) {
return yScale(d);
})
.attr("fill", "steelblue");
svg.selectAll("text")
.data(initialData)
.enter()
.append("text")
.text(function(d) {
return d;
})
.attr("text-anchor", "middle")
.attr("x", function(d, i) {
return xScale(i) + xScale.rangeBand() / 2;
})
.attr("y", function(d) {
return h - yScale(d) + 14;
})
.attr("font-family", "sans-serif")
.attr("font-size", "11px")
.attr("fill", "white");
}
function update(newData) {
yScale.domain([0, d3.max(newData)]);
var rects = d3.select("#chart svg")
.selectAll("rect")
.data(newData);
// enter selection
rects
.enter().append("rect");
// update selection
rects
.transition()
.duration(300)
.attr("y", function(d) {
return h - yScale(d);
})
.attr("height", function(d) {
return yScale(d);
})
// exit selection
rects
.exit().remove();
var texts = d3.select("#chart svg")
.selectAll("text")
.data(newData);
// enter selection
texts
.enter().append("rect");
// update selection
texts
.transition()
.duration(300)
.attr("y", function(d) {
return h - yScale(d) + 14;
})
.text(function(d) {
return d;
})
// exit selection
texts
.exit().remove();
}
window.onload = draw(myData);
setInterval(function() {
var data = d3.range(5).map(function() {
return parseInt(Math.random() * 20 + 1);
});
update(data);
}, 3000)
<div id="chart"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.js"></script>

Chart not updating properly

I am trying to follow Mike Bostock's tutorial on d3js (http://mbostock.github.io/d3/tutorial/bar-2.html) to understand how to update charts dynamically but I am facing some hurdles.
In my chart, my bars on the left, rather than being simply removed, are sent behind my chart and I can't figure out why:
JS:
var t = 1297110663, // start time (seconds since epoch)
v = 70, // start value (subscribers)
data = d3.range(33).map(next); // starting dataset
function next() {
return {
time: ++t,
value: v = ~~Math.max(10, Math.min(90, v + 10 * (Math.random() - .5)))
};
}
setInterval(function(){
data.shift();
data.push(next());
console.log(data);
redraw();
}, 1000);
var w = 20,
h =80;
var x = d3.scale.linear()
.domain([0, 1])
.range([0, w]);
var y = d3.scale.linear()
.domain([0, 100])
.rangeRound([0, h]);
var chart = d3.select(".container").append("svg")
.attr("class", "chart")
.attr("width", w * data.length - 1)
.attr("height", h);
chart.selectAll("rect")
.data(data)
.enter().append("rect")
.attr("x", function(d, i){ return x(i) - 0.5; })
.attr("y", function(d) { return h - y(d.value) - .5; })
.attr("width", w)
.attr("height", function(d) { return y(d.value); });
function redraw(){
console.log(data);
var rect = chart.selectAll('rect')
.data(data, function(d){ return d.time; });
rect.enter().insert("rect")
.attr("x", function(d, i) { return x(i + 1) - .5; })
.attr("y", function(d) { return h - y(d.value) - .5; })
.attr("width", w)
.attr("height", function(d) { return y(d.value); })
rect.transition() // Shouldn't I use .update() here?
.duration(1000)
.attr("x", function(d, i) { return x(i) - .5; });
rect.exit().transition()
.duration(1000)
.attr('x', function(d, i) { return x(i - 1) - .5})
.remove();
}
Here is the jsfiddle: http://jsfiddle.net/kkMR4/
Another thing I don't understand is why we dont use .update()? If I understand correctly .enter() is used to create the DOM element where data didnt find any match in the DOM and .exit() is used to find the DOM elements which are not in data, so shouldn't I use update() to move all the other column to the left?
Many thanks
Best
The problem is in this block:
rect.exit().transition()
.duration(1000)
.attr('x', function(d, i) { return x(i - 1) - .5})
.remove();
The third line (.attr), reassigns the coordinates. If you want them to truly exit, you can remove this line.
rect.exit().transition()
.duration(1000)
.remove();

Categories