d3 Node Labeling - html

I've been using the sample code from this d3 project to learn how to display d3 graphs and I can't seem to get text to show up in the middle of the circles (similar to this example and this example). I've looked at other examples and have tried adding
node.append("title").text("Node Name To Display")
and
node.append("text")
.attr("text-anchor", "middle")
.attr("dy", ".3em").text("Node Name To Display")
right after node is defined but the only results I see is "Node Name To Display" is showing up when I hover over each node. It's not showing up as text inside the circle. Do I have to write my own svg text object and determine the coordinates of that it needs to be placed at based on the coordinates of radius of the circle? From the other two examples, it would seem like d3 already takes cares of this somehow. I just don't know the right attribute to call/set.

There are lots of examples showing how to add labels to graph and tree visualizations, but I'd probably start with this one as the simplest:
http://bl.ocks.org/950642
You haven’t posted a link to your code, but I'm guessing that node refers to a selection of SVG circle elements. You can’t add text elements to circle elements because circle elements are not containers; adding a text element to a circle will be ignored.
Typically you use a G element to group a circle element (or an image element, as above) and a text element for each node. The resulting structure looks like this:
<g class="node" transform="translate(130,492)">
<circle r="4.5"/>
<text dx="12" dy=".35em">Gavroche</text>
</g>
Use a data-join to create the G elements for each node, and then use selection.append to add a circle and a text element for each. Something like this:
var node = svg.selectAll(".node")
.data(nodes)
.enter().append("g")
.attr("class", "node")
.call(force.drag);
node.append("circle")
.attr("r", 4.5);
node.append("text")
.attr("dx", 12)
.attr("dy", ".35em")
.text(function(d) { return d.name });
One downside of this approach is that you may want the labels to be drawn on top of the circles. Since SVG does not yet support z-index, elements are drawn in document order; so, the above approach causes a label to be drawn above its circle, but it may be drawn under other circles. You can fix this by using two data-joins and creating separate groups for circles and labels, like so:
<g class="nodes">
<circle transform="translate(130,492)" r="4.5"/>
<circle transform="translate(110,249)" r="4.5"/>
…
</g>
<g class="labels">
<text transform="translate(130,492)" dx="12" dy=".35em">Gavroche</text>
<text transform="translate(110,249)" dx="12" dy=".35em">Valjean</text>
…
</g>
And the corresponding JavaScript:
var circle = svg.append("g")
.attr("class", "nodes")
.selectAll("circle")
.data(nodes)
.enter().append("circle")
.attr("r", 4.5)
.call(force.drag);
var text = svg.append("g")
.attr("class", "labels")
.selectAll("text")
.data(nodes)
.enter().append("text")
.attr("dx", 12)
.attr("dy", ".35em")
.text(function(d) { return d.name });
This technique is used in the Mobile Patent Suits example (with an additional text element used to create a white shadow).

I found this guide very useful in trying to accomplish something similar :
https://www.dashingd3js.com/svg-text-element
Based on above link this code will generate circle labels :
<!DOCTYPE html>
<html>
<head>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<script src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script>
</head>
<body style="overflow: hidden;">
<div id="canvas" style="overflow: hidden;"></div>
<script type="text/javascript">
var graph = {
"nodes": [
{name: "1", "group": 1, x: 100, y: 90, r: 10 , connected : "2"},
{name: "2", "group": 1, x: 200, y: 50, r: 15, connected : "1"},
{name: "3", "group": 2, x: 200, y: 130, r: 25, connected : "1"}
]
}
$( document ).ready(function() {
var width = 2000;
var height = 2000;
var svg = d3.select("#canvas").append("svg")
.attr("width", width)
.attr("height", height)
.append("g");
var lines = svg.attr("class", "line")
.selectAll("line").data(graph.nodes)
.enter().append("line")
.style("stroke", "gray") // <<<<< Add a color
.attr("x1", function (d, i) {
return d.x
})
.attr("y1", function (d) {
return d.y
})
.attr("x2", function (d) {
return findAttribute(d.connected).x
})
.attr("y2", function (d) {
return findAttribute(d.connected).y
})
var circles = svg.selectAll("circle")
.data(graph.nodes)
.enter().append("circle")
.style("stroke", "gray")
.style("fill", "white")
.attr("r", function (d, i) {
return d.r
})
.attr("cx", function (d, i) {
return d.x
})
.attr("cy", function (d, i) {
return d.y
});
var text = svg.selectAll("text")
.data(graph.nodes)
.enter()
.append("text");
var textLabels = text
.attr("x", function(d) { return d.x; })
.attr("y", function(d) { return d.y; })
.text( function (d) { return d.name })
.attr("font-family", "sans-serif")
.attr("font-size", "10px")
.attr("fill", "red");
});
function findAttribute(name) {
for (var i = 0, len = graph.nodes.length; i < len; i++) {
if (graph.nodes[i].name === name)
return graph.nodes[i]; // Return as soon as the object is found
}
return null; // The object was not found
}
</script>
</body>
</html>

If you want to grow the nodes to fit large labels, you can use the getBBox property of an SVG text node after you've drawn it. Here's how I did it, for a list of nodes with fixed coordinates, and two possible shapes:
nodes.forEach(function(v) {
var nd;
var cx = v.coord[0];
var cy = v.coord[1];
switch (v.shape) {
case "circle":
nd = svg.append("circle");
break;
case "rectangle":
nd = svg.append("rect");
break;
}
var w = 10;
var h = 10;
if (v.label != "") {
var lText = svg.append("text");
lText.attr("x", cx)
.attr("y", cy + 5)
.attr("class", "labelText")
.text(v.label);
var bbox = lText.node().getBBox();
w = Math.max(w,bbox.width);
h = Math.max(h,bbox.height);
}
var pad = 4;
switch (v.shape) {
case "circle":
nd.attr("cx", cx)
.attr("cy", cy)
.attr("r", Math.sqrt(w*w + h*h)/2 + pad);
break;
case "rectangle":
nd.attr("x", cx - w/2 - pad)
.attr("y", cy - h/2 - pad)
.attr("width", w + 2*pad)
.attr("height", h + 2*pad);
break;
}
});
Note that the shape is added, the text is added, then the shape is positioned, in order to get the text to show on top.

Related

Legend doesn't appear, but no errors in the code (?)

I want to create a legend with my 3 classes (a, b, and c), but the legend doesn't appear on my localhost webmap. I couldn't find any errors. This is the assignment I am working on: https://github.com/NieneB/webmapping_for_beginners_v2/wiki/D3-step-3
I have tried to move the codes of the legend to another place, but this doesn't seem to work. I have checked the code if there were any ;)} etc. missing.
And these are some of my codes:
<h1>Bigfoot Field Researchers Organizations</h1>
<script src="https://d3js.org/d3.v5.min.js"></script>
<script>
//Width and height
var w = 1000;
var h = 800;
//Create SVG
var svg = d3.select("body")
.append("svg")
.attr("width", w)
.attr("height", h);
// create a new SVG group element
var layerWorld = svg.append('g');
//Load in GeoJSON data
var data = d3.json("world_simple.json", function(error, data){
if (error) console.log(error);
return data
});
// create a new SVG group element
var layerYeti = svg.append('g');
//Load in GeoJSON data
var yetiData = d3.json("All_BFRO_Reports_points.json", function (error, data) {
if (error) console.log(error);
return data
});
Promise.all([data, yetiData]).then(function (values){
console.log(values[1])
console.log(data)
//Bind data and create one path per GeoJSON feature
layerWorld.selectAll("path")
.data(values[0].features)
.enter()
.append("path")
.attr("class", "countries")
.attr("d", path)
.style("fill", function(d){
return color(d.properties.pop_est)})
.style("stroke", "#5a5959")
.on("mouseover", handleMouseOver)
.on("mouseout", handleMouseOut);
layerYeti.selectAll("circle")
.data(values[1].features)
.enter()
.append("circle")
.attr("cx", function(d) {
//[0] returns the first coordinate (x) of the projected value
return projection(d.geometry.coordinates)[0];})
.attr("cy", function(d) {
//[1] returns the second coordinate (y) of the projected value
return projection(d.geometry.coordinates)[1];})
.attr("r", 2)
.style("fill", function(d){
if (d.properties.styleUrl == "#a") {return "red"}
else if (d.properties.styleUrl == "#b") {return "blue"}
else { return "yellow"}
})
.style("opacity", 0.75);
//Create Legend
var legend = d3.select("body")
.append("svg")
.attr("class", "legend")
.attr("width", 200)
.attr("height", 300);
})
var unique_values = d3.map(data.features, function(d){return d.properties.styleUrl;}).keys();
console.log(unique_values);

d3 line and points on map from csv data

I am trying to plot individual data points and also a line path that runs between these points like this example D3 stop and restart transition along path to allow click through to geo data coordinates that I have been helped with before.
Now I want to use my actual data rather than a test set of coordinates but am running into trouble. I have tried both a geoJson file and also csv for my data. I am using a csv file with lon and lat for the points and was hoping to make the line from that same set of data, ie use one set of data for the points and line.
I can’t get my line to show up in the correct place - it is in top right corner but should be on / through points. I think this is something to do with projection but I am having trouble parsing the data correctly to get a line string as required. I have tried to use the sample here https://bl.ocks.org/alandunning/cfb7dcd7951826b9eacd54f0647f48d3 - but get empty objects??
My question is how to use the csv lon lat with a d3 svg line generator.
This is my code:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Working version 3</title>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://d3js.org/d3-array.v1.min.js"></script>
<script src="https://d3js.org/d3-geo.v1.min.js"></script>
<script src="https://d3js.org/d3-queue.v3.min.js"></script>
<script src="https://d3js.org/topojson.v1.min.js"></script>
<style type="text/css">
circle {
fill: steelblue;
stroke: pink;
stroke-width: 3px;
}
.line{
fill: none;
stroke: red;
stroke-width: 6;
}
</style>
</head>
<body>
<script>
var w = 960,
h = 500;
var projection = d3.geoMercator()
.translate([w/2, h/2])
.scale([w * 0.16]);
var path = d3.geoPath()
.projection(projection);
var duration = 10000;
var svg = d3.select("body").append("svg")
.attr("width", w)
.attr("height", h);
/*
var line = d3.line()
.x(function (d) {return projection([d.lon]);})
.y(function (d) {return projection([d.lat]);})
.curve(d3.curveBasis);
var line = d3.line()
.x(function(d){return projection(d[0].lon);})
.y(function(d){return projection(d[0].lat);})
.curve(d3.curveBasis);
/*ok line shows up but in wrong place
var line = d3.line()
.x(function(d) { return (d.lon); })
.y(function(d) { return (d.lat); })
.curve(d3.curveBasis);
*/
var line = d3.line()
.x(function(d) { return (d.lon); })
.y(function(d) { return (d.lat); })
.curve(d3.curveBasis);
//original
/*
var line = d3.line()
.x(function(d){return projection(d)[0];})
.y(function(d){return projection(d)[1];})
.curve(d3.curveBasis);
*/
//
//bring in data
d3.queue()
.defer(d3.json, "data/oceans.json")
.defer(d3.csv, "data/speckCities.csv")
.await(ready);
function ready (error, oceans, data){
if (error) throw error;
//console.log(data[0]);
//console.log(data[0].lon);
//map
svg.selectAll("path")
.data(oceans.features)
.enter()
.append("path")
.attr("d", path)
.style("fill", "#A8B2C3");
var linepath = svg.append("path")
.datum(data)
.attr("d", line)
.attr('class', 'line');
svg.selectAll("circle")
.data(data)
.enter()
.append("circle")
.attr("cx", function(d) {
return projection([d.lon, d.lat])[0];
})
.attr("cy", function(d) {
return projection([d.lon, d.lat])[1];
})
.attr("r", 5)
.style("fill", "yellow")
.style("stroke", "gray")
.style("stroke-width", 0.25)
.style("opacity", 0.75)
.append("title") //Simple tooltip
.text(function(d) {
return d.name ;
});
//
//
/*svg.selectAll(".point")
.data(coordinates)
.enter()
.append("circle")
.attr("r", 7)
.attr("transform", function(d) { return "translate(" + projection(d) + ")"; });
var circle = svg.append("circle")
.attr("r", 19)
.attr("transform", "translate(" + projection(d) + ")");
/*
var pauseValues = {
lastT: 0,
currentT: 0
};
function transition() {
circle.transition()
.duration(duration - (duration * pauseValues.lastT))
.attrTween("transform", translateAlong(linepath.node()))
.on("end", function(){
pauseValues = {
lastT: 0,
currentT: 0
};
transition()
});
}
function translateAlong(path) {
var l = path.getTotalLength();
return function(d, i, a) {
return function(t) {
t += pauseValues.lastT;
var p = path.getPointAtLength(t * l);
pauseValues.currentT = t;
return "translate(" + p.x + "," + p.y + ")";
};
};
}
d3.select('button').on('click',function(d,i){
var self = d3.select(this);
if (self.text() == "Pause"){
self.text('Play');
circle.transition()
.duration(0);
setTimeout(function(){
pauseValues.lastT = pauseValues.currentT;
}, 100);
}else{
self.text('Pause');
transition();
}
});
*/
}
</script>
</body>
</html>
You are not projecting your line:
var linepath = svg.append("path")
.datum(data)
.attr("d", line)
.attr('class', 'line');
In this case your longitude/latitude pairs in your geojson are converted to straight pixel coordinates:
var line = d3.line()
.x(function(d) { return (d.lon); })
.y(function(d) { return (d.lat); })
.curve(d3.curveBasis);
As svg coordinates start at [0,0] in the top left, and your points appear to be around 10 degrees east or so (positive longitude), and 50 degrees north or so (positive latitude), your first point in the line appears 10 pixels from the left and 50 pixels from the top. Also, because svg y values increase as one moves down, but latitude values increase as one moves north (typcally up on a map), your line also appears inverted on the y axis compared to your points.
You could set your line function to use a projection to set the x and y points:
var line = d3.line()
.x(function(d) { return projection([d.lon,d.lat])[0] ; })
.y(function(d) { return projection([d.lon,d.lat])[1]; })
.curve(d3.curveBasis);
You need both latitude and longitude to project a point, so the projection function takes both, and returns both x and y, hence the [0] and [1], this is why your commented out sections don't work
But this is unnecessary, you can pass geojson straight to the path (the same as you do for the world background), that is if your data is available in geojson (though it is not hard to make a geojson on the fly):
var linepath = svg.append("path")
.datum(data) // in geojson form
.attr("d", path) // use your path
.attr('class', 'line');
This is more accurate than a line - the segments between lines in a d3.line are straight or follow a predefined curve, in Cartesian coordinate space. A d3.geoPath follows great circle distance, so the segments between points follow the shortest path on the planet, a more accurate representation, thought at times perhaps, less stylistic.
To create the geojson on the fly, assuming your data looks like: [{lon:number,lat:number},{lon:number,lat:number}] you could use something like:
var points = data.map(function(d) { return [d.lon,d.lat] })
var geojson = { "type": "LineString", "coordinates": points }

D3.js Heatmap: How to read from a nested json and create a Heatmap using D3

I have been trying for a couple of days now to read a JSON file with data and generate a heatmap from it. The JSON contains the frequency of git commits by day, for every week for 52 weeks.The data in the JSON (frequency each day) is nested hence I do not know how to extract the data and get the code to represent it. I am really new to do and I do not understand what I am missing or doing wrong. Would really appreciate if anyone can help me out. I read several links, but none of them deals with JSON and I am also having trouble understanding it. I would like to create 52 rows representing a week and 7 columns for each day of the week which I already have. The rectangles just dont reflect the frequency. I am not sure how to do that. A sample of the data I have and the code I have done so far is as such:
Code:
<script type='text/javascript' src='http://d3js.org/d3.v4.min.js'></script>
<script>
var url = "data/git-commit-frequency.json"
var color = ["#ffffd9","#c7e9b4","#41b6c4","#1d91c0","#225ea8","#253494","#081d58"]
d3.json(url, function (data) {
let gridSize = 50;
let width = (gridSize + 1) * 7;
let height = (gridSize + 1) * data.length;
// define range!!!
var x = d3.scaleLinear()
.domain([0, 7])
.range([0, width]);
//52 weeks
var y = d3.scaleLinear()
.domain([0, data.length])
.rangeRound([height, 0]);
let svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
// Generate rows for each json object
var rows = svg.selectAll('.row')
.data(data)
.enter()
.append('g')
.attr("transform", function(d, i){
return "translate(0," + y(i) + ")"
})
// Generate rects for the array of days per object
let box = rows.selectAll("rect")
.data(function(d){ return d.days })
.enter()
.append("rect")
.attr("x", function(d, i) { return x(i); })
.attr("width", gridSize)
.attr("height", gridSize)
.style("fill", 'black')
.attr("class", "bordered");
})
</script>
</body>
JSON:
[
{
"days": [
0,
0,
0,
0,
0,
1,
0
],
"total": 1,
"week": 1457827200
}
]
I have 52 of such JSON objects in the file.
Your code attaches values to the rects with this piece of code, and specifically the .data(function(d){ return d.days })
let box = rows.selectAll("rect")
.data(function(d){ return d.days })
.enter()
.append("rect")
.attr("x", function(d, i) { return x(i); })
.attr("width", gridSize)
.attr("height", gridSize)
.style("fill", 'black')
.attr("class", "bordered");
})
Each appended rect will have one of the values from the days array (eg 0, or 1).
You can inspect this in the browser console (right click on the rect, select Inspect and go to Properties > Rect > data and you will see what values have been appended to that element.
From there, you can typically access the value using an anonymous function within setting the style or attr of your rect. For example, to set colour:
.style("fill", function(d) { return d === 0 ? "grey" : "black"; })
which sets rects with 0 value to be grey, and anything else black. Although you as you get more complex mappings, using d3-scale becomes valuable.

How to append different shapes including images depending on data in D3.js?

I tried to solve this by looking up several similar questions, but none could solve it. I want to create a force-directed graph using this form of data out of a JSON-File:
{
"nodes":[
{"name":"A", "imagelink": "url-to-img" ,"shape-type":circle},
{"name":"B", "imagelink": "url-to-img" , "shape-type":rectangle},
{"name":"A", "imagelink": "url-to-img" ,"shape-type":triangle}
]
"links":[
{"source":1,"target":0,"value":1},
{"source":2,"target":1,"value":1},
{"source":2,"target":0,"value":1}
]
}
The graph's nodes should be a group consisting of a geometrical shape (defined by "shape-type") including an image (defined by "imagelink").
This is the basic force-layout code which I used:
var width = 960,
height = 500;
var force = d3.layout.force()
.charge(-1000)
.linkDistance(150)
.size([width, height]);
var svg = d3.select("body").append("svg");
d3.json("json-file.json");
force
.nodes(graph.nodes)
.links(graph.links)
.start();
var link = svg.selectAll(".link")
.data(graph.links)
.enter().append("line")
.attr("class", "link")
.style("stroke-width", function (d) {
return Math.sqrt(d.value);
});
var node = svg.selectAll(".node")
.data(graph.nodes)
.enter().append("g")
.attr("class", "node")
.call(force.drag);
For including an image into a shape I used the "clip-path method" (example for circle):
node.append("circle")
.attr("r", 20)
.attr("cy", 0)
.attr("cx", 0)
.style("fill", "white")
.style("stroke", "black")
.style("stroke-width", "2");
var clippath = node.append('clipPath').attr(function (d) {
return ("clip-" + convert_shape(d.group))
});
clippath.append("circle")
.attr("r", 19)
.attr("cy", 0)
.attr("cx", 0);
node.append("image")
.attr("x", -37.5)
.attr("y", -37.5)
.attr("height", 75)
.attr("width", 75)
.attr("xlink:href", function (d) {return (d.imagelink)})
.attr("clip-path", function (d) {return ("url(#clip-" + d.shapetype) + ")")
});
As you can see, my problem is to make the append()-function data-depended. So how can I realize this?
Thanks.
Here is a working fiddle for drawing a force-directed graph with different shapes and images.
I used the pattern solution to include the image into shapes.

Drawing voronoi diagram from a csv file with d3.js

I want to draw voronoi diagram using http://mbostock.github.com/d3/ex/voronoi.html from a set of points in a csv file.
I have modified the code:
var w = 1200,
h = 800;
d3.text("ReMeRi_4_2_2_0.csv", function(datasetText) {
var vertices = d3.csv.parseRows(datasetText);
var svg = d3.select("#chart")
.append("svg")
.attr("width", w)
.attr("height", h)
.attr("class", "BuPu");
svg.selectAll("path")
.data(d3.geom.voronoi(vertices))
.enter().append("path")
.attr("class", function(d, i) { return i ? "q" + (i % 9) + "-9" : null; })
.attr("d", function(d) { return "M" + d.join("L") + "Z"; })
svg.selectAll("circle")
.data(vertices.slice(1))
.enter().append("circle")
.attr("transform", function(d) { return "translate(" + d + ")"; })
.attr("r", 2)
text1 = svg.append("svg:text")
.text("control")
.attr("x", 150)
.attr("y", 200)
.style("stroke", "orange")
.style("stroke-width", 0)
.style("font-size", "80%")
.style("fill", "orange");
});
The points are drawn correctly but the polygons of the tesselation are not.
I have tried to add header row and the csv.parse() function without success. At the beginning I was thinking the solution was to iterate over the array to parse to float, but I couldn't do it. If that is the reason why the points are drawn correctly anyway?.
The csv file looks like this:
0.0,0.0
116.78032769067718,0.0
193.02842412648215,78.92418723196411
323.01058809711515,54.15210221124609
378.8576448450217,202.5192012545716
...
I think it is, as you suggest, a problem with the numbers getting parsed as Strings rather than Numbers. Even if that's not what's breaking it, it'd be good to fix. This is one way of doing it (might be a more idiomatic way to do it, dunno):
var vertices = d3.csv.parseRows(
datasetText,
function(pt) { return [parseFloat(pt[0]), parseFloat(pt[1])]; })
);
That might fix your problem.