Skip to content

Instantly share code, notes, and snippets.

@louking
Last active November 7, 2017 22:51
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 louking/fd0fdbfc44e2fc0bd5bfb5e861c7222d to your computer and use it in GitHub Desktop.
Save louking/fd0fdbfc44e2fc0bd5bfb5e861c7222d to your computer and use it in GitHub Desktop.
overlapping labeled circles explode - select displays tip using d3-tip
license: apache-2.0
height: 500
scrolling: no
border: yes

Use d3.js and d3-tip.js to draw some circles. When circles are at same location there may be other attributes which are interesting about the overlapping circles. This is a method to separate those circles on the user interface to give the user separate handling. In addtion, the circles are labeled by location, and user can click on a lone circle to see metadata information.

circle {
fill: steelblue;
stroke: black;
stroke-width: 1.5px;
}
circle.handle {
fill: red;
}
text {
font-family: helvetica, sans-serif;
font-weight: bold;
}
.d3-tip {
line-height: 1.2;
font-size: small;
font-weight: bold;
font-family: sans-serif;
padding: 6px;
background: white;
border: 1px;
border-style: solid;
border-radius: 6px;
/* pointer-events: none; */
}
var rcircle = 10, // r for circle
dexp = rcircle * 4, // distance for explosion
durt = 500, // transition duration (msec)
textdy = 5, // a bit of a hack, trial and error
pi = Math.PI;
var t = d3.transition(durt);
// need #vis div for d3.tip to work
var vis = d3.select("#vis")
.append("svg")
.attr("width", 720)
.attr("height", 250);
var svg = d3.select("svg");
// Initialize tooltip - https://github.com/Caged/d3-tip
tip = d3.tip()
.direction('e')
.offset([0,rcircle+1])
.attr("class", "d3-tip")
// .attr("class", function(d) { "tip-" + d.loc })
.html(function(d) { return "ID = " + d.id
+ "<br/>multiple lines"
+ "<br/><a href='https://google.com' target=_blank>link test</a>";
});
// Invoke the tip in the context of visualization
vis.call(tip)
// remove tip when click anywhere other than circle group
svg.on("click", tip.hide);
d3.json(
"locations.json",
function(error, data) {
if (error) throw error;
// showing general pattern from https://bl.ocks.org/mbostock/3808218 but in this case only enter() will be executed
// group idea adapted from https://www.dashingd3js.com/svg-group-element-and-d3js
// data join
var group = svg.selectAll("g")
.data(data)
.enter().append("g")
// cx and cy don't normally apply to group, but this is convenient for later processing
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
.attr("class", function(d) { return "g-loc-" + d.loc; })
.attr("transform", "translate(0,0)")
.on("click", explodeData);
group.append("circle")
.attr("r", rcircle)
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
.attr("cid", function(d) { return d.id; })
.attr("class", function(d) { return "c-loc-" + d.loc; });
group.append("text")
.attr("x", function(d) { return d.x; })
.attr("y", function(d) { return d.y; })
.attr("text-anchor", "middle")
.attr("dy", function(d) { return textdy })
.attr("class", function(d) { return "t-loc-" + d.loc; })
.text(function(d) { return d.loc; });
}
);
// called with group containing circle, text
// if there are other groups in same location, explode
// else special handling for lone group
function explodeData(d, i) {
// Use D3 to select element and also all at same location
var thisg = d3.select(this);
var theselocs = d3.selectAll(".g-loc-" + d.loc)
var numlocs = theselocs.size();
// if only one at location, maybe there is some special processing
if (numlocs == 1) {
// handle single selection click
// don't let this through to svg click event
// http://bl.ocks.org/jasondavies/3186840
d3.event.stopPropagation();
tip.show(d);
// multiple at location, explode
} else {
// if not selected yet, explode all in same loc
if (!thisg.attr("exploded")) {
d3.select(this).raise();
theselocs.attr("exploded", true);
var cx = Number(thisg.attr("cx"));
var cy = Number(thisg.attr("cy"));
// create lines now so they're underneath
// p1 = p3 because we'll be transitioning
theselocs.each(function (d,i) {
svg.append('line')
.attr("class", "l-loc-" + d.loc)
.attr("x1", cx)
.attr("y1", cy)
.attr("x2", cx)
.attr("y2", cy)
.attr("stroke-width", 1.5)
.attr("stroke", "black")
.transition(durt)
.attr("x2", cx + dexp * Math.cos((2*pi/numlocs)*i))
.attr("y2", cy + dexp * Math.sin((2*pi/numlocs)*i))
});
// create handle for original location
svg.append("circle")
.attr("id", "exploded-" + d.loc)
.attr("class", "handle")
.attr("loc", d.loc)
.attr("r", rcircle)
.attr("cx", cx)
.attr("cy", cy)
.on("click", unexplodeData);
// explode
theselocs
.each(function(d, i){
var thisg = d3.select(this);
// transition to new location
thisg.raise().transition(t)
.attr("transform", "translate("
+ dexp * Math.cos((2*pi/numlocs)*i) + ","
+ dexp * Math.sin((2*pi/numlocs)*i) + ")"
);
});
// if exploded and individual selected, maybe there is some special processing
} else {
// handle single selection click
// don't let this through to svg click event
// http://bl.ocks.org/jasondavies/3186840
d3.event.stopPropagation();
tip.show(d);
}
} // multiple at location
};
// called with handle for an exploded group
function unexplodeData(d, i) {
// Use D3 to select element
var handle = d3.select(this);
var loc = handle.attr("loc");
var x = handle.attr("cx");
var y = handle.attr("cy");
var theselocs = d3.selectAll(".g-loc-" + loc);
// set exploded circles to original state
theselocs.transition(t)
.attr("selected", null)
.attr("transform", "translate(0,0)")
.attr("exploded", null);
// shrink lines
d3.selectAll(".l-loc-" + loc)
.transition(t)
.attr("x2", x)
.attr("y2", y)
.remove()
// remove handle
d3.select("#exploded-" + loc).remove();
};
<link href="circles.css" rel="stylesheet">
<div id="vis"></div>
<script src="//d3js.org/d3.v4.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3-tip/0.8.0-alpha.1/d3-tip.js"></script>
<script src="circles.js"></script>
[
{ "id":1, "loc":1, "x":140, "y":180 },
{ "id":2, "loc":1, "x":140, "y":180 },
{ "id":3, "loc":1, "x":140, "y":180 },
{ "id":4, "loc":1, "x":140, "y":180 },
{ "id":5, "loc":2, "x":520, "y":70 },
{ "id":6, "loc":2, "x":520, "y":70 },
{ "id":7, "loc":2, "x":520, "y":70 },
{ "id":8, "loc":2, "x":520, "y":70 },
{ "id":9, "loc":2, "x":520, "y":70 },
{ "id":10, "loc":2, "x":520, "y":70 },
{ "id":11, "loc":3, "x":320, "y":150 }
]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment