"Error: <path> attribute d: Expected number" - javascript

I'm trying to create a cartogram using cartogram.js and d3.js. I've used the examples found in the cartogram.js repo and here to put together a script that generates a world map inside an SVG using the d3.geo.mercator() projection and now I'm trying to distort the map using the cartogram.js library however I'm getting the following error:
d3.js:8756 Error: <path> attribute d: Expected number, "MNaN,NaNLNaN,NaNL…".
(anonymous function) # d3.js:8756
tick # d3.js:8956
(anonymous function) # d3.js:8936
d3_timer_mark # d3.js:2166
d3_timer_step # d3.js:2147
Here's my code I'm using to distort the map:
var dataLoaded = new Event("dataLoaded"),
svg = d3.select("svg"),
proj = d3.geo.mercator(),
path = d3.geo.path()
.projection(proj),
countries = svg.append("g")
.attr("id", "countries")
.selectAll("path"),
carto = d3.cartogram()
.projection(proj)
.properties(function(d) {
return d.properties
}),
mapData = d3.map(),
geometries,
topology
function init() {
d3.csv("data/data.csv", function(data) {
data.forEach(function (d) {
mapData.set(d.COUNTRY, d.VALUE)
})
})
d3.json("data/world.json", function(data) {
topology = data
geometries = topology.objects.countries
var features = carto.features(topology, geometries)
countries = countries
.data(features)
.enter()
.append("path")
.attr("fill", function (e) {
return "#000000"
})
.attr("d", path)
document.dispatchEvent(dataLoaded)
})
}
document.addEventListener("dataLoaded", function() {
$("#container").css("visibility", "visible").hide().fadeIn("fast")
$("header").css("visibility", "visible").hide().fadeIn("slow")
carto.value(function(d) {
return +mapData.get(d.properties.name)
})
countries.data(carto(topology, geometries).features)
countries.transition()
.duration(750)
.attr("d", carto.path);
})
init()
and the CSV file containing the data I want to use to distort the map:
COUNTRY,VALUE
Afghanistan,90
Albania,390
Algeria,90
Andora,110
Angola,10
Antigua,2400
Argentina,320
Armenia,40
Australia,6600
Austria,1300
Axerbaijan,0
Bahamas,1900
Bahrain,90
Bangladesh,50
Barbados,8100
Belarus,20
Belgium,260
Belize,480
Benin,0
Bhutan,170
Bolivia,90
Bosnia,70
Botswana,110
Brazil,1300
Brunei,40
Bulgaria,3600
Burkina Faso,0
Burundi,0
Cabo Verde,0
Cambodia,720
Cameroon,10
Canada,4400
Central African Republic,0
Chad,10
Chile,320
China,1600
Combodia,0
Comoros,10
Congo,20
Costa Rica,2900
Cote d'Ivoire,0
Croatia,9900
Cuba,14800
Cyprus,8100
Czech Republic,70
Denmark,320
Dijbouti,0
Dominica,0
Dominican Republic,4400
Ecuador,90
Egypt,6600
El Salvador,10
Equatorial Guinea,0
Eritrea,10
Estonia,110
Ethiopia,70
Fiji,1900
Finland,720
France,2900
Gabon,10
Gambia,2400
Georgia,70
Germany,880
Ghana,210
Greece,14800
Grenada,720
Guatemala,40
Guinea,0
Guinea - Bissau,0
Guyana,50
Haiti,90
Honduras,110
Hungary,170
Iceland,8100
India,2900
Indonesia,390
Iran,390
Iraq,140
Ireland,1900
Israel,590
Italy,9900
Jamaica,6600
Japan,3600
Jordan,480
Kazakhstan,40
Kenya,1000
Kiribati,10
Kosovo,10
Kuwait,40
Kyrgyzstan,10
Laos,70
Latvia,110
Lebanon,70
Lesotho,0
Liberia,10
Libya,30
Liechtenstein,10
Lithuania,70
Luxembourg,50
Macedonia,70
Madagascar,0
Malawi,40
Malaysia,1300
Maldives,12100
Mali,40
Malta,12100
Marshall Islands,10
Mauritania,10
Mauritius,6600
Mexico,18100
Micronesia,20
Moldova,20
Monaco,590
Mongolia,110
Montenegro,880
Morocco,4400
Mozambique,90
Myanmar,90
Namibia,210
Nauru,10
Nepal,0
Netherlands,50
New Zealand,1900
Nicaragua,50
Niger,10
Nigeria,90
North Korea,390
Norway,1600
Oman,590
Pakistan,110
Palau,50
Palestine,10
Panama,210
Papua New Guinea,40
Paraguay,10
Peru,1000
Philippines,590
Poland,880
Portugal,12100
Qatar,210
Romania,320
Russia,480
Rwanda,20
Saint Kitts and Nevis,0
Saint Lucia,90
Saint Vincent and the Grenadines,0
Samoa,90
San Marino,70
Sao Tome and Principe,10
Saudi Arabia,110
Senegal,70
Serbia,50
Seychelles,1600
Sierra Leone,20
Singapore,880
Slovakia,70
Slovenia,390
Solomon Islands,10
Somalia,70
South Africa,1900
South Korea,140
South Sudan ,0
Spain,14800
Sri Lanka,3600
Sudan,20
Suriname,10
Sweden,720
Switzerland,1300
Syria,590
Taiwan,50
Tajikistan,10
Tanzania,260
Thailand,14800
Timor-Leste,0
Togo,10
Tonga,50
Trinidad and Tobago,140
Tunisia,4400
Turkey,9900
Turkmenistan,10
Tuvalu,30
Uganda,50
Ukraine,70
United Arab Emirates,20
United Kingdom,50
United States of America,3600
Uruguay,50
Uzbekistan,30
Vanuatu,30
Vatican City,30
Venezuela,170
Vietnam,2400
Yemen,20
Zambia,90
Zimbabwe,70
I don't have any experience using d3.js prior to this project so I would appreciate any feedback/guidance you can give me.
I'm using version 3.5.17 of d3, fyi.
Thanks.
UPDATE - 9/8/2016 15:22 BST
As per #Mark's suggestion, I've implemented d3-queue, although the problem still persists. If I've done anything wrong with this implementation, however, I'd be grateful for any insight anyone can give me! :)
var svg = d3.select("svg"),
proj = d3.geo.mercator(),
path = d3.geo.path()
.projection(proj),
countries = svg.append("g")
.attr("id", "countries")
.selectAll("path"),
carto = d3.cartogram()
.projection(proj)
.properties(function(d) {
return d.properties
}),
queue = d3.queue()
.defer(csv)
.defer(json)
.awaitAll(ready),
mapData = d3.map(),
geometries,
topology
function json(callback) {
d3.json("data/world.json", function(data) {
topology = data
geometries = topology.objects.countries
var features = carto.features(topology, geometries)
countries = countries
.data(features)
.enter()
.append("path")
.attr("fill", function (e) {
return "#000000"
})
.attr("d", path)
callback()
})
}
function csv(callback) {
d3.csv("data/data.csv", function(data) {
data.forEach(function (d) {
mapData.set(d.COUNTRY, +d.VALUE)
})
callback()
})
}
function ready() {
$("#container").css("visibility", "visible").hide().fadeIn("fast")
$("header").css("visibility", "visible").hide().fadeIn("slow")
carto.value(function(d) {
if (mapData.has(d.properties.name)) {
return +mapData.get(d.properties.name)
}
})
countries.data(carto(topology, geometries).features)
countries.transition()
.duration(750)
.attr("d", carto.path);
}
UPDATE 2 - 9/8/2016 18:05 BST
Here is the latest version of the script on Plunker which can be used for testing, courtesy of #Mark: http://plnkr.co/edit/iK9EZSIfwIXjIEBHhlep?p=preview
It seems my initial error has been fixed although the resulting cartogram isn't displaying correctly.
UPDATE 3 - 10/8/2016 20:45 BST
#Mark's answer helped clarify a lot of my issues and I had a partially functioning cartogram as a result however to fix the issue detailed here, I regenerated my map file using the --stitch-poles false parameter and and after doing this I am once again receiving the following error:
d3.js:8756 Error: <path> attribute d: Expected number, "MNaN,NaNLNaN,NaNL…".
#Mark's initial fix for this error is still in place therefore I'm quite confused as to why this has resurfaced. You can see my latest code here and my new map topojson file here. Thanks again.

Okay, I'm making progress. It turns out that after fixing your .value function the reason you don't get a catrogram is the your values are too disparate. Why this throws off cartogram.js, I'm unsure, but the problem can be easily solved by introducing a scale.
With your data:
s = d3.scale.linear().range([1,100]).domain(d3.extent(data, function(d){ return +d.VALUE}));
And then in your .value accessor:
carto.value(function(d,i) {
if (mapData.has(d.properties.name)) {
return s(mapData.get(d.properties.name));
} else {
return 1;
}
});
Alas, though, all your problems aren't fixed. It seems that countries that "wrap" the projection (ie Russia and Fiji) get distorted by the paths generated by cartogram.js. Here's a fix though, discussed at length here
Regardless of that, here's what we've got so far.

Related

Leaflet control layer: selector

I am displaying two Geojson files on leaflet. The two files are displaying fine on my map but I want to be able to have the selector working on both layers.
As you can see here, my selector only display on province : http://bl.ocks.org/renauld94/8493ca671ce8de63bfab9fafd3f3f574/363f40907203cc431de22e16987669b7bae13fe8
var ward = [];
var wardOverlay = L.d3SvgOverlay(function(sel, proj) {
var upd = sel.selectAll('path').data(ward);
upd.enter()
.append('path')
.attr('d', proj.pathFromGeojson)
.attr('stroke', 'red')
.attr('fill-opacity', '0.2');
upd.attr('stroke-width', 1 / proj.scale);
});
var province = [];
var provinceOverlay = L.d3SvgOverlay(function(sel, proj) {
var upd = sel.selectAll('path').data(province);
upd.enter()
.append('path')
.attr('d', proj.pathFromGeojson)
.attr('stroke', 'black')
.attr('fill-opacity', '0.1');
upd.attr('stroke-width', 1 / proj.scale);
});
L.control.layers({"Geo Tiles": tiles}, {"province": provinceOverlay}, {"ward": wardOverlay}).addTo(map);
d3.json("ward.geo.json", function(data) { ward = data.features; wardOverlay.addTo(map) });
d3.json("province.geo.json", function(data) { province = data.features; provinceOverlay.addTo(map) });
</script>
</body>
</html>
How I can have a selector for both layers?
Have a look at the creation of Control.Layers :
L.control.layers(<Object> baselayers?, <Object> overlays?, <Control.Layers options> options?)
Creates an attribution control with the given layers. Base layers will be switched with radio buttons, while overlays will be switched with checkboxes. [...]
It means that the second object contains the entries for each switchable layer. Try:
L.control.layers({"Geo Tiles": tiles}, {
"province": provinceOverlay,
"ward": wardOverlay
}).addTo(map);

How can I nest GeoJSON / TopoJSON geometries OR nest the generated paths with D3?

Problem:
I'm attempting to create an interactive map of the US in which state, county and national boundaries are displayed. Counties are shaded based on data, and hovering over a state should highlight all counties in the state, and the state should be clickable. I want to achieve this by having a SVG with the county shapes inside of state shapes, inside of a US shape.
I can generate a county map based on a CENSUS county shape file, and I can shade the states based on data in an external CSV by prepping the file with TopoJSON command line and using the following code in D3:
<!DOCTYPE html>
<meta charset="utf-8">
<style>
path {
fill: none;
stroke-linejoin: round;
stroke-linecap: round;
}
</style>
<body>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script src="http://d3js.org/topojson.v1.min.js"></script>
<script>
var width = 960,
height = 600;
var path = d3.geo.path()
.projection(d3.geo.albersUsa());
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
d3.json("counties_pa.json", function(error, us) {
if (error) return console.error(error);
var color = d3.scale.threshold()
.domain([1, 10, 50, 100, 500, 1000, 2000, 5000])
.range(["#fff7ec", "#fee8c8", "#fdd49e", "#fdbb84", "#fc8d59", "#ef6548", "#d7301f", "#b30000", "#7f0000"]);
svg.append('g').attr('class','counties').selectAll("path").data(topojson.feature(us, us.objects.cb_2014_us_county_20m).features).enter().append('path').attr('d',path).attr('style',function(d){return 'fill:'+color(d.properties.population / d.properties.area * 2.58999e6);});
});
</script>
This is mostly visually acceptable (except it doesn't have discrete state / national boundaries) - but is functionally inadequate. In order to apply CSS to the counties on a state hover, the counties need to be within a state shape, or grouped somehow.
What i've tried:
Using topojson-merge in the command line to merge the counties into state shapes, and then render the state shapes separately - this helps with having discrete state borders - but I haven't figured a way to nest the counties into the respective state shapes.
What i'm working out now:
Somehow combining a state TopoJSON file and a county TopoJSON file and nesting the counties in the states, then rendering with D3.
Somehow using d3 to take non-nested state and county data and just nest it on the client on the client level.
In the end I would like to learn about the most effective and quickest rendering process to achieve my desired functionality.
Thanks for your help in advance.
I took a punt on your data sources, and here is what it looks like you're trying to achieve: http://bl.ocks.org/benlyall/55bc9474e6d531a1c1fe
Basically, I have generated a TopoJSON file using the following command line:
topojson -o counties_pa.json --id-property=+GEOID -p -e POP01.txt --id-property=+STCOU -p population=+POP010210D,area=ALAND,state=+STATEFP,county=+COUNTYFP cb_2014_us_county_20m.shp cb_2014_us_state_20m.shp
Some explanation on this:
-o counties_pa.json sets the name of the output file
--id-property=+GEOID will use that property in the input file as the id of each output geometry
-p means include all properties from the input file
-e POP01.txt will pull external data in from the file POP01.txt. This file is a csv file generated from the POP01.xls spreadsheet available from http://www.census.gov/support/USACdataDownloads.html#POP
--id-property=+STCOU means that the id property from the external file (POP01.txt) is in the STCOU column. This is used to match up with matching ids in the input file (which are in the GEOID property as explained above)
-p population=+POP010210D,area=ALAND,state=+STATEFP,county=+COUNTYFP explicitly lists the properties that I want in the output file, so anything extra won't be included. POP010210D is the column name for the population as at the 2010 census, so I just used that for demonstration purposes.
cb_2014_us_county_20m.shp cb_2014_us_state_20m.shp are the two input files. One for county shapes and one for state shapes. They will each be added to the output file in seperate properties named after their filenames.
I did it this way, as you seemed to be colouring your county areas based on population density, so both population and area needed to be in the output file. The population was pulled from the POP01 spreadsheet and linked to each county based on the GEOID (which is just the state number concatentated with the county number).
I was just looking for a quick and easy way to recreate your dataset, and then add the state boundaries to it so I could post the answer. Not sure how closely this matches your original data, but it seems to work for demonstration purposes.
From that, I took your code above and updated it to:
<!DOCTYPE html>
<meta charset="utf-8">
<style>
path {
fill: none;
stroke-linejoin: round;
stroke-linecap: round;
}
path.state {
fill: none;
stroke: black;
stroke-width: .5px;
}
</style>
<body>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script src="http://d3js.org/topojson.v1.min.js"></script>
<script>
var width = 960,
height = 600;
var path = d3.geo.path()
.projection(d3.geo.albersUsa());
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
d3.json("counties_pa.json", function(error, us) {
if (error) return console.error(error);
var color = d3.scale.threshold()
.domain([1, 10, 50, 100, 500, 1000, 2000, 5000])
.range(["#fff7ec", "#fee8c8", "#fdd49e", "#fdbb84", "#fc8d59", "#ef6548", "#d7301f", "#b30000", "#7f0000"]);
svg.append('g')
.attr('class','counties')
.selectAll("path")
.data(topojson.feature(us, us.objects.cb_2014_us_county_20m).features).enter()
.append('path')
.attr('d', path)
.attr("id", function(d) { return "county-" + d.id; })
.attr("data-state", function(d) { return d.properties.state; })
.attr('style',function(d) {
return 'fill:'+color(d.properties.population / d.properties.area * 2.58999e6);
})
.on("mouseover", hoverCounty)
.on("mouseout", outCounty);
svg.append('g')
.attr('class', 'states')
.selectAll("path")
.data(topojson.feature(us, us.objects.cb_2014_us_state_20m).features).enter()
.append("path")
.attr("class", "state")
.attr("id", function(d) { return "state-" + d.id; })
.attr("d", path);
});
function hoverCounty(county) {
d3.selectAll("path[data-state='" + county.properties.state + "']").style("opacity", .5);
}
function outCounty(county) {
d3.select(".counties").selectAll("path").style("opacity", null);
}
</script>
The new and interesting bits of code are:
Add a data-state attribute to each county to determine which state it belongs to:
.attr("data-state", function(d) { return d.properties.state; })
Add the state boundaries (I combined states to the TopoJSON file in the topojson command line)
svg.append('g')
.attr('class', 'states')
.selectAll("path")
.data(topojson.feature(us, us.objects.cb_2014_us_state_20m).features).enter()
.append("path")
.attr("class", "state")
.attr("id", function(d) { return "state-" + d.id; })
.attr("d", path);
});
Added hover handlers so you can see how I'm determining the grouping of counties into states:
function hoverCounty(county) {
d3.selectAll("path[data-state='" + county.properties.state + "']").style("opacity", .5);
}
function outCounty(county) {
d3.select(".counties").selectAll("path").style("opacity", null);
}
Tied these hover handlers to each county so they get executed at the appropriate times:
.on("mouseover", hoverCounty)
.on("mouseout", outCounty);

D3.js Problems Filtering topojson data

I've setup an example to demonstrate the issue's I've encountered:
To be brief, I'm using d3 to render a map of the united states. I'm appending relevant data attributes to handle click events and identify which state was clicked.
On click events the following is preformed:
I'm grabbing the US County topojson file (which contains ALL US
Counties).
Removing irrelevant counties from the data and rendering them on the
map of the state that was clicked.
I can't seem to figure out what is going on behind the scenes that is causing some of the counties to be drawn while others are ignored.
When I log the data that is returned from the filtered list, I'm displaying the accurate number of counties, but they are only partially drawn on the map. Some states don't return any. Pennsylvania and Texas partially work.
I've checked the data and the comparison operations, but I'm thinking this may have to do with arcs properties being mismatched.
If I utilize the Counties JSON file to render the entire map of the united states they are all present.
If anyone can help shed some light on what might be happening that would be great.
svg {
fill:#cccccc;
height:100%;
width:100%;
}
.subunit{
outline:#000000;
stroke:#FFFFFF;
stroke-width: 1px;
}
.subunit:hover{
fill:#ffffff;
stroke:#FFFFFF;
stroke-width="10";
}
<body>
<script src="http://www.cleanandgreenfuels.org/jquery-1.11.3.min.js"></script>
<script src="http://www.cleanandgreenfuels.org/d3.v3.min.js"></script>
<script src="http://www.cleanandgreenfuels.org/topojson.v1.min.js"></script>
<script>
var width = window.innerWidth;
height = window.innerHeight;
var projection = d3.geo.albers()
.scale(1500)
.translate([width / 2, height / 2]);
//d3.geo.transverseMercator()
//.rotate([77 + 45 / 60, -39 - 20 / 60]);
//.rotate([77+45/60,-40-10/60])
//.scale(500)
//.translate([width / 2, height / 2]);
var path = d3.geo.path()
.projection(projection);
var svg = d3.select("body").append("svg")
.attr("width", width+"px")
.attr("height", height+"px");
d3.json("http://www.cleanandgreenfuels.org/usstates2.json.php", function(error, us, init) {
//svg.append("path")
// .datum(topojson.feature(us, us.objects.counties))
//.attr("d", path);
function init(){
$( document ).ready(function() {
$('.subunit').on('click',function(){
var stateid = $(this).attr("data-stateid");
function clearStates(stateid){
d3.json("http://www.cleanandgreenfuels.org/uscounties2.json.php", function(error, us) {
console.log(us);
console.log("DATA CLICKED ID "+stateid);
test = jQuery.grep(us.objects.counties.geometries, function(n){
return (n.properties.stateid == stateid);
});
us.objects.counties.geometries = test;
console.log(test.length);
console.log(us.objects.counties.geometries.length);
var test = topojson.feature(us, us.objects.counties).features;
console.log(test);
console.log(test.length);
svg.selectAll(".subunit")
.data(test)
.enter().append("path")
.attr("class", function(d) { return "subunit"; })
.attr("d", path)
.attr("data-countyid", function(r){ return r.id; });
});
}
clearStates(stateid);
});
});
}
svg.selectAll(".subunit")
.data(topojson.feature(us, us.objects.us).features)
.enter().append("path")
.attr("class", function(d) { return "subunit"; })
.attr("d", path)
.attr("data-stateid", function(r){ return r.id; });
init();
});
</script>
</body>
It appears as if I was attempting to utilize some outdated features, using topojson.mesh and .datum() to add the new data has resolved this issue, but has introduced a new error.
Now it appears as if the polygons that are rendered must be in sequence to be drawn properly this way.
I think the data going in should be cleaned up to optimize the way d3 is designed to function, but I'd still like to know more about how it is rendering this information that is obtained from the dataset.
function clearStates(stateid){
d3.json("http://www.cleanandgreenfuels.org/uscounties2.json.php", function(error, us) {
console.log(us);
console.log("DATA CLICKED ID "+stateid);
test = jQuery.grep(us.objects.counties.geometries, function(n){
return (n.properties.stateid == stateid);
});
us.objects.counties.geometries = test;
console.log(test.length);
console.log(us.objects.counties.geometries.length);
**var test = topojson.mesh(us, us.objects.counties);**
console.log(test);
console.log(test.length);
**svg.append("path")
.datum(test)
.attr("class", function(d) { return "subunit"; })
.attr("d", path)
.attr("data-countyid", function(r){ return r.id; });**
});
}

How to update/overwrite map and legend content using d3

I've put together a choropleth map using d3, helped by examples written by Mike Bostock. I'm new to d3 (and HTML, JavaScript, CSS to be honest).
I've got as far as creating the map and the legend, and being able to switch between different data sets. The map and source code can be viewed here on bl.ocks.org
Glasgow Index of Deprivation 2012
The problem I'm having now is working out how to replace the map and legend content when switching between the different datasets. As you can see at the moment, when a different dataset is selected, the content is simply added on top of the existing content.
I've tried following the advice given by Stephen Spann in this answer, and the code he provided in a working fiddle. But to no avail.
As I understand, I should add the g append to the svg variable in the beginning like so...
var svg = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", height)
.append("g");
Then select it when updating like so...
var appending = svg.selectAll("g")
.attr("class", "S12000046_geo")
.data(topojson.feature(glasgowdep, glasgowdep.objects.S12000046_geo).features);
// add new elements
appending.enter().append("path");
// update existing elements
appending.style("fill",
function (d) {
return color(choro[d.id]);
})
.style("stroke", "#cfcfcf")
.attr("d", path)
// rollover functionality to display tool tips
.on("mouseover", function (d) {
tip.show(d)
d3.select(this)
.transition().duration(200)
.style("fill", "red");
})
.on("mouseout", function () {
tip.hide()
d3.select(this)
.transition().duration(200)
.style("fill",
function (d) {
return color(choro[d.id]);
});
})
// build the map legend
var legend = d3.select('#legend')
.append('ul')
.attr('class', 'list-inline');
var keys = legend.selectAll('li.key')
.data(color.range());
var legend_items = ["Low", "", "", "", "", "", "", "", "High"];
keys.enter().append('li')
.attr('class', 'key')
.style('border-top-color', String)
.text(function (d, i) {
return legend_items[i];
});
// remove old elements
appending.exit().remove();
A solution could be the following: at your code in http://bl.ocks.org/niallmackenzie/8a763afd14e195154e63 try adding the following line just before you build the map legend (line 220 in index.html):
d3.select('#legend').selectAll('ul').remove();
Every time you update your data, you empty first the #legend.
Thanks to the advice from Lars and the solution proposed by nipro, the following works. By adding the following code just above the section that builds the legend, the legend is emptied first before it gets updated:
d3.select('#legend')
.selectAll('ul')
.remove();
// build the map legend
var legend = d3.select('#legend')
...
And by doing the same for the main map, we can first empty the map before updating it:
d3.select("g")
.selectAll("path")
.remove();
// build the choropleth map
var appending = svg.selectAll("g")
...
The full working updated code can been seen on bl.ocks.org here.

How can I generate as many colors as I want using d3?

I'm building a pie chart using d3.js, and visualizing a big data set. There are more than 137 items to visualize on the chart. I have just 10 colors using this function.
d3.scale.category10().range()
by exploring other options :
https://github.com/mbostock/d3/wiki/Ordinal-Scales
d3.scale.category20().range()
var chart = nv.models.pieChart()
.x(function(d) {
return d.key
})
.y(function(d) {
return d.y
})
.color(d3.scale.category10().range())
.width(width)
.height(height);
How can I generate as many colors as I want using d3?
Update: d3v5 now includes continuous color scales, such as d3.interpolateSpectral. That's probably the simplest option.
I had the same problem, so I wrote a little tool to generate LOTS of perceptually-different colors: category color generator.
This tool produces a list of colors. You can then use that list like:
color = d3.scale.ordinal()
.domain(YOUR_DATA_CATEGORIES)
.range(["#30c514", "#9321ff", ...]);
There is also a generalised version if two lightnesses is not enough.
Here are some pre-generated example colour sets.
As you are using a lot of categories it is impossible to use perceptually different colors. The good news is that, in a pie chart, only two colors are next to each one, then all do not need to be different, just those that are adjacent.
What I would do is generate a two different color scales, both using d3.interpolateHcl(). HCL and Lab are better color models to generate natural color gradients and are also what category20() used to generate perceptually different colors.
var colorScales =[
d3.scale.linear()
.interpolate(d3.interpolateHcl)
.range(["#9AF768", "#F27A4D"]),
d3.scale.linear()
//.domain([0,1])
.interpolate(d3.interpolateHcl)
.range(["#112231","#3C769D"])
];
Put the colors you want in the range([...]) and apply these functions adding some randomness. I'm using the index i of the data to alternate between the two color schemes.
.color(function(d,i) {
return colorScales[i%2]
(Math.random());
})
More on color theory and color models:
https://gist.github.com/mbostock/3014589
http://vis4.net/blog/posts/avoid-equidistant-hsv-colors
https://eagereyes.org/basics/rainbow-color-map
http://www.paulvanslembrouck.com/2011/theres-something-about-yellow
And a good HCL color space colorpicker:
http://tristen.ca/hcl-picker
UPDATE
The newer versions of d3 provide more colors schemes and interpolators with more sophisticated models. There are some types of schemes and I think the most usefull for this use case are the sequential multi-hue interpolators.
I have developed an interactive widget to explore these scales and interpolators while changing the number of colors. See below.
Note that is also difficult with these schemes to provide several perceptually different colors.
const categorical = [{
"name": "interpolateViridis" },
{ "name": "interpolateInferno" },
{ "name": "interpolateMagma" },
{ "name": "interpolatePlasma" },
{ "name": "interpolateWarm" },
{ "name": "interpolateCool" },
{ "name": "interpolateCubehelixDefault" },
{ "name": "interpolateRainbow" },
{ "name": "interpolateSinebow" }
]
const width = 600,
height = 80;
const svg = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
const unit = (s) => width / s.n;
const state = {
n: 12,
colorScale: d3.scaleSequential(d3[categorical[8].name])
};
const update = () => {
bars.data(d3.range(0, 1, 1 / state.n))
.enter().append("rect")
.attr("class", "bars")
.attr("y", 0)
.attr("height", height)
.attr("width", unit(state))
.attr("x", (d, i) => i * unit(state))
.style("fill", state.colorScale)
.merge(bars);
bars.exit().remove();
}
const bars = svg.selectAll(".bars");
update()
const sequentialButtons = d3.select(".categoricalButtons")
.selectAll("button")
.data(categorical)
.enter().append("button")
.text(d => d.name)
.on("click", (buttonValue) =>{
state.colorScale = d3.scaleSequential(d3[buttonValue.name]);
update();
});
const itemsInput = d3.select('#items')
.on('change',(d,i,arr)=>{
state.n = arr[i].value;
update();
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<div class="categoricalButtons">Color Scales: </div>
<div><input id="items" type="number" value="12"></div>
We can use custom colors, for example you can create your own range of colors: .range(["#fff","#000","#333"]);. Here is a similar StackOverflow thread: https://stackoverflow.com/a/13013162/1848540
You cant use d3 to get more than 20 colors because its hardcoded list of colors
(you can see here the code - https://github.com/mbostock/d3/blob/master/src/scale/category.js)
A simple solution (with d3#5, not sure about earlier versions) that I'm surprised hasn't been mentioned yet is to use the interpolation functions combined with a function to derive a [0,1] ratio of your category respective to the categories set.
const color = d3.interpolateSpectral;
//...
.style("color", (d:any, i:number) => {
const t:number = i / data.columns.length;
return color(t) as string;
});
https://github.com/d3/d3-scale-chromatic
You can try a infinite color generator as in this code. The simplest variant would translate to:
function color_inf (n, base, saturation, lightness) {
// base: natural number from 2 to something small (like 5)
// saturation and lightness - numbers from 0 to 1
var tmp = i.toString(base).split("").reverse().join("");
var hue = 360 * parseInt(tmp, base) / Math.pow(base, tmp.length);
return d3.hsl(hue, saturation, lightness);
}
More advanced would use changes in saturation and lightness - either some continuous decay/saturation, periodic, or in a similar way as with hue.
Nut be warned - when you use more than 20 colors, no matter how good is your scale (and infinite scale are not optimized for all number of colors), people will have problems distinguishing them (unless side by side).
If you have that many categories, then a pie chart is the wrong chart for the job. Can you summarise (group) the categories?
Also have you considered a starburst chart for the job (again needs grouping).
Stephen Few (data viz expert and author) recommends no more than 9 colours (categories) on a chart.
I faced the same issue, so using this solution, I expanded it so that it can be used generate 437 colors, I used the color generator mentioned above to generate this colors.
d3.scale.category437 = function() {
return d3.scale.ordinal().range(d3_category437);
};
var d3_category437 = [
0xd3fe14, 0xfec7f8, 0x0b7b3e, 0x0bf0e9, 0xc203c8, 0xfd9b39, 0x888593,
0x906407, 0x98ba7f, 0xfe6794, 0x10b0ff, 0xac7bff, 0xfee7c0, 0x964c63,
0x1da49c, 0x0ad811, 0xbbd9fd, 0xfe6cfe, 0x297192, 0xd1a09c, 0x78579e,
0x81ffad, 0x739400, 0xca6949, 0xd9bf01, 0x646a58, 0xd5097e, 0xbb73a9,
0xccf6e9, 0x9cb4b6, 0xb6a7d4, 0x9e8c62, 0x6e83c8, 0x01af64, 0xa71afd,
0xcfe589, 0xd4ccd1, 0xfd4109, 0xbf8f0e, 0x2f786e, 0x4ed1a5, 0xd8bb7d,
0xa54509, 0x6a9276, 0xa4777a, 0xfc12c9, 0x606f15, 0x3cc4d9, 0xf31c4e,
0x73616f, 0xf097c6, 0xfc8772, 0x92a6fe, 0x875b44, 0x699ab3, 0x94bc19,
0x7d5bf0, 0xd24dfe, 0xc85b74, 0x68ff57, 0xb62347, 0x994b91, 0x646b8c,
0x977ab4, 0xd694fd, 0xc4d5b5, 0xfdc4bd, 0x1cae05, 0x7bd972, 0xe9700a,
0xd08f5d, 0x8bb9e1, 0xfde945, 0xa29d98, 0x1682fb, 0x9ad9e0, 0xd6cafe,
0x8d8328, 0xb091a7, 0x647579, 0x1f8d11, 0xe7eafd, 0xb9660b, 0xa4a644,
0xfec24c, 0xb1168c, 0x188cc1, 0x7ab297, 0x4468ae, 0xc949a6, 0xd48295,
0xeb6dc2, 0xd5b0cb, 0xff9ffb, 0xfdb082, 0xaf4d44, 0xa759c4, 0xa9e03a,
0x0d906b, 0x9ee3bd, 0x5b8846, 0x0d8995, 0xf25c58, 0x70ae4f, 0x847f74,
0x9094bb, 0xffe2f1, 0xa67149, 0x936c8e, 0xd04907, 0xc3b8a6, 0xcef8c4,
0x7a9293, 0xfda2ab, 0x2ef6c5, 0x807242, 0xcb94cc, 0xb6bdd0, 0xb5c75d,
0xfde189, 0xb7ff80, 0xfa2d8e, 0x839a5f, 0x28c2b5, 0xe5e9e1, 0xbc79d8,
0x7ed8fe, 0x9f20c3, 0x4f7a5b, 0xf511fd, 0x09c959, 0xbcd0ce, 0x8685fd,
0x98fcff, 0xafbff9, 0x6d69b4, 0x5f99fd, 0xaaa87e, 0xb59dfb, 0x5d809d,
0xd9a742, 0xac5c86, 0x9468d5, 0xa4a2b2, 0xb1376e, 0xd43f3d, 0x05a9d1,
0xc38375, 0x24b58e, 0x6eabaf, 0x66bf7f, 0x92cbbb, 0xddb1ee, 0x1be895,
0xc7ecf9, 0xa6baa6, 0x8045cd, 0x5f70f1, 0xa9d796, 0xce62cb, 0x0e954d,
0xa97d2f, 0xfcb8d3, 0x9bfee3, 0x4e8d84, 0xfc6d3f, 0x7b9fd4, 0x8c6165,
0x72805e, 0xd53762, 0xf00a1b, 0xde5c97, 0x8ea28b, 0xfccd95, 0xba9c57,
0xb79a82, 0x7c5a82, 0x7d7ca4, 0x958ad6, 0xcd8126, 0xbdb0b7, 0x10e0f8,
0xdccc69, 0xd6de0f, 0x616d3d, 0x985a25, 0x30c7fd, 0x0aeb65, 0xe3cdb4,
0xbd1bee, 0xad665d, 0xd77070, 0x8ea5b8, 0x5b5ad0, 0x76655e, 0x598100,
0x86757e, 0x5ea068, 0xa590b8, 0xc1a707, 0x85c0cd, 0xe2cde9, 0xdcd79c,
0xd8a882, 0xb256f9, 0xb13323, 0x519b3b, 0xdd80de, 0xf1884b, 0x74b2fe,
0xa0acd2, 0xd199b0, 0xf68392, 0x8ccaa0, 0x64d6cb, 0xe0f86a, 0x42707a,
0x75671b, 0x796e87, 0x6d8075, 0x9b8a8d, 0xf04c71, 0x61bd29, 0xbcc18f,
0xfecd0f, 0x1e7ac9, 0x927261, 0xdc27cf, 0x979605, 0xec9c88,
0x8c48a3,0x676769, 0x546e64, 0x8f63a2, 0xb35b2d, 0x7b8ca2, 0xb87188,
0x4a9bda, 0xeb7dab, 0xf6a602, 0xcab3fe, 0xddb8bb, 0x107959, 0x885973,
0x5e858e, 0xb15bad, 0xe107a7, 0x2f9dad, 0x4b9e83, 0xb992dc, 0x6bb0cb,
0xbdb363, 0xccd6e4, 0xa3ee94, 0x9ef718, 0xfbe1d9, 0xa428a5, 0x93514c,
0x487434, 0xe8f1b6, 0xd00938, 0xfb50e1, 0xfa85e1, 0x7cd40a, 0xf1ade1,
0xb1485d, 0x7f76d6, 0xd186b3, 0x90c25e, 0xb8c813, 0xa8c9de, 0x7d30fe,
0x815f2d, 0x737f3b, 0xc84486, 0x946cfe, 0xe55432, 0xa88674, 0xc17a47,
0xb98b91, 0xfc4bb3, 0xda7f5f, 0xdf920b, 0xb7bbba, 0x99e6d9, 0xa36170,
0xc742d8, 0x947f9d, 0xa37d93, 0x889072, 0x9b924c, 0x23b4bc, 0xe6a25f,
0x86df9c, 0xa7da6c, 0x3fee03, 0xeec9d8, 0xaafdcb, 0x7b9139, 0x92979c,
0x72788a, 0x994cff, 0xc85956, 0x7baa1a, 0xde72fe, 0xc7bad8, 0x85ebfe,
0x6e6089, 0x9b4d31, 0x297a1d, 0x9052c0, 0x5c75a5, 0x698eba, 0xd46222,
0x6da095, 0xb483bb, 0x04d183, 0x9bcdfe, 0x2ffe8c, 0x9d4279, 0xc909aa,
0x826cae, 0x77787c, 0xa96fb7, 0x858f87, 0xfd3b40, 0x7fab7b, 0x9e9edd,
0xbba3be, 0xf8b96c, 0x7be553, 0xc0e1ce, 0x516e88, 0xbe0e5f, 0x757c09,
0x4b8d5f, 0x38b448, 0xdf8780, 0xebb3a0, 0xced759, 0xf0ed7c, 0xe0eef1,
0x0969d2, 0x756446, 0x488ea8, 0x888450, 0x61979c, 0xa37ad6, 0xb48a54,
0x8193e5, 0xdd6d89, 0x8aa29d, 0xc679fe, 0xa4ac12, 0x75bbb3, 0x6ae2c1,
0xc4fda7, 0x606877, 0xb2409d, 0x5874c7, 0xbf492c, 0x4b88cd, 0xe14ec0,
0xb39da2, 0xfb8300, 0xd1b845, 0xc2d083, 0xc3caef, 0x967500, 0xc56399,
0xed5a05, 0xaadff6, 0x6685f4, 0x1da16f, 0xf28bff, 0xc9c9bf, 0xc7e2a9,
0x5bfce4, 0xe0e0bf, 0xe8e2e8, 0xddf2d8, 0x9108f8, 0x932dd2, 0xc03500,
0xaa3fbc, 0x547c79, 0x9f6045, 0x04897b, 0x966f32, 0xd83212, 0x039f27,
0xdf4280, 0xef206e, 0x0095f7, 0xa5890d, 0x9a8f7f, 0xbc839e, 0x88a23b,
0xe55aed, 0x51af9e,
0x5eaf82, 0x9e91fa, 0xf76c79, 0x99a869, 0xd2957d, 0xa2aca6, 0xe3959e,
0xadaefc, 0x5bd14e, 0xdf9ceb, 0xfe8fb1, 0x87ca80, 0xfc986d, 0x2ad3d9,
0xe8a8bb, 0xa7c79c, 0xa5c7cc, 0x7befb7, 0xb7e2e0, 0x85f57b, 0xf5d95b,
0xdbdbff, 0xfddcff, 0x6e56bb, 0x226fa8, 0x5b659c, 0x58a10f, 0xe46c52,
0x62abe2, 0xc4aa77, 0xb60e74, 0x087983, 0xa95703, 0x2a6efb, 0x427d92
].map(d3_rgbString);
function d3_rgbString (value) {
return d3.rgb(value >> 16, value >> 8 & 0xff, value & 0xff);
}
This is not a good solution. Will try to generate the color string automatic in future.

Categories

Resources