diff --git a/apps/_dashboard/__init__.py b/apps/_dashboard/__init__.py index fcab85dce..15feeab79 100644 --- a/apps/_dashboard/__init__.py +++ b/apps/_dashboard/__init__.py @@ -351,23 +351,58 @@ def url(*args): ] if len(args) == 1: + # for model d3 graphs + nodes = list() + links = list() + def tables(name): db = getattr(module, name) make_safe(db) - return [ - { + tablelist = list() + for t in getattr(module, name): + + # add links and nodes for the db graph + # code from web2py appadmin + fields = [] + for field in t: + f_type = field.type + if not isinstance(f_type, str): + disp = ' ' + elif f_type == 'string': + disp = field.length + elif f_type == 'id': + disp = 'PK' + elif f_type.startswith('reference') or \ + f_type.startswith('list:reference'): + disp = 'FK' + else: + disp = ' ' + fields.append(dict(name=field.name, type=field.type, disp=disp)) + + if isinstance(f_type, str) and ( + f_type.startswith('reference') or + f_type.startswith('list:reference')): + referenced_table = f_type.split()[1].split('.')[0] + + links.append(dict(source=t._tablename, target = referenced_table)) + + nodes.append(dict(name=t._tablename, type='table', fields = fields)) + # end of code for d3 graphs + + tablelist.append({ "name": t._tablename, "fields": t.fields, "link": url(name, t._tablename) + "?model=true", - } - for t in getattr(module, name) - ] + }) + return tablelist return { "databases": [ {"name": name, "tables": tables(name)} for name in databases - ] - } + ], + "links": links, + "nodes": nodes} + elif len(args) > 2 and args[1] in databases: db = getattr(module, args[1]) make_safe(db) diff --git a/apps/_dashboard/static/css/d3_graph.css b/apps/_dashboard/static/css/d3_graph.css new file mode 100644 index 000000000..6e9564e15 --- /dev/null +++ b/apps/_dashboard/static/css/d3_graph.css @@ -0,0 +1,33 @@ + .node {fill: steelblue; + stroke: #636363; + stroke-width: 1px;} + + .auth {fill: lightgrey;} + + .table {r: 10;} + + .link {stroke: #bbbbbb; + stroke-width: 2px;} + td {padding: 4px;} + + div.tooltip { + position: absolute; + text-align: left; + /* width: 140px; */ + /* height: 28px; */ + padding: 0px 5px 0px 5px; + padding-top: 0px; + font: 12px sans-serif; + background: #fff7bc; + border: solid 1px #aaa; + border-radius: 6px; + pointer-events: none;} + + h5 { font: 14px sans-serif; + background : #ec7014; + color: #ffffe5; + padding: 5px 2px 5px 2px; + margin-top: 1px;} + path { + fill: #aaaaaa;} + diff --git a/apps/_dashboard/static/js/d3_graph.js b/apps/_dashboard/static/js/d3_graph.js new file mode 100644 index 000000000..dff9a8da9 --- /dev/null +++ b/apps/_dashboard/static/js/d3_graph.js @@ -0,0 +1,207 @@ +// Some reference links: +// How to get link ids instead of index +// http://stackoverflow.com/questions/23986466/d3-force-layout-linking-nodes-by-name-instead-of-index +// embedding web2py in d3 +// http://stackoverflow.com/questions/34326343/embedding-d3-js-graph-in-a-web2py-bootstrap-page + +// nodes and links are retrieved by init.js from rest service +var links = Array(); +var nodes = Array(); + +function populateNodes(data){ + nodes.splice(0, nodes.length); + data.forEach(function(e){ + nodes.push(e); + }); +} + +function populateLinks(data){ + links.splice(0, links.length); + data.forEach(function(e){ + links.push(e); + }); +} + +function d3_graph(nodes, links) { + // code to flush the d3 object + var myvisdiv = document.getElementById("vis"); + myvisdiv.innerHTML = ""; + + // erease old and get the new tables and relations + populateNodes(nodes); + populateLinks(links); + + if (nodes.length == 0){ + myvisdiv.innerHTML = "No diagrams to draw"; + return + } + + var edges = []; + + links.forEach(function(e) { + var sourceNode = nodes.filter(function(n) { + return n.name === e.source; + })[0], + targetNode = nodes.filter(function(n) { + return n.name === e.target; + })[0]; + + edges.push({ + source: sourceNode, + target: targetNode, + value: 1}); + + }); + + edges.forEach(function(e) { + + if (!e.source["linkcount"]) e.source["linkcount"] = 0; + if (!e.target["linkcount"]) e.target["linkcount"] = 0; + + e.source["linkcount"]++; + e.target["linkcount"]++; + }); + + var width = window.innerWidth, height = window.innerHeight/3; + // var height = window.innerHeight|| docEl.clientHeight|| bodyEl.clientHeight; + // var width = window.innerWidth || docEl.clientWidth || bodyEl.clientWidth; + var svg = d3.select("#vis").append("svg") + .attr("width", width) + .attr("height", height); + + // updated for d3 v4. + var simulation = d3.forceSimulation() + .force("link", d3.forceLink().id(function(d) { return d.id; })) + .force("charge", d3.forceManyBody().strength(strength)) + .force("center", d3.forceCenter(width / 2, height / 2)) + .force("collision", d3.forceCollide(35)); + + // Node charge strength. Repel strength greater for less links. + //function strength(d) { return -50/d["linkcount"] ; } + function strength(d) { return -25 ; } + + // Link distance. Distance increases with number of links at source and target + function distance(d) { return (60 + (d.source["linkcount"] * d.target["linkcount"])) ; } + + // Link strength. Strength is less for highly connected nodes (move towards target dist) + function strengthl(d) { return 5/(d.source["linkcount"] + d.target["linkcount"]) ; } + + simulation + .nodes(nodes) + .on("tick", tick); + + simulation.force("link") + .links(edges) + .distance(distance) + .strength(strengthl); + + // build the arrow. + svg.append("svg:defs").selectAll("marker") + .data(["end"]) // Different link/path types can be defined here + .enter().append("svg:marker") // This section adds in the arrows + .attr("id", String) + .attr("viewBox", "0 -5 10 10") + .attr("refX", 25) // Moves the arrow head out, allow for radius + .attr("refY", 0) // -1.5 + .attr("markerWidth", 6) + .attr("markerHeight", 6) + .attr("orient", "auto") + .append("svg:path") + .attr("d", "M0,-5L10,0L0,5"); + + var link = svg.selectAll('.link') + .data(edges) + .enter().append('line') + .attr("class", "link") + .attr("marker-end", "url(#end)"); + + var node = svg.selectAll(".node") + .data(nodes) + .enter().append("g") + .attr("class", function(d) { return "node " + d.type;}) + .attr('transform', function(d) { + return "translate(" + d.x + "," + d.y + ")"}) + .classed("auth", function(d) { return (d.name.startsWith("auth") ? true : false);}); + + node.call(d3.drag() + .on("start", dragstarted) + .on("drag", dragged) + .on("end", dragended)); + + // add the nodes + node.append('circle') + .attr('r', 16) + ; + + // add text + node.append("text") + .attr("x", 12) + .attr("dy", "-1.1em") + .text(function(d) {return d.name;}); + + node.on("mouseover", function(e, d) { + + var g = d3.select(this); // the node (table) + + // tooltip + var fields = d.fields; + var fieldformat = "
"+ d.name+" | "+ d.type+" | "+ d.disp+" |