Skip to content

Instantly share code, notes, and snippets.

@JacquesJahnichen
Last active November 22, 2017 03:20
Show Gist options
  • Save JacquesJahnichen/42afd0cde7cbf72ecb81 to your computer and use it in GitHub Desktop.
Save JacquesJahnichen/42afd0cde7cbf72ecb81 to your computer and use it in GitHub Desktop.
d3.js - A zoomable treemap, dynamic width, with colors and wrap text

A zoomable treemap, dynamic width for mobile, wrap text and colors. Created for the Open Data Portal of Seine-Saint-Denis, with the datasets grouped by political publics drawn based on the budget of the individual datasets

The d3 code is heavily based on code shared by Mike Bostock, Zan Armstron and PMeinshausen.

{
"name": "Budget du Departement",
"rate": 0.03,
"children": [
{
"name": "Aménagement et développement durable",
"rate": -0.01,
"children": [
{
"name": "Réseau routier et mobilité durable",
"rate": 0,
"children": [
{
"name": "Mobilité durable",
"rate": 0.02,
"value": 47.1
},
{
"name": "PDU et infrastructures de transports en commun",
"rate": -2.92,
"value": 0.21
},
{
"name": "Réseau routier",
"rate": -0.07,
"value": 5.23
}
]
},
{
"name": "Développement et attractivité des territoires",
"rate": -0.1,
"children": [
{
"name": "Aménagement",
"rate": -0.16,
"value": 1.52
},
{
"name": "Développement économique et promotion du territoire",
"rate": -0.17,
"value": 3.03
},
{
"name": "Economie sociale et solidaire, emploi et formation",
"rate": -0.05,
"value": 1.82
},
{
"name": "Politique de la ville et habitat",
"rate": 0.36,
"value": 0.49
}
]
},
{
"name": "Ecologie urbaine",
"rate": -0.02,
"children": [
{
"name": "Biodiversité et nature en ville",
"rate": -0.01,
"value": 8.96
},
{
"name": "Cadre de vie",
"rate": 0.42,
"value": 0.01
},
{
"name": "Qualité de l'environnement urbain",
"rate": -0.03,
"value": 3.82
}
]
}
]
},
{
"name": "Direction générale",
"rate": -0.05,
"children": [
{
"name": "Direction et animation de l'action départementale",
"rate": -0.07,
"children": [
{
"name": "Animation du développement social local",
"rate": 1,
"value": 0.01
},
{
"name": "Animation et gestion de projets européens",
"rate": -0.48,
"value": 0.07
},
{
"name": "Audit interne",
"rate": -0.86,
"value": 0.02
},
{
"name": "Information et communication",
"rate": -0.07,
"value": 2.9
},
{
"name": "Pilotage et Stratégie",
"rate": -0.02,
"value": 0.68
}
]
},
{
"name": "Conduite des politiques départementales",
"rate": -0.03,
"children": [
{
"name": "Assemblée départementale",
"rate": -0.02,
"value": 2.5
},
{
"name": "Moyens des groupes politiques",
"rate": -0.11,
"value": 0.39
}
]
}
]
},
{
"name": "Personnel et relations sociales",
"rate": 0.02,
"children": [
{
"name": "Ressources humaines",
"rate": 0.02,
"children": [
{
"name": "Gestion prévisionnelle des emplois et des compétences",
"rate": -0.14,
"value": 1.32
},
{
"name": "Politique salariale et gestion des carrières",
"rate": 0.02,
"value": 328.73
},
{
"name": "Relations sociales et santé au travail",
"rate": 0.02,
"value": 3.7
}
]
}
]
},
{
"name": "Ressources et moyens des services",
"rate": 0.05,
"children": [
{
"name": "Finances",
"rate": 0.07,
"children": [
{
"name": "Commande publique",
"rate": 1,
"value": 0.02
},
{
"name": "Stratégie budgétaire et financière",
"rate": 0.07,
"value": 75.1
}
]
},
{
"name": "Supports internes",
"rate": 0.01,
"children": [
{
"name": "Bâtiments départementaux",
"rate": 0.04,
"value": 11.02
},
{
"name": "Logistique",
"rate": 0.02,
"value": 15.11
},
{
"name": "Patrimoine immobilier et sécurisation juridique",
"rate": 0.01,
"value": 19.27
},
{
"name": "Systèmes d'information",
"rate": -0.08,
"value": 3.25
}
]
}
]
},
{
"name": "Société et citoyenneté",
"rate": 0.05,
"children": [
{
"name": "Culture, patrimoine, archives",
"rate": -0.01,
"children": [
{
"name": "Archives",
"rate": 0.14,
"value": 0.16
},
{
"name": "Culture",
"rate": -0.01,
"value": 18.71
},
{
"name": "Patrimoine culturel",
"rate": -0.06,
"value": 0.59
}
]
},
{
"name": "Education et jeunesse",
"rate": 0.14,
"children": [
{
"name": "Accueil des collégiens et de la communauté éducative",
"rate": 0.17,
"value": 33.85
},
{
"name": "Actions éducatives en direction des collégiens",
"value": 0
},
{
"name": "Actions pour la jeunesse",
"rate": 1,
"value": 0.07
},
{
"name": "Projet éducatif départemental",
"rate": 0.17,
"value": 13.58
}
]
},
{
"name": "Citoyenneté active, solidarité internationale et Sport",
"rate": -0.25,
"children": [
{
"name": "Promotion de la vie associative et démocratie participative",
"rate": 1,
"value": 0
},
{
"name": "Solidarité, coopération internationales",
"rate": -0.04,
"value": 0.3
},
{
"name": "Sport et loisirs",
"rate": -0.26,
"value": 11.53
}
]
}
]
},
{
"name": "Solidarité",
"rate": 0.03,
"children": [
{
"name": "Enfance et famille",
"rate": -0.02,
"children": [
{
"name": "Accueil de la petite enfance",
"rate": -0.04,
"value": 20.84
},
{
"name": "Aide sociale à l'enfance",
"rate": -0.02,
"value": 250.75
},
{
"name": "Protection maternelle et infantile, planification familiale",
"rate": 0.01,
"value": 19.16
}
]
},
{
"name": "Solidarité, insertion, santé",
"rate": 0.07,
"children": [
{
"name": "Accompagnement social",
"rate": -0.05,
"value": 17.58
},
{
"name": "Insertion",
"rate": 0.08,
"value": 448.12
},
{
"name": "Prévention des conduites à risques",
"rate": 0.04,
"value": 0.51
},
{
"name": "Prévention et promotion de la santé",
"rate": -0.02,
"value": 3.06
},
{
"name": "Promotion des droits fondamentaux",
"rate": 0.03,
"value": 0.25
}
]
},
{
"name": "Autonomie",
"rate": 0.01,
"children": [
{
"name": "Personnes âgées",
"rate": 0.01,
"value": 197.43
},
{
"name": "Personnes handicapées",
"rate": 0,
"value": 166.87
}
]
}
]
}
]
}
<meta http-equiv="Content-Type" content="text/html; charset=#CHARSET" />
<style>
#chart {
max-width: 100%;
overflow:auto;
}
text {
pointer-events: none;
}
.grandparent text {
font-weight: bold;
}
rect {
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 {
margin-bottom:8px !important;
}
.legend rect {
stroke-width: 0px;
}
.legend text {
text-anchor: middle;
pointer-events: auto;
font-size: 13px;
font-family: sans-serif;
fill: black;
}
.form-group {
text-align:left;
}
.textdiv {
font-family: "Open Sans",Helvetica,Arial,sans-serif;
font-size: 14px;
padding: 7px;
cursor: pointer;
overflow:none;
}
.textdiv .title {
font-size: 102%;
font-weight: bold;
margin-top: 8px;
font-size:11px !important;
}
.textdiv p{
line-height: 13px;
margin:0 0 4px !important;
padding:0px;
font-size:10px !important;
}
</style>
<p id="chart"></p>
<div class="col-md-23 legend" style="margin-top:-10px;margin-bottom:0px;">
<strong>Title Legend</strong>
<div id="legend"></div>
</div>
<script type="text/javascript" src="http://d3js.org/d3.v3.min.js"></script>
<script type="text/javascript">
// Largeur de la width
var obj = document.getElementById('chart');
var divWidth = obj.offsetWidth;
var margin = {top: 30, right: 0, bottom: 20, left: 0},
width = divWidth -25,
height = 540 - margin.top - margin.bottom,
formatNumber = d3.format(",%"),
colorDomain = [-.1, 0, .1],
colorRange = ['#dda8db', '#ebf2f7', '#9cbdd9'],
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
var color = d3.scale.linear()
.domain(colorDomain)
.range(colorRange);
// 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])
//.data([0,2,4,5,8,9,10,11,12,14,16,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);
});
}
}
function colorIncrements(d){
return (colorDomain[colorDomain.length - 1] - colorDomain[0])/18*d + colorDomain[0];
}
legend.append("rect")
.attr("x", function(d){return margin.left + d * 35})
.attr("y", 0)
.attr("fill", function(d) {return color(colorIncrements(d))})
.attr('width', '35px')
.attr('height', '40px')
legend.append("text")
.text(function(d){return formatNumber(colorIncrements(d))})
.attr('y', 20)
.attr('x', function(d){return margin.left + d * 35 + 20});
// determines if white or black will be better contrasting color
function getContrast50(hexcolor){
return (parseInt(hexcolor.replace('#', ''), 16) > 0xffffff/3) ? 'black':'white';
}
d3.json("budget-data.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 rate
grandparent
.datum(d.parent)
.select("rect")
.attr("fill", function(){console.log(color(d.rate)); return color(d['rate'])})
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");
/* Adding a foreign object instead of a text object, allows for text wrapping */
g.append("foreignObject")
.call(rect)
/* open new window based on the json's URL value for leaf nodes */
/* Firefox displays this on top
.on("click", function(d) {
if(!d.children){
window.open(d.url);
}
})*/
.attr("class","foreignobj")
.append("xhtml:div")
.attr("dy", ".75em")
.html(function(d) { return '' +
' <p class="title"> ' + d.name + '</p>' +
' <p> En 2014 : ' + d3.round(d.value,2) + ' Million(s) d\047euros </p>' +
' <p> 2013/2014 : ' + formatNumber(d.rate);
;})
.attr("class","textdiv"); //textdiv class allows us to style the text easily with CSS
function transition(d) {
if (transitioning || !d) return;
transitioning = true;
var g2 = display(d),
t1 = g1.transition().duration(650),
t2 = g2.transition().duration(650);
// 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);
g2.selectAll("foreignObject div").style("display", "none"); /*added*/
// 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);
/* Foreign object */
t1.selectAll(".textdiv").style("display", "none"); /* added */
t1.selectAll(".foreignobj").call(foreign); /* added */
t2.selectAll(".textdiv").style("display", "block"); /* added */
t2.selectAll(".foreignobj").call(foreign); /* added */
// 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) + 6; })
.attr("y", function(d) { return y(d.y) + 6; });
}
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(parseFloat(d.rate));});
}
function foreign(foreign){ /* added */
foreign.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); });
}
function name(d) {
return d.parent
? "Budget du Département" + " - " + d.name + " - Cliquez ici pour dézoomer"
: "Budget du Département - Cliquez pour zoomer";
}
function nameSave(d) {
return d.parent
? name(d.parent) + " - " + d.name + " - Cliquez ici pour dézoomer"
: d.name;
}
});
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment