Based on http://bl.ocks.org/MoritzStefaner/1377729
A Pen by Andreas Borgen on CodePen.
| <script src="//d3js.org/d3.v3.js"></script> | |
| <label> | |
| <input type="checkbox" onchange="toggleFreehand(this.checked)" /> | |
| Freehand | |
| </label> |
Based on http://bl.ocks.org/MoritzStefaner/1377729
A Pen by Andreas Borgen on CodePen.
| 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; | |
| } |