Skip to content

Instantly share code, notes, and snippets.

@guglielmo
Last active September 2, 2017 06:23
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/3b6440d98cb852237a18f8447ee7b32f to your computer and use it in GitHub Desktop.
Save guglielmo/3b6440d98cb852237a18f8447ee7b32f to your computer and use it in GitHub Desktop.
d3.js v4 - Reusable zoomable treemap
license: gpl-3.0
height: 600

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

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)

This is based on the original work of Jacques Jahnichen.

A complete zoomable treemap can be created using the d3ZoomableTreemap function, after having included the d3-zoomable-treemap.js script and the d3-zoomable-treemap.css stylesheet. See index.html for usage.

Configuration options can be passed as an object.

{
"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
}
}
]
}
]
}
]
}
#chart {
max-width: 100%;
overflow: auto;
}
text {
pointer-events: none;
}
.grandparent text {
font-family: "Open Sans", Helvetica, Arial, sans-serif;
font-weight: bold;
}
rect {
stroke: #fff;
stroke-width: 1px;
}
rect.parent,
.grandparent rect {
stroke-width: 3px;
}
.grandparent:hover rect {
fill: #ED8E7D;
}
.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;
}
.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;
}
function d3ZoomableTreemap(el_id, data, options) {
options = options || {};
// options and defaults
var sum_function =
options.sum_function || function (d) {
return d.value ? 1 : 0;
},
sort_function =
options.sort_function || function (a, b) {
return b.height - a.height || b.value - a.value;
},
margin_top =
options.margin_top === undefined ? 30 : options.margin_top,
margin_left =
options.margin_left === undefined ? 0 : options.margin_left,
margin_right =
options.margin_right === undefined ? 0 : options.margin_right,
margin_bottom =
options.margin_bottom === undefined ? 20 : options.margin_bottom,
full_height =
options.height === undefined ? 600 : options.height,
full_width =
options.width || document.getElementById(el_id).offsetWidth,
formatNumber =
options.format_number || d3.format(","),
navigation_height =
options.navigation_height === undefined
? 40 : options.navigation_height,
zoom_out_msg =
options.zoom_out_msg || " - Click here to zoom out",
zoom_in_msg =
options.zoom_in_msg || " - Click inside squares to zoom in",
fill_color =
options.fill_color || "#bbbbbb",
debug =
options.debug === undefined ? false : options.debug
;
var margin = {
top: margin_top,
right: margin_right,
bottom: margin_bottom,
left: margin_left
},
width = full_width - margin.left - margin.right,
height = full_height - margin.top - margin.bottom,
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)
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
.append("g")
.style("shape-rendering", "crispEdges");
var grandparent = svg.append("g")
.attr("class", "grandparent");
grandparent.append("rect")
.attr("width", width)
.attr("height", navigation_height)
.attr("fill", fill_color);
grandparent.append("text")
.attr("x", 6)
.attr("y", 6)
.attr("dy", "1em");
treemap(data
.sum(sum_function)
.sort(sort_function)
);
if (debug)
console.log(data);
display(data);
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 fill_color
});
var g1 = svg.insert("g", ".grandparent")
.datum(d)
.attr("class", "depth")
.attr("transform", "translate(0," + navigation_height + ")");
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 title(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 title(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 fill_color;
});
}
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 title(d) {
return name(d) + ": " + formatNumber(d.value);
}
function name(d) {
return d.data.label.toUpperCase();
}
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 ? zoom_out_msg : zoom_in_msg);
}
}
<meta charset="utf-8">
<link rel="stylesheet" href="d3-zoomable-treemap.css">
<p id="chart"></p>
<script src="//d3js.org/d3.v4.min.js"></script>
<script src="d3-zoomable-treemap.js"></script>
<script>
// used to extract value from the budget
function abs_in_year(year, values) {
val = values.filter(function (el) {
var key = parseInt(Object.keys(el));
return (key === year);
})[0][year]['abs'];
return val;
}
// locale format defaults
// ex: € 5,6 Mld
d3.formatDefaultLocale({
decimal: ',',
thousands: '.',
currency: ["€", ""]
});
formatSi = d3.format("$.2s");
function formatAbbreviation(x) {
var s = formatSi(x);
switch (s[s.length - 1]) {
case "G": return s.slice(0, -1) + " Mld";
case "M": return s.slice(0, -1) + " Mln";
}
return s;
}
d3.json("bilancio.json", function(data) {
var root = d3.hierarchy(data);
d3ZoomableTreemap(
'chart', root,
{
sum_function: function(d) {
if (!d.hasOwnProperty('children'))
return abs_in_year(2016, d.values);
else
return 0.0;
},
height: 550,
zoom_out_msg: " - Click here to zoom out",
zoom_in_msg: " - Click in squares to zoom in",
fill_color: "#EDC4BD",
format_number: formatAbbreviation
}
);
});
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment