I've created a Beeswarm plot with d3v4
and d3.forceSimulation
, and the points are where I want them to be:
var data = [
{ country: "Algeria", amount: 22, year: 2000 },
{ country: "Argentina", amount: 49, year: 1990 },
{ country: "Armenia", amount: 3, year: 1990 },
{ country: "Australia", amount: 9, year: 2010 },
{ country: "Austria", amount: 1, year: 2010 },
{ country: "Bahamas", amount: 5, year: 2018 },
{ country: "Bahrain", amount: 22, year: 2018 },
{ country: "Belarus", amount: 9, year: 2010 },
{ country: "Belgium", amount: 46, year: 2018 },
{ country: "Brazil", amount: 79, year: 1990 },
{ country: "Canada", amount: 12, year: 2000 },
{ country: "China", amount: 26, year: 2018 },
{ country: "Colombia", amount: 9, year: 2010 },
{ country: "Croatia", amount: 8, year: 2000 },
{ country: "Cuba", amount: 14, year: 1990 },
{ country: "Czech Republic", amount: 11, year: 2018 },
{ country: "Denmark", amount: 125, year: 2010 },
{ country: "Canada", amount: 124, year: 2018 },
{ country: "Bahrain", amount: 39, year: 2010 },
{ country: "Estonia", amount: 141, year: 2018 },
{ country: "Ethiopia", amount: 38, year: 1990 },
{ country: "France", amount: 4, year: 2018 },
{ country: "Germany", amount: 15, year: 2000 },
{ country: "Greece", amount: 16, year: 2010 },
{ country: "Grenada", amount: 241, year: 2010 },
{ country: "Hungary", amount: 135, year: 1990 },
{ country: "India", amount: 22, year: 1990 },
{ country: "Indonesia", amount: 31, year: 1990 },
{ country: "Iran", amount: 88, year: 2010 },
{ country: "Ireland", amount: 12, year: 2018 },
{ country: "Italy", amount: 128, year: 2000 },
{ country: "Jamaica", amount: 1, year: 2018 },
{ country: "Japan", amount: 41, year: 1990 },
{ country: "Jordan", amount: 137, year: 2010 },
{ country: "Iran", amount: 13, year: 1990 },
{ country: "Malaysia", amount: 25, year: 2018 },
{ country: "Mexico", amount: 59, year: 2010 },
{ country: "Moldova", amount: 71, year: 2000 },
{ country: "Mongolia", amount: 22, year: 2018 },
{ country: "Morocco", amount: 131, year: 1990 },
{ country: "Netherlands", amount: 129, year: 2018 },
{ country: "New Zealand", amount: 148, year: 2018 },
{ country: "Niger", amount: 1, year: 2010 },
{ country: "Nigeria", amount: 41, year: 1990 },
{ country: "Norway", amount: 14, year: 2010 },
{ country: "Philippines", amount: 15, year: 2018 },
{ country: "Poland", amount: 12, year: 2010 },
{ country: "Portugal", amount: 31, year: 2000 },
{ country: "Puerto Rico", amount: 51, year: 2000 },
{ country: "Romania", amount: 15, year: 2000 },
{ country: "Serbia", amount: 18, year: 2000 },
{ country: "South Africa", amount: 14, year: 2010 },
{ country: "Sweden", amount: 11, year: 2018 },
{ country: "Switzerland", amount: 7, year: 2010 },
{ country: "Thailand", amount: 61, year: 2018 },
{ country: "Trinidad and Tobago", amount: 12, year: 2018 },
{ country: "Tunisia", amount: 34, year: 2010 },
{ country: "Turkey", amount: 28, year: 2010 },
{ country: "Ukraine", amount: 11, year: 2010 },
{ country: "Uzbekistan", amount: 123, year: 2018 },
{ country: "Venezuela", amount: 23, year: 2018 },
{ country: "Iran", amount: 13, year: 2018 }
];
var width = 1000,
height = 500;
var svg = d3.select("#chart")
.append("svg")
.attr("width", width)
.attr("height", height);
var x = d3.scaleLinear()
.range([95, 650]);
var y = d3.scaleLinear()
.range([100, 450]);
data.forEach(d => {
d.amount = +d.amount;
});
var sort = data.sort((a, b) => d3.descending(a, b));
y.domain(d3.extent(data, function(d) {
return d.amount;
}));
x.domain(d3.extent(data, function(d) {
return d.year;
}));
var simulation = d3.forceSimulation(data)
.force("x", d3.forceX(function(d) {
return x(d.year);
}).strength(3))
.force("y", d3.forceY(function(d) {
return y(d.amount)
}).strength(2))
.force("collide", d3.forceCollide(7).strength(7))
.stop();
for (var i = 0; i < data.length * 2; ++i) simulation.tick();
var circles = svg.selectAll(".circles")
.data(data);
var circlesEnter = circles.enter()
.append("circle");
circlesEnter.attr("r", 4)
.attr("cx", function(d) {
return d.x
})
.attr("cy", function(d) {
return d.y
})
.attr("fill", function(d) {
if (d.country == "Iran") {
return "#FF0044"
} else if (d.country == "Canada") {
return "#00A9E9"
} else if (d.country == "Bahrain") {
return "#6BF4C6"
} else {
return '#333'
}
})
.attr('class', function(d) {
return d.amount + ' ' + d.year + ' ' + d.country
})
// connector lines
var byCountry = d3.nest()
.key(function(d) {
return d.country;
})
.entries(data);
var countryNames = d3.values(byCountry).map(function(d) {
return d.values.map(function(v) {
return v.country;
}).join(', ');
});
for (i = 0; i < countryNames.length; i++) {
eaco = countryNames[i].split(',')[0]
const filterByCountry = (country, data) => item => item.country === country
connectData = data.filter(filterByCountry(eaco))
var linesGroup = svg.append("g")
.attr("class", "connectors");
var linec = d3.line()
.x(function(d) {
return x(d.year)
})
.y(function(d) {
return y(d.amount)
})
// using below as the points does not work
// .x(function(d) { return x(d.x)})
// .y(function(d) { return y(d.y)})
var lineGraph = linesGroup.selectAll('.connect')
.data(connectData)
.enter()
.append("path")
.attr('class', function(d) {
return d.amount + ' ' + d.year + ' ' + d.country
})
.attr("d", linec(connectData))
.attr("stroke", function(d) {
if (d.country == "Iran") {
return "#FF0044"
} else if (d.country == "Canada") {
return "#00A9E9"
} else if (d.country == "Bahrain") {
return "#6BF4C6"
} else {
return '#333'
}
})
.attr("stroke-width", 1)
.attr("fill", "none")
}
<script src="https://d3js.org/d3.v4.min.js"></script>
<body><div id="chart"></div></body>
The x axis shows years. The y axis shows amount. Each point is a country.
I'd like to add connecting lines between points that are the same country over different years. I've color-coded these to clarify.
The problem is, I can't get the x/y to match up with the points based on the force. I commented out what I thought would work. Any ideas?
Once circles are created, it is possible to retrieve their exact coordinates. With these circle coordinates, it becomes very easy to set the extremities of the lines:
var data = [
{ country: "Algeria", amount: 22, year: 2000 },
{ country: "Argentina", amount: 49, year: 1990 },
{ country: "Armenia", amount: 3, year: 1990 },
{ country: "Australia", amount: 9, year: 2010 },
{ country: "Austria", amount: 1, year: 2010 },
{ country: "Bahamas", amount: 5, year: 2018 },
{ country: "Bahrain", amount: 22, year: 2018 },
{ country: "Belarus", amount: 9, year: 2010 },
{ country: "Belgium", amount: 46, year: 2018 },
{ country: "Brazil", amount: 79, year: 1990 },
{ country: "Canada", amount: 12, year: 2000 },
{ country: "China", amount: 26, year: 2018 },
{ country: "Colombia", amount: 9, year: 2010 },
{ country: "Croatia", amount: 8, year: 2000 },
{ country: "Cuba", amount: 14, year: 1990 },
{ country: "Czech Republic", amount: 11, year: 2018 },
{ country: "Denmark", amount: 125, year: 2010 },
{ country: "Canada", amount: 124, year: 2018 },
{ country: "Bahrain", amount: 39, year: 2010 },
{ country: "Estonia", amount: 141, year: 2018 },
{ country: "Ethiopia", amount: 38, year: 1990 },
{ country: "France", amount: 4, year: 2018 },
{ country: "Germany", amount: 15, year: 2000 },
{ country: "Greece", amount: 16, year: 2010 },
{ country: "Grenada", amount: 241, year: 2010 },
{ country: "Hungary", amount: 135, year: 1990 },
{ country: "India", amount: 22, year: 1990 },
{ country: "Indonesia", amount: 31, year: 1990 },
{ country: "Iran", amount: 88, year: 2010 },
{ country: "Ireland", amount: 12, year: 2018 },
{ country: "Italy", amount: 128, year: 2000 },
{ country: "Jamaica", amount: 1, year: 2018 },
{ country: "Japan", amount: 41, year: 1990 },
{ country: "Jordan", amount: 137, year: 2010 },
{ country: "Iran", amount: 13, year: 1990 },
{ country: "Malaysia", amount: 25, year: 2018 },
{ country: "Mexico", amount: 59, year: 2010 },
{ country: "Moldova", amount: 71, year: 2000 },
{ country: "Mongolia", amount: 22, year: 2018 },
{ country: "Morocco", amount: 131, year: 1990 },
{ country: "Netherlands", amount: 129, year: 2018 },
{ country: "New Zealand", amount: 148, year: 2018 },
{ country: "Niger", amount: 1, year: 2010 },
{ country: "Nigeria", amount: 41, year: 1990 },
{ country: "Norway", amount: 14, year: 2010 },
{ country: "Philippines", amount: 15, year: 2018 },
{ country: "Poland", amount: 12, year: 2010 },
{ country: "Portugal", amount: 31, year: 2000 },
{ country: "Puerto Rico", amount: 51, year: 2000 },
{ country: "Romania", amount: 15, year: 2000 },
{ country: "Serbia", amount: 18, year: 2000 },
{ country: "South Africa", amount: 14, year: 2010 },
{ country: "Sweden", amount: 11, year: 2018 },
{ country: "Switzerland", amount: 7, year: 2010 },
{ country: "Thailand", amount: 61, year: 2018 },
{ country: "Trinidad and Tobago", amount: 12, year: 2018 },
{ country: "Tunisia", amount: 34, year: 2010 },
{ country: "Turkey", amount: 28, year: 2010 },
{ country: "Ukraine", amount: 11, year: 2010 },
{ country: "Uzbekistan", amount: 123, year: 2018 },
{ country: "Venezuela", amount: 23, year: 2018 },
{ country: "Iran", amount: 13, year: 2018 }
];
var width = 1000,
height = 500;
var svg = d3.select("#chart")
.append("svg")
.attr("width", width)
.attr("height", height);
var x = d3.scaleLinear()
.range([95, 650]);
var y = d3.scaleLinear()
.range([100, 450]);
data.forEach(d => {
d.amount = +d.amount;
});
var sort = data.sort((a, b) => d3.descending(a, b));
y.domain(d3.extent(data, function(d) {
return d.amount;
}));
x.domain(d3.extent(data, function(d) {
return d.year;
}));
var simulation = d3.forceSimulation(data)
.force("x", d3.forceX(function(d) {
return x(d.year);
}).strength(3))
.force("y", d3.forceY(function(d) {
return y(d.amount)
}).strength(2))
.force("collide", d3.forceCollide(7).strength(7))
.stop();
for (var i = 0; i < data.length * 2; ++i) simulation.tick();
var circles = svg.selectAll(".circles").data(data);
var circlesEnter = circles.enter().append("circle");
circlesEnter.attr("r", 4)
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
.attr("fill", function(d) {
if (d.country == "Iran") {
return "#FF0044"
} else if (d.country == "Canada") {
return "#00A9E9"
} else if (d.country == "Bahrain") {
return "#6BF4C6"
} else {
return '#333'
}
})
.attr('class', function(d) {
return d.country + '-' + d.year + '-' + d.amount
});
// connector lines
var byCountry = d3.nest().key(function(d) { return d.country; }).entries(data);
var countryNames = d3.values(byCountry).map(function(d) {
return d.values.map(function(v) {
return v.country;
}).join(', ');
});
for (i = 0; i < countryNames.length; i++) {
eaco = countryNames[i].split(',')[0]
const filterByCountry = (country, data) => item => item.country === country
connectData = data.filter(filterByCountry(eaco))
if (connectData.length >= 2) {
var linesGroup = svg.append("g")
.attr("class", "connectors");
var linec = d3.line()
.x(d => d3.select("circle." + d.country + "-" + d.year + "-" + d.amount).attr("cx"))
.y(d => d3.select("circle." + d.country + "-" + d.year + "-" + d.amount).attr("cy"));
linesGroup
.datum(connectData)
.append("path")
.attr('class', d => d[0].amount + '-' + d[0].year + '-' + d[0].country)
.attr("d", linec)
.attr("stroke", function(d) {
if (d[0].country == "Iran") return "#FF0044";
else if (d[0].country == "Canada") return "#00A9E9";
else if (d[0].country == "Bahrain") return "#6BF4C6";
else return '#333';
})
.attr("stroke-width", 1)
.attr("fill", "none");
}
}
<script src="https://d3js.org/d3.v5.min.js"></script>
<body><div id="chart"></div></body>
This way, lines extremities exactly match the coordinates of the circles.
To do so, we can select the circles corresponding to the extremities of the line in question using:
d3.select("circle." + d.country + "-" + d.year + "-" + d.amount)
on which we can retrieve the cx
and cy
attributes (x and y position of the circle):
d3.select("circle." + d.country + "-" + d.year + "-" + d.amount).attr("cx")
to finally create the line with these coordinates:
var linec = d3.line()
.x(d => d3.select("circle." + d.country + "-" + d.year + "-" + d.amount).attr("cx"))
.y(d => d3.select("circle." + d.country + "-" + d.year + "-" + d.amount).attr("cy"));
Side notes:
Notice how I modified the classes you used to name circles. As the class names contained spaces, we couldn't select them; I used -
instead. In addition we apparently can't select a class which starts with a number, so I changed the class names to start with the country instead of the amount.
Since, here, classes are used to uniquely define circles and lines, it would probably make more sense to set id
s rather than class
es.
As noticed by @Gerardo, each line is actually created several times (once per circle; such that a line joining 3 circles would be created 3 times). Here is one possible way you can fix that:
var linec = d3.line()
.x(d => d3.select("circle." + d.country + "-" + d.year + "-" + d.amount).attr("cx"))
.y(d => d3.select("circle." + d.country + "-" + d.year + "-" + d.amount).attr("cy"));
linesGroup.datum(connectData).append("path").attr("d", linec)...