Skip to content

Instantly share code, notes, and snippets.

@Sphinxxxx
Last active October 3, 2017 19:16
Show Gist options
  • Save Sphinxxxx/232cb4c6fe832bbfc0e11f2947f762f7 to your computer and use it in GitHub Desktop.
Save Sphinxxxx/232cb4c6fe832bbfc0e11f2947f762f7 to your computer and use it in GitHub Desktop.
Label placement using d3 force layout
<script src="//d3js.org/d3.v3.js"></script>
<label>
<input type="checkbox" onchange="toggleFreehand(this.checked)" />
Freehand
</label>
let vis, nodes;
/* First, create (and render) a set of fixed nodes */
function createNodes() {
//https://www.merriam-webster.com/word-of-the-day/calendar
const labels = "confrere,enthrall,vociferous,fruition,scrupulous,robot,propagate,extemporaneous,disport,conversant,bibelot,scour,precocious,marginalia,inoculate,travesty,portentous,amanuensis,glabrous,holus-bolus,shofar,yeasty,toothsome,legerity,diminution,arbitrary,splenetic,manumit,regimen,chthonic,schadenfreude,immense,garble,élan,Pandemonium,temporize,vermicular,bifurcate,lamster,perfunctory,oppugn,waif,alleviate,depredate,hebetude,nobby,flagrant,opine,picaresque,burgle,accoutrement,emissary,coalesce,interstice,soi-disant,sericeous,dithyramb,construe,inalienable,flat-hat,poltroon,ludic,turpitude,bosky,assay,repudiate,onerous,meme,savant,copacetic,yawp,steadfast,vindicate,edacious,crucible,gauche,tare,advise,haphazard,wreak,akimbo,fester,inhere,melee,bona fides,plausible,valedictory,plagiary,oracular,redoubt,adumbrate,catercorner,incoherent,penchant,squinny,ascetic,calaboose,oftentimes,engender,moue,loquacious,yips,haywire,garner,animus,pervade,duende,bilious,threshold,volplane,argy-bargy,perspicuous,scapegrace,tristful,culminate,fidelity,fey,majordomo,refurbish,visceral,acronym,demean,tetchy,panacea,lanuginous,erstwhile,supposititious,grok,microcosm,indurate,peregrinate,hoodlum,castigate,luscious,rebus,baroque,philately,acerbic,nudnik,malleable,inanition,torpedo,concatenate,unabashed,skosh,abdicate,whirligig,vaticination,ignoble,brachiate,gimcrack,defile,cloying,flack,alow,hyperbole,widdershins,livelong,snaffle,napery,magnanimous,pittance,interminable,reciprocate,grimalkin,tatterdemalion,eighty-six,bucolic,cartographer,factoid,ambiguous,hummock,upbraid,junket,slough,lethargic,orientate,exponent,laconic,cabotage,nugatory,affable,postulate,syncretism,ramify,ominous,tattoo,minuscule,vade mecum,irenic,besmirch,gadzookery,decry,effulgence,furtive,chaffer,hackle,ameliorate,lief,watershed,nightmare,unreconstructed,perpend,sarcasm,retrospective,Occam's razor,transpicuous,munificent,hard-boiled,imprecate,abyssal,grandee,luculent,extremophile,nexus,carceral,peradventure,adjure,ragtag,weltanschauung,transpontine,billet-doux,voluble,sward,cachet,protean,bemuse,onus,distaff,mayhap,finesse,kudos,hoary,indigenous,shunpike,genuflect,rejuvenate,baleful,oligopsony,deem,maelstrom,factitious,beguile,haberdasher,immutable,jitney,gambol,lachrymose,effrontery,neologism,cantankerous,paladin,abstemious,raiment,whimsical,tenet,xylography,vanguard,sanction,uncouth,ab ovo,barbican,omnibus,dander,macerate,fastidious,kibitzer,thaumaturgy,wane,vulpine,muckrake,ziggurat,salient,bamboozle,qui vive,daedal,objurgation,fillip,Methuselah,hors de combat,kapellmeister,jubilee,impetuous,lave,gallimaufry,nosocomial,eternize,purlieu,crepuscular,ruminate,dreidel,wassail,Kwanzaa,veridical,ultima,beleaguer,solicitous,zeitgeist,gravid,temerarious,echelon,dicker,cavalcade,mollify,auriferous,bully pulpit,triptych,elicit,ukase,facetious,sabot,hoke,quid pro quo,jejune,obfuscate,lambent,meshuggener,nictitate,Kafkaesque,protocol,impute,riddle,genteel,wistful,dynasty,vicissitude,cabbage,soporific,empyreal,deliquesce,guerdon,banausic,invective,vulnerary,macadam,truncate,odious,roister,scion,phlegmatic,univocal,nemesis,waggish,lavation,ab initio,jacquerie,colubrine,hoick,evanescent,frieze,glaucous,domicile,imbue,beatific,myriad,variegated,osculate,titivate,sepulchre,oenophile,adjuvant,quodlibet,asperse,sagacious,will-o'-the-wisp,untoward,bevy,sylvan,devolve,myrmidon,flippant,kibosh,hare,implacable,juggernaut,gadarene,loll,eclogue,nefarious,consigliere,palpable,mettle,reconcile,iota,tantivy,peculiar,vamoose,cabal,woebegone".split(','),
labelDistance = 8,
nodeCount = 66,
w = 800,
h = 600;
vis = d3.select("body").append("svg:svg")
.attr("width", w).attr("height", h);
function randInt(max) {
return Math.floor(Math.random() * max);
}
nodes = [];
for(var i = 0; i < nodeCount; i++) {
var node = {
label : labels[randInt(labels.length)],
labelDistance: labelDistance,
x: randInt(w*.6) + w*.2,
y: randInt(h*.8) + h*.1,
};
nodes.push(node);
}
var node = vis.selectAll("g.node").data(nodes).enter().append("svg:g").attr("class", "node");
node.append("svg:circle").attr("r", 5).attr("cx", d => d.x).attr("cy", d => d.y);
/*
//Create dummy text elements and calculate the actual width of our labels:
node.append("svg:text").attr("class", "label").text(d => d.label);
node.each(function(d, i) {
//d.llen = this.childNodes[1].clientWidth;
//Firefox/IE:
d.llen = this.childNodes[1].getBoundingClientRect().width;
});
//https://stackoverflow.com/questions/16260285/d3-removing-elements
vis.selectAll(".node text").remove();
*/
}
createNodes();
/* Create a force layout for the nodes' labels */
function renderLabels(nodes, svg) {
//https://stackoverflow.com/questions/21990857/d3-js-how-to-get-the-computed-width-and-height-for-an-arbitrary-element
//http://es6-features.org/#ObjectMatchingDeepMatching
const { width: w, height: hh } = svg.node().getBoundingClientRect();
var labelAnchors = [];
var labelAnchorLinks = [];
for(var i = 0; i < nodes.length; i++) {
const node = nodes[i];
labelAnchors.push({
node: node,
_ldist: node.labelDistance || 0,
x: node.x,
y: node.y,
});
labelAnchors.push({
node: node,
_ldist: node.labelDistance || 0,
x: node.x + ((node.x < w/2) ? -100 : 100),
y: node.y, //+ ((node.y < h/2) ? -10 : 10),
});
};
for(var i = 0; i < nodes.length; i++) {
labelAnchorLinks.push({
source : i * 2,
target : i * 2 + 1,
});
};
//Render the UI before we set up the force,
//because we need to know the length of each label:
var anchorLink = svg.selectAll("line.anchorLink").data(labelAnchorLinks).enter().append("svg:line").attr("class", "anchorLink");
var anchorNode = svg.selectAll("g.anchorNode").data(labelAnchors).enter().append("svg:g").attr("class", "anchorNode");
anchorNode.append("svg:circle").attr("r", 10).attr('class', (d, i) => (i % 2) ? 'drag' : '');
anchorNode.append("svg:text").attr("class", "label")
//Add labels to all the anchors (i.e. duplicate labels),
//because we need to measure the label length at the draggable outliers which do all the forcing:
.text((d, i) => /*(i % 2) ? '' :*/ `${d.node.label}`);
//Measure the length of each label's <text> element:
anchorNode.each(function(d, i) {
//d.llen = this.childNodes[1].clientWidth;
//Firefox/IE:
d._llen = this.childNodes[1].getBoundingClientRect().width;
});
//Now that we have measured our node labels, remove the duplicate labels:
//https://stackoverflow.com/questions/16260285/d3-removing-elements
svg.selectAll(".anchorNode circle.drag ~ text").remove();
const force2 = d3.layout.force()
.size([w, h])
.nodes(labelAnchors)
.links(labelAnchorLinks)
//https://stackoverflow.com/questions/34355120/d3-js-linkstrength-influence-on-linkdistance-in-a-force-graph/34376334#34376334
//https://github.com/d3/d3-3.x-api-reference/blob/master/Force-Layout.md
.gravity(0)
.linkDistance(x => x.source._ldist + (x.source._llen/2))
.linkStrength(10)
//.charge(-100)
.charge(d => {
return -d._llen;
})
.chargeDistance(100)
;
force2.start();
anchorNode.call(force2.drag);
function updateLink() {
this.attr("x1", function(d) {
return d.source.x;
}).attr("y1", function(d) {
return d.source.y;
}).attr("x2", function(d) {
return d.target.x;
}).attr("y2", function(d) {
return d.target.y;
});
}
function updateNode() {
this.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
});
}
force2.on("tick", function() {
anchorNode.each(function(d, i) {
if(i % 2 === 0) {
d.x = d.node.x;
d.y = d.node.y;
var text = this.childNodes[1],
target = labelAnchors[i+1];
//https://gist.github.com/conorbuck/2606166
var angleDeg = Math.round(Math.atan2(target.y - d.y, target.x - d.x) * 180/Math.PI);
if(angleDeg < 0) { angleDeg += 360; }
if((angleDeg > 90) && (angleDeg < 270)) {
text.setAttribute('text-anchor', 'end');
text.setAttribute('x', -d._ldist);
text.setAttribute('transform', `rotate(${angleDeg-180} ${0} ${0})`);
}
else {
text.setAttribute('text-anchor', 'start');
text.setAttribute('x', d._ldist);
text.setAttribute('transform', `rotate(${angleDeg} ${0} ${0})`);
}
}
//d.x = Math.round(d.x);
//d.y = Math.round(d.y);
});
anchorNode.call(updateNode);
anchorLink.call(updateLink);
});
return force2;
}
let labelForce = renderLabels(nodes, vis);
function toggleFreehand(free) {
//https://stackoverflow.com/questions/22121682/how-to-set-d3-layout-force-charge-once-d3-layout-force-has-already-been-initiali
labelForce = free ? labelForce.charge(d => 0)
: labelForce.charge(d => -d._llen);
labelForce.start();
}
label {
display: table;
cursor: pointer;
}
.node circle {
fill: blue;
}
text.label {
font-size: 20px;
dominant-baseline: middle;
pointer-events: none;
}
.anchorNode circle {
fill: transparent;
stroke: gainsboro;
&.drag {
fill: rgba(yellow, .3);
stroke: gold;
cursor: move;
}
}
.anchorLink {
stroke: gainsboro;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment