Skip to content

Instantly share code, notes, and snippets.

@musically-ut
Last active July 7, 2016 18:56
Show Gist options
  • Save musically-ut/5278614 to your computer and use it in GitHub Desktop.
Save musically-ut/5278614 to your computer and use it in GitHub Desktop.
Bubble treemap with better labels

This is an example of how one might place the intermediate labels on circle-packing graphs using the circle-text plugin.

Transitions on position and radius of the path are now possible. Click on the circles to see them in action.

/*globals d3*/
(function () {
d3.circleText = function () {
"use strict";
var radius = function (d) { return d.r; },
value = function (d) { return d.value; },
fontSize = '100%',
method = "stretch", spacing = "auto",
position = "50%", precision = null;
function _draw(selection) {
selection.each(function (d, i) {
var g = d3.select(this);
// Reuse the old id, if present. Otherwise, generate a unique id
// for the path.
var oldPath = g.select('path.arc-path');
var arcId = (oldPath.node() && oldPath.attr('id')) ||
'id-' + guid();
g.selectAll('path.arc-path')
.data([d])
.enter()
.append('path')
.classed('arc-path', true)
.attr('id', arcId)
.attr('d', function (d) { return circle_d(radius(d)); });
d3.transition(g.select('path.arc-path'))
.attr('d', function (d) { return circle_d(radius(d)); });
var arcText = g.selectAll('text.arc-text').data([d]);
arcText.enter()
.append('text')
.classed('arc-text', true)
.attr('text-anchor', 'middle')
.append('textPath');
// Not transitioning the `font-size` style since getComptedStyles
// for it may not return the actual value which was set in CSS,
// e.g., "font-size: 100%" may return "16px" when the font-size
// is requested via getComptedStyles. Hence, the font-size will
// be unnecessarily transitioned.
// Also see: http://stackoverflow.com/a/10145250/987185
arcText.style('font-size', fontSize);
/* There is a bug in Chrome which makes it impossible to select
* camel case tags, like textPath. Hence, using the :first-child
* selector to select the embedded textPath element.
*
* https://github.com/mbostock/d3/issues/925
* https://bugs.webkit.org/show_bug.cgi?id=46800
* https://bugs.webkit.org/show_bug.cgi?id=83438
*
* Keep the textPath hidden until the best position
* has been found.
*/
d3.transition(arcText.select(':first-child'))
.attr('xlink:href', '#' + arcId)
.attr('method', method)
.attr('spacing', spacing)
.text(value)
.attr('startOffset', position);
});
return selection.selectAll('text.arc-text');
}
/***************************************
* Private functions
*/
/* Code for generating UUID version 4 from:
* http://note19.com/2007/05/27/javascript-guid-generator/
*/
function s4() {
return Math.floor((1 + Math.random()) * 0x10000)
.toString(16)
.substring(1);
}
function guid() {
return s4() + s4() + '-' + s4() + '-' + s4() + '-' +
s4() + '-' + s4() + s4() + s4();
}
function circle_d(r) {
return [ "M0,0",
"m", "0,", -r,
"a", r, ",", r, " 0 1,0 0,", 2*r,
"a", r, ",", r, " 0 1,0 0,", -2*r
].join('');
}
/***************************************
* Public properties
*/
_draw.radius = function (_) {
if (arguments.length === 0) return radius;
radius = d3.functor(_);
return _draw;
};
_draw.value = function (_) {
if (arguments.length === 0) return value;
value = d3.functor(_);
return _draw;
};
_draw.precision = function (_) {
try {
console.warn('circleText.precision has been deprecated.');
} catch (e) {
// Ignore if the warning could not be displayed.
}
if (arguments.length === 0) return precision;
precision = _;
return _draw;
};
_draw.fontSize = function (_) {
if (arguments.length === 0) return fontSize;
fontSize = d3.functor(_);
return _draw;
};
_draw.method = function (_) {
if (arguments.length === 0) return method;
method = _;
return _draw;
};
_draw.spacing = function (_) {
if (arguments.length === 0) return spacing;
spacing = _;
return _draw;
};
_draw.position = function (_) {
if (arguments.length === 0) return position;
position = _;
return _draw;
};
return _draw;
};
})();
<!DOCTYPE html>
<head>
<meta charset="utf-8">
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js"></script>
<script src="circle-text.js"></script>
<script>
var focussedNode = null, root = null;
var w = 800, h = 500, r = Math.min(w, h),
x = d3.scale.linear().range([(w - r)/2 + 0, (w - r)/2 + r]),
y = d3.scale.linear().range([(h - r)/2 + 0, (h - r)/2 + r]),
color = d3.scale.category10(),
offset = 50, swingDirection = 1;
var fontSize = function (d) { return (200 / (d.depth + 1)) + "%"; };
var circleText = d3.circleText()
.radius(function (d) { return d.r - 5; })
.value(function (d) { return d.name; })
.precision(0.1)
.fontSize(fontSize);
function _nest(values, depth) {
var firstLevel = {},
res = {};
res.name = values[0]
.slice((depth >= 1) ? depth - 1 : 0, depth)
.reverse()
.join('.');
values.forEach(function (v) {
if (v[depth]) {
if (!firstLevel.hasOwnProperty(v[depth])) {
firstLevel[v[depth]] = [];
}
firstLevel[v[depth]].push(v);
}
});
var nextLevels = Object.keys(firstLevel);
if (nextLevels.length > 0) {
res.children = [];
nextLevels.forEach(function (key) {
res.children.push(_nest(firstLevel[key], depth + 1));
});
}
// Collapsing domains
while ( res.children &&
res.children.length === 1) {
res.name = res.children[0].name+ '.' + res.name;
res.children = res.children[0].children;
}
return res;
}
function nest(domains) {
"use strict";
var splitDomains = domains.map(function (d) {
return d.split('.').reverse();
});
return _nest(splitDomains, 1);
}
function darkenRGB(strColor) {
var rgb = d3.rgb(strColor);
return rgb.darker().toString();
}
function _bubbleChart(vis, nestedDomains, opts) {
var packer = d3.layout.pack()
.value(function (d) { return 1; })
.size([opts.w, opts.h]);
var nodes = packer.nodes(nestedDomains);
root = nodes.filter(function (n) { return n.parent == null; })[0];
var gCircles = vis.selectAll('g.circle')
.data(nodes)
.enter()
.append('g')
.classed('circle', true)
.attr('transform', function (d) {
return 'translate(' + d.x + ',' + d.y + ')';
});
gCircles.append('circle')
.attr({
'class': function (d) { return d.children ? "parent" : "child"; },
cx: 0,
cy: 0,
r: function (d) { return d.r; }
})
.style({
stroke: function (d) { return darkenRGB("#777"); },
'stroke-width': '1px',
fill: function (d) { return "#777"; },
'fill-opacity': function (d) { return d.children ? 0.5 : 1; },
})
.on("click", function(d) {
zoom(focussedNode == d ? root : d);
d3.event.stopPropagation();
});
var gTexts = vis.selectAll('g.label')
.data(nodes)
.enter()
.append('g')
.classed('label', true)
.attr('transform', function (d) {
return 'translate(' + d.x + ',' + d.y + ')';
});
gTexts.filter(function (d) { return !!d.children; })
.call(circleText)
.style('fill', 'white');
gTexts.filter(function (d) { return !d.children; })
.append('text')
.attr('dy', '0.35em')
.style({
'text-anchor': "middle",
stroke: "none",
"font-size": fontSize,
fill: 'white',
})
.text(function (d) { return d.name; });
}
function draw(domains) {
"use strict";
var opts = {
w: w,
h: h,
x: x,
y: y,
color: color
};
var nestedDomains = nest(domains);
var vis = d3.select('#container')
.append('svg')
.attr({
width: w,
height: h
})
.append('g')
.attr('transform',
'translate(' + 0 + ',' + 0 + ')');
_bubbleChart(vis, nestedDomains, opts);
}
function zoom(d, i) {
var k = r / d.r / 2;
x.domain([d.x - d.r, d.x + d.r]);
y.domain([d.y - d.r, d.y + d.r]);
var t = d3.select('svg').transition()
.duration(d3.event.altKey ? 7500 : 750);
t.selectAll("g.circle")
.attr("transform", function(d) {
return 'translate(' + x(d.x) + ',' + y(d.y) + ')';
})
.select('circle')
.attr("r", function(d) {
return k * d.r;
});
// Swing the label from left to right.
swingDirection = swingDirection * ((offset >= 75 || offset <= 25) ? -1 : 1);
offset = offset + swingDirection * 25;
circleText
.radius(function (d) { return k * d.r - 5; })
.position(offset + '%');
t.selectAll('g.label')
.attr("transform", function(d) {
return 'translate(' + x(d.x) + ',' + y(d.y) + ')';
})
.filter(function (d) { return !!d.children; })
.call(circleText);
focussedNode = d;
d3.event.stopPropagation();
}
</script>
<style>
#container { width: 100%; }
svg {
display: block;
margin: auto;
}
text {
font-size: 11px;
pointer-events: none;
}
text.parent {
fill: #1f77b4;
}
circle {
pointer-events: all;
}
circle.parent {
stroke: steelblue;
}
circle.child {
pointer-events: none;
}
.arc-path {
visibility: hidden;
}
</style>
</head>
<body>
<div id="container"></div>
<script>
var domains = [
'00.names.playground.utkarshu.in',
'00.db.playground.utkarshu.in',
'00.server.playground.utkarshu.in',
'01.server.playground.utkarshu.in',
'new.stable.utkarshu.in',
'db.stable.utkarshu.in',
'mailbox.stable.utkarshu.in',
'data.stable.utkarshu.in',
'server.stable.utkarshu.in',
'login.utkarshu.in',
'00.redirects.fun.utkarshu.in',
'01.redirects.fun.utkarshu.in',
'private.experiments.utkarshu.in',
'public.experiments.utkarshu.in',
'00.serenity.utkarshu.in',
'01.serenity.utkarshu.in',
'integration.serenity.utkarshu.in',
'00.db.other.utkarshu.in',
'00.server.other.utkarshu.in',
'old.data.utkarshu.in',
'monitor.utkarshu.in',
'00.new.archive.utkarshu.in',
'00.db.archive.utkarshu.in',
'00.server.archive.utkarshu.in',
'00.redirects.unstable.utkarshu.in',
'01.redirects.unstable.utkarshu.in',
]
</script>
<script>
draw(domains);
</script>
</body>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment