Skip to content

Instantly share code, notes, and snippets.

@harlantwood
Last active March 19, 2018 16:08
Show Gist options
  • Star 7 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save harlantwood/1091420 to your computer and use it in GitHub Desktop.
Save harlantwood/1091420 to your computer and use it in GitHub Desktop.
CoffeeScript->Javascript D3 Force-Directed Layout (Multiple Foci)
.idea
Cakefile.js
tmp/

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

So I took my favorite D3 visualization, the Multiple Foci Force-Directed Layout, and converted it to CoffeeScript, a fantastically slick modern language, drawing on Ruby and Python, which compiles seamlessly to cogent JavaScript.

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/1091420

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 bl.ocks.org.

w = 960
h = 500
fill = d3.scale.category10()
nodes = d3.range(100).map(Object)
vis = d3.select("#chart").append("svg:svg")
.attr("width", w)
.attr("height", h)
force = d3.layout.force()
.nodes(nodes)
.links([])
.size([w, h])
.start()
node = vis.selectAll("circle.node")
.data(nodes)
.enter().append("svg:circle")
.attr("class", "node")
.attr("cx", (d) -> d.x)
.attr("cy", (d) -> return d.y)
.attr("r", 8)
.style("fill", (d, i) -> fill(i & 3) )
.style("stroke", (d, i) -> d3.rgb(fill(i & 3)).darker(2) )
.style("stroke-width", 1.5)
.call(force.drag)
vis.style("opacity", 1e-6)
.transition()
.duration(1000)
.style("opacity", 1)
force.on "tick", (e) ->
# Push different nodes in different directions for clustering.
k = 6 * e.alpha
nodes.forEach (o, i) ->
o.x += if i & 2 then k else -k
o.y += if i & 1 then k else -k
node.attr("cx", (d) -> d.x )
.attr("cy", (d) -> d.y )
d3.select("body").on "click", () ->
nodes.forEach (o, i) ->
o.x += (Math.random() - 0.5) * 40
o.y += (Math.random() - 0.5) * 40
force.resume()
// Generated by CoffeeScript 1.6.3
var fill, force, h, node, nodes, vis, w;
w = 960;
h = 500;
fill = d3.scale.category10();
nodes = d3.range(100).map(Object);
vis = d3.select("#chart").append("svg:svg").attr("width", w).attr("height", h);
force = d3.layout.force().nodes(nodes).links([]).size([w, h]).start();
node = vis.selectAll("circle.node").data(nodes).enter().append("svg:circle").attr("class", "node").attr("cx", function(d) {
return d.x;
}).attr("cy", function(d) {
return d.y;
}).attr("r", 8).style("fill", function(d, i) {
return fill(i & 3);
}).style("stroke", function(d, i) {
return d3.rgb(fill(i & 3)).darker(2);
}).style("stroke-width", 1.5).call(force.drag);
vis.style("opacity", 1e-6).transition().duration(1000).style("opacity", 1);
force.on("tick", function(e) {
var k;
k = 6 * e.alpha;
nodes.forEach(function(o, i) {
o.x += i & 2 ? k : -k;
return o.y += i & 1 ? k : -k;
});
return node.attr("cx", function(d) {
return d.x;
}).attr("cy", function(d) {
return d.y;
});
});
d3.select("body").on("click", function() {
nodes.forEach(function(o, i) {
o.x += (Math.random() - 0.5) * 40;
return o.y += (Math.random() - 0.5) * 40;
});
return force.resume();
});
var w = 960,
h = 500,
fill = d3.scale.category10(),
nodes = d3.range(100).map(Object);
var vis = d3.select("#chart").append("svg:svg")
.attr("width", w)
.attr("height", h);
var force = d3.layout.force()
.nodes(nodes)
.links([])
.size([w, h])
.start();
var node = vis.selectAll("circle.node")
.data(nodes)
.enter().append("svg:circle")
.attr("class", "node")
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
.attr("r", 8)
.style("fill", function(d, i) { return fill(i & 3); })
.style("stroke", function(d, i) { return d3.rgb(fill(i & 3)).darker(2); })
.style("stroke-width", 1.5)
.call(force.drag);
vis.style("opacity", 1e-6)
.transition()
.duration(1000)
.style("opacity", 1);
force.on("tick", function(e) {
// Push different nodes in different directions for clustering.
var k = 6 * e.alpha;
nodes.forEach(function(o, i) {
o.x += i & 2 ? k : -k;
o.y += i & 1 ? k : -k;
});
node.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
});
d3.select("body").on("click", function() {
nodes.forEach(function(o, i) {
o.x += (Math.random() - .5) * 40;
o.y += (Math.random() - .5) * 40;
});
force.resume();
});
{exec} = require 'child_process'
fs = require 'fs'
task 'build', 'Compile coffeescript', ->
exec 'coffee --compile --bare 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 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 Layout (Multiple Foci)</title>
<script src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script>
</head>
<body>
<div id="chart"></div>
<script type="text/javascript" src="app.js"></script>
</body>
</html>
@DanielBaird
Copy link

If you used coffee's --bare arg, coffeescript wouldn't add a function wrapper, and you wouldn't have to strip it out in your sanitise task.

@harlantwood
Copy link
Author

Thanks @DanielBaird, I did as you suggested.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment