Skip to content

Instantly share code, notes, and snippets.

@harlantwood
Last active September 8, 2018 17:45
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save harlantwood/1098889 to your computer and use it in GitHub Desktop.
Save harlantwood/1098889 to your computer and use it in GitHub Desktop.
CoffeeScript->Javascript D3 Force-Directed Graph
.idea
Cakefile.js
tmp/

I'm loving exploring the supernaturally awesome D3 Javascript information visualization framework.

Just one problem: I hate JavaScript.

Enter CoffeeScript, a fantastically slick modern language, drawing on Ruby and Python, which compiles seamlessly to cogent JavaScript.

So I converted the D3 Force-Directed Graph example from JavaScript to CoffeeScript.

The files included are:

  • app.original.js - the original JavaScript example from D3
  • app.coffee - my CoffeeScript translation of the above example
  • app.js - the CoffeeScript above compiled back into JavaScript

Since I'm learning CoffeeScript, I wanted to check that the compiled JavaScript matched the original JavaScript. This is not as simple as comparing the two files, as there are non-operational difference that I'd like to ignore. So I did what they do in CoffeeScript land: wrote a task in my Cakefile to check for differences. So if you have a CoffeeScript environment set up, you can compile the CoffeeScript to JavaScript with:

cake build

and you can check that the output is (pretty much) the same as the original with:

cake check

This will "sanitize" the JavaScript, ironing out minor distractions, and output the remaining differences (if any) in standard unix "diff" format.

If you are looking at this on Github, you can see a live demo here: bl.ocks.org/1098889

Please feel free to fork this gist, or send me a note, if you see things to improve.

Finally, a big thanks to Mike Bostock, the creator of both D3, the infoviz framework I've been seeking for a long time, and also the awesometown Gist viewer at bl.ocks.org.

w = 960
h = 500
fill = d3.scale.category20()
vis = d3.select("#chart")
.append("svg:svg")
.attr("width", w)
.attr("height", h)
d3.json "miserables.json", (json) ->
force = d3.layout.force()
.charge(-120)
.linkDistance(30)
.nodes(json.nodes)
.links(json.links)
.size([w, h])
.start();
link = vis.selectAll("line.link")
.data(json.links)
.enter().append("svg:line")
.attr("class", "link")
.style("stroke-width", (d) -> Math.sqrt(d.value) )
.attr("x1", (d) -> d.source.x )
.attr("y1", (d) -> d.source.y )
.attr("x2", (d) -> d.target.x )
.attr("y2", (d) -> d.target.y )
node = vis.selectAll("circle.node")
.data(json.nodes)
.enter().append("svg:circle")
.attr("class", "node")
.attr("cx", (d) -> d.x )
.attr("cy", (d) -> d.y )
.attr("r", 5)
.style("fill", (d) -> fill(d.group) )
.call(force.drag)
node.append("svg:title")
.text( (d) -> d.name )
vis.style("opacity", 1e-6)
.transition()
.duration(1000)
.style("opacity", 1)
force.on "tick", () ->
link.attr("x1", (d) -> d.source.x )
.attr("y1", (d) -> d.source.y )
.attr("x2", (d) -> d.target.x )
.attr("y2", (d) -> d.target.y )
node.attr("cx", (d) -> d.x )
.attr("cy", (d) -> d.y )
circle.node {
stroke: #fff;
stroke-width: 1.5px;
}
line.link {
stroke: #999;
stroke-opacity: .6;
}
(function() {
var fill, h, vis, w;
w = 960;
h = 500;
fill = d3.scale.category20();
vis = d3.select("#chart").append("svg:svg").attr("width", w).attr("height", h);
d3.json("miserables.json", function(json) {
var force, link, node;
force = d3.layout.force().charge(-120).linkDistance(30).nodes(json.nodes).links(json.links).size([w, h]).start();
link = vis.selectAll("line.link").data(json.links).enter().append("svg:line").attr("class", "link").style("stroke-width", function(d) {
return Math.sqrt(d.value);
}).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;
});
node = vis.selectAll("circle.node").data(json.nodes).enter().append("svg:circle").attr("class", "node").attr("cx", function(d) {
return d.x;
}).attr("cy", function(d) {
return d.y;
}).attr("r", 5).style("fill", function(d) {
return fill(d.group);
}).call(force.drag);
node.append("svg:title").text(function(d) {
return d.name;
});
vis.style("opacity", 1e-6).transition().duration(1000).style("opacity", 1);
return force.on("tick", function() {
link.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;
});
return node.attr("cx", function(d) {
return d.x;
}).attr("cy", function(d) {
return d.y;
});
});
});
}).call(this);
var w = 960,
h = 500,
fill = d3.scale.category20();
var vis = d3.select("#chart")
.append("svg:svg")
.attr("width", w)
.attr("height", h);
d3.json("miserables.json", function(json) {
var force = d3.layout.force()
.charge(-120)
.linkDistance(30)
.nodes(json.nodes)
.links(json.links)
.size([w, h])
.start();
var link = vis.selectAll("line.link")
.data(json.links)
.enter().append("svg:line")
.attr("class", "link")
.style("stroke-width", function(d) { return Math.sqrt(d.value); })
.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; });
var node = vis.selectAll("circle.node")
.data(json.nodes)
.enter().append("svg:circle")
.attr("class", "node")
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
.attr("r", 5)
.style("fill", function(d) { return fill(d.group); })
.call(force.drag);
node.append("svg:title")
.text(function(d) { return d.name; });
vis.style("opacity", 1e-6)
.transition()
.duration(1000)
.style("opacity", 1);
force.on("tick", function() {
link.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; });
node.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
});
});
{exec} = require 'child_process'
fs = require 'fs'
task 'build', 'Compile coffeescript', ->
exec 'coffee --compile app.coffee', (err, stdout, stderr) ->
throw err if err
console.log stdout + stderr
task 'check', 'Check whether compiled Javascript matches the original', ->
check_dir = './tmp/check'
exec "mkdir -p #{check_dir}", (err, stdout, stderr) ->
throw err if err
console.log stdout + stderr
orig = 'app.original.js'
compiled = 'app.js'
for filename in [ orig, compiled ]
do (filename) ->
fs.readFile filename, (err, contents) ->
sanitized_js = sanitize_js( contents.toString() )
fs.writeFile "#{check_dir}/#{filename}", sanitized_js, (err) ->
throw err if err
exec "diff -w #{check_dir}/#{orig} #{check_dir}/#{compiled}", (err, stdout, stderr) ->
console.log stdout + stderr
sanitize_js = (js) ->
replacements = [
# remove outer function wrapper that coffee compiler creates:
[ /^\s*\(function\(\)\s*\{/, '' ],
[ /\s*\}\)\.call\(this\);\s*$/, '' ],
# remove comments
[ /\/\/.*$/mg, '' ],
# remove "var " delclarations
[ /^\s*var\s+[^=]+$\n/mg, '' ],
[ /^\s*var\s+/mg, '' ],
# remove 'return' keyword
[ /[ \t]*return\b/mg, '' ],
# from "0.5" to ".5"
[ /(^|[^\w])0\./mg, '.' ],
# "{ foo; }" becomes "{\nfoo;\n}"
[ /\{([^\n]*\S)/mg, "{\n$1" ],
[ /(\S[^\n]*)\}/mg, "$1\n}" ],
# "foo(). \n bar()" becomes "foo().bar()"
[ /(\S)\s*\n\s*(\.[a-z])/mg, "$1$2" ],
# remove trailing commas and semicolons
[ /,\s*$/mg, '' ],
[ /;\s*$/mg, '' ],
# remove whitespace at the beginning/end of the file
[ /^\s+/, '' ],
[ /\s+$/, '' ],
]
for [ before, after ] in replacements
do (before, after) ->
js = js.replace before, after
js
<!DOCTYPE html>
<html>
<head>
<title>CoffeeScript->Javascript D3 Force-Directed Graph</title>
<link type="text/css" rel="stylesheet" href="app.css"/>
</head>
<body>
<div class="gallery" id="chart"> </div>
<script src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script>
<script type="text/javascript" src="app.js"> </script>
</body>
</html>
{"nodes":[{"name":"Myriel","group":1},{"name":"Napoleon","group":1},{"name":"Mlle.Baptistine","group":1},{"name":"Mme.Magloire","group":1},{"name":"CountessdeLo","group":1},{"name":"Geborand","group":1},{"name":"Champtercier","group":1},{"name":"Cravatte","group":1},{"name":"Count","group":1},{"name":"OldMan","group":1},{"name":"Labarre","group":2},{"name":"Valjean","group":2},{"name":"Marguerite","group":3},{"name":"Mme.deR","group":2},{"name":"Isabeau","group":2},{"name":"Gervais","group":2},{"name":"Tholomyes","group":3},{"name":"Listolier","group":3},{"name":"Fameuil","group":3},{"name":"Blacheville","group":3},{"name":"Favourite","group":3},{"name":"Dahlia","group":3},{"name":"Zephine","group":3},{"name":"Fantine","group":3},{"name":"Mme.Thenardier","group":4},{"name":"Thenardier","group":4},{"name":"Cosette","group":5},{"name":"Javert","group":4},{"name":"Fauchelevent","group":0},{"name":"Bamatabois","group":2},{"name":"Perpetue","group":3},{"name":"Simplice","group":2},{"name":"Scaufflaire","group":2},{"name":"Woman1","group":2},{"name":"Judge","group":2},{"name":"Champmathieu","group":2},{"name":"Brevet","group":2},{"name":"Chenildieu","group":2},{"name":"Cochepaille","group":2},{"name":"Pontmercy","group":4},{"name":"Boulatruelle","group":6},{"name":"Eponine","group":4},{"name":"Anzelma","group":4},{"name":"Woman2","group":5},{"name":"MotherInnocent","group":0},{"name":"Gribier","group":0},{"name":"Jondrette","group":7},{"name":"Mme.Burgon","group":7},{"name":"Gavroche","group":8},{"name":"Gillenormand","group":5},{"name":"Magnon","group":5},{"name":"Mlle.Gillenormand","group":5},{"name":"Mme.Pontmercy","group":5},{"name":"Mlle.Vaubois","group":5},{"name":"Lt.Gillenormand","group":5},{"name":"Marius","group":8},{"name":"BaronessT","group":5},{"name":"Mabeuf","group":8},{"name":"Enjolras","group":8},{"name":"Combeferre","group":8},{"name":"Prouvaire","group":8},{"name":"Feuilly","group":8},{"name":"Courfeyrac","group":8},{"name":"Bahorel","group":8},{"name":"Bossuet","group":8},{"name":"Joly","group":8},{"name":"Grantaire","group":8},{"name":"MotherPlutarch","group":9},{"name":"Gueulemer","group":4},{"name":"Babet","group":4},{"name":"Claquesous","group":4},{"name":"Montparnasse","group":4},{"name":"Toussaint","group":5},{"name":"Child1","group":10},{"name":"Child2","group":10},{"name":"Brujon","group":4},{"name":"Mme.Hucheloup","group":8}],"links":[{"source":1,"target":0,"value":1},{"source":2,"target":0,"value":8},{"source":3,"target":0,"value":10},{"source":3,"target":2,"value":6},{"source":4,"target":0,"value":1},{"source":5,"target":0,"value":1},{"source":6,"target":0,"value":1},{"source":7,"target":0,"value":1},{"source":8,"target":0,"value":2},{"source":9,"target":0,"value":1},{"source":11,"target":10,"value":1},{"source":11,"target":3,"value":3},{"source":11,"target":2,"value":3},{"source":11,"target":0,"value":5},{"source":12,"target":11,"value":1},{"source":13,"target":11,"value":1},{"source":14,"target":11,"value":1},{"source":15,"target":11,"value":1},{"source":17,"target":16,"value":4},{"source":18,"target":16,"value":4},{"source":18,"target":17,"value":4},{"source":19,"target":16,"value":4},{"source":19,"target":17,"value":4},{"source":19,"target":18,"value":4},{"source":20,"target":16,"value":3},{"source":20,"target":17,"value":3},{"source":20,"target":18,"value":3},{"source":20,"target":19,"value":4},{"source":21,"target":16,"value":3},{"source":21,"target":17,"value":3},{"source":21,"target":18,"value":3},{"source":21,"target":19,"value":3},{"source":21,"target":20,"value":5},{"source":22,"target":16,"value":3},{"source":22,"target":17,"value":3},{"source":22,"target":18,"value":3},{"source":22,"target":19,"value":3},{"source":22,"target":20,"value":4},{"source":22,"target":21,"value":4},{"source":23,"target":16,"value":3},{"source":23,"target":17,"value":3},{"source":23,"target":18,"value":3},{"source":23,"target":19,"value":3},{"source":23,"target":20,"value":4},{"source":23,"target":21,"value":4},{"source":23,"target":22,"value":4},{"source":23,"target":12,"value":2},{"source":23,"target":11,"value":9},{"source":24,"target":23,"value":2},{"source":24,"target":11,"value":7},{"source":25,"target":24,"value":13},{"source":25,"target":23,"value":1},{"source":25,"target":11,"value":12},{"source":26,"target":24,"value":4},{"source":26,"target":11,"value":31},{"source":26,"target":16,"value":1},{"source":26,"target":25,"value":1},{"source":27,"target":11,"value":17},{"source":27,"target":23,"value":5},{"source":27,"target":25,"value":5},{"source":27,"target":24,"value":1},{"source":27,"target":26,"value":1},{"source":28,"target":11,"value":8},{"source":28,"target":27,"value":1},{"source":29,"target":23,"value":1},{"source":29,"target":27,"value":1},{"source":29,"target":11,"value":2},{"source":30,"target":23,"value":1},{"source":31,"target":30,"value":2},{"source":31,"target":11,"value":3},{"source":31,"target":23,"value":2},{"source":31,"target":27,"value":1},{"source":32,"target":11,"value":1},{"source":33,"target":11,"value":2},{"source":33,"target":27,"value":1},{"source":34,"target":11,"value":3},{"source":34,"target":29,"value":2},{"source":35,"target":11,"value":3},{"source":35,"target":34,"value":3},{"source":35,"target":29,"value":2},{"source":36,"target":34,"value":2},{"source":36,"target":35,"value":2},{"source":36,"target":11,"value":2},{"source":36,"target":29,"value":1},{"source":37,"target":34,"value":2},{"source":37,"target":35,"value":2},{"source":37,"target":36,"value":2},{"source":37,"target":11,"value":2},{"source":37,"target":29,"value":1},{"source":38,"target":34,"value":2},{"source":38,"target":35,"value":2},{"source":38,"target":36,"value":2},{"source":38,"target":37,"value":2},{"source":38,"target":11,"value":2},{"source":38,"target":29,"value":1},{"source":39,"target":25,"value":1},{"source":40,"target":25,"value":1},{"source":41,"target":24,"value":2},{"source":41,"target":25,"value":3},{"source":42,"target":41,"value":2},{"source":42,"target":25,"value":2},{"source":42,"target":24,"value":1},{"source":43,"target":11,"value":3},{"source":43,"target":26,"value":1},{"source":43,"target":27,"value":1},{"source":44,"target":28,"value":3},{"source":44,"target":11,"value":1},{"source":45,"target":28,"value":2},{"source":47,"target":46,"value":1},{"source":48,"target":47,"value":2},{"source":48,"target":25,"value":1},{"source":48,"target":27,"value":1},{"source":48,"target":11,"value":1},{"source":49,"target":26,"value":3},{"source":49,"target":11,"value":2},{"source":50,"target":49,"value":1},{"source":50,"target":24,"value":1},{"source":51,"target":49,"value":9},{"source":51,"target":26,"value":2},{"source":51,"target":11,"value":2},{"source":52,"target":51,"value":1},{"source":52,"target":39,"value":1},{"source":53,"target":51,"value":1},{"source":54,"target":51,"value":2},{"source":54,"target":49,"value":1},{"source":54,"target":26,"value":1},{"source":55,"target":51,"value":6},{"source":55,"target":49,"value":12},{"source":55,"target":39,"value":1},{"source":55,"target":54,"value":1},{"source":55,"target":26,"value":21},{"source":55,"target":11,"value":19},{"source":55,"target":16,"value":1},{"source":55,"target":25,"value":2},{"source":55,"target":41,"value":5},{"source":55,"target":48,"value":4},{"source":56,"target":49,"value":1},{"source":56,"target":55,"value":1},{"source":57,"target":55,"value":1},{"source":57,"target":41,"value":1},{"source":57,"target":48,"value":1},{"source":58,"target":55,"value":7},{"source":58,"target":48,"value":7},{"source":58,"target":27,"value":6},{"source":58,"target":57,"value":1},{"source":58,"target":11,"value":4},{"source":59,"target":58,"value":15},{"source":59,"target":55,"value":5},{"source":59,"target":48,"value":6},{"source":59,"target":57,"value":2},{"source":60,"target":48,"value":1},{"source":60,"target":58,"value":4},{"source":60,"target":59,"value":2},{"source":61,"target":48,"value":2},{"source":61,"target":58,"value":6},{"source":61,"target":60,"value":2},{"source":61,"target":59,"value":5},{"source":61,"target":57,"value":1},{"source":61,"target":55,"value":1},{"source":62,"target":55,"value":9},{"source":62,"target":58,"value":17},{"source":62,"target":59,"value":13},{"source":62,"target":48,"value":7},{"source":62,"target":57,"value":2},{"source":62,"target":41,"value":1},{"source":62,"target":61,"value":6},{"source":62,"target":60,"value":3},{"source":63,"target":59,"value":5},{"source":63,"target":48,"value":5},{"source":63,"target":62,"value":6},{"source":63,"target":57,"value":2},{"source":63,"target":58,"value":4},{"source":63,"target":61,"value":3},{"source":63,"target":60,"value":2},{"source":63,"target":55,"value":1},{"source":64,"target":55,"value":5},{"source":64,"target":62,"value":12},{"source":64,"target":48,"value":5},{"source":64,"target":63,"value":4},{"source":64,"target":58,"value":10},{"source":64,"target":61,"value":6},{"source":64,"target":60,"value":2},{"source":64,"target":59,"value":9},{"source":64,"target":57,"value":1},{"source":64,"target":11,"value":1},{"source":65,"target":63,"value":5},{"source":65,"target":64,"value":7},{"source":65,"target":48,"value":3},{"source":65,"target":62,"value":5},{"source":65,"target":58,"value":5},{"source":65,"target":61,"value":5},{"source":65,"target":60,"value":2},{"source":65,"target":59,"value":5},{"source":65,"target":57,"value":1},{"source":65,"target":55,"value":2},{"source":66,"target":64,"value":3},{"source":66,"target":58,"value":3},{"source":66,"target":59,"value":1},{"source":66,"target":62,"value":2},{"source":66,"target":65,"value":2},{"source":66,"target":48,"value":1},{"source":66,"target":63,"value":1},{"source":66,"target":61,"value":1},{"source":66,"target":60,"value":1},{"source":67,"target":57,"value":3},{"source":68,"target":25,"value":5},{"source":68,"target":11,"value":1},{"source":68,"target":24,"value":1},{"source":68,"target":27,"value":1},{"source":68,"target":48,"value":1},{"source":68,"target":41,"value":1},{"source":69,"target":25,"value":6},{"source":69,"target":68,"value":6},{"source":69,"target":11,"value":1},{"source":69,"target":24,"value":1},{"source":69,"target":27,"value":2},{"source":69,"target":48,"value":1},{"source":69,"target":41,"value":1},{"source":70,"target":25,"value":4},{"source":70,"target":69,"value":4},{"source":70,"target":68,"value":4},{"source":70,"target":11,"value":1},{"source":70,"target":24,"value":1},{"source":70,"target":27,"value":1},{"source":70,"target":41,"value":1},{"source":70,"target":58,"value":1},{"source":71,"target":27,"value":1},{"source":71,"target":69,"value":2},{"source":71,"target":68,"value":2},{"source":71,"target":70,"value":2},{"source":71,"target":11,"value":1},{"source":71,"target":48,"value":1},{"source":71,"target":41,"value":1},{"source":71,"target":25,"value":1},{"source":72,"target":26,"value":2},{"source":72,"target":27,"value":1},{"source":72,"target":11,"value":1},{"source":73,"target":48,"value":2},{"source":74,"target":48,"value":2},{"source":74,"target":73,"value":3},{"source":75,"target":69,"value":3},{"source":75,"target":68,"value":3},{"source":75,"target":25,"value":3},{"source":75,"target":48,"value":1},{"source":75,"target":41,"value":1},{"source":75,"target":70,"value":1},{"source":75,"target":71,"value":1},{"source":76,"target":64,"value":1},{"source":76,"target":65,"value":1},{"source":76,"target":66,"value":1},{"source":76,"target":63,"value":1},{"source":76,"target":62,"value":1},{"source":76,"target":48,"value":1},{"source":76,"target":58,"value":1}]}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment