Skip to content

Instantly share code, notes, and snippets.

@guglielmo
Last active September 1, 2017 14:17
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 guglielmo/f28d653dce899affeec6e34a733325e4 to your computer and use it in GitHub Desktop.
Save guglielmo/f28d653dce899affeec6e34a733325e4 to your computer and use it in GitHub Desktop.
d3.js v4 - Zoomable treemap
license: gpl-3.0
height: 600

A zoomable treemap, with dynamic width for mobile, and wrapped texts.

The code has been ported to D3v4, and humbly improved from the original Jacques Jahnichen's Block.

The data visualizes the income provisional budget for the city of Rome, as of year 2016.

This gist is part of the OpenBilanci project.

Modifications and improvements include: colors are not used, breadcrumbs have been added to the navigation bar, titles have been added to the svg rects (popover effect on some browser)

{
"slug": "pcox-quadro-2-10",
"label": "Totale Titoli",
"values": [
{
"2016": {
"abs": 8681137901.28,
"pc": 3021.1087621133
}
},
{
"2017": {
"abs": 4590785250.4,
"pc": 1597.63175089282
}
},
{
"2018": {
"abs": 4522976016.27,
"pc": 1574.03356898257
}
}
],
"children": [
{
"slug": "pcox-quadro-2-2",
"label": "Entrate correnti di natura tributaria, contributiva e perequativa",
"values": [
{
"2016": {
"abs": 2436732707.86,
"pc": 848.003409041397
}
},
{
"2017": {
"abs": 2423253331.86,
"pc": 843.312473198134
}
},
{
"2018": {
"abs": 2426668331.86,
"pc": 844.500921825485
}
}
],
"children": [
{
"slug": "pcox-quadro-2-2-9",
"label": "Imposte,tasse e proventi assimilati",
"values": [
{
"2016": {
"abs": 2436732707.86,
"pc": 848.003409041397
}
},
{
"2017": {
"abs": 2423253331.86,
"pc": 843.312473198134
}
},
{
"2018": {
"abs": 2426668331.86,
"pc": 844.500921825485
}
}
]
},
{
"slug": "pcox-quadro-2-2-11",
"label": "Compartecipazioni di tributi",
"values": [
{
"2016": {
"abs": 0.0,
"pc": 0.0
}
},
{
"2017": {
"abs": 0.0,
"pc": 0.0
}
},
{
"2018": {
"abs": 0.0,
"pc": 0.0
}
}
]
},
{
"slug": "pcox-quadro-2-2-13",
"label": "Fondi perequativi da Amministrazioni Centrali",
"values": [
{
"2016": {
"abs": 0.0,
"pc": 0.0
}
},
{
"2017": {
"abs": 0.0,
"pc": 0.0
}
},
{
"2018": {
"abs": 0.0,
"pc": 0.0
}
}
]
},
{
"slug": "pcox-quadro-2-2-15",
"label": "Fondi perequativi dalla Regione o Provincia autonoma",
"values": [
{
"2016": {
"abs": 0.0,
"pc": 0.0
}
},
{
"2017": {
"abs": 0.0,
"pc": 0.0
}
},
{
"2018": {
"abs": 0.0,
"pc": 0.0
}
}
]
}
]
},
{
"slug": "pcox-quadro-2-3",
"label": "Trasferimenti correnti",
"values": [
{
"2016": {
"abs": 1400175099.01,
"pc": 487.2726718796
}
},
{
"2017": {
"abs": 1285994083.09,
"pc": 447.53672117986
}
},
{
"2018": {
"abs": 1278995698.69,
"pc": 445.101224742422
}
}
],
"children": [
{
"slug": "pcox-quadro-2-3-19",
"label": "Trasferimenti correnti da Amministrazioni pubbliche - previsioni di competenza",
"values": [
{
"2016": {
"abs": 1398135574.29,
"pc": 486.56290018006
}
},
{
"2017": {
"abs": 1284583039.12,
"pc": 447.045666049764
}
},
{
"2018": {
"abs": 1278083039.12,
"pc": 444.783611561395
}
}
]
},
{
"slug": "pcox-quadro-2-3-21",
"label": "Trasferimenti correnti da Famiglie",
"values": [
{
"2016": {
"abs": 0.0,
"pc": 0.0
}
},
{
"2017": {
"abs": 0.0,
"pc": 0.0
}
},
{
"2018": {
"abs": 0.0,
"pc": 0.0
}
}
]
},
{
"slug": "pcox-quadro-2-3-23",
"label": "Trasferimenti correnti da Imprese",
"values": [
{
"2016": {
"abs": 924000.0,
"pc": 0.321559745731155
}
},
{
"2017": {
"abs": 544000.0,
"pc": 0.189316560257303
}
},
{
"2018": {
"abs": 544000.0,
"pc": 0.189316560257303
}
}
]
},
{
"slug": "pcox-quadro-2-3-25",
"label": "Trasferimenti correnti da Istituzioni Sociali Private",
"values": [
{
"2016": {
"abs": 0.0,
"pc": 0.0
}
},
{
"2017": {
"abs": 0.0,
"pc": 0.0
}
},
{
"2018": {
"abs": 0.0,
"pc": 0.0
}
}
]
},
{
"slug": "pcox-quadro-2-3-27",
"label": "Trasferimenti correnti dall'Unione europea e dal Resto del Mondo",
"values": [
{
"2016": {
"abs": 1115524.72,
"pc": 0.388211953809543
}
},
{
"2017": {
"abs": 867043.97,
"pc": 0.30173856983867
}
},
{
"2018": {
"abs": 368659.57,
"pc": 0.128296620769001
}
}
]
}
]
},
{
"slug": "pcox-quadro-2-4",
"label": "Entrate extratributarie",
"values": [
{
"2016": {
"abs": 921210810.41,
"pc": 320.589084372544
}
},
{
"2017": {
"abs": 689780498.6,
"pc": 240.049395822647
}
},
{
"2018": {
"abs": 671267634.33,
"pc": 233.606763866568
}
}
],
"children": [
{
"slug": "pcox-quadro-2-4-31",
"label": "Vendita di beni e servizi e proventi derivanti dalla gestione dei beni",
"values": [
{
"2016": {
"abs": 399375084.47,
"pc": 138.985877287372
}
},
{
"2017": {
"abs": 344825369.1,
"pc": 120.002119057844
}
},
{
"2018": {
"abs": 341500299.19,
"pc": 118.844966855682
}
}
]
},
{
"slug": "pcox-quadro-2-4-33",
"label": "Proventi derivanti dall'attività di controllo e repressione delle irregolarità e degli illeciti",
"values": [
{
"2016": {
"abs": 383453963.14,
"pc": 133.445193600543
}
},
{
"2017": {
"abs": 232943995.81,
"pc": 81.0664632708473
}
},
{
"2018": {
"abs": 217934257.14,
"pc": 75.8429483896608
}
}
]
},
{
"slug": "pcox-quadro-2-4-35",
"label": "Interessi attivi",
"values": [
{
"2016": {
"abs": 9349323.61,
"pc": 3.25364299003234
}
},
{
"2017": {
"abs": 8190072.48,
"pc": 2.85021387899192
}
},
{
"2018": {
"abs": 8180965.38,
"pc": 2.84704453184868
}
}
]
},
{
"slug": "pcox-quadro-2-4-37",
"label": "Altre entrate da redditi da capitale",
"values": [
{
"2016": {
"abs": 64700000.0,
"pc": 22.5161423688374
}
},
{
"2017": {
"abs": 47300000.0,
"pc": 16.4607965076663
}
},
{
"2018": {
"abs": 47300000.0,
"pc": 16.4607965076663
}
}
]
},
{
"slug": "pcox-quadro-2-4-39",
"label": "Rimborsi e altre entrate correnti",
"values": [
{
"2016": {
"abs": 64332439.19,
"pc": 22.3882281257591
}
},
{
"2017": {
"abs": 56521061.21,
"pc": 19.6698031072972
}
},
{
"2018": {
"abs": 56352112.62,
"pc": 19.6110075817106
}
}
]
}
]
},
{
"slug": "pcox-quadro-2-5",
"label": "Entrate in conto capitale",
"values": [
{
"2016": {
"abs": 447186182.39,
"pc": 155.624540155643
}
},
{
"2017": {
"abs": 159685421.42,
"pc": 55.5718652692506
}
},
{
"2018": {
"abs": 126272435.96,
"pc": 43.9438662339298
}
}
],
"children": [
{
"slug": "pcox-quadro-2-5-43",
"label": "Tributi in conto capitale",
"values": [
{
"2016": {
"abs": 9048415.13,
"pc": 3.14892431652894
}
},
{
"2017": {
"abs": 0.0,
"pc": 0.0
}
},
{
"2018": {
"abs": 0.0,
"pc": 0.0
}
}
]
},
{
"slug": "pcox-quadro-2-5-45",
"label": "Contributi agli investimenti",
"values": [
{
"2016": {
"abs": 258553557.62,
"pc": 89.9788054612259
}
},
{
"2017": {
"abs": 91239719.53,
"pc": 31.7521872431263
}
},
{
"2018": {
"abs": 123851000.0,
"pc": 43.1011862213737
}
}
]
},
{
"slug": "pcox-quadro-2-5-47",
"label": "Altri trasferimenti in conto capitale",
"values": [
{
"2016": {
"abs": 1459198.26,
"pc": 0.507813226685004
}
},
{
"2017": {
"abs": 0.0,
"pc": 0.0
}
},
{
"2018": {
"abs": 0.0,
"pc": 0.0
}
}
]
},
{
"slug": "pcox-quadro-2-5-49",
"label": "Entrate da alienazione di beni materiali e immateriali",
"values": [
{
"2016": {
"abs": 7265868.04,
"pc": 2.52858298642698
}
},
{
"2017": {
"abs": 0.0,
"pc": 0.0
}
},
{
"2018": {
"abs": 0.0,
"pc": 0.0
}
}
]
},
{
"slug": "pcox-quadro-2-5-51",
"label": "Altre entrate in conto capitale",
"values": [
{
"2016": {
"abs": 170859143.34,
"pc": 59.4604141647764
}
},
{
"2017": {
"abs": 68445701.89,
"pc": 23.8196780261243
}
},
{
"2018": {
"abs": 2421435.96,
"pc": 0.842680012556142
}
}
]
}
]
},
{
"slug": "pcox-quadro-2-6",
"label": "Entrate da riduzione di attività finanziarie",
"values": [
{
"2016": {
"abs": 168564839.81,
"pc": 58.6619773035893
}
},
{
"2017": {
"abs": 19771915.43,
"pc": 6.88079231416526
}
},
{
"2018": {
"abs": 19771915.43,
"pc": 6.88079231416526
}
}
],
"children": [
{
"slug": "pcox-quadro-2-6-55",
"label": "Alienazione di attività finanziarie",
"values": [
{
"2016": {
"abs": 51500000.0,
"pc": 17.9224317155352
}
},
{
"2017": {
"abs": 0.0,
"pc": 0.0
}
},
{
"2018": {
"abs": 0.0,
"pc": 0.0
}
}
]
},
{
"slug": "pcox-quadro-2-6-57",
"label": "Riscossione crediti di breve termine",
"values": [
{
"2016": {
"abs": 97292924.38,
"pc": 33.8587532738889
}
},
{
"2017": {
"abs": 0.0,
"pc": 0.0
}
},
{
"2018": {
"abs": 0.0,
"pc": 0.0
}
}
]
},
{
"slug": "pcox-quadro-2-6-59",
"label": "Riscossione crediti di medio - lungo termine",
"values": [
{
"2016": {
"abs": 19771915.43,
"pc": 6.88079231416526
}
},
{
"2017": {
"abs": 19771915.43,
"pc": 6.88079231416526
}
},
{
"2018": {
"abs": 19771915.43,
"pc": 6.88079231416526
}
}
]
},
{
"slug": "pcox-quadro-2-6-61",
"label": "Altre entrate per riduzione di attività finanziarie",
"values": [
{
"2016": {
"abs": 0.0,
"pc": 0.0
}
},
{
"2017": {
"abs": 0.0,
"pc": 0.0
}
},
{
"2018": {
"abs": 0.0,
"pc": 0.0
}
}
]
}
]
},
{
"slug": "pcox-quadro-2-7",
"label": "Accensione prestiti",
"values": [
{
"2016": {
"abs": 1150000.0,
"pc": 0.400209640249814
}
},
{
"2017": {
"abs": 12300000.0,
"pc": 4.28050310875888
}
},
{
"2018": {
"abs": 0.0,
"pc": 0.0
}
}
],
"children": [
{
"slug": "pcox-quadro-2-7-65",
"label": "Emissione di titoli obbligazionari - previsioni di competenza",
"values": [
{
"2016": {
"abs": 0.0,
"pc": 0.0
}
},
{
"2017": {
"abs": 0.0,
"pc": 0.0
}
},
{
"2018": {
"abs": 0.0,
"pc": 0.0
}
}
]
},
{
"slug": "pcox-quadro-2-7-67",
"label": "Accensione prestiti a breve termine",
"values": [
{
"2016": {
"abs": 0.0,
"pc": 0.0
}
},
{
"2017": {
"abs": 0.0,
"pc": 0.0
}
},
{
"2018": {
"abs": 0.0,
"pc": 0.0
}
}
]
},
{
"slug": "pcox-quadro-2-7-69",
"label": "Accensione mutui e altri finanziamenti a medio lungo termine",
"values": [
{
"2016": {
"abs": 1150000.0,
"pc": 0.400209640249814
}
},
{
"2017": {
"abs": 12300000.0,
"pc": 4.28050310875888
}
},
{
"2018": {
"abs": 0.0,
"pc": 0.0
}
}
]
},
{
"slug": "pcox-quadro-2-7-71",
"label": "Altre forme di indebitamento",
"values": [
{
"2016": {
"abs": 0.0,
"pc": 0.0
}
},
{
"2017": {
"abs": 0.0,
"pc": 0.0
}
},
{
"2018": {
"abs": 0.0,
"pc": 0.0
}
}
]
}
]
},
{
"slug": "pcox-quadro-2-8",
"label": "Anticipazioni da istituto tesoriere/cassiere",
"values": [
{
"2016": {
"abs": 300000000.0,
"pc": 104.402514847778
}
},
{
"2017": {
"abs": 0.0,
"pc": 0.0
}
},
{
"2018": {
"abs": 0.0,
"pc": 0.0
}
}
],
"children": [
{
"slug": "pcox-quadro-2-8-75",
"label": "Anticipazione da istituto tesoriere/cassiere - previsioni di competenza",
"values": [
{
"2016": {
"abs": 300000000.0,
"pc": 104.402514847778
}
},
{
"2017": {
"abs": 0.0,
"pc": 0.0
}
},
{
"2018": {
"abs": 0.0,
"pc": 0.0
}
}
]
}
]
},
{
"slug": "pcox-quadro-2-9",
"label": "Entrate per conto terzi e partite di giro",
"values": [
{
"2016": {
"abs": 3006118261.8,
"pc": 1046.1543548725
}
},
{
"2017": {
"abs": 0.0,
"pc": 0.0
}
},
{
"2018": {
"abs": 0.0,
"pc": 0.0
}
}
],
"children": [
{
"slug": "pcox-quadro-2-9-79",
"label": "Entrate per partite di giro",
"values": [
{
"2016": {
"abs": 2930428126.38,
"pc": 1019.81355324911
}
},
{
"2017": {
"abs": 0.0,
"pc": 0.0
}
},
{
"2018": {
"abs": 0.0,
"pc": 0.0
}
}
]
},
{
"slug": "pcox-quadro-2-9-81",
"label": "Entrate per conto terzi",
"values": [
{
"2016": {
"abs": 75690135.42,
"pc": 26.3408016233895
}
},
{
"2017": {
"abs": 0.0,
"pc": 0.0
}
},
{
"2018": {
"abs": 0.0,
"pc": 0.0
}
}
]
}
]
}
]
}
<meta charset="utf-8">
<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: 3px;
}
.grandparent:hover rect {
fill: grey;
}
.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;
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>
<script src="//d3js.org/d3.v4.min.js"></script>
<script>
/**
* Interactive, zoomable treemap, using D3 v4
*
* A port to D3 v4 of Jacques Jahnichen's Block, using the same budget data
* see: http://bl.ocks.org/JacquesJahnichen/42afd0cde7cbf72ecb81
*
* Author: Guglielmo Celata
* Date: sept 1st 2017
**/
var el_id = 'chart';
var obj = document.getElementById(el_id);
var divWidth = obj.offsetWidth;
var margin = {top: 30, right: 0, bottom: 20, left: 0},
width = divWidth -25,
height = 600 - margin.top - margin.bottom,
formatNumber = d3.format(","),
transitioning;
// sets x and y scale to determine size of visible boxes
var x = d3.scaleLinear()
.domain([0, width])
.range([0, width]);
var y = d3.scaleLinear()
.domain([0, height])
.range([0, height]);
var treemap = d3.treemap()
.size([width, height])
.paddingInner(0)
.round(false);
var svg = d3.select('#'+el_id).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)
.attr("fill", '#bbbbbb');
grandparent.append("text")
.attr("x", 6)
.attr("y", 6 - margin.top)
.attr("dy", ".75em");
var year = 2016;
var abs_in_year = function (year, values) {
val = values.filter(function (el) {
var key = parseInt(Object.keys(el));
return (key === year);
})[0][year]['abs'];
return val;
};
d3.json("bilancio.json", function(data) {
var root = d3.hierarchy(data);
console.log(root);
treemap(root
.sum(function (d) {
if (!d.hasOwnProperty('children'))
return abs_in_year(year, d.values);
else
return 0.0;
})
.sort(function (a, b) {
return b.height - a.height || b.value - a.value
})
);
display(root);
function display(d) {
// write text into grandparent
// and activate click's handler
grandparent
.datum(d.parent)
.on("click", transition)
.select("text")
.text(breadcrumbs(d));
// grandparent color
grandparent
.datum(d.parent)
.select("rect")
.attr("fill", function () {
return '#bbbbbb'
});
var g1 = svg.insert("g", ".grandparent")
.datum(d)
.attr("class", "depth");
var g = g1.selectAll("g")
.data(d.children)
.enter().
append("g");
// add class and click handler to all g's with children
g.filter(function (d) {
return d.children;
})
.attr("class", "children")
.style("cursor", "pointer")
.on("click", transition);
g.selectAll(".child")
.data(function (d) {
return d.children || [d];
})
.enter().append("rect")
.attr("class", "child")
.call(rect);
// add title to parents
g.append("rect")
.attr("class", "parent")
.call(rect)
.append("title")
.text(function (d){
return name(d);
});
/* Adding a foreign object instead of a text object, allows for text wrapping */
g.append("foreignObject")
.call(rect)
.attr("class", "foreignobj")
.append("xhtml:div")
.attr("title", function(d) {
return name(d);
})
.html(function (d) {
return '' +
'<p class="title">' + name(d) + '</p>' +
'<p>' + formatNumber(d.value) + '</p>'
;
})
.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.x0, d.x1]);
y.domain([d.y0, d.y1]);
// 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.on("end.remove", function(){
this.remove();
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.x0);
})
.attr("y", function (d) {
return y(d.y0);
})
.attr("width", function (d) {
return x(d.x1) - x(d.x0);
})
.attr("height", function (d) {
return y(d.y1) - y(d.y0);
})
.attr("fill", function (d) {
return '#bbbbbb';
});
}
function foreign(foreign) { /* added */
foreign
.attr("x", function (d) {
return x(d.x0);
})
.attr("y", function (d) {
return y(d.y0);
})
.attr("width", function (d) {
return x(d.x1) - x(d.x0);
})
.attr("height", function (d) {
return y(d.y1) - y(d.y0);
});
}
function name(d) {
return d.data.label;
}
function breadcrumbs(d) {
var res = "";
var sep = " > ";
d.ancestors().reverse().forEach(function(i){
res += name(i) + sep;
});
res = res
.split(sep)
.filter(function(i){
return i!== "";
})
.join(sep);
return res +
(d.parent
? " - Click to zoom out"
: " - Click inside square to zoom in");
}
});
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment