xxxxxxxxxx
<html lang="en">
<head>
<meta charset="utf-8">
<title>Choropleth of income in France</title>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.6/d3.min.js"></script>
<script src="https://d3js.org/topojson.v1.min.js"></script>
<style type="text/css">
body {
background-color:#000000;
font-family: sans-serif;
color: white;
margin: 5px;
}
.overlay {
opacity : 0;
}
svg {
background-color:#3e3e3e;
border-right : 1px solid black;
margin-top: 8px;
cursor: pointer;
width: 100%;
}
h1 {
margin-top: 0;
font-family: serif;
}
h2 {
font-size: 100%;
}
#texte {
position:absolute;
right: 0px;
width: 500px;
height: 93%;
background-image: url("bckgrd.png");
padding-left: 30px;
padding-top:30px;
}
#map_title {
background-color: rgba(0, 0, 0, 0.5);
font-size : 80%;
position: absolute;
padding : 10px;
}
#map_title span{
font-style: italic;
}
#details {
background-color:#000000;
width: 100%;
}
.legend {
font-size:70%;
user-select: none;
fill: white;
}
#legendemere, #titremere{
fill: #000000;
opacity: 0.5;
}
.departement {
stroke: grey;
}
#texttag{
fill : black;
font-style: bold;
pointer-events: none;
}
.ville{
pointer-events: none;
}
</style>
</head>
<body>
<div id="texte">
<h1>Where high-income households live in metropolitan France</h1>
<p>This choropleth map shows all the "départements" (and communes if you zoom in) of metropolitan France. The colors are mapped to the disposable income per consumption unit (CU, i.e. per capita adjusted for household size and age composition) in each département or commune.</p>
<p>Communes for which data was not available are in grey.</p>
<p>Double-click to zoom in, hold shift and double-click to zoom out. Drag the map to move it around.</p>
<br><br>
<h2>Details (hover over a commune) :</h2>
<div id="details">
<table>
<tbody>
<tr>
<td>Commune : </td>
<td id="detail_name"></td>
</tr>
<!--<tr>
<td>Region : </td>
<td id="detail_region"></td>
</tr>-->
<tr>
<td>Departement : </td>
<td id="detail_departement"></td>
</tr>
<tr>
<td>Disposable income per CU : </td>
<td id="detail_income"></td>
</tr>
</tbody>
</table>
</div>
</div>
<div id="carte"></div>
<div id="map_title">
Disposable income per consumption unit in the communes of France<br>
<span>Source : <a href="https://www.insee.fr/fr/bases-de-donnees/default.asp?page=statistiques-locales/pauvrete.htm">INSEE</a> (data on income) and <a href="https://professionnels.ign.fr/catalogue">IGN</a> (geographic data).</span>
</div>
<script type="text/javascript">
// A FAIRE :
// - zoom
// - donnes (dept vs commune)
// - problème des contours
// Add the French locale (and format) for displaying income
var loc = d3.locale({
decimal: ",",
thousands: " ",
grouping: [3],
currency: ["", " €"],
dateTime: "%A, le %e %B %Y, %X",
date: "%d/%m/%Y",
time: "%H:%M:%S",
periods: ["AM", "PM"], // unused
days: ["dimanche", "lundi", "mardi", "mercredi", "jeudi", "vendredi", "samedi"],
shortDays: ["dim.", "lun.", "mar.", "mer.", "jeu.", "ven.", "sam."],
months: ["janvier", "février", "mars", "avril", "mai", "juin", "juillet", "août", "septembre", "octobre", "novembre", "décembre"],
shortMonths: ["janv.", "févr.", "mars", "avr.", "mai", "juin", "juil.", "août", "sept.", "oct.", "nov.", "déc."]
});
var f = loc.numberFormat("$,.2f");
//Width and height
var w = 1100;
var h = 600;
var villesFont = 14, villesStroke = 0.5, deptStroke = 0.8;
var tagFont = 18;
var domaineVilles = [8802, 45463];
var domaineDept = [];
// For mouse position (for the tag)
var coord = [], scaling = 1;
var communes = {};
var listDept = {'94': 'Val-de-Marne', '04': 'Alpes-de-Haute-Provence', '77': 'Seine-et-Marne', '71': 'Saône-et-Loire', '65': 'Hautes-Pyrénées', '24': 'Dordogne', '92': 'Hauts-de-Seine', '82': 'Tarn-et-Garonne', '45': 'Loiret', '17': 'Charente-Maritime', '58': 'Nièvre', '28': 'Eure-et-Loir', '44': 'Loire-Atlantique', '30': 'Gard', '60': 'Oise', '52': 'Haute-Marne', '86': 'Vienne', '29': 'Finistère', '70': 'Haute-Saône', '91': 'Essonne', '16': 'Charente', '59': 'Nord', '35': 'Ille-et-Vilaine', '53': 'Mayenne', '88': 'Vosges', '27': 'Eure', '25': 'Doubs', '84': 'Vaucluse', '43': 'Haute-Loire', '89': 'Yonne', '13': 'Bouches-du-Rhône', '90': 'Territoire de Belfort', '23': 'Creuse', '50': 'Manche', '67': 'Bas-Rhin', '19': 'Corrèze', '39': 'Jura', '76': 'Seine-Maritime', '11': 'Aude', '83': 'Var', '78': 'Yvelines', '93': 'Seine-Saint-Denis', '61': 'Orne', '63': 'Puy-de-Dôme', '54': 'Meurthe-et-Moselle', '02': 'Aisne', '62': 'Pas-de-Calais', '41': 'Loir-et-Cher', '51': 'Marne', '40': 'Landes', '85': 'Vendée', '26': 'Drôme', '2A': 'Corse-du-Sud', '72': 'Sarthe', '36': 'Indre', '22': "Côtes-d'Armor", '81': 'Tarn', '03': 'Allier', '87': 'Haute-Vienne', '74': 'Haute-Savoie', '42': 'Loire', '38': 'Isère', '06': 'Alpes-Maritimes', '73': 'Savoie', '95': "Val-d'Oise", '37': 'Indre-et-Loire', '80': 'Somme', '33': 'Gironde', '05': 'Hautes-Alpes', '56': 'Morbihan', '64': 'Pyrénées-Atlantiques', '66': 'Pyrénées-Orientales', '07': 'Ardèche', '08': 'Ardennes', '34': 'Hérault', '79': 'Deux-Sèvres', '31': 'Haute-Garonne', '55': 'Meuse', '10': 'Aube', '48': 'Lozère', '21': "Côte-d'Or", '12': 'Aveyron', '2B': 'Haute-Corse', '46': 'Lot', '18': 'Cher', '69': 'Rhône', '14': 'Calvados', '15': 'Cantal', '09': 'Ariège', '68': 'Haut-Rhin', '32': 'Gers', '49': 'Maine-et-Loire', '47': 'Lot-et-Garonne', '57': 'Moselle', '01': 'Ain', '75': 'Paris'};
//Define map projection
var projection = d3.geo.mercator()
.center([ 3, 46.5 ])
.translate([ w/2, h/2 ])
.scale([ w*2 ]);
//Define path generator
var path = d3.geo.path()
.projection(projection);
//Define quantize scale to sort data values into buckets of color
//Colors by Cynthia Brewer (colorbrewer2.org), YlOrRd
var colorDept = d3.scale.quantize()
.range([ "#ffffb2", "#fecc5c", "#fd8d3c", "#f03b20", "#bd0026" ]);
var colorVille = d3.scale.quantize()
.range([ "#ffffb2", "#fecc5c", "#fd8d3c", "#f03b20", "#bd0026" ]);
var villScale = d3.scale.quantile()
.range([1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20]);
//Create SVG
var svg = d3.select("#carte")
.append("svg")
//.attr("width", w)
.attr("height", h)
//.attr("viewBox", "0 0 " + w + " " + h )
//.attr("preserveAspectRatio","None")
.append("g").attr("id","super") // important for zoom
.call(d3.behavior.zoom().scaleExtent([1, 20]).on("zoom", zoom).on("zoomend",zoomfinish))
.append("g").attr("id","maingroup"); // important for the same reason (??)
// Overlay for the zoom and pan behaviour
svg.append("rect")
.attr("class", "overlay")
.attr("width", w)
.attr("height", h);
// To show that map is loading...
svg.append("text")
.attr("x",w/2)
.attr("y",h/2)
.attr("text-anchor","middle")
.text("Loading map...");
var legende = d3.select("#super").append("g");
var don = [{"name":"e","color":"#bd0026"},
{"name":"d","color":"#f03b20"},
{"name":"c","color":"#fd8d3c"},
{"name":"b","color":"#fecc5c"},
{"name":"a","color":"#ffffb2"},
];
legende.append("rect")
.attr("id","legendemere")
.attr("width",160)
.attr("height",90)
.attr("x",0)
.attr("y",0);
legende.selectAll(".legendbar")
.data(don)
.enter()
.append("rect")
.attr("class","legendbar")
.attr("width",30)
.attr("height",10)
.attr("x",120)
.attr("y",function(d,i){return 10 + 15*i;})
.attr("fill",function(d){return d.color;});
legende.append("text")
.attr("class","legend")
.attr("x",10)
.attr("y",20)
.text("Higher per CU income");
legende.append("text")
.attr("class","legend")
.attr("x",10)
.attr("y",80)
.text("Lower per CU income");
// For the map's title
d3.select("#map_title").style("top",(h - 35).toString() + "px");
//Load in GeoJSON data
d3.json("donnees_depart.json", function(json) {
domaineDept = [d3.min(json.features, function(d) { return +d.properties.income; }),
d3.max(json.features, function(d) { return +d.properties.income; })
];
colorDept.domain(domaineDept);
colorVille.domain(domaineVilles);
d3.json("donnees_villes.json", function(datVilles){
/*villScale.domain([d3.min(datVilles, function(d){return +d.properties.population;}) - 100,
d3.max(datVilles, function(d){return +d.properties.population;}) + 100]);*/
var dom = [];
for (var i= 0; i < datVilles.length; i++){
dom.push(datVilles[i].properties.population);
}
villScale.domain(dom);
//Bind data and create one path per GeoJSON feature
svg.selectAll(".departement")
.data(json.features)
.enter()
.append("path")
.attr("d", path)
.attr("class","departement")
.attr("id", function(d){
if (d.properties.CODE_DEPT == "69") {
return "d69";
}
})
.style("fill", function(d) {
//Get data value
var value = d.properties.income;
if (value) {
//If value exists…
return colorDept(value);
} else {
//If value is undefined…
return "#cccccc";
}
});
svg.select("text").remove();
communes = svg.append("g");
// Put the main cities on the map
villes = svg.append("g");
villes.selectAll(".ville")
.data(datVilles)
.enter()
.append("text")
.attr("class",function(d){
return "vill" + villScale(d.properties.population).toString();
})
.attr("text-anchor","middle")
.attr("x", function(d){
return path.centroid(d)[0];
})
.attr("y", function(d){
return path.centroid(d)[1];
})
.text(function(d){
return d.properties.commune;
})
.style("fill","black")
.classed("ville",true)
.style("font-size", villesFont + "px")
.style("visibility", function(d){
if (villScale(d.properties.population) >= 20){
return "visible";
}
else {
return "hidden";
}
});
var tag = svg.append("g").attr("id","tag");
tag.append("text").attr("id","texttag");
svg.selectAll("path")
.on("mouseover",function(){
var sel = d3.select(this);
d3.select("#detail_income").html(f(sel.datum().properties.income));
d3.select("#detail_name").html("");
d3.select("#detail_region").html(sel.datum().properties.NAME_1);
d3.select("#detail_departement").html(sel.datum().properties.NOM_DEPT);
d3.select("#texttag")
.attr("opacity",1)
.text(sel.datum().properties.NOM_DEPT)
.attr("x",d3.mouse(this)[0] + 20 / scaling)
.attr("y",d3.mouse(this)[1] - 20 / scaling);
coord = d3.mouse(this);
});
}); // End of json for villes
}); //End d3.json()
var marqueur = 0;
var eventVal = {}, scaleOld = 1;
var currCommunes = {};
var strokeCommune = 0.3;
// To handle the "end" of the zoom transitions (actually only X s after the first zoom event).
function endHandle(){
// Set marqueur back to 0 to let zoom() trigger this again.
marqueur = 0;
// Calculate which department is visible in the current viewport
var toShow = [];
if (eventVal.scale >= 4){
var correctX = eventVal.translate[0] / eventVal.scale,
correctY = eventVal.translate[1] / eventVal.scale;
var limX = w/eventVal.scale,
limY = h/eventVal.scale;
svg.selectAll(".departement")
.each(function(d){
var ident = d.properties.CODE_DEPT;
if ( (d3.max([path.bounds(d)[0][0], path.bounds(d)[1][0]]) + correctX) > 0 &&
(d3.min([path.bounds(d)[0][0], path.bounds(d)[1][0]]) + correctX) < limX &&
(d3.max([path.bounds(d)[0][1], path.bounds(d)[1][1]]) + correctY) > 0 &&
(d3.min([path.bounds(d)[0][1], path.bounds(d)[1][1]]) + correctY) < limY
) {
toShow.push(ident);
if ( !currCommunes[ident] ){
currCommunes[ident] = 'new';
}
else if ( currCommunes[ident] ){
currCommunes[ident] = 'keep';
}
}
else {
if (currCommunes[ident]){
currCommunes[ident] = "delete";
}
}
});
for (var key in currCommunes){
if (currCommunes[key] == 'new'){
loadCommunes(key);
}
else if (currCommunes[key] == 'delete'){
d3.selectAll(".com" + key).remove();
delete currCommunes[key];
}
else if (currCommunes[key] == 'keep'){
svg.selectAll(".com" + key)
.style("stroke-width", (strokeCommune / eventVal.scale).toString() + "px");
}
}
}
else {
d3.selectAll(".commune").remove();
}
d3.selectAll(".ville")
.style("font-size", (villesFont / eventVal.scale).toString() + "px");
// Set visibility of cities (depending on scale)
d3.selectAll(".ville").style("visibility","hidden");
for (var i = 0; i < eventVal.scale; i++){
var num = 20 - i;
d3.selectAll(".vill" + num.toString()).style("visibility","visible");
}
// To stop the timer.
return true;
}
// FOR ZOOM (initially stolen from Mike Bostock's example
function zoom() {
// Part of the hack (sample the value of the event each time in a global var).
eventVal = d3.event;
d3.select("#maingroup").attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")");
d3.select("#texttag")
.style("font-size", (tagFont / d3.event.scale).toString() + "px")
.attr("x",coord[0] + 5/d3.event.scale)
.attr("y",coord[1] - 5/d3.event.scale);
d3.selectAll(".departement").style("stroke-width", (deptStroke / eventVal.scale).toString() + "px");
scaling = d3.event.scale;
}
// Zoomend : load new data only when last zoom event has finished.
function zoomfinish(){
if ((d3.event.sourceEvent == null) || (d3.event.scale == scaleOld)){
endHandle();
}
scaleOld = d3.event.scale;
}
// Cache the loaded data.
var cache = {};
function getData(url, callback, dept){
if(cache[url]){
callback(cache[url], dept);
}
else{
d3.json(url, function(data){
cache[url] = data;
callback(data, dept);
});
}
}
// Callback that actually draws the communes
function drawCommunes(donnees, dept){
communes.selectAll(".commune.com" + dept)
.data(donnees)
.enter()
.append("path")
.attr("d", path)
.attr("class","commune com" + dept)
.style("stroke", "grey")
.style("stroke-width", (strokeCommune / eventVal.scale).toString() + "px")
.style("fill", function(d) {
//Get data value
var value = d.properties.income;
if (value) {
//If value exists…
return colorVille(value);
} else {
//If value is undefined…
return "#cccccc";
}
});
communes.selectAll(".commune")
.on("mouseover",function(){
var sel = d3.select(this);
d3.select("#detail_income").html(f(sel.datum().properties.income));
d3.select("#detail_name").html(sel.datum().properties.NOM_COMM);
d3.select("#detail_region").html(sel.datum().properties.NAME_1);
d3.select("#detail_departement").html(listDept[sel.datum().properties.CODE_DEPT].toUpperCase());
d3.select("#texttag")
.attr("opacity",1)
.text(sel.datum().properties.NOM_COMM)
.attr("x",d3.mouse(this)[0] + 20 / scaling)
.attr("y",d3.mouse(this)[1] - 20 / scaling);
coord = d3.mouse(this);
});
}
// To load data for communes when zoomed enough and dept is in viewport.
function loadCommunes(dept) {
var url = "https://silentway.github.io/data-departements/" + dept + ".json";
getData(url, drawCommunes, dept);
}
</script>
</body>
</html>
Modified http://d3js.org/topojson.v1.min.js to a secure url
https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.6/d3.min.js
https://d3js.org/topojson.v1.min.js