Skip to content

Instantly share code, notes, and snippets.

@ygrenzinger
Last active August 29, 2015 14:27
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ygrenzinger/22a8233549fd9a9e50f0 to your computer and use it in GitHub Desktop.
Save ygrenzinger/22a8233549fd9a9e50f0 to your computer and use it in GitHub Desktop.

Hello,

I have done a lot of hard work to have an acceptable dataviz. D3.js is powerfull but really difficult to tweak when you want a perfect design. Hope you will find it interesting.

I wanted to use a Treemap because I love this dataviz but I should first found a good data for this. The budget data is perfect for this and Treemap is a great layout to have a quick and clear understanding where the budget is spend.

I have finally used the Paris (France) annual budget available here: http://opendata.paris.fr/explore/dataset/budget-votes-ap-autotrisations-de-programmes/?tab=metas

I had first to filter the data to keep only the expenditure then convert the CSV format to a nested JSON using the following code:

      var root = {
        "key": "Budget Paris 2015",
        "values": d3.nest()
          .key(function(d) {
            return d["Mission (AP) - Texte"];
          })
          .key(function(d) {
            return d["Mission-Activité (AP) - Texte"];
          })
          .rollup(function(v) {
            return d3.sum(v, function(d) {
              return +d["Crédits votés (eng)"];
            });
          })
          .entries(data)
      }

I successfully added the zoom / unzoom capability on nested data but also tooltip on block. Hover on any cell to see next level of detail, or click on a cell to zoom in. Click on the top label to zoom out.

Sadly I have not found a better solution that hide the name of block when this name is too long (cliping the text in SVG is very hard to do :( ). The user can use the tooltip mouseover to show information on the block of budget.

{
"name": "Budget Paris 2015",
"children": [{
"name": "RESSOURCES HUMAINES",
"children": [{
"name": "Divers",
"value": 950956.15
}, {
"name": "Service médical",
"value": 128044.03
}]
}, {
"name": "FINANCES ET ACHATS",
"children": [{
"name": "Achats",
"value": 332000
}, {
"name": "Finances",
"value": 3735000
}]
}, {
"name": "ESPACES VERTS ET ENVIRONNEMENT",
"children": [{
"name": "Divers",
"value": 1317412.75
}, {
"name": "Plantations",
"value": 9000000
}, {
"name": "Cimetières",
"value": 11589782.48
}, {
"name": "Créations d'espaces verts",
"value": 52609586.33
}, {
"name": "Entretien d'espaces verts",
"value": 36311255.059999995
}, {
"name": "Services communs",
"value": 3486482.5
}, {
"name": "Bois",
"value": 6389978.65
}]
}, {
"name": "AFFAIRES CULTURELLES",
"children": [{
"name": "Bibliothèques",
"value": 28304081.78
}, {
"name": "Salles de spectacle",
"value": 10695064.639999999
}, {
"name": "Enseignement artistique",
"value": 25436375.34
}, {
"name": "Edifices cultuels",
"value": 41338744.42000001
}, {
"name": "Services centraux",
"value": 963357.14
}, {
"name": "Musées",
"value": 6347564.34
}, {
"name": "Art dans la ville",
"value": 3640025.9
}, {
"name": "Nouveaux projets",
"value": 20124034.16
}, {
"name": "Périmètre des Halles",
"value": 140450
}, {
"name": "Monuments",
"value": 1613894.99
}, {
"name": "BATIMENTS",
"value": 210000
}, {
"name": "ARCHIVES",
"value": 945037.16
}]
}, {
"name": "URBANISME",
"children": [{
"name": "Services communs",
"value": 468001.54
}, {
"name": "Politique foncière",
"value": 236924000
}, {
"name": "Etudes d'urbanisme",
"value": 3770956.43
}]
}, {
"name": "AFFAIRES SCOLAIRES",
"children": [{
"name": "Périscolaire - Services communs",
"value": 3642784.8000000003
}, {
"name": "Opérations individualisées 1er degré",
"value": 70979558.56
}, {
"name": "Entretien Enseignement Supérieur",
"value": 430310.1
}, {
"name": "Opérations individualisées Ens. Sup.",
"value": 7762408.72
}, {
"name": "Entretien 1er degré",
"value": 52910868.019999996
}, {
"name": "MATERIEL - INFORMATIQUE - 2EME DEGRE",
"value": 1854218.59
}, {
"name": "2EME DEGRE OPE.IND. COLLEGES AUTONOMES",
"value": 13120421.54
}, {
"name": "ENTRETIEN COLLEGES AUTONOMES-2EME DEGRE",
"value": 14436528.04
}, {
"name": "ENTRETIEN COLLEGES IMBRIQUES-2EME DEGRE",
"value": 10329416.12
}, {
"name": "2EME DEGRE OPE.IND. COLLEGES IMBRIQUES",
"value": 4287152.0200000005
}]
}, {
"name": "JEUNESSE ET SPORTS",
"children": [{
"name": "Piscines",
"value": 23270315.3
}, {
"name": "Stades",
"value": 8649021.600000001
}, {
"name": "Autres équipements sportifs",
"value": 637657.67
}, {
"name": "Mobilier jeunesse",
"value": 44419.6
}, {
"name": "Gymnases et salles de sports",
"value": 48298922.34
}, {
"name": "Bains douches",
"value": 120000
}, {
"name": "Entretien équipements sportifs",
"value": 26488649
}, {
"name": "Mobilier équipements sportifs",
"value": 1100303.36
}, {
"name": "Centres d'animation",
"value": 12732323.129999999
}, {
"name": "Autres équipements de jeunesse",
"value": 1253364.17
}, {
"name": "Services communs",
"value": 1995195.56
}]
}, {
"name": "ETUDES",
"children": [{
"name": "DAC",
"value": 2500000
}, {
"name": "DEVE",
"value": 300000
}, {
"name": "DPA",
"value": 1062677.3399999999
}, {
"name": "DVD",
"value": 1246534.57
}, {
"name": "DU",
"value": 116164.75
}, {
"name": "DASCO",
"value": 2056608.08
}, {
"name": "DFPE",
"value": 708129.3300000001
}, {
"name": "DUCT/DPA",
"value": 100000
}, {
"name": "DPE",
"value": 109099.6
}, {
"name": "DJS",
"value": 926354.22
}, {
"name": "DLH / DPA",
"value": 45000
}, {
"name": "DFPE / DPA",
"value": 16685.71
}, {
"name": "DASES / DPA",
"value": 365000
}, {
"name": "DASCO / DPA",
"value": 133855.66999999998
}, {
"name": "DJS/DPA",
"value": 950541
}, {
"name": "DDEEES",
"value": 12495.78
}]
}, {
"name": "ACTION EN FAVEUR DES PERSON. HANDICAPEES",
"children": [{
"name": "DUCT",
"value": 520000
}, {
"name": "DRH",
"value": 403541.96
}, {
"name": "DVD",
"value": 3210861.73
}, {
"name": "DPA",
"value": 550000
}, {
"name": "DJS",
"value": 456918.41
}, {
"name": "DAC",
"value": 122720.95999999999
}, {
"name": "DEVE",
"value": 1452372
}, {
"name": "DASES",
"value": 890000
}, {
"name": "DASCO",
"value": 200000
}]
}, {
"name": "SCHEMA DIRECTEUR INFORMATIQUE",
"children": [{
"name": "DSTI",
"value": 54913590.19
}]
}, {
"name": "SUBVENTIONS D'EQUIPEMENT",
"children": [{
"name": "DFPE",
"value": 16351698
}, {
"name": "DJS",
"value": 2531792
}, {
"name": "DLH",
"value": 875832000
}, {
"name": "DUCT",
"value": 400000
}, {
"name": "FIN",
"value": 33420000
}, {
"name": "DAC",
"value": 327195175.32
}, {
"name": "DU",
"value": 1850003.51
}, {
"name": "DRH",
"value": 1360000
}, {
"name": "DDEEES",
"value": 67107447.02
}, {
"name": "DPVI",
"value": 520815
}, {
"name": "DVD",
"value": 172780220.41
}, {
"name": "DASCO",
"value": 8705000
}, {
"name": "DASES",
"value": 11085100.139999999
}]
}, {
"name": "AVANCES SUR MARCHES",
"children": [{
"name": "DJS",
"value": 2219429.85
}, {
"name": "DEVE",
"value": 2222234.37
}, {
"name": "DVD",
"value": 5000000
}, {
"name": "DFPE",
"value": 1000000
}, {
"name": "FIN",
"value": 12000000
}, {
"name": "DPA",
"value": 2500000
}, {
"name": "DAC",
"value": 2000000
}, {
"name": "DASCO",
"value": 3500000
}, {
"name": "DPE",
"value": 650000
}, {
"name": "DASES",
"value": 3000000
}]
}, {
"name": "INSPECTION GENERALE",
"children": [{
"name": "Services communs",
"value": 14582
}]
}, {
"name": "PREVENTION ET PROTECTION",
"children": [{
"name": "Sécurité travaux",
"value": 180340.23
}, {
"name": "Services communs",
"value": 85090.43
}, {
"name": "Sécurité équipements",
"value": 372000
}]
}, {
"name": "POLITIQUE DE LA VILLE ET L'INTEGRATION",
"children": [{
"name": "Politique de la Ville",
"value": 130743.23
}]
}, {
"name": "PATRIMOINE ET ARCHITECTURE",
"children": [{
"name": "Bâtiments",
"value": 481381388.10999995
}, {
"name": "Travaux",
"value": 8858910.2
}, {
"name": "Travaux dans les mairies",
"value": 116194.11
}, {
"name": "Mairies modernisation",
"value": 7125250.65
}, {
"name": "Services communs",
"value": 2494973.33
}, {
"name": "Ravalements",
"value": 4879061.5
}, {
"name": "Fontaines",
"value": 4180544.71
}, {
"name": "SERVICES COMMUNS",
"value": 1279096.53
}, {
"name": "TRAVAUX",
"value": 1163495.96
}]
}, {
"name": "VOIRIE ET DEPLACEMENTS",
"children": [{
"name": "Patrimoine de voirie",
"value": 80647414.9
}, {
"name": "Déplacements",
"value": 21778744.56
}, {
"name": "Vélos",
"value": 11717223.67
}, {
"name": "Opérations d'aménagement",
"value": 120230242.36
}, {
"name": "Eclairage",
"value": 15297400
}, {
"name": "Bus",
"value": 1017250.45
}, {
"name": "Carrières",
"value": 4218582.5600000005
}, {
"name": "Stationnement payant de surface",
"value": 1646683.51
}, {
"name": "Canaux",
"value": 17735048.12
}, {
"name": "Tramways",
"value": 215215208.64
}, {
"name": "Services communs de voirie",
"value": 1105334.55
}, {
"name": "Création d'ouvrage d'art",
"value": 10861.59
}, {
"name": "Infrastructures de transports",
"value": 1500000
}, {
"name": "Espaces civilisés",
"value": 522337.58
}]
}, {
"name": "PROPRETE ET EAU",
"children": [{
"name": "Divers",
"value": 3021836.87
}, {
"name": "Garages",
"value": 153958413.41
}, {
"name": "Lieux d'appel",
"value": 27965935.52
}, {
"name": "Propreté",
"value": 56709861.8
}]
}, {
"name": "LOGEMENT ET HABITAT",
"children": [{
"name": "Services communs",
"value": 115600
}, {
"name": "Démolitions et travaux",
"value": 15240098.08
}, {
"name": "TRAVAUX",
"value": 632017.96
}]
}, {
"name": "FAMILLES ET PETITE ENFANCE",
"children": [{
"name": "Etablissements de petite enfance",
"value": 121090423.42
}, {
"name": "Services communs",
"value": 1000000
}, {
"name": "SERVICES COMMUNS",
"value": 157343.47
}, {
"name": "CENTRES DE PMI",
"value": 2644000
}]
}, {
"name": "DONS ET LEGS",
"children": [{
"name": "DUCT",
"value": 394946.42
}, {
"name": "DASES",
"value": 35894
}, {
"name": "DAC",
"value": 4238320.82
}]
}, {
"name": "CABINET DU MAIRE",
"children": [{
"name": "Divers",
"value": 39664.05
}]
}, {
"name": "INFORMATION ET COMMUNICATION",
"children": [{
"name": "Matériel et mobilier",
"value": 145000
}, {
"name": "Travaux",
"value": 10000
}]
}, {
"name": "AFFAIRES JURIDIQUES",
"children": [{
"name": "Services communs",
"value": 45052.53
}]
}, {
"name": "GRANDS PROJETS DE RENOVATION URBAINE",
"children": [{
"name": "DEVE",
"value": 1865217
}, {
"name": "DU",
"value": 1250000
}, {
"name": "DVD",
"value": 10987950.95
}, {
"name": "DASCO",
"value": 27437134.93
}, {
"name": "DJS",
"value": 5416300.789999998
}]
}, {
"name": "FINANCES (SERVICES COMMUNS)",
"children": [{
"name": "Services communs",
"value": 41000000
}, {
"name": "SERVICES COMMUNS",
"value": 6000000
}]
}, {
"name": "OPERATIONS SOUS MANDAT",
"children": [{
"name": "EP MUSEES",
"value": 1731000
}, {
"name": "DFPE",
"value": 4000000
}, {
"name": "DLH",
"value": 1700000
}]
}, {
"name": "COMPTE FONCIER",
"children": [{
"name": "DU",
"value": 849974311.1
}]
}, {
"name": "BUDGET PARTICIPATIF PARISIEN",
"children": [{
"name": "Edition 2014",
"value": 17700000
}]
}, {
"name": "SECRETARIAT GENERAL",
"children": [{
"name": "Restructuration",
"value": 4000000
}, {
"name": "Matériel et mobilier",
"value": 30000
}]
}, {
"name": "AVANCES DE TRESORERIE",
"children": [{
"name": "DDEEES",
"value": 27120000
}, {
"name": "DASES",
"value": 6823438
}]
}, {
"name": "DEVELOPPEMENT ECO. EMPLOI ET ENS. SUP.",
"children": [{
"name": "AUTRES STRUCTURES",
"value": 2159000.1100000003
}, {
"name": "SERVICES COMMUNS",
"value": 20000
}]
}, {
"name": "SECRETARIAT GENERAL DU CONSEIL DE PARIS",
"children": [{
"name": "Divers",
"value": 100000
}]
}, {
"name": "FINANCES",
"children": [{
"name": "Services communs",
"value": 114155000
}]
}, {
"name": "GRAND PROJETS RENOVATION URBAINE - GPRU",
"children": [{
"name": "GRANDS PROJETS RENOVATION URBAINE GPRU",
"value": 10000000
}]
}]
}
<!DOCTYPE html>
<meta charset="utf-8">
<title>Zoomable Treemaps with Color</title>
<style>
@import url(../style.css?aea6f0a);
#chart {
width: 960px;
height: 500px;
}
text {
pointer-events: none;
}
.grandparent text {
font-weight: bold;
}
rect {
/* fill: none; */
stroke: #fff;
stroke-width: 1px;
}
rect.parent,
.grandparent rect {
stroke-width: 2px;
}
.grandparent:hover rect {
fill: darkgrey;
}
.children rect.parent,
.grandparent rect {
cursor: pointer;
}
.children rect.child {
opacity: 0;
}
.children rect.parent {}
.children:hover rect.child {
opacity: 1;
stroke-width: 1px;
}
.children:hover rect.parent {
opacity: 0;
}
.legend rect {
stroke-width: 0px;
}
.legend text {
text-anchor: middle;
pointer-events: auto;
font-size: 15px;
font-family: sans-serif;
fill: black;
}
</style>
<p>Hover on any cell to see next level of detail, or click on a cell to zoom in. Click on the top label to zoom out.</p>
<p id="chart">
<p id="chart">
<!-- load D3js -->
<script src="http://www.d3plus.org/js/d3.js"></script>
<!-- load D3plus after D3js -->
<script src="http://www.d3plus.org/js/d3plus.js"></script>
<script>
Number.prototype.format = function(n, x) {
var re = '\\d(?=(\\d{' + (x || 3) + '})+' + (n > 0 ? '\\.' : '$') + ')';
return this.toFixed(Math.max(0, ~~n)).replace(new RegExp(re, 'g'), '$&,');
};
var margin = {
top: 30,
right: 0,
bottom: 20,
left: 0
},
width = 960,
height = 500 - margin.top - margin.bottom,
formatNumber = d3.format(",%"),
colorDomain = [0, 100],
colorRange = ["#373a93", "#936638"],
transitioning;
// sets x and y scale to determine size of visible boxes
var x = d3.scale.linear()
.domain([0, width])
.range([0, width]);
var y = d3.scale.linear()
.domain([0, height])
.range([0, height]);
// adding a color scale
color = d3.scale.category20c();
// introduce color scale here
var treemap = d3.layout.treemap()
.children(function(d, depth) {
return depth ? null : d._children;
})
.sort(function(a, b) {
return a.value - b.value;
})
.ratio(height / width * 0.5 * (1 + Math.sqrt(5)))
.round(false);
var svg = d3.select("#chart").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.bottom + margin.top)
.style("margin-left", -margin.left + "px")
.style("margin.right", -margin.right + "px")
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
.style("shape-rendering", "crispEdges");
var grandparent = svg.append("g")
.attr("class", "grandparent");
grandparent.append("rect")
.attr("y", -margin.top)
.attr("width", width)
.attr("height", margin.top);
grandparent.append("text")
.attr("x", 6)
.attr("y", 6 - margin.top)
.attr("dy", ".75em");
var legend = d3.select("#legend").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", 30)
.attr('class', 'legend')
.selectAll("g")
.data([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18])
.enter()
.append('g')
// functions
function initialize(root) {
root.x = root.y = 0;
root.dx = width;
root.dy = height;
root.depth = 0;
}
// Aggregate the values for internal nodes. This is normally done by the
// treemap layout, but not here because of our custom implementation.
// We also take a snapshot of the original children (_children) to avoid
// the children being overwritten when when layout is computed.
function accumulate(d) {
return (d._children = d.children)
// recursion step, note that p and v are defined by reduce
? d.value = d.children.reduce(function(p, v) {
return p + accumulate(v);
}, 0) : d.value;
}
// Compute the treemap layout recursively such that each group of siblings
// uses the same size (1×1) rather than the dimensions of the parent cell.
// This optimizes the layout for the current zoom state. Note that a wrapper
// object is created for the parent node for each group of siblings so that
// the parent’s dimensions are not discarded as we recurse. Since each group
// of sibling was laid out in 1×1, we must rescale to fit using absolute
// coordinates. This lets us use a viewport to zoom.
function layout(d) {
if (d._children) {
// treemap nodes comes from the treemap set of functions as part of d3
treemap.nodes({
_children: d._children
});
d._children.forEach(function(c) {
c.x = d.x + c.x * d.dx;
c.y = d.y + c.y * d.dy;
c.dx *= d.dx;
c.dy *= d.dy;
c.parent = d;
// recursion
layout(c);
});
}
}
d3.json("budget-Paris-2015.json", function(root) {
console.log(root)
initialize(root);
accumulate(root);
layout(root);
display(root);
function display(d) {
grandparent
.datum(d.parent)
.on("click", transition)
.select("text")
.text(name(d))
// color header based on grandparent's value
grandparent
.datum(d.parent)
.select("rect")
.attr("fill", function() {
return color(d.name);
})
var g1 = svg.insert("g", ".grandparent")
.datum(d)
.attr("class", "depth");
var g = g1.selectAll("g")
.data(d._children)
.enter().append("g");
g.filter(function(d) {
return d._children;
})
.classed("children", true)
.on("click", transition);
g.selectAll(".child")
.data(function(d) {
return d._children || [d];
})
.enter().append("rect")
.attr("class", "child")
.call(rect);
g.append("rect")
.attr("class", "parent")
.call(rect)
.append("title")
.text(function(d) {
return d.name + ' with budget: ' + d.value.format() + '€';
});
g.append("text")
.attr("dy", ".80em")
.attr("font-size", "10")
.text(function(d) {
return d.name;
})
.call(text)
.style("opacity", function(d) {
d.w = this.getComputedTextLength();
return d.dx > d.w ? 1 : 0;
});
function transition(d) {
if (transitioning || !d) return;
transitioning = true;
var g2 = display(d),
t1 = g1.transition().duration(750),
t2 = g2.transition().duration(750);
// Update the domain only after entering new elements.
x.domain([d.x, d.x + d.dx]);
y.domain([d.y, d.y + d.dy]);
// Enable anti-aliasing during the transition.
svg.style("shape-rendering", null);
// Draw child nodes on top of parent nodes.
svg.selectAll(".depth").sort(function(a, b) {
return a.depth - b.depth;
});
// Fade-in entering text.
g2.selectAll("text").style("fill-opacity", 0);
// Transition to the new view.
t1.selectAll("text").call(text).style("fill-opacity", 0);
t2.selectAll("text").call(text).style("fill-opacity", 1);
t1.selectAll("rect").call(rect);
t2.selectAll("rect").call(rect);
// Remove the old node when the transition is finished.
t1.remove().each("end", function() {
svg.style("shape-rendering", "crispEdges");
transitioning = false;
});
}
return g;
}
function text(text) {
text.attr("x", function(d) {
return x(d.x) + 2;
})
.attr("y", function(d) {
return y(d.y) + 2;
})
.attr("fill", function(d) {
return getContrast50(color(d.name));
});
}
function rect(rect) {
rect.attr("x", function(d) {
return x(d.x);
})
.attr("y", function(d) {
return y(d.y);
})
.attr("width", function(d) {
return x(d.x + d.dx) - x(d.x);
})
.attr("height", function(d) {
return y(d.y + d.dy) - y(d.y);
})
.attr("fill", function(d) {
return color(d.name);
});
}
function name(d) {
return d.parent ? name(d.parent) + "." + d.name : d.name;
}
// determines if white or black will be better contrasting color
function getContrast50(hexcolor) {
return (parseInt(hexcolor.replace('#', ''), 16) > 0xffffff / 3) ? 'black' : 'white';
}
});
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment