When this image rendered, I've was overwhelmed with the similarity to a neuron. There's really a beautiful correspondence between the bonkerfield and the brain that hadn't even occurred to me as I was building it. The bonkerfield of any information in a human mind could be traced down through the individual neurons that collectively record it. So it's really fitting that like in this classic illustration from Santiago Ramón y Cajal.
What's more amazing, was the how the parameters that I used to generate the random graph varied the the overall structure
See below for implementation details. I will be back soon with an analysis of how a few parameters can control the architecture of the complicated. And once I get a really good example where I can work through and compile the data, I'll update with a thorough explanation of a rendering of a true bonkerfield.
The rest of this article can help walk you through adapting my code and data to build your own bonkerfields.
I had to hand annotate a json document in the structure needed for d3 to render it using the d3 "bundle" layout. Even though my data isn't really any kind of hierarchy, I'm sort of hacking the format that the d3 hierarchical bundling wants.
The minimal format to make it work requires two things:
.
delimited naming structure to define the hierarchy I want everything to be drawn for the lowest nodes back up to the "paragraph" node, so I only need imports from the leafs to the paragraph node. To create a hierarchy for the leafs, I just needed to come up with higher order groupings that link things so their lines get drawn together on the way back up to the "paragraph" node. The following document shows a simple example that would take two leaves for bible
and melville
and link them back to the paragraph via mobydick
[ {"name":"paragraph.mobydick.bible","imports":["paragraph"]}, {"name":"paragraph.mobydick.melville","imports":["paragraph"]}, {"name":"paragraph.mobydick","imports":[]}, {"name":"paragraph","imports":[]}, ]
To generate the full vizualization I added a couple more routes for the information to flow up to the paragraph. I also added an additional time
, size
, and weight
params that would set the x-position, the thickness of the line, and the transparency of the stroke, respectively.
[ {"name":"paragraph.gap.visit.mom","time":100,"size":20,"weight":0.1,"imports":["paragraph"]}, {"name":"paragraph.gap.visit.memory","time":100,"size":10,"weight":0.01,"imports":["paragraph"]}, {"name":"paragraph.gap.blog.quotes","time":70,"size":10,"weight":0.05,"imports":["paragraph"]}, {"name":"paragraph.gap.blog.mobydick.bible","time":10,"size":10,"weight":0.01,"imports":["paragraph"]}, {"name":"paragraph.gap.blog.mobydick.melville","time":10,"size":10,"weight":0.1,"imports":["paragraph"]}, {"name":"paragraph.gap.blog.mobydick","time":70,"size":10,"weight":0.05,"imports":[]}, {"name":"paragraph.gap.visit","time":200,"size":10,"weight":0.05,"imports":[]}, {"name":"paragraph.gap.blog","time":200,"size":10,"weight":0.05,"imports":[]}, {"name":"paragraph.gap","time":300,"size":10,"weight":0.05,"imports":[]}, {"name":"paragraph","time":350,"size":10,"weight":0.05,"imports":[]}, {"name":"","time":350,"size":10,"weight":0.05,"imports":[]} ]
I also made a similar document to render the other side of the bonkerfield visualization.
Starting from the example code that I found on bl.ocks.org, I started making tweaks to the code. Most of them were fairly minor sizing issues and rotations of things. The only thing that was really crucial was figuring out how to set the x-position using the "time" field from the document. Without that, the layout would put everything on one vertical line, which doesn't quite work for me.
The only crucial modification was to the node location, which required adding two bits of code. The first was to modify the function that does the data load, in oder to transfer the data from the file into the node
object that d3 uses from rendering the nodes of the graph.
function find(name, data) { var node = map[name], i; if (!node) { node = map[name] = data || {name: name, children: []}; if (name.length) { node.parent = find(name.substring(0, i = name.lastIndexOf("."))); node.parent.children.push(node); node.key = name.substring(i + 1); > if (data){ > node.time = data.time; > }else{ > node.time = null; > } } > } else { > if(!node.time){ > if (data){ > node.time = data.time > } > } } return node; }
With that added, it's just necessary too use the time
attribute when rendering the node SVG and the link SVG.
... var line_post = d3.svg.line() ... .x(function(d) {return d.time;}) ... svg_post.selectAll(".node") ... .attr("transform", function(d) {return "translate(" + d.time + "," + d.x + ")";}) ...
For a little extra flair, I also wanted to make more widely dispersed data show up as a thicker line. To do that, I just added duplicated paths with randomly jittered endpoints. You can check the code if you want to see how I duplicated the paths, but for the random jitter I used a simple rough approximation of a normal distribution in x and y.
function myrandom(){ /* approximate a normal distribution (sort of) */ /* using straight uniform makes everything look square */ var r = 0; for(var i = 3; i > 0; i --){ r += Math.random(); } return 1.25*(r/3 - 0.5) } var line_pre = d3.svg.line() ... .x(function(d) {return d.time+myrandom()*d.size;}) .y(function(d) {return d.x+myrandom()*d.size; });
There's some additional styling that yu can check out in the code directly. To make the opposite side of the bonkerfield, I duplicated all the above, but inverted the x-axis by sutracting the x positions from width (eg .x(function(d) {return width - d.time+myrandom()*d.size;})
).
Since I didn't want to hand generate a really big graph, I used some python code to generate the structure for me. The function is really quite simple; it just randomly branches a tree with probability child_prob
, and then adds an arm to the tree that is also randomly chosen from time_inc_rand
.
def add_node(node_list, parent_name, parent_time, parent_size, parent_weight): name = parent_name+'.'+''.join([random.choice(string.ascii_letters) for n in range(5)]) imports = [center] time = parent_time - int(time_inc_min + time_inc_rand * random.random()) if time < 50: return while random.random() < child_prob: add_node(node_list, name, time, parent_size, parent_weight) while random.random() < child_prob: node_list.append({"name":name+'.'+''.join([random.choice(string.ascii_letters) for n in range(5)]), "time":time-5, "size":parent_size, "weight":parent_weight, "imports":[center]}) imports = [] node_list.append({"name":name,"time":time,"size":parent_size,"weight":parent_weight,"imports":[]})
However, the variety of output that it could generate by varying those parameters was astonishing to me. This led me on another analytical meandering into the shapes that develop from hierarchies with random children, random edge lengths, and random leaf dispersions. I've started a project with the code to do the generation of the json objects with random parameters. It's really fascinating, and I will add a link here when I finish exploring.
There's still much more to do to make the bonkerfield concept clearer. You can fork the code to render bonkerfields from either the simple or the complex bl.ocks, and the code for generating the json objects is on github. If someone out there likes this idea and would like to take it further, please feel free. Let me know what you come up with.