D3.js data groups and transitions - javascript

Inspired by Mike Bostock's Wealth of Nations, I'm trying to illustrate infection rates over time. I'm trying to group by Month and transition() a bubble along the x-axis (Month).
I'm stuck on grouping by Month...
I've edited this post significantly following helpful feedback from Lars and Christopher below.
A jsFiddle example here - hhttp://jsfiddle.net/Nyquist212/JSsHL/1/
<div id="chart"></div>
<script type="text/javascript">
var json =
[
{
"Month":1,
"VisitCount":894,
"DiagnosisName":"ACUTE PHARYNGITIS"
},
{
"Month":1,
"VisitCount":807,
"DiagnosisName":"PNEUMONIA ORGANISM NOS"
},
{
"Month":2,
"VisitCount":566,
"DiagnosisName":"ACUTE PHARYNGITIS"
},
{
"Month":2,
"VisitCount":456,
"DiagnosisName":"PNEUMONIA ORGANISM NOS"
},
{
"Month":3,
"VisitCount":273,
"DiagnosisName":"ACUTE PHARYNGITIS"
},
{
"Month":3,
"VisitCount":189,
"DiagnosisName":"PNEUMONIA ORGANISM NOS"
}
]
var svgContainer = d3.select("#chart")
.append("svg")
.attr("height", 250)
.attr("width",750);
var bubbleGroup = svgContainer.append("g");
var bubble = bubbleGroup.selectAll("circle")
.data(json)
.enter()
.append("circle");
var bubbleAttributes = bubble
.style("stroke", "blue")
.style("fill", "white")
.attr("r", function(d){return (d.VisitCount/10);})
.attr("cy", 150)
.attr("cx", function(d){return (d.Month * 100);});
d3.select("Body").selectAll("p")
.data(json)
.enter()
.append("p")
.text(function(d){return d.Month + " " + d.DiagnosisName + " " + d.VisitCount;})
</script>
EDIT: Updated with corrections from Christopher Chiche
EDIT: Updated with partially working example as suggested by Lars Kotthoff

I would use a combination of d3.nest and a transition loop for this. Best illustrated by an example:
svg.selectAll("circle")
.data(d3.nest()
.key(function(d) { return d.DiagnosisName; })
.entries(json))
.enter().append("circle")
.style("stroke", "blue")
.style("fill", "white")
.attr("cy", 150)
.attr("cx", 0)
.attr("r", 0)
.each(function(d) {
for(var i = 0; i < d.values.length; i++) {
d3.select(this).transition().delay(1000 * i).duration(1000)
.attr("r", function(d){return (d.values[i].VisitCount/10);})
.attr("cx", function(d){return (d.values[i].Month * 100);});
}
});
Complete jsfiddle here.

your problem is that dataset does not contain any data. It is a call to a d3 function that does not return anything. However, you have this csv variable that you pass as an argument to the drawChart function.
You should thus write:
var circleGroup = svgContainer.append("g")
.selectAll("circles")
.data(csv)
Same for every time you use 'dataset' in a data() call.
If you have no data, then d3 does not plot anything. So looking at the data you attach when you have this kind of problem helps most of the times.
Also, interpolateData won't work for the same reason, you should probably pass data as an argument.

Related

How to oscillate and tween between multiple number states?

I would like to be able to do something similair to what Mike Bostock does in his second example in the documentation for .textTween. I've managed to do a lot of work towards solving this, but I can't quite get it right. I'm completely new to JavaScript so perhaps thats the problem.
In the case of the observable notebook, the number oscilates between different random variables, which are assigned to the _current parameter for the next oscillation. How would I do this with only two numbers, which I would like to go back and forth between?
I tried working it into some code like this but to no avail -
var svg = d3.select("body")
.append("svg")
.attr("width", 960)
.attr("height", 500);
function textrepeat() {
var textrepeat = svg.append("text")
.attr("fill", "steelblue")
.attr("class", "txt")
.attr("x", 30)
.attr("y", 30)
repeat();
function repeat() {
textrepeat
.text("300")
.transition()
.duration(2000)
.tweening ??? //here is where to insert it?
.text("1000")
.transition()
.duration(2000)
.text("300")
.on("end", repeat);
};
};
textrepeat();
Thanks in advance
If I correctly understand what you want, all you need is two textTween functions. For instance, going from 300 to 1000 and back:
var svg = d3.select("body")
.append("svg");
function textrepeat() {
var textrepeat = svg.append("text")
.attr("fill", "steelblue")
.attr("x", 30)
.attr("y", 50);
repeat();
function repeat() {
textrepeat.transition()
.duration(2000)
.textTween(function() {
return function(t) {
return ~~d3.interpolate(300, 1001)(t)
};
})
.transition()
.duration(2000)
.textTween(function() {
return function(t) {
return ~~d3.interpolate(1001, 300)(t)
};
})
.on("end", repeat);
};
};
textrepeat();
text {
font-size: 46px;
font-weight: 700;
}
<script src="https://d3js.org/d3.v5.min.js"></script>
PS: transition.textTween was added in D3 v5.14. If you're using a previous version, change it for .tween("text", function() { etc....

Plotting svg circles based off csv data

I'm trying to plot circles from data in my csv file, but the circles are not appearing on the svg canvas. I believe the problem stems from how I load in the data (it gets loaded as an array of objects), but I'm not quite sure how to figure out what to do next.
Based off this tutorial: https://www.dashingd3js.com/svg-text-element
D3.js code:
var circleData = d3.csv("files/data.csv", function (error, data) {
data.forEach(function (d) {
d['KCComment'] = +d['KCComment'];
d['pscoreResult'] = +d['pscoreResult'];
d['r'] = +d['r'];
});
console.log(data);
});
var svg = d3.select("body").append("svg")
.attr("width", 480)
.attr("height", 480);
var circles = svg.selectAll("circle")
.data(circleData)
.enter()
.append("circle");
var circleAttributes = circles
.attr("cx", function (d) { return d.KCComment; })
.attr("cy", function (d) { return d.pscoreResult; })
.attr("r", function (d) { return d.r; })
.style("fill", "green");
var text = svg.selectAll("text")
.data(circleData)
.enter()
.append("text");
var textLabels = text
.attr("x", function(d) { return d.KCComment; })
.attr("y", function(d) { return d.pscoreResult; })
.text(function (d) { return "( " + d.KCComment + ", " + d.pscoreResult + " )"; })
.attr("font-family", "sans-serif")
.attr("font-size", "20px")
.attr("fill", "red");
What the CSV looks like:
fmname, fmtype, KCComment, pscoreResult, r
test1, type1, 7.1, 8, 39
test2, type2, 1.2, 3, 12
You should have the circle-drawing code within the d3.csv function's callback, so it's only processed when the data is available.
d3.csv("data.csv", function (error, circleData) {
circleData.forEach(function (d) {
d['KCComment'] = +d['KCComment'];
d['pscoreResult'] = +d['pscoreResult'];
d['r'] = +d['r'];
});
console.log(circleData);
// Do the SVG drawing stuff
...
// Finished
});
Also note that instead of setting var circleData = d3.csv(... you should just define it in the callback function.
Here's a plunker with the working code: http://embed.plnkr.co/fzBX0o/preview
You'll be able to see a number of further issues now: both circles are overlapping and only one quarter is visible. That's because your KCComment and pscoreResult values used to define the circles' cx and cy are too small. Try multiplying them up so that the circles move right and down and are a bit more visible! Same is true of the text locations, but I'll leave those problems for you to solve

How to update selection with new data in D3?

I'm trying to edit the data of created circles in D3. Below my code is pasted of me creating a lot of circles based on some data from graphData.
Supposed I'd want to re-arrange my circles Y position with a new dataset, by transitioning them to their new destinations. How would perform this task? I've tried using attr.("cy", function(d){return yScale(parseFloat(d))} ) to update my Y-coordinates by adding data(graphData[i], function(d){return d;}) with my new data, but this does not work.
You can take a look at my JSFiddle: http://jsfiddle.net/RBr8h/1/
Instead of the for-loop in the following code I've created circles on 2 ticks of my X-axis. I have 3 sets of data and I've used to of them in the example in the fiddle. I'd like to able to use the 3rd dataset instead of the 2 first ones on both circles.
var circle;
for(var i = 0;i < graphData.length;i++){
circle = SVGbody
.selectAll("circle")
.data(graphData[i], function(d){return d;})
.enter()
.append("circle")
.attr("cx",xScale(0))
.attr("cy", yScale(minAxisY))
.attr("r",4)
.style('opacity', 0)
.transition()
.duration(1000)
.attr("cx", function(d){
return spreadCircles(i);
})
//.attr("cy", function (d, i){ return yScale(i); })
.style('opacity', 1)
.transition()
.duration(1500)
.attr("cy", function(d){return yScale(parseFloat(d))} );
Thank you for your help in advance!
To put some flesh on Lars comment, here is a FIDDLE leveraging the enter/update/exit pattern to help you out. I have altered and simplified your code (and data) just enough to demonstrate the principle.
function updateCircles(dataset,color) {
var circle = SVGbody
.selectAll("circle")
.data(dataset, function(d) { return d; });
circle
.exit()
.transition().duration(750)
.attr("r", 0)
.remove();
circle
.enter()
.append("circle");
circle
.attr("cx",function(d){return xScale(100);})
.attr("cy",function(d){return yScale(parseFloat(d))})
.attr("r",0)
.transition().duration(1500)
.attr("r",5)
.style("fill", color);
};
Update fiddle with data keyed off by index...so, circles just have their position updated.

d3.js - connecting shapes with lines (without using force or other layouts)

I have a bunch of static circles and I want to connect them with lines (it's a dependency graph). All the examples I see are done with d3's ready-made layouts and I'm not sure how to approach this efficiently. I also want to highlight lines related to a node when I mouse-over that node, as well as fade any other shapes/lines.
This is what I have for now: (it just draws evenly spaced and sized circles according to area size given)
<!DOCTYPE html>
<html>
<head>
<script type="text/javascript" src="http://mbostock.github.com/d3/d3.js"></script>
</head>
<body style="overflow: hidden;">
<div id="drawarea" style="overflow: hidden;"></div>
<script type="text/javascript">
var dataset = [],
i = 0;
for(i=0; i<45; i++){
dataset.push(Math.round(Math.random()*100));
}
var width = 5000,
height = 3000;
var svg = d3.select("#drawarea").append("svg")
.attr("width", width)
.attr("height", height)
.call(d3.behavior.zoom().scaleExtent([1, 8]).on("zoom", zoom))
.append("g");
var div_area = width*height,
num_nodes = dataset.length,
node_area = div_area/num_nodes*0.7,
node_to_padding_ratio = 0.50,
node_dia_inc_pad = Math.sqrt(node_area),
node_radius_wo_pad = node_dia_inc_pad/2*node_to_padding_ratio,
node_padding = node_dia_inc_pad/2*(1-node_to_padding_ratio),
nodes_in_width = parseInt(width/(node_dia_inc_pad)),
nodes_in_height = parseInt(height/(node_dia_inc_pad));
svg.selectAll("circle")
.data(dataset)
.enter().append("circle")
.style("stroke", "gray")
.style("fill", "white")
.attr("r", node_radius_wo_pad)
.attr("cx", function(d, i){ return 2*node_radius_wo_pad+i%nodes_in_width*node_dia_inc_pad;})
.attr("cy", function(d, i){ return 2*node_radius_wo_pad+(parseInt(i/nodes_in_width))*node_dia_inc_pad})
.on("mouseover", function(){d3.select(this).style("fill", "aliceblue");})
.on("mouseout", function(){d3.select(this).style("fill", "white");})
function zoom() {
svg.attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")");
}
</script>
</body>
</html>
EDIT: My revised code:
<!DOCTYPE html>
<html>
<head>
<script type="text/javascript" src="http://mbostock.github.com/d3/d3.js"></script>
</head>
<body style="overflow: hidden;">
<div id="canvas" style="overflow: hidden;"></div>
<script type="text/javascript">
var graph = {
"nodes":[
{"name":"Myriel","group":1},
{"name":"Napoleon","group":1}
],
"links":[
{"source":1,"target":0,"value":1}
]
}
var width = 2000,
height = 1000;
var svg = d3.select("#canvas").append("svg")
.attr("width", width)
.attr("height", height)
.call(d3.behavior.zoom().scaleExtent([1, 8]).on("zoom", zoom))
.append("g");
var div_area = width*height,
num_nodes = graph.nodes.length,
node_area = div_area/num_nodes,
node_to_padding_ratio = 0.50,
node_dia_inc_pad = Math.sqrt(node_area),
node_radius_wo_pad = node_dia_inc_pad/2*node_to_padding_ratio,
node_padding = node_dia_inc_pad/2*(1-node_to_padding_ratio),
nodes_in_width = parseInt(width/(node_dia_inc_pad)),
nodes_in_height = parseInt(height/(node_dia_inc_pad));
var xScale = d3.scale.linear()
.domain([0,nodes_in_width])
.range([node_radius_wo_pad,width-node_radius_wo_pad]);
var yScale = d3.scale.linear()
.domain([0,nodes_in_height])
.range([node_radius_wo_pad,height-node_radius_wo_pad]);
var lines = svg.attr("class", "line")
.selectAll("line").data(graph.links)
.enter().append("line")
.attr("x1", function(d) { return xScale(d.source%nodes_in_width); })
.attr("y1", function(d) { return yScale(parseInt(d.source/nodes_in_width)); })
.attr("x2", function(d) { return xScale(d.target%nodes_in_width); })
.attr("y2", function(d) { return yScale(parseInt(d.target/nodes_in_width)); })
.attr("src", function(d) { return d.source; })
.attr("trgt", function(d) { return d.target; })
.style("stroke", "grey");
var circles = svg.selectAll("circle")
.data(graph.nodes)
.enter().append("circle")
.style("stroke", "gray")
.style("fill", "white")
.attr("r", node_radius_wo_pad)
.attr("cx", function(d, i){ return xScale(i%nodes_in_width);})
.attr("cy", function(d, i){ return yScale(parseInt(i/nodes_in_width));})
.attr("index", function(d, i){return i;})
.on("mouseover", function(){
var that = this;
lines.filter(function() {
return d3.select(this).attr("src") == d3.select(that).attr("index");
}).style("stroke", "red");
lines.filter(function() {
return d3.select(this).attr("trgt") == d3.select(that).attr("index");
}).style("stroke", "green");
lines.filter(function() {
return (d3.select(this).attr("trgt") != d3.select(that).attr("index") && d3.select(this).attr("src") != d3.select(that).attr("index"));
}).style("display", "none");
d3.select(this).style("fill", "aliceblue");
})
.on("mouseout", function(){
lines.style("stroke", "grey")
.style("display", "block");
d3.select(this).style("fill", "white");
});
function zoom() {
svg.attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")");
}
</script>
</body>
</html>
What I want to do now is have the circles the lines point to and from be colored similarly. I'm not sure how to make the reference to them from the "mouseover" event of a circle though. Will do some testing...
You haven't specified how your nodes are connected, so I'm assuming that everything is connected to everything. The principle is the same as for any other layout -- you take the data you have that determines the links and pass it to .data(). In your code, the coordinates aren't part of the data, which makes it a bit more verbose, but still quite straightforward.
To add the links, I'm using a nested selection -- I'm adding a g element for each node and underneath the connections to all the other nodes.
var lines = svg.selectAll("g.line").data(dataset)
.enter().append("g").attr("class", "line")
.selectAll("line").data(dataset)
.enter().append("line")
.attr("x1", function(d, i) { return 2*node_radius_wo_pad+i%nodes_in_width*node_dia_inc_pad; })
.attr("y1", function(d, i) { return 2*node_radius_wo_pad+(parseInt(i/nodes_in_width))*node_dia_inc_pad; })
.attr("x2", function(d, i, j) { return 2*node_radius_wo_pad+j%nodes_in_width*node_dia_inc_pad; })
.attr("y2", function(d, i, j) { return 2*node_radius_wo_pad+(parseInt(j/nodes_in_width))*node_dia_inc_pad; });
This adds a line for every pair of nodes. Note that it will add links between the same nodes (which you won't be able to see) and 2 links between each pair of nodes -- once starting at one node and once at the other. I haven't filtered out these cases here to keep the code simple. In your particular application, I'm guessing that the connections are determined in another way anyway.
To highlight the links that are connected a particular node on highlight, I'm using the links variable that contains all of them and filtering out the ones whose start coordinates are different from the coordinates of the circle. The filtered selection is then painted red.
.on("mouseover", function(){
var that = this;
lines.filter(function() {
return d3.select(this).attr("x1") == d3.select(that).attr("cx") && d3.select(this).attr("y1") == d3.select(that).attr("cy");
}).style("stroke", "red");
d3.select(this).style("fill", "aliceblue");
})
If the coordinates are part of the data, everything will become a bit easier and look more like the examples you may have seen for the force layout for example. I would recommend to create a data structure much like what's used there for your links, with source and target attributes that determine the source and target nodes.
Complete example here.

Binding Data from tutorial example

I'm pretty new to d3 and have been following this tutorial: http://christopheviau.com/d3_tutorial/
I'm stuck on the 'Binding Data' example - it's pretty simple but the code just won't produce anything. I've poked around here and haven't found the question listed so I thought I'd ask away.
Here's the code:
var dataset = [],
i = 0;
for(i = 0; i < 5; i++) {
dataset.push(Math.round(Math.random() * 100));
}
var sampleSVG = d3.select("#viz")
.append("svg")
.attr("width", 400)
.attr("height", 75);
sampleSVG.selectAll("circle")
.data(dataset)
.enter().append("circle")
.style("stroke", "gray")
.style("fill", "white")
.attr("height", 40)
.attr("width", 75)
.attr("x", function (d, i) {
return i * 80
})
.attr("y", 20);
Other examples on the site work fine.
Thanks in advance - any ideas would be appreciated.
Unfortunately the code listed in the tutorial is incorrect. The svg element "circle" is specified by three attributes, "cx", x-axis coordinate of the center of the circle, "cy", y-axis coordinate of the center of the circle, and "r", the radius of the circle. I got this information from the w3 specification for an SVG circle.
I would recommend inspecting the JavaScript in the tutorial page to help iron out any other inconsistencies. Here it is:
<script type="text/javascript">
var dataset = [],
i = 0;
for(i=0; i<5; i++){
dataset.push(Math.round(Math.random()*100));
}
var sampleSVG = d3.select("#viz5")
.append("svg")
.attr("width", 400)
.attr("height", 100);
sampleSVG.selectAll("circle")
.data(dataset)
.enter().append("circle")
.style("stroke", "gray")
.style("fill", "white")
.attr("r", 40)
.attr("cx", function(d, i){return i*80+40})
.attr("cy", 50)
.on("mouseover", function(){d3.select(this).style("fill", "aliceblue");})
.on("mouseout", function(){d3.select(this).style("fill", "white");})
.on("mousedown", animateFirstStep);
function animateFirstStep(){
d3.select(this)
.transition()
.delay(0)
.duration(1000)
.attr("r", 10)
.each("end", animateSecondStep);
};
function animateSecondStep(){
d3.select(this)
.transition()
.duration(1000)
.attr("r", 40);
};
</script>
I also created a JSFiddle which you can utilize to get the basic idea that the author of the tutorial is trying to convey, with respect to utilizing d3.js data, here.
svg circles use cx, cy, and r - not x, y, height, and width. I've correct the example code below:
var dataset = [];
for(var i = 0; i < 5; i++) {
dataset.push(Math.round(Math.random() * 100));
}
var sampleSVG = d3.select("#viz")
.append("svg")
.attr("width", 400)
.attr("height", 400);
sampleSVG.selectAll("circle")
.data(dataset)
.enter().append("circle")
.style("stroke", "black")
.attr("r", 10)
.attr("cx", function (d, i) {
return i * 80 + 10;
})
.attr("cy", function (d, i) {
return d;
});
http://jsfiddle.net/q3P4v/7/
MDN on svg circles: https://developer.mozilla.org/en-US/docs/SVG/Element/circle

Categories

Resources