forked from GitHub/gf-core
Wide Coverage Translation Demo: zoomable panable collapsible syntax trees
This is an experimental solution using JavaScript code from https://github.com/christos-c/tree-viewer, d3js.org and jquery.com.
This commit is contained in:
314
src/www/js/d3Tree.js
vendored
Normal file
314
src/www/js/d3Tree.js
vendored
Normal file
@@ -0,0 +1,314 @@
|
|||||||
|
// Copied from https://github.com/christos-c/tree-viewer (TH 2015-03-24)
|
||||||
|
|
||||||
|
// Inspired by "D3.js Drag and Drop Zoomable Tree" by Rob Schmuecker <robert.schmuecker@gmail.com>
|
||||||
|
// https://gist.github.com/robschmuecker/7880033
|
||||||
|
|
||||||
|
function d3Tree(treeData) {
|
||||||
|
// panning variables
|
||||||
|
var panSpeed = 200;
|
||||||
|
// Misc. variables
|
||||||
|
var i = 0;
|
||||||
|
var duration = 450;
|
||||||
|
var root;
|
||||||
|
|
||||||
|
// size of the diagram
|
||||||
|
var pageWidth = $(document).width();
|
||||||
|
var viewerWidth = pageWidth - (0.2 * pageWidth);
|
||||||
|
var viewerHeight = 500;
|
||||||
|
|
||||||
|
var tree = d3.layout.tree()
|
||||||
|
.size([viewerWidth-20, viewerHeight]);
|
||||||
|
|
||||||
|
// define a d3 diagonal projection for use by the node paths later on.
|
||||||
|
var diagonal = d3.svg.diagonal()
|
||||||
|
.projection(function(d) {
|
||||||
|
return [d.x, d.y];
|
||||||
|
});
|
||||||
|
|
||||||
|
// Can be used to draw the links between nodes instead of the diagonal
|
||||||
|
// TODO Doesn't work with the collapse/expand transition
|
||||||
|
//function straightLine(d) {
|
||||||
|
// return "M" + d.source.x + "," + d.source.y + "L" + d.target.x + "," + d.target.y;
|
||||||
|
//}
|
||||||
|
|
||||||
|
// A recursive helper function for performing some setup by walking through all nodes
|
||||||
|
function visit(parent, visitFn, childrenFn) {
|
||||||
|
if (!parent) return;
|
||||||
|
|
||||||
|
visitFn(parent);
|
||||||
|
|
||||||
|
var children = childrenFn(parent);
|
||||||
|
if (children) {
|
||||||
|
var count = children.length;
|
||||||
|
for (var i = 0; i < count; i++) {
|
||||||
|
visit(children[i], visitFn, childrenFn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Pan function, can be better implemented.
|
||||||
|
function pan(domNode, direction) {
|
||||||
|
var speed = panSpeed;
|
||||||
|
if (panTimer) {
|
||||||
|
clearTimeout(panTimer);
|
||||||
|
translateCoords = d3.transform(svgGroup.attr("transform"));
|
||||||
|
if (direction == 'left' || direction == 'right') {
|
||||||
|
translateX = direction == 'left' ? translateCoords.translate[0] + speed : translateCoords.translate[0] - speed;
|
||||||
|
translateY = translateCoords.translate[1];
|
||||||
|
} else if (direction == 'up' || direction == 'down') {
|
||||||
|
translateX = translateCoords.translate[0];
|
||||||
|
translateY = direction == 'up' ? translateCoords.translate[1] + speed : translateCoords.translate[1] - speed;
|
||||||
|
}
|
||||||
|
scaleX = translateCoords.scale[0];
|
||||||
|
scaleY = translateCoords.scale[1];
|
||||||
|
scale = zoomListener.scale();
|
||||||
|
svgGroup.transition().attr("transform", "translate(" + translateX + "," + translateY + ")scale(" + scale + ")");
|
||||||
|
d3.select(domNode).select('g.node').attr("transform", "translate(" + translateX + "," + translateY + ")");
|
||||||
|
zoomListener.scale(zoomListener.scale());
|
||||||
|
zoomListener.translate([translateX, translateY]);
|
||||||
|
panTimer = setTimeout(function() {
|
||||||
|
pan(domNode, speed, direction);
|
||||||
|
}, 50);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define the zoom function for the zoomable tree
|
||||||
|
function zoom() {
|
||||||
|
svgGroup.attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// define the zoomListener which calls the zoom function on the "zoom" event constrained within the scaleExtents
|
||||||
|
var zoomListener = d3.behavior.zoom().scaleExtent([0.1, 3]).on("zoom", zoom);
|
||||||
|
|
||||||
|
// remove the previous svg if there
|
||||||
|
d3.select("svg").remove();
|
||||||
|
|
||||||
|
// define the baseSvg, attaching a class for styling and the zoomListener
|
||||||
|
var baseSvg = d3.select("#tree-container").append("svg")
|
||||||
|
.attr("width", viewerWidth)
|
||||||
|
.attr("height", viewerHeight)
|
||||||
|
.attr("class", "overlay")
|
||||||
|
.call(zoomListener)
|
||||||
|
.on("dblclick.zoom", null);
|
||||||
|
|
||||||
|
// The arrowmarker to be appended at the end of each path
|
||||||
|
// TODO Looks terrible (not currently used)
|
||||||
|
baseSvg.append("marker")
|
||||||
|
.attr("id", "markerArrow")
|
||||||
|
.attr("markerWidth", 4)
|
||||||
|
.attr("markerHeight", 4)
|
||||||
|
.attr("refY","2")
|
||||||
|
.attr("refX", "10")
|
||||||
|
.attr("orient", "auto")
|
||||||
|
.append("polygon")
|
||||||
|
.attr("points", "0,0 4,2 0,4")
|
||||||
|
.attr("style", "fill: #ccc");
|
||||||
|
|
||||||
|
// Helper functions for collapsing and expanding nodes.
|
||||||
|
function collapse(d) {
|
||||||
|
if (d.children) {
|
||||||
|
d._children = d.children;
|
||||||
|
d._children.forEach(collapse);
|
||||||
|
d.children = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function expand(d) {
|
||||||
|
if (d._children) {
|
||||||
|
d.children = d._children;
|
||||||
|
d.children.forEach(expand);
|
||||||
|
d._children = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var overCircle = function(d) {
|
||||||
|
selectedNode = d;
|
||||||
|
updateTempConnector();
|
||||||
|
};
|
||||||
|
var outCircle = function(d) {
|
||||||
|
selectedNode = null;
|
||||||
|
updateTempConnector();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Toggle children function
|
||||||
|
function toggleChildren(d) {
|
||||||
|
if (d.children) {
|
||||||
|
d._children = d.children;
|
||||||
|
d.children = null;
|
||||||
|
} else if (d._children) {
|
||||||
|
d.children = d._children;
|
||||||
|
d._children = null;
|
||||||
|
}
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle children on click.
|
||||||
|
function click(d) {
|
||||||
|
if (d3.event.defaultPrevented) return; // click suppressed
|
||||||
|
d = toggleChildren(d);
|
||||||
|
update(d);
|
||||||
|
}
|
||||||
|
|
||||||
|
function update(source) {
|
||||||
|
// Compute the new height, function counts total children of root node and sets tree height accordingly.
|
||||||
|
// This prevents the layout looking squashed when new nodes are made visible or looking sparse when nodes are removed
|
||||||
|
// This makes the layout more consistent.
|
||||||
|
var levelHeight = [1];
|
||||||
|
var childCount = function(level, n) {
|
||||||
|
if (n.children && n.children.length > 0) {
|
||||||
|
if (levelHeight.length <= level + 1) levelHeight.push(0);
|
||||||
|
|
||||||
|
levelHeight[level + 1] += n.children.length;
|
||||||
|
n.children.forEach(function(d) {
|
||||||
|
childCount(level + 1, d);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
childCount(0, root);
|
||||||
|
var maxLevel = levelHeight.length+2;
|
||||||
|
|
||||||
|
// Compute the new tree layout.
|
||||||
|
var nodes = tree.nodes(root).reverse(),
|
||||||
|
links = tree.links(nodes);
|
||||||
|
|
||||||
|
// Set heights between levels based on maxLevel.
|
||||||
|
nodes.forEach(function(d) {
|
||||||
|
d.y = (d.depth * (viewerHeight/(maxLevel)));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update the nodes…
|
||||||
|
var node = svgGroup.selectAll("g.node")
|
||||||
|
.data(nodes, function(d) {
|
||||||
|
return d.id || (d.id = ++i);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Enter any new nodes at the parent's previous position.
|
||||||
|
var nodeEnter = node.enter().append("g")
|
||||||
|
.attr("class", "node")
|
||||||
|
.attr("transform", function(d) {
|
||||||
|
return "translate(" + source.x0 + "," + source.y0 + ")";
|
||||||
|
})
|
||||||
|
.on('click', click);
|
||||||
|
|
||||||
|
nodeEnter.append("rect")
|
||||||
|
.attr('class', 'nodeRect')
|
||||||
|
// Size of the rectangle/2
|
||||||
|
.attr("x", function(d){return -(d.name.length*5+10)/2})
|
||||||
|
.attr("y", -10)
|
||||||
|
.attr("width", 0)
|
||||||
|
.attr("height", 0)
|
||||||
|
.style("fill", function(d) {
|
||||||
|
return d._children ? "lightsteelblue" : "#fff";
|
||||||
|
});
|
||||||
|
|
||||||
|
nodeEnter.append("text")
|
||||||
|
.attr("y", 0)
|
||||||
|
.attr("dy", ".35em")
|
||||||
|
.attr('class', 'nodeText')
|
||||||
|
.attr("text-anchor", "middle")
|
||||||
|
.text(function(d) {
|
||||||
|
return d.name;
|
||||||
|
})
|
||||||
|
.style("fill-opacity", 0);
|
||||||
|
|
||||||
|
// Update the text to reflect whether node has children or not.
|
||||||
|
node.select('text')
|
||||||
|
.attr("y", 0)
|
||||||
|
.attr("text-anchor", "middle")
|
||||||
|
.text(function(d) {
|
||||||
|
return d.name;
|
||||||
|
});
|
||||||
|
|
||||||
|
node.select("rect.nodeRect")
|
||||||
|
.attr("width", function(d) {
|
||||||
|
// Adjust the size of the square according to the label
|
||||||
|
return d.children || d._children ? d.name.length*5+10 : 0;
|
||||||
|
})
|
||||||
|
.attr("height", function(d) {
|
||||||
|
return d.children || d._children ? 20 : 0;
|
||||||
|
})
|
||||||
|
.style("fill", function(d) {
|
||||||
|
return d._children ? "lightsteelblue" : "#fff";
|
||||||
|
});
|
||||||
|
|
||||||
|
// Transition nodes to their new position.
|
||||||
|
var nodeUpdate = node.transition()
|
||||||
|
.duration(duration)
|
||||||
|
.attr("transform", function(d) {
|
||||||
|
return "translate(" + d.x + "," + d.y + ")";
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fade the text in
|
||||||
|
nodeUpdate.select("text")
|
||||||
|
.style("fill-opacity", 1);
|
||||||
|
|
||||||
|
// Transition exiting nodes to the parent's new position.
|
||||||
|
var nodeExit = node.exit().transition()
|
||||||
|
.duration(duration)
|
||||||
|
.attr("transform", function(d) {
|
||||||
|
return "translate(" + source.x + "," + source.y + ")";
|
||||||
|
})
|
||||||
|
.remove();
|
||||||
|
|
||||||
|
nodeExit.select("circle")
|
||||||
|
.attr("r", 0);
|
||||||
|
|
||||||
|
nodeExit.select("text")
|
||||||
|
.style("fill-opacity", 0);
|
||||||
|
|
||||||
|
// Update the links…
|
||||||
|
var link = svgGroup.selectAll("path.link")
|
||||||
|
.data(links, function(d) {
|
||||||
|
return d.target.id;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Enter any new links at the parent's previous position.
|
||||||
|
link.enter().insert("path", "g")
|
||||||
|
.attr("class", "link")
|
||||||
|
//TODO MARKERS LOOK TERRIBLE
|
||||||
|
// .attr("marker-end", "url(#markerArrow)")
|
||||||
|
.attr("d", function(d) {
|
||||||
|
var o = {x: source.x, y: source.y};
|
||||||
|
return diagonal({source: o,target: o});
|
||||||
|
});
|
||||||
|
// TODO doesn't work with the transition
|
||||||
|
// .attr("d", straightLine);
|
||||||
|
|
||||||
|
// Transition links to their new position.
|
||||||
|
link.transition()
|
||||||
|
.duration(duration)
|
||||||
|
.attr("d", diagonal);
|
||||||
|
// TODO doesn't work with the transition
|
||||||
|
// .attr("d", straightLine);
|
||||||
|
|
||||||
|
// Transition exiting nodes to the parent's new position.
|
||||||
|
link.exit().transition()
|
||||||
|
.duration(duration)
|
||||||
|
.attr("d", function(d) {
|
||||||
|
var o = {x: source.x, y: source.y};
|
||||||
|
return diagonal({source: o,target: o});
|
||||||
|
})
|
||||||
|
// TODO doesn't work with the transition
|
||||||
|
// .attr("d", straightLine)
|
||||||
|
.remove();
|
||||||
|
|
||||||
|
// Stash the old positions for transition.
|
||||||
|
nodes.forEach(function(d) {
|
||||||
|
d.x0 = d.x;
|
||||||
|
d.y0 = d.y;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append a group which holds all nodes and which the zoom Listener can act upon.
|
||||||
|
var svgGroup = baseSvg.append("g");
|
||||||
|
|
||||||
|
// Define the root
|
||||||
|
root = treeData;
|
||||||
|
root.x0 = viewerWidth / 2;
|
||||||
|
root.y0 = 0;
|
||||||
|
|
||||||
|
// Layout the tree initially and center on the root node.
|
||||||
|
update(root);
|
||||||
|
d3.select('g').attr("transform", "translate(0,20)");
|
||||||
|
}
|
||||||
@@ -61,7 +61,7 @@ gftranslate.translate=function(source,from,to,start,limit,cont) {
|
|||||||
cont(unspace_translations(g,result[0].translations))
|
cont(unspace_translations(g,result[0].translations))
|
||||||
}
|
}
|
||||||
if(encsrc.length<length_limit(from))
|
if(encsrc.length<length_limit(from))
|
||||||
gftranslate.call("?command=c-translate&input="+encsrc
|
gftranslate.call("?command=c-translate&jsontree=true&input="+encsrc
|
||||||
+lexer+"&unlexer=text&from="+g+from+"&to="+enc_langs(g,to)
|
+lexer+"&unlexer=text&from="+g+from+"&to="+enc_langs(g,to)
|
||||||
+"&start="+start+"&limit="+limit,extract,errcont)
|
+"&start="+start+"&limit="+limit,extract,errcont)
|
||||||
else cont([{error:"sentence too long"}])
|
else cont([{error:"sentence too long"}])
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ wc.translate=function() {
|
|||||||
if(wc.e2) wc.e2.innerHTML=lins[0].text
|
if(wc.e2) wc.e2.innerHTML=lins[0].text
|
||||||
}
|
}
|
||||||
function get_inflections() {
|
function get_inflections() {
|
||||||
var tree="MkDocument+%22%22+(Inflection"+wcls+" "+w+") %22%22"
|
var tree="MkDocument+%22%22+(Inflection"+wcls+"+"+w+")+%22%22"
|
||||||
var l=gftranslate.grammar+f.to.value
|
var l=gftranslate.grammar+f.to.value
|
||||||
gftranslate.call("?command=c-linearize&to="+l+"&tree="+tree,show_inflections)
|
gftranslate.call("?command=c-linearize&to="+l+"&tree="+tree,show_inflections)
|
||||||
}
|
}
|
||||||
@@ -134,12 +134,12 @@ wc.translate=function() {
|
|||||||
if(e) {
|
if(e) {
|
||||||
e.innerHTML=prob+"<br>"
|
e.innerHTML=prob+"<br>"
|
||||||
if(r.tree) {
|
if(r.tree) {
|
||||||
wc.e2=empty_class("div","e2")
|
wc.e2=node("div",{id:"tree-container","class":"e2"})
|
||||||
var t=wrap("span",treetext(r.tree))
|
e.appendChild(wrap("span",treetext(r.tree)))
|
||||||
e.appendChild(t)
|
/*
|
||||||
var g=gftranslate.jsonurl
|
var g=gftranslate.jsonurl
|
||||||
var u="format=svg&tree="+encodeURIComponent(r.tree)
|
var u="format=svg&tree="+encodeURIComponent(r.tree)
|
||||||
var from="&from="+gftranslate.grammar+f.to.value
|
var from="&from="+r.grammar+f.to.value
|
||||||
r.imgurls=[g+"?command=c-abstrtree&"+u,
|
r.imgurls=[g+"?command=c-abstrtree&"+u,
|
||||||
g+"?command=c-parsetree&"+u+from]
|
g+"?command=c-parsetree&"+u+from]
|
||||||
if(!r.img) {
|
if(!r.img) {
|
||||||
@@ -153,7 +153,9 @@ wc.translate=function() {
|
|||||||
else if(r.img.src!=r.imgurls[r.img_ix]) // language change?
|
else if(r.img.src!=r.imgurls[r.img_ix]) // language change?
|
||||||
r.img.src=r.imgurls[r.img_ix]
|
r.img.src=r.imgurls[r.img_ix]
|
||||||
wc.e2.appendChild(r.img)
|
wc.e2.appendChild(r.img)
|
||||||
|
*/
|
||||||
e.appendChild(wc.e2)
|
e.appendChild(wc.e2)
|
||||||
|
d3Tree(wc.bracketsToD3(r.jsontree))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if(wc.p /*&& so.rs.length>1*/) show_picks()
|
if(wc.p /*&& so.rs.length>1*/) show_picks()
|
||||||
@@ -351,6 +353,17 @@ wc.try_google=function() {
|
|||||||
w.focus()
|
w.focus()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
wc.bracketsToD3=function(bs) {
|
||||||
|
if(bs.token) return {name:bs.token}
|
||||||
|
else if(bs.other) return {name:bs.other}
|
||||||
|
else if(bs.fun) {
|
||||||
|
var t={name:bs.fun}
|
||||||
|
if(bs.children/* && bs.children.length>0*/)
|
||||||
|
t.children=bs.children.map(wc.bracketsToD3)
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
else return {name:"??"}
|
||||||
|
}
|
||||||
|
|
||||||
// Update language selection menus with the languages supported by the grammar
|
// Update language selection menus with the languages supported by the grammar
|
||||||
function init_languages() {
|
function init_languages() {
|
||||||
|
|||||||
@@ -24,8 +24,14 @@ small { color: #666; }
|
|||||||
.colors .bad_quality { background-color: #f89; }
|
.colors .bad_quality { background-color: #f89; }
|
||||||
.placeholder { color: #999; }
|
.placeholder { color: #999; }
|
||||||
.error { color: #c00; }
|
.error { color: #c00; }
|
||||||
div.e2 { background: white; }
|
div.e2 table { background: white; }
|
||||||
span.inflect { color: blue; }
|
span.inflect { color: blue; }
|
||||||
|
|
||||||
|
.node { cursor: pointer; }
|
||||||
|
/*.overlay { background-color: #eed; }*/
|
||||||
|
.node rect { fill: #fff; stroke: black; stroke-width: 1.5px; }
|
||||||
|
.node text { font-size: 10px; font-family: serif; }
|
||||||
|
.link { fill: none; stroke: #ccc; stroke-width: 1.5px; }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
@@ -91,7 +97,7 @@ span.inflect { color: blue; }
|
|||||||
</div>
|
</div>
|
||||||
<hr>
|
<hr>
|
||||||
<div class=modtime><small>
|
<div class=modtime><small>
|
||||||
<!-- hhmts start -->Last modified: Sun Mar 22 23:30:45 CET 2015 <!-- hhmts end -->
|
<!-- hhmts start -->Last modified: Tue Mar 24 16:59:23 CET 2015 <!-- hhmts end -->
|
||||||
</small></div>
|
</small></div>
|
||||||
<a href="http://www.grammaticalframework.org/demos/translation.html">About</a>
|
<a href="http://www.grammaticalframework.org/demos/translation.html">About</a>
|
||||||
<script src="js/support.js"></script>
|
<script src="js/support.js"></script>
|
||||||
@@ -100,6 +106,10 @@ span.inflect { color: blue; }
|
|||||||
<script src="js/langcode.js"></script>
|
<script src="js/langcode.js"></script>
|
||||||
<script src="js/pgf_online.js"></script>
|
<script src="js/pgf_online.js"></script>
|
||||||
<script src="js/wc.js"></script>
|
<script src="js/wc.js"></script>
|
||||||
</script>
|
|
||||||
|
<script src="http://code.jquery.com/jquery-1.10.2.min.js"></script>
|
||||||
|
<script src="http://d3js.org/d3.v3.min.js"></script>
|
||||||
|
<script src="js/d3Tree.js"></script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user