summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorhallgren <hallgren@chalmers.se>2015-03-24 18:03:10 +0000
committerhallgren <hallgren@chalmers.se>2015-03-24 18:03:10 +0000
commit12f5dc9ecedf0ad74cc1303c63743b6647e4081e (patch)
tree0a1e772f8a93b77546faefe6830c94fad60cef0f /src
parent6d72126ffced8aaee001d1c766dfce41935d9b6d (diff)
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.
Diffstat (limited to 'src')
-rw-r--r--src/www/js/d3Tree.js314
-rw-r--r--src/www/js/gftranslate.js2
-rw-r--r--src/www/js/wc.js23
-rw-r--r--src/www/wc.html16
4 files changed, 346 insertions, 9 deletions
diff --git a/src/www/js/d3Tree.js b/src/www/js/d3Tree.js
new file mode 100644
index 000000000..c0365117a
--- /dev/null
+++ b/src/www/js/d3Tree.js
@@ -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)");
+}
diff --git a/src/www/js/gftranslate.js b/src/www/js/gftranslate.js
index 2d96bd2eb..1fb1e8357 100644
--- a/src/www/js/gftranslate.js
+++ b/src/www/js/gftranslate.js
@@ -61,7 +61,7 @@ gftranslate.translate=function(source,from,to,start,limit,cont) {
cont(unspace_translations(g,result[0].translations))
}
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)
+"&start="+start+"&limit="+limit,extract,errcont)
else cont([{error:"sentence too long"}])
diff --git a/src/www/js/wc.js b/src/www/js/wc.js
index 67fb742ca..31fc9a950 100644
--- a/src/www/js/wc.js
+++ b/src/www/js/wc.js
@@ -112,7 +112,7 @@ wc.translate=function() {
if(wc.e2) wc.e2.innerHTML=lins[0].text
}
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
gftranslate.call("?command=c-linearize&to="+l+"&tree="+tree,show_inflections)
}
@@ -134,12 +134,12 @@ wc.translate=function() {
if(e) {
e.innerHTML=prob+"<br>"
if(r.tree) {
- wc.e2=empty_class("div","e2")
- var t=wrap("span",treetext(r.tree))
- e.appendChild(t)
+ wc.e2=node("div",{id:"tree-container","class":"e2"})
+ e.appendChild(wrap("span",treetext(r.tree)))
+ /*
var g=gftranslate.jsonurl
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,
g+"?command=c-parsetree&"+u+from]
if(!r.img) {
@@ -153,7 +153,9 @@ wc.translate=function() {
else if(r.img.src!=r.imgurls[r.img_ix]) // language change?
r.img.src=r.imgurls[r.img_ix]
wc.e2.appendChild(r.img)
+ */
e.appendChild(wc.e2)
+ d3Tree(wc.bracketsToD3(r.jsontree))
}
}
if(wc.p /*&& so.rs.length>1*/) show_picks()
@@ -351,6 +353,17 @@ wc.try_google=function() {
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
function init_languages() {
diff --git a/src/www/wc.html b/src/www/wc.html
index 3d2b8e12f..b5edef790 100644
--- a/src/www/wc.html
+++ b/src/www/wc.html
@@ -24,8 +24,14 @@ small { color: #666; }
.colors .bad_quality { background-color: #f89; }
.placeholder { color: #999; }
.error { color: #c00; }
-div.e2 { background: white; }
+div.e2 table { background: white; }
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>
</head>
@@ -91,7 +97,7 @@ span.inflect { color: blue; }
</div>
<hr>
<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>
<a href="http://www.grammaticalframework.org/demos/translation.html">About</a>
<script src="js/support.js"></script>
@@ -100,6 +106,10 @@ span.inflect { color: blue; }
<script src="js/langcode.js"></script>
<script src="js/pgf_online.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>
</html>