From 12f5dc9ecedf0ad74cc1303c63743b6647e4081e Mon Sep 17 00:00:00 2001 From: hallgren Date: Tue, 24 Mar 2015 18:03:10 +0000 Subject: [PATCH] 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. --- src/www/js/d3Tree.js | 314 ++++++++++++++++++++++++++++++++++++++ src/www/js/gftranslate.js | 2 +- src/www/js/wc.js | 23 ++- src/www/wc.html | 16 +- 4 files changed, 346 insertions(+), 9 deletions(-) create mode 100644 src/www/js/d3Tree.js 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 +// 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" 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; } @@ -91,7 +97,7 @@ span.inflect { color: blue; }
-Last modified: Sun Mar 22 23:30:45 CET 2015 +Last modified: Tue Mar 24 16:59:23 CET 2015
About @@ -100,6 +106,10 @@ span.inflect { color: blue; } - + + + + +