This work represents Songs' diffusion by geographical features to identify the impact of geographical distance on song popularity.
Currently the visualization only works on France. Click to see the concept of song diffusion.
xxxxxxxxxx
<head>
<meta charset="utf-8">
<title>Song diffusion by geographical features</title>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
<style>
body { margin:0;position:fixed;top:0;right:0;bottom:0;left:0; }
.country{
stroke: #333;
stroke-width: 1px;
}
.country:hover{
stroke: #fff;
stroke-width: 1.5px;
}
.text{
fill: #dfff6d;
font-size:10px;
text-transform:capitalize;
text-rendering: optimizeLegibility;
}
.point{
fill: #dfff6d;
}
#container {
/*margin:10px 10%;*/
margin-top: 0%;
border:2px solid #000;
border-radius: 5px;
height:100%;
overflow:hidden;
/*background: #F0F8FF;*/
background: #333;
}
.hidden {
display: none;
}
div.tooltip {
color: #222;
background: #fff;
padding: .5em;
text-shadow: #f5f5f5 0 1px 0;
border-radius: 2px;
box-shadow: 0px 0px 2px 0px #a6a6a6;
opacity: 0.9;
position: absolute;
}
.graticule {
fill: none;
stroke: #bbb;
stroke-width: .5px;
stroke-opacity: .5;
}
.equator {
stroke: #ccc;
stroke-width: 1px;
}
.arc {
fill: #898;
stroke: red;
stroke-width: 3px;
stroke-linecap: round;
}
path.edgeFrom{
stroke-dasharray: 0 5 0 5;
stroke-dashoffset: 1000;
animation: dash 50s linear forwards infinite;
}
path.edgeTo{
stroke-linejoin: round;
stroke-dasharray: 20;
stroke-dashoffset: 1000;
animation: dash 30s linear forwards infinite;
}
@keyframes dash {
to{
stroke-dashoffset: 0;
}
}
</style>
</head>
<body>
<div id="container"></div>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://d3js.org/d3-scale-chromatic.v1.min.js"></script>
<script src="https://d3js.org/topojson.v1.min.js"></script>
<script>
d3.select(window).on("resize", throttle);
var zoom = d3.zoom()
//.extent([1,9])
.scaleExtent([1, 9])
.on("zoom", move);
var c = document.getElementById('container');
var width = c.offsetWidth;
var height = width / 2;
var centered;
//offsets for tooltips
var offsetL = c.offsetLeft+20;
var offsetT = c.offsetTop+10;
var topo, projection, path, svg, g;
var centroids, arcs;
//var graticule = d3.geo.graticule();
var graticule = d3.geoGraticule();
var tooltip = d3.select("#container").append("div").attr("class", "tooltip hidden");
var color = d3.scaleThreshold()
.domain(d3.range(2, 10))
.range(d3.schemeBuPu[9]);
var nodeDataByCode = {}, links = [];
var attributeArray = [], currentAttribute = 0;
var useGreatCircles = false;
setup(width,height);
// set map
function setup(width,height){
// use Equirectangular
projection = d3.geoEquirectangular()
// projection = d3.geoMercator()
.translate([(width/2), (height/2)])
.scale( width / 2 / Math.PI);
path = d3.geoPath().projection(projection);
svg = d3.select("#container").append("svg")
.attr("width", width)
.attr("height", height)
// .call(zoom)
//.on("click", click)
.append("g");
g = svg.append("g")
// .on("click", click);
loadData();
}
function loadData() {
d3.queue() // queue function loads all external data files asynchronously
.defer(d3.json,"world-topo.json") // our geometries
.defer(d3.csv, "countriesRandom.csv") // and associated data in csv file
.defer(d3.csv, "refugee-nodes.csv")
.defer(d3.csv, "refugee-flows.csv")
.await(processData); // once all files are loaded, call the processData function passing
// the loaded objects as arguments
}
function processData(error, world, countryData, countryInfo, countryFlow) {
if(error) throw error;
var countries = world.objects.countries.geometries;
for(var i in countries){ // for each geometry object(country)
for(var j in countryData){ // for each row in the CSV file
if (countries[i].properties.id == countryData[j].id){ // if their name(id) matches each other
for (var k in countryData[i]){ // for each column in the row within the CSV
if(k != 'name' && k != 'id'){ // csv 각 칼럼안에 name이 아닌 경우 attributes에 집어넣기, (id(지금 안씀 ), 년도별 수치값)
if(attributeArray.indexOf(k) == -1){
attributeArray.push(k); // add new column headings to our array for later
}
countries[i].properties[k] = Number(countryData[j][k]); // add each CSV column key/value to geometry object
}
}
break; // stop looking through the CSV since we made our match (매칭되는 이름이 없기 때문에 break)
}
}
}
// 연도별 데이터가 world에 반영됨. 데이터의 reference를 world로 참조해서 그런듯.
topo = topojson.feature(world, world.objects.countries).features;
drawMap(topo);
// drawNetwork
drawNetwork(countryInfo, countryFlow);
}
function drawMap(topo) {
console.log("start drawing Map");
// *********** Parsing the topoJson as json object **********
// topo = topojson.feature(world, world.objects.countries);
// console.log(topo);
// I don't want to draw graticule and equator
/*
// graticule
svg.append("path")
.datum(graticule)
.attr("class", "graticule")
.attr("d", path);
// equator
g.append("path")
.datum({type: "LineString", coordinates: [[-180, 0], [-90, 0], [0, 0], [90, 0], [180, 0]]})
.attr("class", "equator")
.attr("d", path);
*/
var country = g.selectAll(".country").data(topo);
country.enter().insert("path")
.attr("class", "country")
.attr("d", path)
.attr("id", function(d,i) { return d.id; })
.attr("title", function(d,i) { return d.properties.id; })
// .style("fill", function(d, i) { return d.properties.color; }) // 2017.03.24 현재는 데이터에 미리 바인딩된 컬러로 그려짐. 아래 코드로 choropleth map으로 변환
.style("fill", function(d, i) { return color(d.properties['2008']) }) // choropleth
.on("mouseover", handleMouseOver) // tooltips
.on("mouseout", handleMouseOut)
.on("click", CountryClicked);
//EXAMPLE: adding some capitals from external CSV file
d3.csv("country-capitals.csv", function(err, capitals) {
capitals.forEach(function(i){
addpoint(i.CapitalLongitude, i.CapitalLatitude, i.CapitalName );
});
});
console.log("end of drawing Map");
}
function CountryClicked(d) {
var x, y, k;
if (d && centered !== d) {
var centroid = path.centroid(d);
x = centroid[0];
y = centroid[1];
k = 3;
centered = d;
drawFocalNetwork(d.properties.id);
} else {
x = width / 2;
y = height / 2;
k = 1;
centered = null;
cleanFocalNetwork();
}
g.selectAll("path")
.classed("active", centered && function(d) { return d === centered; });
g.transition()
.duration(750)
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")scale(" + k + ")translate(" + -x + "," + -y + ")")
.style("stroke-width", 1.5 / k + "px");
}
function cleanFocalNetwork() {
g.select("#arcs").remove();
svg.selectAll("defs").remove();
links = [];
}
function drawFocalNetwork(cur_country) {
// 이전 네트워크 지우기.
cleanFocalNetwork();
var focal_country = cur_country;
var year = '2008'; // the last year of the flow data
// ********* variables for EDGE Settings **************
var maxMagnitude, magnitudeFormat;
var arcWidth, minColor, maxColor, arcColor, arcOpacity;
// from data parsing, we only get row which 'to' == focal_country (at t-1)
d3.csv("from.csv", function (error, edges) {
if(error) throw error;
// ******************** settings for the "FROM" edge ********************
maxMagnitude = d3.max(edges, function(d) { return parseFloat(d[year])});
magnitudeFormat = d3.format(",.0f");
arcWidth = d3.scaleLinear().domain([1, maxMagnitude]).range([.1, 7]);
minColor = '#d46a6a', maxColor = '#aa3939';
arcColor = d3.scaleLog().domain([1, maxMagnitude]).range([minColor, maxColor]);
arcOpacity = d3.scaleLog().domain([1, maxMagnitude]).range([0.3, 1]);
// ******************** File parsing and set links ********************
console.log(edges);
edges.forEach(function (edge) {
if(edge.Dest == focal_country){
console.log(edge.Dest);
var o = nodeDataByCode[edge.Origin], co = o.coords, po = o.projection;
var d = nodeDataByCode[edge.Dest], cd = d.coords, pd = d.projection;
var magnitude = parseFloat(edge[year]);
if(co && cd && !isNaN(magnitude)) {
links.push({
source: co, target: cd,
magnitude: magnitude,
origin: o, dest: d,
originp: po, destp: pd
});
}
}
});
// draw FROM edges
arcs = g.append("g").attr("id", "arcs");
var strokeFun = function (d) { return arcColor(d.magnitude); };
var gradientNameFun = function (d) { return "grd"+d.origin.name + d.dest.name; };
var gradientRefNameFun = function (d) { return "url(#"+gradientNameFun(d) +")"; };
var defs = svg.append("svg:defs");
// see https://apike.ca/prog_svg_patterns.html
defs.append("marker")
.attr("id", "arrowHead")
.attr("viewBox", "0 0 10 10")
.attr("refX", 10)
.attr("refY", 5)
.attr("orient", "auto")
.attr("markerUnits", "userSpaceOnUse")
.attr("markerWidth", 4*2)
.attr("markerHeight", 3*2)
.append("polyline")
.attr("points", "0,0 10,5 0,10 1,5")
.attr("fill", maxColor);
var gradient = defs.selectAll("linearGradient")
.data(links)
.enter()
.append("svg:linearGradient")
.attr("id", gradientNameFun)
.attr("gradientUnits", "userSpaceOnUse")
.attr("x1", function (d) { return d.originp[0]; })
.attr("y1", function (d) { return d.originp[1]; })
.attr("x2", function (d) { return d.destp[0]; })
.attr("y2", function (d) { return d.destp[1]; });
gradient.append("svg:stop")
.attr("offset", "0%")
.attr("stop-color", minColor)
.attr("stop-opacity", .0)
gradient.append("svg:stop")
.attr("offset", "80%")
.attr("stop-color", strokeFun)
gradient.append("svg:stop")
.attr("offset", "100%")
.attr("stop-color", strokeFun)
.attr("stop-opacity", 1.0);
var arcNodes = arcs.selectAll("path")
.data(links)
.enter().append("path")
.attr("class", "edgeFrom")
.attr("stroke", gradientRefNameFun)
.attr("fill", "none")
.attr("stroke-linecap", "round")
// .transition().duration(2000)
.attr("stroke-width", function(d) { return arcWidth(d.magnitude) +1; })
.attr("d", function (d) {
return path({
type: "LineString",
coordinates: [d.source, d.target]
});
})
.sort(function (a, b) {
var a = a.magnitude, b = b.magnitude;
if(isNaN(a)) if (isNaN(b)) return 0;
else return -1;
if(isNaN(b)) return 1;
return d3.ascending(a,b);
});
});
// from data parsing, we only get row which 'from' == focal_country (at t)
d3.csv("to.csv", function (error, edges) {
if(error) throw error;
// ******************** settings for the "FROM" edge ********************
maxMagnitude = d3.max(edges, function(d) { return parseFloat(d[year])});
magnitudeFormat = d3.format(",.0f");
arcWidth = d3.scaleLinear().domain([1, maxMagnitude]).range([.1, 7]);
var minColor = '#CFEC9d', maxColor = '#dfff6d';
arcColor = d3.scaleLog().domain([1, maxMagnitude]).range([minColor, maxColor]);
arcOpacity = d3.scaleLog().domain([1, maxMagnitude]).range([0.3, 1]);
// ******************** File parsing and set links ********************
links = [];
edges.forEach(function (edge) {
if(edge.Origin == focal_country){
console.log(edge.Dest);
var o = nodeDataByCode[edge.Origin], co = o.coords, po = o.projection;
var d = nodeDataByCode[edge.Dest], cd = d.coords, pd = d.projection;
var magnitude = parseFloat(edge[year]);
if(co && cd && !isNaN(magnitude)) {
links.push({
source: co, target: cd,
magnitude: magnitude,
origin: o, dest: d,
originp: po, destp: pd
});
}
}
});
// draw FROM edges
arcs = g.append("g").attr("id", "arcs");
var strokeFun = function (d) { return arcColor(d.magnitude); };
var gradientNameFun = function (d) { return "grd"+d.origin.name + d.dest.name; };
var gradientRefNameFun = function (d) { return "url(#"+gradientNameFun(d) +")"; };
var defs = svg.append("svg:defs");
// see https://apike.ca/prog_svg_patterns.html
defs.append("marker")
.attr("id", "arrowHead")
.attr("viewBox", "0 0 10 10")
.attr("refX", 10)
.attr("refY", 5)
.attr("orient", "auto")
.attr("markerUnits", "userSpaceOnUse")
.attr("markerWidth", 4*2)
.attr("markerHeight", 3*2)
.append("polyline")
.attr("points", "0,0 10,5 0,10 1,5")
.attr("fill", maxColor);
var gradient = defs.selectAll("linearGradient")
.data(links)
.enter()
.append("svg:linearGradient")
.attr("id", gradientNameFun)
.attr("gradientUnits", "userSpaceOnUse")
.attr("x1", function (d) { return d.originp[0]; })
.attr("y1", function (d) { return d.originp[1]; })
.attr("x2", function (d) { return d.destp[0]; })
.attr("y2", function (d) { return d.destp[1]; });
gradient.append("svg:stop")
.attr("offset", "0%")
.attr("stop-color", minColor)
.attr("stop-opacity", .0)
gradient.append("svg:stop")
.attr("offset", "80%")
.attr("stop-color", strokeFun)
gradient.append("svg:stop")
.attr("offset", "100%")
.attr("stop-color", strokeFun)
.attr("stop-opacity", 1.0);
var arcNodes = arcs.selectAll("path")
.data(links)
.enter().append("path")
.attr("class", "edgeTo")
.attr("stroke", gradientRefNameFun)
.attr("fill", "none")
.attr("stroke-linecap", "round")
// .transition().duration(2000)
.attr("stroke-width", function(d) { return arcWidth(d.magnitude) +1; })
.attr("d", function (d) {
if(useGreatCircles)
return splitPath(path(arc(d)));
else
return path({
type: "LineString",
coordinates: [d.source, d.target]
});
})
.sort(function (a, b) {
var a = a.magnitude, b = b.magnitude;
if(isNaN(a)) if (isNaN(b)) return 0;
else return -1;
if(isNaN(b)) return 1;
return d3.ascending(a,b);
});
})
}
function handleMouseOver(){
var mouse = d3.mouse(svg.node()).map( function(d) { return parseInt(d); } );
tooltip.classed("hidden", false)
.attr("style", "left:"+(mouse[0]+offsetL)+"px;top:"+(mouse[1]+offsetT)+"px")
.html(this.__data__.properties.admin);
}
function handleMouseOut(){
tooltip.classed("hidden", true);
}
function redraw() {
width = c.offsetWidth;
height = width / 2;
d3.select('svg').remove();
setup(width,height);
drawMap(topo);
}
function move() {
//var t = d3.event.translate;
var t = [d3.event.transform.x,d3.event.transform.y];
//var s = d3.event.scale;
var s = d3.event.transform.k;
zscale = s;
var h = height/4;
t[0] = Math.min(
(width/height) * (s - 1),
Math.max( width * (1 - s), t[0] )
);
t[1] = Math.min(
h * (s - 1) + h * s,
Math.max(height * (1 - s) - h * s, t[1])
);
//zoom.translateBy(t);
g.attr("transform", "translate(" + t + ")scale(" + s + ")");
//adjust the country hover stroke width based on zoom level
d3.selectAll(".country").style("stroke-width", 1.5 / s);
}
var throttleTimer;
function throttle() {
window.clearTimeout(throttleTimer);
throttleTimer = window.setTimeout(function() {
redraw();
}, 200);
}
//geo translation on mouse click in map
function click() {
var latlon = projection.invert(d3.mouse(this));
console.log(latlon);
}
//function to add points and text to the map (used in plotting capitals)
function addpoint(lon,lat,text) {
var gpoint = g.append("g").attr("class", "gpoint");
var x = projection([lon,lat])[0];
var y = projection([lon,lat])[1];
gpoint.append("svg:circle")
.attr("cx", x)
.attr("cy", y)
.attr("class","point")
.attr("r", 1.5);
//conditional in case a point has no associated text
if(text.length>0){
gpoint.append("text")
.attr("x", x+2)
.attr("y", y+2)
.attr("class","text")
.text(text);
}
}
function drawNetwork(nodes, edges) {
console.log("start drawing network among country")
// ******************* NODE (centroid of each Country) ***********************
centroids = g.append("g").attr("id", "centroids");
// Set node data by countries' id(name)
nodes.forEach(function (node) {
node.coords = nodeCoords(node);
node.projection = node.coords ? projection(node.coords) : undefined;
nodeDataByCode[node.id] = node;
});
// append node(circle) to map
centroids.selectAll("circle")
.data(nodes.filter(function (node) { return node.projection ? true : false } ))
.enter().append("circle")
.attr("cx", function (d) {
return d.projection[0]
})
.attr("cy", function (d) {
return d.projection[1]
})
.attr("r", function () {
return Math.random()*10
})
.attr("fill", "#fff")
.attr("opacity", 0.5);
}
function nodeCoords(node) {
var lon = parseFloat(node.lon), lat = parseFloat(node.lat);
if(isNaN(lon) || isNaN(lat)) return null;
return [lon, lat];
}
</script>
</body>
Modified http://d3js.org/d3.v4.min.js to a secure url
Modified http://d3js.org/topojson.v1.min.js to a secure url
https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js
https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js
https://d3js.org/d3.v4.min.js
https://d3js.org/d3-scale-chromatic.v1.min.js
https://d3js.org/topojson.v1.min.js