How can I calculate the degree of nodes in D3 v5? - d3.js

I am learning D3 and trying to scale the radius of nodes based on its degree in D3 v5. My code is like
nodes.forEach(function(d){
d.degree=0;});
links.forEach(function(d){
nodes[d.source].degree += 1;
nodes[d.target].degree += 1;});
It returns error that
Uncaught TypeError: nodes.forEach is not a function
Anyone can provide a good solution?

Something like this will work for d3.js V5:
// Add degree
d3.selectAll('g.node')
.each(function(d) {
d.degree = 0;
});
// Calculate degree
links.forEach(function(d){
d.source.degree += 1;
d.target.degree += 1;
});
// Accessor functions to get min & max
var minDegree = d3.min(
d3.values(nodes), function(d) {
return d.degree; })
var maxDegree = d3.max(
d3.values(nodes), function(d) {
return d.degree; })
// Create node scale based on degree
var nodescale = d3.scaleSqrt()
.domain( [minDegree, maxDegree] )
.range( [3, 15] ); // Change this to your desired range
// Add the node circles
node.append("circle")
.attr("r", function(d) {
return nodescale(d.degree);
})

Related

My cities are being plotted with the wrong projection (quite small and to the left). Using d3.geo.albersUSA()

D3 newbie.
My cities are being plotted with the wrong projection (quite small and to the left) as compared to my map which is the correct size. It also looks like the longitude may be reversed. Using d3.geo.albersUSA().
The .scale of the map is set to 1000 and the map looks great. But my data is not projecting on the same scale.
//Width and height of map (adding relative margins to see if that effects placement of circles. makes no apparent difference.)
var margin = { top: 0, left: 0, right: 0, bottom: 0},
height = 500 - margin.top - margin.bottom,
width = 960 - margin.left - margin.right;
// D3 Projection
var projection = d3.geo.albersUsa()
.translate([width/2, height/2]) // translate to center of screen
.scale([1000]); // scale things down so see entire US
// Define path generator
var path = d3.geo.path() // path generator that will convert GeoJSON to SVG paths
.projection(projection); // tell path generator to use albersUsa projection
// Define linear scale for output
var color = d3.scale.linear()
.range(["rgb(165,110,255)","rgb(0,45,150)","rgb(0,157,154)","rgb(250,77,86)"]);
var legendText = ["High Demand, High Supply", "Low Demand, Low Supply", "Low Demand, High Supply", "High Demand, Low Supply"];
//Create SVG element and append map to the SVG
var svg = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", height);
// Append Div for tooltip to SVG
var div = d3.select("body")
.append("div")
.attr("class", "tooltip")
.style("opacity", 0);
// Load in my states data!
d3.csv("https://raw.githubusercontent.com/sjpozzuoli/Daves_Eagles/main/Data_Main/data_clusters_latlong_ready.csv", function(data) {
color.domain([0,1,2,3]); // setting the range of the input data
// Load GeoJSON data and merge with states data
d3.json("https://gist.githubusercontent.com/michellechandra/0b2ce4923dc9b5809922/raw/a476b9098ba0244718b496697c5b350460d32f99/us-states.json", function(json) {
// Loop through each state data value in the .csv file
for (var i = 0; i < data.length; i++) {
// Grab State Name
var dataState = data[i].state;
// Grab data value
var dataValue = data[i].visited;
// Find the corresponding state inside the GeoJSON
for (var j = 0; j < json.features.length; j++) {
var jsonState = json.features[j].properties.name;
if (dataState == jsonState) {
// Copy the data value into the JSON
json.features[j].properties.visited = dataValue;
// Stop looking through the JSON
break;
}
}
}
// Bind the data to the SVG and create one path per GeoJSON feature
svg.selectAll("path")
.data(json.features)
.enter()
.append("path")
.attr("d", path)
.style("stroke", "#fff")
.style("stroke-width", "1")
.style("fill", function(d) {
// Get data value
var value = d.properties.visited;
if (value) {
//If value exists…
return color(value);
} else {
//If value is undefined…
return "rgb(213,222,217)";
}
});
//this piece of code brings in the data and reshapes it to numberic from string. Runs great.
// Map the cities I have lived in!
d3.csv("https://raw.githubusercontent.com/sjpozzuoli/Daves_Eagles/main/Data_Main/data_clusters_latlong_ready.csv", function(data) {
var data;
data.forEach(function(d){
//create number values from strings
d.demand_score = +d.demand_score;
d.hotness_rank = +d.hotness_rank;
d.hotness_rank_yy = +d.hotness_rank_yy;
d.unique_viewers_per_property_yy =+ d.unique_viewers_per_property_yy;
d.median_days_on_market_yy =+ d.median_days_on_market_yy ;
d.median_listing_price_yy =+ d.median_listing_price_yy;
d.mortgage_rate =+ d.mortgage_rate;
d.supply_score =+ d.supply_score;
d.date = new Date(d.date);
d.latitude =+ d.latitude;
d.longitude =+ d.longitude;
d.class =+ d.class;
//console.log(d);
//console.log(d.city);
var city_state = d.city + ", " + d.state;
//console.log(city_state);
});
console.log(data, "data");
svg.selectAll("circle")
.data(data)
.enter()
.append("circle")
//dots are being drawn just with reverse longitude (neg?)and off the map
.attr("cx", function(d) {
var coords = ([d.longitude, d.latitude])
console.log("coords", coords)
//console.log(d.longitude, "d.longitude");
return coords[0];
})
.attr("cy", function(d) {
var coords = ([d.longitude, d.latitude])
return coords[1];
})
//size of circle working
.attr("r", function(d) {
return Math.sqrt(d.hotness_rank) * .05;
})
//todo: add if statement to correspond color with class
.style("fill", "rgb(217,91,67)")
.style("opacity", 0.85)
The United States in the Western hemisphere, so its longitudes are negative, the behavior you are seeing is expected and the data is correct in this regard.
The problem is that you are treating longitude and latitude as pixel coordinates rather than points on a three dimensional globe. This is why they appear to the left of your SVG. To convert from points on a globe to Cartesian pixels we need a projection.
While you use a projection to project outline of the United States (when you provide the projection to the path generator), you don't use one to project your points. Whenever working with multiple sources of geographic data you need to ensure consistency in projection, otherwise features from different data sources will not align.
The solution is quite simple - project your points with the same projection you use for the outline: projection([longitude,latitude]) which will return a two element array containing the projected x and y (in pixels) coordinates of that point:
.attr("cx", function(d) {
var coords = projection([d.longitude, d.latitude])
return coords[0];
})
.attr("cy", function(d) {
var coords = projection([d.longitude, d.latitude])
return coords[1];
})
And here's a snippet (limiting the total circle count to 3000 for demonstration and performance):
//Width and height of map (adding relative margins to see if that effects placement of circles. makes no apparent difference.)
var margin = { top: 0, left: 0, right: 0, bottom: 0},
height = 500 - margin.top - margin.bottom,
width = 960 - margin.left - margin.right;
// D3 Projection
var projection = d3.geo.albersUsa()
.translate([width/2, height/2]) // translate to center of screen
.scale([1000]); // scale things down so see entire US
// Define path generator
var path = d3.geo.path() // path generator that will convert GeoJSON to SVG paths
.projection(projection); // tell path generator to use albersUsa projection
// Define linear scale for output
var color = d3.scale.linear()
.range(["rgb(165,110,255)","rgb(0,45,150)","rgb(0,157,154)","rgb(250,77,86)"]);
var legendText = ["High Demand, High Supply", "Low Demand, Low Supply", "Low Demand, High Supply", "High Demand, Low Supply"];
//Create SVG element and append map to the SVG
var svg = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", height);
// Append Div for tooltip to SVG
var div = d3.select("body")
.append("div")
.attr("class", "tooltip")
.style("opacity", 0);
// Load in my states data!
d3.csv("https://raw.githubusercontent.com/sjpozzuoli/Daves_Eagles/main/Data_Main/data_clusters_latlong_ready.csv", function(data) {
color.domain([0,1,2,3]); // setting the range of the input data
// Load GeoJSON data and merge with states data
d3.json("https://gist.githubusercontent.com/michellechandra/0b2ce4923dc9b5809922/raw/a476b9098ba0244718b496697c5b350460d32f99/us-states.json", function(json) {
// Loop through each state data value in the .csv file
for (var i = 0; i < data.length; i++) {
// Grab State Name
var dataState = data[i].state;
// Grab data value
var dataValue = data[i].visited;
// Find the corresponding state inside the GeoJSON
for (var j = 0; j < json.features.length; j++) {
var jsonState = json.features[j].properties.name;
if (dataState == jsonState) {
// Copy the data value into the JSON
json.features[j].properties.visited = dataValue;
// Stop looking through the JSON
break;
}
}
}
// Bind the data to the SVG and create one path per GeoJSON feature
svg.selectAll("path")
.data(json.features)
.enter()
.append("path")
.attr("d", path)
.style("stroke", "#fff")
.style("stroke-width", "1")
.style("fill", function(d) {
// Get data value
var value = d.properties.visited;
if (value) {
//If value exists…
return color(value);
} else {
//If value is undefined…
return "rgb(213,222,217)";
}
});
//this piece of code brings in the data and reshapes it to numberic from string. Runs great.
// Map the cities I have lived in!
d3.csv("https://raw.githubusercontent.com/sjpozzuoli/Daves_Eagles/main/Data_Main/data_clusters_latlong_ready.csv", function(data) {
var data;
data.forEach(function(d){
//create number values from strings
d.demand_score = +d.demand_score;
d.hotness_rank = +d.hotness_rank;
d.hotness_rank_yy = +d.hotness_rank_yy;
d.unique_viewers_per_property_yy =+ d.unique_viewers_per_property_yy;
d.median_days_on_market_yy =+ d.median_days_on_market_yy ;
d.median_listing_price_yy =+ d.median_listing_price_yy;
d.mortgage_rate =+ d.mortgage_rate;
d.supply_score =+ d.supply_score;
d.date = new Date(d.date);
d.latitude =+ d.latitude;
// ensure longitue for US is negative:
d.longitude = d.longitude > 0 ? -d.longitude : + d.longitude;
d.class =+ d.class;
});
data = data.filter(function(d,i) {
return d.longitude && d.latitude && i++ < 3000;
})
svg.selectAll("circle")
.data(data)
.enter()
.append("circle")
.attr("cx", function(d) {
var coords = projection([d.longitude, d.latitude])
return coords[0]
})
.attr("cy", function(d) {
var coords = projection([d.longitude, d.latitude])
return coords[1];
})
//size of circle working
.attr("r", function(d) {
return Math.sqrt(d.hotness_rank) * .05;
})
//todo: add if statement to correspond color with class
.style("fill", "rgb(217,91,67)")
.style("opacity", 0.85)
})
})
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js"></script>

possible to tween d3.geo.circle().angle()

I'm trying to recreate the "pulse" effect from this example:
https://anthonyskelton.com/2016/d3-js-earthquake-visualizations/
on a rotating globe... so I have to use d3.geo.circle() to generate paths (rather than svg circles) to manage the clipping properly.
I can transition() other attributes but am guessing I'll need to tween the path for each circle... I just don't know where to start and can't find any examples... there are very few examples using d3.geo.circle() and they are all static.
Thanks for any pointers!
The solution to this question came by way of pursuing a related question:
D3: Accessing bound data after using .datum()
The first step was understanding d3.geo.path().pointRadius() and creating a function to pass into .attr('d', f(d){})
The parameter i is unused but serves as a placeholder so that the radius r can be passed.
The pointPath() function is used elsewhere in update() and reDraw() functions, so it looks for a radius attribute that may already be present in bound data.
geoPath = d3.geo.path().projection(projection);
// var pointPath = function(d, i, data, r) { // d3v4 adds extra param
var pointPath = function(d, i, r) {
if (d.properties && d.properties.radius != undefined) {
r = r || d.properties.radius;
}
r = r || 1.5;
var coords = [d.geometry.coordinates[0], d.geometry.coordinates[1]];
var pr = geoPath.pointRadius(globe.scale() / 100 * r);
return pr({ type: "Point", coordinates: coords })
}
The animation was then possible using an .attrTween()
pulse = function() {
surface.selectAll('.pulse-circle')
.attr("d", function(d) { return pointPath(d, 0, 0); })
.style("fill-opacity", 1 )
.transition()
.delay(function(d, i) { return i * 200; })
.duration(3000)
.style("fill-opacity", 0 )
.attrTween("d", function(d) {
rinterp = d3.interpolate(0, 10);
var fn = function(t) {
d.r = rinterp(t);
return pointPath(d, 0, d.r) || 'M0,0';
};
return fn;
});
}
Because this occurs on a rotating globe I had to add return 'M0,0' if pointPath() returned undefined... to avoid console errors.

Grouped pie chart segments in d3js using .padAngle()

How would I add padding between some groups of segments in a donut/pie chart using d3?
UPDATE
I am using the d3.svg.arc shape generator and .padAngle(paddingFunction) where paddingFunction is defined as:
var paddingFunction = function(d,i) { return i%1==0 ? 0.1 : 0 };
This image is using the paddingFunction described above.
But if I change the padding function to this:
var paddingFunction = function(d,i) { return i%5==0 ? 0.1 : 0 };
I get this image:
Shouldn't the code return two groups of segments with a gap in-between?
Complete code:
// magic numbers
var t = 2 * Math.PI;
var arcHeight = 100;
var innerRadius = 50;
var hours = 10;
function postion(i,offset) {
offset = offset || 0;
return i*(t / hours) + offset*(t / hours)
}
var paddingFunction = function(d,i) { return i%1==0 ? 0.1 : 0 };
// arc generators
var arc = d3.svg.arc()
.innerRadius(function(d,i) { return innerRadius; })
.outerRadius(function(d,i) { return innerRadius + arcHeight; })
.startAngle(function(d, i){ return postion(d.hour);})
.endAngle(function(d, i){ return postion(d.hour,1);})
.padAngle(paddingFunction);
// data
var data = d3.range(0,hours);
data = data.map(function(d) {
return {
hour: d,
color: Math.random(),
}
});
// Scales
var colorScale = d3.scale.linear()
.domain([0, 1])
.interpolate(d3.interpolateRgb)
.range(["#ffffff", "#ffba19"]);
// render viz
var svg = d3.select("svg")
.append("g")
.attr("transform", "translate(320, 320)");
var select = svg.selectAll(".fore").data(data).enter();
select.append("path")
.classed("fore", true)
.style({
"stroke": "black",
"stroke-width": 1,
"fill": function(d) { return colorScale(d.color); }
})
.attr("d", arc);
UPDATE 2
Seems that I misunderstood how the .padAngle() method works. It adds padding on BOTH sides of a segment, I thought it added a gap between segments.
Is there an alternative method in d3 which adds a gap between segements (whilst recalculating the area of the segments so they all keep their proportions)?
Running code: http://blockbuilder.org/GitNoise/13f38aa8f4f2f06dd869

D3 Multiple Pie Chart Updates

I am quite new to D3 but have been working through some mbostocks examples but hitting an issue when trying to update multiple pie charts. I can generate these fine from my data array but when I want to update them I run into an issue.
The issue is quite simple but I am a little stuck on how to fix this. I have run up my code in js fiddle that can be found here. You will see that in my example I build three pies, then wait 3 seconds and update these to new data. The issue I have is that all pies always seem to get updated with the same data.
I believe this is due to the way I am making the path selection in order to update the pie. it looks like I am updating each all the paths each time with each data array so they all end up being updated with the last dataset in my array.
If anyone knows how I can update this in order to correctly build the pies I would be very grateful of any help, pointers or comments.
var data = [
[3, 4, 5, 9],
[1, 7, 3, 4],
[4, 3, 2, 1],
];
function getData() {
// Generate some random data to update the pie with
tdata = []
for(i in data) {
rdata = []
for(c in data[i]) {
rdata.push(Math.floor((Math.random() * 10) + 1) )
}
tdata.push(rdata)
}
return tdata
}
// ------------
var m = 10,
r = 100
var mycolors = ["red","#FF7F00","#F5CC11","#D61687","#1E93C1","#64B72D","#999999"]
var arc = d3.svg.arc()
.innerRadius(r / 2)
.outerRadius(r)
var pie = d3.layout.pie()
.value(function(d) { return d; })
.sort(null);
var svg = d3.select("body").selectAll("svg")
.data(data)
.enter()
.append("svg")
.attr("width", (r + m) * 2)
.attr("height", (r + m) * 2)
.attr("id", function(d,i) {return 'pie'+i;})
.append("svg:g")
.attr("transform", "translate(" + (r + m) + "," + (r + m) + ")");
var path = svg.selectAll("path")
.data(pie)
.enter()
.append("svg:path")
.attr("d", arc)
.style("fill", function(d, i) { return mycolors[i]; })
.each(function(d) { this._current = d; }); // store the initial angles
var titles = svg.append("svg:text")
.attr("class", "title")
.text(function(d,i) {return i;})
.attr("dy", "5px")
.attr("text-anchor", "middle")
// -- Do the updates
//------------------------
setInterval(function() {
change()
}, 3000);
function change() {
// Update the Pie charts with random data
piedata = getData()
svg.each(function(d,i) {
path = path.data(pie(piedata[i]))
path.transition().duration(1000).attrTween("d", arcTween);
})
// temp, print new array to screen
tdata = ""
for(x in piedata) {
tdata += "<strong>"+x+":</strong> "+piedata[x]+"<br>"
}
$('#pieData').html(tdata)
}
function arcTween(a) {
var i = d3.interpolate(this._current, a);
this._current = i(0);
return function(t) {
return arc(i(t));
};
}
Right, I finally got this working and am posting the working solution incase others are trying to do the same thing.
I expect this might not be the best nor most efficient way of doing it but this is going to be fine for what I need (at this point). But if anyone still has any better solutions it would be good to hear from you.
I ended up selecting the paths based on a unique id that I gave the individual SVG elements which I created, then just updated these paths only. Sounds simple now when I say it like this but did have me stumped for a while.
function change() {
// Update the Pie charts with random data
var newdata = getData()
for(x in newdata) {
var npath = d3.select("#pie"+x).selectAll("path").data(pie(newdata[x]))
npath.transition().duration(1000).attrTween("d", arcTween); // redraw the arcs
}
}
Full working copy can be found at http://jsfiddle.net/THT75/nskwwbnf/

How do I adjust zoom size for a point in D3?

This could be a classic case of "you're doing it wrong", but all of my searching to date hasn't warranted any help.
Here's my scenario:
I'm using an albersUSA map projection in conjunction with the national and county GeoJson files to draw everything.
I also have a self created "cities" file that contains major cities for each state. The coordinates are accurate and everything looks good.
When a user clicks on a given state, I hide all state shapes and then calculate the transform needed to get the county shapes for that state to fit within my viewport. I then apply that transform to all the necessary county shapes in order to get the "zoomed" view. My code is as follows:
function CalculateTransform(objectPath)
{
var results = '';
// Define bounds/points of viewport
var mapDimensions = getMapViewportDimensions();
var baseWidth = mapDimensions[0];
var baseHeight = mapDimensions[1];
var centerX = baseWidth / 2;
var centerY = baseHeight / 2;
// Get bounding box of object path and calculate centroid and zoom factor
// based on viewport.
var bbox = objectPath.getBBox();
var centroid = [bbox.x + bbox.width / 2, bbox.y + bbox.height / 2];
var zoomScaleFactor = baseHeight / bbox.height;
var zoomX = -centroid[0];
var zoomY = -centroid[1];
// If the width of the state is greater than the height, scale by
// that property instead so that state will still fit in viewport.
if (bbox.width > bbox.height) {
zoomScaleFactor = baseHeight / bbox.width;
}
// Calculate how far to move the object path from it's current position to
// the center of the viewport.
var augmentX = -(centroid[0] - centerX);
var augmentY = -(centroid[1] - centerY);
// Our transform logic consists of:
// 1. Move the state to the center of the screen.
// 2. Move the state based on our anticipated scale.
// 3. Scale the state.
// 4. Move the state back to accomodate for the scaling.
var transform = "translate(" + (augmentX) + "," + (augmentY) + ")" +
"translate(" + (-zoomX) + "," + (-zoomY) + ")" +
"scale(" + zoomScaleFactor + ")" +
"translate(" + (zoomX) + "," + (zoomY) + ")";
return results;
}
...and the binding function
// Load county data for the state specified.
d3.json(jsonUrl, function (json) {
if (json === undefined || json == null || json.features.length == 0)
{
logging.error("Failed to retrieve county structure data.");
showMapErrorMessage("Unable to retrieve county structure data.");
return false;
}
else
{
counties.selectAll("path")
.data(json.features)
.enter()
.append("path")
.attr("id", function (d, i) {
return "county_" + d.properties.GEO_ID
})
.attr("data-id", function (d, i) { return d.properties.GEO_ID })
.attr("data-name", function (d, i) { return countyLookup[d.properties.GEO_ID] })
.attr("data-stateid", function (d, i) { return d.properties.STATE })
.attr("d", path);
// Show all counties for state specified and apply zoom transform.
d3.selectAll(countySelector).attr("visibility", "visible");
d3.selectAll(countySelector).attr("transform", stateTransform);
// Show all cities for the state specified and apply zoom transform
d3.selectAll(citySelector).attr("visibility", "visible");
d3.selectAll(citySelector).attr("transform", stateTransform);
}
});
This works fine here, except for really small states, the zoom factor is much larger, and the circles get distored.
Is there a way to force the size of the points to be a fixed size (say a 15px radius) even after the transform occurs?
For things you don't want to scale, just make them divided by 'scale' . In my case,
var zoom = d3.behavior.zoom()
.on("zoom",function() {
g.attr("transform","translate("+d3.event.translate.join(",")+")scale("+d3.event.scale+")");
g.selectAll(".mapmarker")
.attr("r",6/d3.event.scale)
.attr("stroke-width",1/d3.event.scale);
});
This is happening because you are setting a scale transform instead of scaling the positions. You can see the difference here Basically it is the difference between:
// Thick lines because they are scaled too
var bottom = svg.append('g').attr('transform', 'scale('+scale+','+scale+')');
bottom.selectAll('circle')
.data(data)
.enter().append('circle')
.attr('cx', function(d) { return d.x; })
.attr('cy', function(d) { return d.y; });
and
// line thicknesses are nice and thin
var top = svg.append('g');
top.selectAll('circle')
.data(data)
.enter().append('circle')
.attr('cx', function(d) { return d.x * scale; })
.attr('cy', function(d) { return d.y * scale; });
With mapping probably you best solution is to compute your offset and scale as you do and then add them into your projection function - you want to directly modify the post-projection x and y values. If you update your projection function properly you should not have to do anything else to apply the appropriate zoom to your map.

Resources