|
graph = { |
|
nodes: [ |
|
{id: 0, type: 'class', terms: [], label: 'Thing'}, |
|
{id: 1, type: 'class', terms: [], label: 'Physical_Object'}, |
|
{id: 2, type: 'class', terms: ['Stellula'], label: 'Celestial_Body'}, |
|
{id: 3, type: 'class', terms: [], label: 'Wandering_Star'}, |
|
{id: 4, type: 'class', terms: ['inerrans stella'], label: 'Fixed_Star'}, |
|
{id: 5, type: 'class', label: 'Galilean_Moon', terms: []}, |
|
{id: 6, type: 'instance', terms: [], label: 'Moon'}, |
|
{id: 7, type: 'instance', terms: [], label: 'Earth'}, |
|
{id: 8, type: 'instance', terms: ['Iuppiter'], label: 'Jupiter'}, |
|
{id: 9, type: 'instance', terms: []}, |
|
{id: 10, type: 'instance', terms: []}, |
|
{id: 11, type: 'instance', terms: []} |
|
], |
|
links: [ |
|
{id: 1, source: 1, target: 0, type: 'is_a'}, |
|
{id: 2, source: 2, target: 1, type: 'is_a'}, |
|
{id: 3, source: 3, target: 2, type: 'is_a'}, |
|
{id: 4, source: 4, target: 2, type: 'is_a'}, |
|
{id: 5, source: 5, target: 4, type: 'is_a'}, |
|
{id: 6, source: 6, target: 3, type: 'instance_of'}, |
|
{id: 7, source: 7, target: 3, type: 'instance_of'}, |
|
{id: 8, source: 8, target: 3, type: 'instance_of'}, |
|
{id: 9, source: 9, target: 5, type: 'instance_of'}, |
|
{id: 10, source: 10, target: 5, type: 'instance_of'}, |
|
{id: 11, source: 11, target: 5, type: 'instance_of'}, |
|
{id: 13, source: 9, target: 8, type: 'is_near'}, |
|
{id: 14, source: 10, target: 8, type: 'is_near'}, |
|
{id: 15, source: 11, target: 8, type: 'is_near'} |
|
]} |
|
|
|
|
|
### objectify the graph ### |
|
### resolve node IDs (not optimized at all!) ### |
|
for l in graph.links |
|
for n in graph.nodes |
|
if l.source is n.id |
|
l.source = n |
|
|
|
if l.target is n.id |
|
l.target = n |
|
|
|
### store the node index into the node itself ### |
|
for n, i in graph.nodes |
|
n.i = i |
|
|
|
### store neighbor nodes into each node ### |
|
for n, i in graph.nodes |
|
n.in_neighbors = [] |
|
n.out_neighbors = [] |
|
|
|
for l in graph.links |
|
l.source.out_neighbors.push l.target |
|
l.target.in_neighbors.push l.source |
|
|
|
### compute longest distances ### |
|
topological_order = tsort(graph.links.filter((l) -> l.type is 'is_a' or l.type is 'instance_of').map (l) -> [l.source.i, l.target.i]) |
|
# console.debug 'Topological order: ' + topological_order |
|
|
|
for n in graph.nodes |
|
n.longest_dist = -Infinity |
|
|
|
# Thing (0) is the root node |
|
graph.nodes[0].longest_dist = 0 |
|
|
|
for i in topological_order.reverse() |
|
n = graph.nodes[i] |
|
for nn in n.in_neighbors |
|
if nn.longest_dist < n.longest_dist+1 |
|
nn.longest_dist = n.longest_dist+1 |
|
|
|
# console.debug graph.nodes |
|
|
|
graph.constraints = [] |
|
|
|
### create the alignment contraints ### |
|
levels = _.uniq graph.nodes.filter((n) -> n.type is 'class' or n.type is 'instance').map (n) -> n.longest_dist |
|
# console.log levels |
|
levels.forEach (depth) -> |
|
graph.constraints.push { |
|
type: 'alignment', |
|
axis: 'y', |
|
offsets: graph.nodes.filter((n) -> n.longest_dist is depth).map (n) -> { |
|
node: n.i, |
|
offset: 0 |
|
} |
|
} |
|
|
|
R = 18 |
|
PROP_R = 1 |
|
PAD_MULTIPLIER = 3.5 |
|
LABEL_WIDTH = 100 |
|
|
|
### create the position contraints ### |
|
levels.forEach (depth, i) -> |
|
if i < levels.length-1 |
|
n1 = _.find graph.nodes, (n) -> n.longest_dist is depth |
|
n2 = _.find graph.nodes, (n) -> n.longest_dist is depth+1 |
|
graph.constraints.push { |
|
axis: 'y', |
|
left: n1.i, |
|
right: n2.i, |
|
gap: 2*R |
|
} |
|
|
|
svg = d3.select('svg') |
|
width = svg.node().getBoundingClientRect().width |
|
height = svg.node().getBoundingClientRect().height |
|
|
|
defs = svg.append('defs') |
|
|
|
vis = svg.append('g') |
|
.attr |
|
transform: 'translate(20,80)' |
|
|
|
### define arrow markers for graph links ### |
|
defs.append('marker') |
|
.attr |
|
id: 'end_arrow_is_a' |
|
viewBox: '-2 -2 10 14' |
|
refX: 5+R |
|
refY: 5 |
|
markerWidth: 6.3 |
|
markerHeight: 4.5 |
|
orient: 'auto' |
|
.append('path') |
|
.attr |
|
d: 'M0,0 L0,10 L9,5 z' |
|
|
|
defs.append('marker') |
|
.attr |
|
id: 'end_arrow_instance_of' |
|
viewBox: '-2 -2 10 14' |
|
refX: 5+R |
|
refY: 5 |
|
markerWidth: 6.3 |
|
markerHeight: 4.5 |
|
orient: 'auto' |
|
.append('path') |
|
.attr |
|
d: 'M0,0 L0,10 L9,5 z' |
|
|
|
### create nodes and links ### |
|
links = vis.selectAll('.link') |
|
.data(graph.links, (d) -> d.id) |
|
|
|
enter_links = links |
|
.enter().append('line') |
|
.attr('class', (d) -> "link #{d.type}") |
|
|
|
enter_links.append('title') |
|
.text (d) -> |
|
s = if d.source.label? then d.source.label.toUpperCase() else "Unlabeled #{d.source.type}" |
|
t = if d.target.label? then d.target.label.toUpperCase() else "Unlabeled #{d.target.type}" |
|
|
|
return "#{s} #{d.type} #{t}" |
|
|
|
nodes = vis.selectAll('.node') |
|
.data(graph.nodes, (d) -> d.id) |
|
|
|
enter_nodes = nodes.enter().append('g') |
|
.attr('class', (d) -> "node #{d.type}") |
|
|
|
enter_nodes.append('circle') |
|
.attr('r', R) |
|
|
|
enter_nodes.append('title') |
|
.text (d) -> |
|
if d.label? |
|
return "#{d.label.toUpperCase()} (#{d.type})" |
|
else |
|
return "Unlabeled #{d.type}" |
|
|
|
### draw the label box ### |
|
enter_label_box = enter_nodes.append('g') |
|
|
|
enter_label_box.append('text') |
|
.text((d) -> d.label) |
|
.attr |
|
class: 'label' |
|
x: R+4 |
|
dy: (d) -> if d.terms? and d.terms.length > 0 then '-0.25em' else '0.35em' |
|
|
|
### draw the terms ### |
|
enter_terms = enter_label_box.append('text') |
|
.attr |
|
class: 'terms' |
|
|
|
tspans = enter_terms.selectAll('tspan') |
|
.data((d) -> if d.terms? then d.terms else []) |
|
|
|
tspans.enter().append('tspan') |
|
.text((d) -> d) |
|
.attr |
|
x: R+6 |
|
y: (d,i) -> "#{i}em" |
|
dy: '0.85em' |
|
|
|
### cola layout ### |
|
graph.nodes.forEach (v) -> |
|
if v.type is 'property' |
|
v.width = 2*PROP_R |
|
v.height = 2*PROP_R |
|
else |
|
v.width = PAD_MULTIPLIER*R + if v.label? or v.terms? and v.terms.length > 0 then LABEL_WIDTH else 0 |
|
v.height = PAD_MULTIPLIER*R |
|
|
|
d3cola = cola.d3adaptor() |
|
.size([width, height]) |
|
.linkDistance((l) -> if l.type is 'instance_of' then 60 else if l.type is '_has_property' then 20 else 80) |
|
.constraints(graph.constraints) |
|
.avoidOverlaps(true) |
|
.nodes(graph.nodes) |
|
.links(graph.links) |
|
.on 'tick', () -> |
|
### update nodes and links ### |
|
nodes |
|
.attr('transform', (d) -> "translate(#{d.x},#{d.y})") |
|
|
|
links |
|
.attr('x1', (d) -> d.source.x) |
|
.attr('y1', (d) -> d.source.y) |
|
.attr('x2', (d) -> d.target.x) |
|
.attr('y2', (d) -> d.target.y) |
|
|
|
d3cola.start(100,30,30) |
|
|
|
svg.append('foreignObject') |
|
.html('<div>Die itaque septima Ianuarii, instantis anni millesimi sexcentesimi decimi, hora sequentis noctis prima, cum caelestia sydera per Perspicillum spectarem, <b>Iuppiter</b> sese obviam fecit; cumque admodum excellens mihi parassem instrumentum (quod antea ob alterius organi debilitatem minime contingerat), tres illi adstare <b>Stellulas</b>, exiguas quidem, veruntamen clarissimas, cognovi; quae, licet e numero <b>inerrantium</b> a me crederentur, nonnullam tamen intulerunt admirationem, eo quod secundum exactam lineam rectam atque Eclipticae parallelam dispositae videbantur, ac caeteris magnitudine paribus splendidiores.</div>') |
|
.attr |
|
class: 'text' |
|
width: 360 |
|
height: 200 |
|
x: 30 |
|
y: 30 |