Related
I'm new to d3 and I'm trying to color each US state based on the total energy consumption value. When I use :
d3.selectAll("path")
.attr("fill", "blue");
I can successfully change the color to blue, so I think I have the right foundation. But when I try to apply the colorgradient function that I have defined myself, it doesn't work anymore. Most of the examples I have seen online use the same json file to create the map and to assign color values. But I have two json files I'm working with--one that contains the US state path shape and the other with meta data for each state. This is what I have:
const data = await d3.json("states.json");
const energy_data = await d3.csv("Energy Census and Economic Data US 2010-2014.csv");
console.log(energy_data);
const states = topojson.feature(data, data.objects.usStates).features
// draw the states
svg.selectAll(".state")
.data(states)
.enter().append("path")
.attr("class", "state")
.attr("d", path);
const min = d3.min(energy_data, d => parseInt(d.TotalC2010));
const max = d3.max(energy_data, d => parseInt(d.TotalC2010));
const colorgradient = d3.scaleSqrt()
.domain([min, max])
.range(["green", "blue"]);
d3.selectAll("path")
.data(energy_data)
.enter()
.attr("fill", d => colorgradient(d.TotalC2010));
Any advice?
Thanks in advance!
EDIT: I finally got it to work, thanks to Andrew Reid.
const state_data = await d3.json("states.json");
const energy_data_raw = await d3.csv("Energy Census and Economic Data US 2010-2014.csv");
const energy_data = new Map(energy_data_raw.map(d => [d.StateCodes, d]))
const states = topojson.feature(state_data, state_data.objects.usStates).features
const min = d3.min(energy_data_raw, d => parseInt(d.TotalC2010));
const max = d3.max(energy_data_raw, d => parseInt(d.TotalC2010));
let colorgradient = d3.scaleSqrt()
.domain([min, max])
.range(["green", "blue"]);
svg.selectAll(".state")
.data(states)
.enter()
.append("path")
.attr("class", "state")
.attr("d", path)
.attr("fill", d => colorgradient(energy_data.get(d.properties.STATE_ABBR).TotalC2010))
Problem
This code here:
d3.selectAll("path")
.data(energy_data)
.enter()
.attr("fill", d => colorgradient(d.TotalC2010));
Shouldn't do anything at all.
First, assuming your geographic data and your energy data have the same number of items, you are selecting the existing paths (d3.selectAll("path")).
Then you are assigning those existing paths new data (.data(energy_data)) matching each existing path - in order of their index.
Next you create what is likely an empty enter selection. The enter selection creates an element for every item in the data array that does not have a corresponding element (matched by index here): if you have more items in your data array than elements, you'll enter new elements. Otherwise, you'll have an empty selection because you do no need to enter any new elements to represent the data in energy_data.
Lastly you style an empty enter selection (normally you'd use .append() to specify what type of element you'd want to append, otherwise, the enter selection is just a placeholder. As this is an empty selection anyways, nothing is done.
Possible Solution
I'm going through this solution as it looks like what you are trying to do, though it is not what I would recommend.
It appears as though you are trying to assign new data to an existing selection - something that is absolutely possible. In this approach you'd use the geographic data to draw the features, then assign a new dataset to each feature and modify the features based on this new data.
Data arrays ordered the same
If our data is in the same order in both geojson and csv, then we can simply use:
selection.data(states)
.enter()
.append("path")
.attr("d", path)
.data(energy_data)
.attr("fill", ...)
Because .data() by default binds items in the data array to elements in the selection by matching their indices, each feature is updated with the correct data in energy_data only when the two data arrays are ordered the same. This is an obvious limitation, but one that can be overcome.
Data arrays ordered differently
If the arrays are not ordered the same, we need to have a way to match existing features with the new data set. By default the .data() method assigns data to existing elements by index. But we can use the second parameter of .data() to assign a unique identifier using a key function.
For this case I'm assuming our identifier for both states and energy_data resides at d.properties.id.
When we enter our paths, we don't need the key function, there is no data to join to existing elements.
When we update our paths with the energy_data data, we want to use a key function to ensure we update each element with the correct new data. The key function is evaluated on each existing element's datum first, and then on each item in the new data array. Where a match in key is found, the matching new datum will replace the old.eg:
svg.selectAll("path")
.data(energy_data, function(d) { return d.properties.id; })
.attr("fill",...
Here's a quick example with contrived data:
let data = [
{ value: 4, properties: {id: "A" }},
{ value: 6, properties: {id: "B" }},
{ value: 2, properties: {id: "C" }}
]
let color = d3.scaleLinear()
.domain([1,6]).range(["red","yellow"]);
let geojson = {
"type":"FeatureCollection",
"features": [
{
"type": "Feature",
"properties": { id: "C"},
"geometry": {
"type": "Polygon",
"coordinates": [
[
[ 0, 0 ],
[ 100, 0 ],
[ 100,100 ],
[ 0, 100 ],
[ 0, 0 ]
]
]
}
},
{
"type": "Feature",
"properties": { id: "B"},
"geometry": {
"type": "Polygon",
"coordinates": [
[
[ 100, 0 ],
[ 200, 0 ],
[ 200, 100 ],
[ 100, 100 ],
[ 100, 0 ]
]
]
}
},
{
"type": "Feature",
"properties": { id: "A"},
"geometry": {
"type": "Polygon",
"coordinates": [
[
[ 200, 0 ],
[ 300, 0 ],
[ 300,100 ],
[ 200, 100 ],
[ 200, 0 ]
]
]
}
}
]
}
let svg = d3.select("body")
.append("svg")
.attr("width", 300);
svg.selectAll("path")
.data(geojson.features)
.enter()
.append("path")
.attr("d", d3.geoPath(null));
svg.selectAll("path") // Note: You can just chain .data() to .attr() omitting this line.
.data(data, d=>d.properties.id)
.attr("fill", d=>color(d.value));
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.1.0/d3.min.js"></script>
Data arrays ordered differently with different key accessors
However, if the location or name of the identifier is different than before, we need to change the key function.
For this case, I'm assuming that the states identifier is located at d.properties.id. For energy_data, I'm assuming that the identifier resides at d.id, a more common parsed DSV location.
As noted, the key function is evaluated for existing element's data and then new data. this means we need a key function that works for both datasets, which means we need a slightly more complicated key function to compare items from both datasets, for example:
.data(energy_data, function(d) {
if(d.properties)
return d.properties.id; // get the key from items in `states`
else
return d.id; // get the key from items in `energy_data`
})
.attr("fill",...
The key function now will be able to have the new datum replace the old ensuring that the correct feature has the correct data.
Assuming all your identifiers match properly (and are strings) you'll have assigned new data to the existing features.
The downside of this approach is you've lost the original data - if you want to do semantic zooming, check different properties of the geographic data, or revisit the data in the geojson, you need to rebind the original data. Selecting the paths takes time as well, and it assumes there are no other paths that might be mistakenly selected.
Here's a quick example:
let csv = d3.csvParse(d3.select("pre").remove().text());
let color = d3.scaleLinear()
.domain([1,6]).range(["red","yellow"]);
let geojson = {
"type":"FeatureCollection",
"features": [
{
"type": "Feature",
"properties": { id: "C"},
"geometry": {
"type": "Polygon",
"coordinates": [
[
[ 0, 0 ],
[ 100, 0 ],
[ 100,100 ],
[ 0, 100 ],
[ 0, 0 ]
]
]
}
},
{
"type": "Feature",
"properties": { id: "B"},
"geometry": {
"type": "Polygon",
"coordinates": [
[
[ 100, 0 ],
[ 200, 0 ],
[ 200, 100 ],
[ 100, 100 ],
[ 100, 0 ]
]
]
}
},
{
"type": "Feature",
"properties": { id: "A"},
"geometry": {
"type": "Polygon",
"coordinates": [
[
[ 200, 0 ],
[ 300, 0 ],
[ 300,100 ],
[ 200, 100 ],
[ 200, 0 ]
]
]
}
}
]
}
let svg = d3.select("body")
.append("svg")
.attr("width", 300);
svg.selectAll("path")
.data(geojson.features)
.enter()
.append("path")
.attr("d", d3.geoPath(null));
svg.selectAll("path") // Note: You can just chain .data() to the .attr() omitting this line.
.data(csv, d=>d.properties?d.properties.id:d.id)
.attr("fill", d=>color(d.value));
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.1.0/d3.min.js"></script>
<pre>id,value
A,4
B,6
C,2</pre>
Recommended Approach
To join the spatial data and non spatial data, I suggest using a javascript map. This allows you do look up values in your non spatial data using the shared identifier:
let map = new Map(energy_data.map(function(d) { return [d.id, d] }))
We can look up any item in energy_data now with map.get("someIdentifier")
Which we can use as follows:
.attr("fill", d=> colorgradient(map.get(d.properties.id).TotalC2010))
This way our spatial features retain their spatial data, but we can easily access the nonspatial data using the common identifier and the javascript map.
Here's a quick example using the same contrived geojson and DSV data as above:
let csv = d3.csvParse(d3.select("pre").remove().text());
let map = new Map(csv.map(function(d) { return [d.id, d] }))
let color = d3.scaleLinear()
.domain([1,6]).range(["red","yellow"]);
let geojson = {
"type":"FeatureCollection",
"features": [
{
"type": "Feature",
"properties": { id: "C"},
"geometry": {
"type": "Polygon",
"coordinates": [
[
[ 0, 0 ],
[ 100, 0 ],
[ 100,100 ],
[ 0, 100 ],
[ 0, 0 ]
]
]
}
},
{
"type": "Feature",
"properties": { id: "B"},
"geometry": {
"type": "Polygon",
"coordinates": [
[
[ 100, 0 ],
[ 200, 0 ],
[ 200, 100 ],
[ 100, 100 ],
[ 100, 0 ]
]
]
}
},
{
"type": "Feature",
"properties": { id: "A"},
"geometry": {
"type": "Polygon",
"coordinates": [
[
[ 200, 0 ],
[ 300, 0 ],
[ 300,100 ],
[ 200, 100 ],
[ 200, 0 ]
]
]
}
}
]
}
let svg = d3.select("body")
.append("svg")
.attr("width", 300);
svg.selectAll("path")
.data(geojson.features)
.enter()
.append("path")
.attr("d", d3.geoPath(null))
.attr("fill", d=> color(map.get(d.properties.id).value));
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.1.0/d3.min.js"></script>
<pre>id,value
A,4
B,6
C,2</pre>
Other Approaches
A third option would be to combine the data arrays - iterating through the geojson and adding values contained in energy_data to each feature manually so that you have only one data array containing everything you need to draw and style the visualization.
I'm having a problem plotting points from a geoJSON file over a map using D3.js. The map is rendering fine, but the points are not showing up. I'm not receiving any error messages at this time.
I'm following along with this tutorial but using my own geoJSON file to plot the data.
This is what I have:
var width = 960,
height = 500;
var svg = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", height);
var g = svg.append("g");
var projection = d3.geoAlbers()
.scale(1000)
.translate([width / 2, height / 2]);
var path = d3.geoPath()
.projection(projection);
d3.queue()
.defer(d3.json, 'states.json') // Load US States
.defer(d3.json, 'trump_geoJson.json') // Load tweet lat/long data
.await(makeMyMap); // Run 'ready' when JSONs are loaded
function makeMyMap(error,states,tweets) {
svg.append('path')
.datum(topojson.feature(states, states.objects.usStates))
.attr('d', path)
.attr('class', 'states');
svg.selectAll('.tweets')
.data(tweets.features)
.enter()
.append('path')
.attr('d',path)
.attr('class', 'tweets');
}
I'm expecting about 600 points to be plotted, but getting none.
The json file trump_geoJson looks like:
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"id": 0,
"properties": {
"primary_geo": "Utah, USA",
"tag": "#Bernie",
"text": "text",
"user_id": "id"
},
"geometry": {
"type": "Point",
"coordinates": [
39.32373809814453,
-111.67823791503906
]
}
},
{
"type": "Feature",
"id": 1,
"properties": {
"primary_geo": "New York, NY",
"tag": "#Bernie",
"text": "text",
"user_id": "id"
},
"geometry": {
"type": "Point",
"coordinates": [
40.71455001831055,
-74.00714111328125
]
}
},... ]
Your geojson uses the wrong coordinate convention. You have:
"coordinates": [ latitude, longitude ]
But, you must use:
"coordinates": [ longitude, latitude ]
From the spec:
Point coordinates are in x, y order (easting, northing for projected
coordinates, longitude, and latitude for geographic coordinates)
It is funny that the spec considers eastings and northings for projected coordinates given the spec also states geojson must use unprojected (lat/long) coordinates using the WGS84 datum
Here's a demo of the first two items in your geojson feature collection:
var data = {
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [-111.6782379150,39.32373809814]
}
},
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [-74.00714111328,40.71455001831]
}
}]};
var width = 500,
height = 300;
var svg = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", height);
var projection = d3.geoAlbers()
.scale(600)
.translate([width / 2, height / 2]);
var path = d3.geoPath()
.projection(projection);
d3.json("https://unpkg.com/world-atlas#1/world/110m.json", function(error, world) {
if (error) throw error;
svg.append("path")
.attr("d", path(topojson.mesh(world)))
.attr("fill","none")
.attr("stroke","black")
.attr("stroke-width",1);
svg.selectAll('.tweets')
.data(data.features)
.enter()
.append('path')
.attr('d',path)
.attr('class', 'tweets');
});
.tweets {
fill: red;
opacity: 0.7;
}
<script src="http://d3js.org/d3.v4.min.js" charset="utf-8"></script>
<script src="http://d3js.org/topojson.v1.min.js"></script>
<script src="https://d3js.org/d3-queue.v2.min.js"></script>
I am trying to make a sunburst by following the 3-part tutorial on https://bl.ocks.org/denjn5/3b74baf5edc4ac93d5e487136481c601 My json contains sell information based on country and product division. I am trying to show in the first layer sell based on country and in the 2nd layer sell based on product division. My Json file looks like this:
{
"country": "All",
"shares":[
{
"country": "at",
"shares":[
{
"productdivision": "accessorie",
"label": 53222
},
{
"productdivision": "apparel",
"label": 365712
},
{
"productdivision": "footwear",
"label": 523684
}
]
},
{
"country": "be",
"shares":[
{
"productdivision": "accessorie",
"label": 57522
},
{
"productdivision": "apparel",
"label": 598712
},
{
"productdivision": "footwear",
"label": 52284
}
]
},
{
"country": "DE",
"shares":[
{
"productdivision": "accessorie",
"label": 56982
},
{
"productdivision": "apparel",
"label": 55312
},
{
"productdivision": "footwear",
"label": 67284
}
]
},
{
"country": "Fr",
"shares":[
{
"productdivision": "accessorie",
"label": 5862
},
{
"productdivision": "apparel",
"label": 45312
},
{
"productdivision": "footwear",
"label": 26284
}
]
}
]
}
This json file's name is kpiDrillDown2.json and I call it in my code with d3.json(). I have made slight changes to the code to work for my data. The code is as follows:
<html>
<head>
<style>
#import url('https://fonts.googleapis.com/css?family=Raleway');
body {
font-family: "Raleway", "Helvetica Neue", Helvetica, Arial, sans-serif;
}
</style>
<script src="https://d3js.org/d3.v4.min.js"></script>
</head>
<body>
<svg></svg>
<script>
//initialize variables
var width = 500;
var height = 500;
var radius = Math.min(width, height) / 2;
var color = d3.scaleOrdinal(d3.schemeCategory20b);
//setting up svg workspace
var g = d3.select('svg')
.attr('width', width)
.attr('height', height)
.append('g')
.attr('transform', 'translate(' + width / 2 + ',' + height / 2 + ')');
//formatting the data
var partition = d3.partition()
.size([2 * Math.PI, radius]);
function draw(nodeData){
debugger;
//finding the root node
var root = d3.hierarchy(nodeData)
.sum(function (d) { return d.label});
//calculating each arc
partition(root);
var arc = d3.arc()
.startAngle(function (d) { return d.x0; })
.endAngle(function (d) { return d.x1; })
.innerRadius(function (d) { return d.y0; })
.outerRadius(function (d) { return d.y1; });
g.selectAll('g')
.data(root.descendants())
.enter()
.append('g')
.attr("class", "node")
.append('path')
.attr("display", function (d) { return d.depth ? null : "none"; })
.attr("d", arc)
.style('stroke', '#fff')
.style("fill", function (d) { return color((d.parent ? d : d.parent).data.productdivision); })
g.selectAll(".node")
.append("text")
.attr("transform", function(d) {
return "translate(" + arc.centroid(d) + ")rotate(" + computeTextRotation(d) + ")"; })
.attr("dx", "-20")
.attr("dy", ".5em")
.text(function(d) { return d.parent ? d.data.productdivision : "" });
function computeTextRotation(d) {
var angle = (d.x0 + d.x1) / Math.PI * 90;
// Avoid upside-down labels
return (angle < 90 || angle > 270) ? angle : angle + 180;
}
}
d3.json('kpiDrillDown3.json', draw);
</script>
</body>
</html>
I put a debbuger in the draw functin to inspect root element. Root doesn't have any children. This is what I see in the console:
When I continue it gives me the error:"Cannot read property 'data' of null". As shown in console, root doesn't have children. My question is, do I need to change my json data format to make root recogninze the chilren, or am I doing something wrong. I am new to d3js and basically by getting the source code and modifying it, I am making my way through. This is the error in console:
I appreciate your help and thank you very much.
According to the API:
The specified children accessor function is invoked for each datum, starting with the root data, and must return an array of data representing the children, or null if the current datum has no children. If children is not specified, it defaults to:
function children(d) {
return d.children;
}
However, in your data structure, you don't have children, but shares instead.
So, the hierarchy should be:
var root = d3.hierarchy(data, function(d) {
return d.shares;
})
Pay attention to the fact that in the JSON of that tutorial you linked (just like in the API's example) the children's array is named children.
Here is a demo, look at the console (your browser's console, not the snippet one):
var data = {
"country": "All",
"shares": [{
"country": "at",
"shares": [{
"productdivision": "accessorie",
"label": 53222
},
{
"productdivision": "apparel",
"label": 365712
},
{
"productdivision": "footwear",
"label": 523684
}
]
},
{
"country": "be",
"shares": [{
"productdivision": "accessorie",
"label": 57522
},
{
"productdivision": "apparel",
"label": 598712
},
{
"productdivision": "footwear",
"label": 52284
}
]
},
{
"country": "DE",
"shares": [{
"productdivision": "accessorie",
"label": 56982
},
{
"productdivision": "apparel",
"label": 55312
},
{
"productdivision": "footwear",
"label": 67284
}
]
},
{
"country": "Fr",
"shares": [{
"productdivision": "accessorie",
"label": 5862
},
{
"productdivision": "apparel",
"label": 45312
},
{
"productdivision": "footwear",
"label": 26284
}
]
}
]
};
var root = d3.hierarchy(data, function(d) {
return d.shares;
})
.sum(function(d) {
return d.label
});
console.log(root)
<script src="https://d3js.org/d3.v4.min.js"></script>
Im trying to make a hierarchical edge bundling like this using d3 v4. I use d3.curveBundle to make the line curved. But in my project, the line doesn't seem to be curved.
What I get is a straight line like this:
This is my code:
var data = {
"name": "Eve",
"children": [
{
"name": "Cain"
},
{
"name": "Seth",
"children": [
{
"name": "Enos"
},
{
"name": "Noam"
}
]
},
{
"name": "Abel"
},
{
"name": "Awan",
"children": [
{
"name": "Enoch"
}
]
},
{
"name": "Azura"
}
]
};
var cluster = d3.cluster()
.size([360, 120]);
var nodes = cluster(d3.hierarchy(data));
var links = nodes.links();
var svg = d3.select("div.radial-network")
.append("svg")
.attr("width", "100%")
.attr("height", "300")
.attr("transform", "translate(120,120)");
const line = d3.radialLine()
.curve(d3.curveBundle.beta(0.95))
.angle(function(d,i){ return d.x*Math.PI/180;})
.radius(function(d,i) {return d.y;});
const edges = svg.selectAll('.link').data(links);
edges.enter().append('path')
.attr('class', 'link')
.attr('stroke', 'red')
.attr('d', function(d, i) {return line(d.source.path(d.target));});
I'm trying to configure a map that shows a view of the US with some of the buttom of Canada and some of the top of Mexico. I did a basic capture of this in www.geojson.io:
http://geojson.io/#id=gist:anonymous/b2f65ddc2a28013b0c9c7ecab5d7427f&map=4/39.37/-97.16
How can I display this in a browser map using d3.js? I'm able to display a browser map of the world using world-110m.json which seems to be a commonly used geojson file:
https://gist.githubusercontent.com/mbostock/4090846/raw/d534aba169207548a8a3d670c9c2cc719ff05c47/world-110m.json
Here's the d3 code I'm using to display the geojson as a browser map:
d3.json("json/world-110m.json", function (error, topology) {
if (error) return console.warn(error);
g.selectAll("path")
.data(topojson.object(topology, topology.objects.countries)
.geometries)
.enter()
.append("path")
.attr("d", path)
});
World-110m.json seems to have a lot of additional info I don't need because I just need to display a flat map of the area captured in my geojson.io map above. Do I need to build out my geojson, update my d3 code or both?
I'm not sure what geojson.io is doing but the geojson it's returning (two identical polygons) doesn't seem to correlate to anything resembling your selection.
For what it is worth, here's the "map" from that geojson:
<!DOCTYPE html>
<html>
<head>
<script src="http://d3js.org/d3.v3.min.js"></script>
</head>
<body>
<script>
var geoJson = {
"type": "FeatureCollection",
"features": [{
"type": "Feature",
"properties": {},
"geometry": {
"type": "Polygon",
"coordinates": [
[
[-129.0234375,
23.160563309048314
],
[-129.0234375,
52.53627304145948
],
[-65.302734375,
52.53627304145948
],
[-65.302734375,
23.160563309048314
],
[-129.0234375,
23.160563309048314
]
]
]
}
}, {
"type": "Feature",
"properties": {},
"geometry": {
"type": "Polygon",
"coordinates": [
[
[-129.0234375,
23.160563309048314
],
[-129.0234375,
52.53627304145948
],
[-65.302734375,
52.53627304145948
],
[-65.302734375,
23.160563309048314
],
[-129.0234375,
23.160563309048314
]
]
]
}
}]
};
var width = 960,
height = 600;
var projection = d3.geo.albersUsa()
.scale(500)
.translate([width / 2, height / 2]);
var path = d3.geo.path()
.projection(projection);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
svg.append("g")
.selectAll("path")
.data(geoJson.features)
.enter().append("path")
.attr("d", path);
</script>
</body>
</html>