Skip to content

d3 model diagrams for dashboard #950

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 42 additions & 7 deletions apps/_dashboard/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
33 changes: 33 additions & 0 deletions apps/_dashboard/static/css/d3_graph.css
Original file line number Diff line number Diff line change
@@ -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;}

207 changes: 207 additions & 0 deletions apps/_dashboard/static/js/d3_graph.js
Original file line number Diff line number Diff line change
@@ -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 = "<TABLE>";
fields.forEach(function(d) {
fieldformat += "<TR><TD><B>"+ d.name+"</B></TD><TD>"+ d.type+"</TD><TD>"+ d.disp+"</TD></TR>";
});
fieldformat += "</TABLE>";
var tiplength = d.fields.length;

// Define 'div' for tooltips
var div = d3.select("body").append("div") // declare the tooltip div
.attr("class", "tooltip") // apply the 'tooltip' class
.style("opacity", 0)
.html('<h5>' + d.name + '</h5>' + fieldformat)
.style("left", 20 + (e.pageX) + "px")// or just (d.x + 50 + "px")
.style("top", tooltop(e, tiplength))// or ...
.transition()
.duration(800)
.style("opacity", 0.9);
});

function tooltop(e, tiplength) {
//aim to ensure tooltip is fully visible whenver possible
return (Math.max(e.pageY - 20 - (tiplength * 14),0)) + "px"
}

node.on("mouseout", function(e, d) {
d3.select("body").select('div.tooltip').remove();
});

// instead of waiting for force to end with : force.on('end', function()
// use .on("tick", instead. Here is the tick function
function tick() {
node.attr('transform', function(d) {
d.x = Math.max(30, Math.min(width - 16, d.x));
d.y = Math.max(30, Math.min(height - 16, d.y));
return "translate(" + d.x + "," + d.y + ")"; });

link.attr('x1', function(d) {return d.source.x;})
.attr('y1', function(d) {return d.source.y;})
.attr('x2', function(d) {return d.target.x;})
.attr('y2', function(d) {return d.target.y;});
};

function dragstarted(e, d) {
if (!e.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
};

function dragged(e, d) {
d.fx = e.x;
d.fy = e.y;
};

function dragended(e, d) {
if (!e.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
};

};
7 changes: 6 additions & 1 deletion apps/_dashboard/static/js/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,12 @@ let init = (app) => {
app.vue.walk = [];
var name = app.vue.selected_app.name;
Q.get('../walk/'+name).then(r=>{app.vue.walk=r.json().payload;});
Q.get('../rest/'+name).then(r=>{app.vue.databases=r.json().databases;});
Q.get('../rest/'+name).then(r=>{
var restpayload = r.json();
app.vue.databases=restpayload.databases;
// d3 database graphs
d3_graph(restpayload.nodes, restpayload.links);
});
app.vue.selected_filename = null;
}
app.clear_tickets = () => {
Expand Down
9 changes: 9 additions & 0 deletions apps/_dashboard/templates/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
<link rel="shortcut icon" href="data:image/x-icon;base64,AAABAAEAAQEAAAEAIAAwAAAAFgAAACgAAAABAAAAAgAAAAEAIAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAPAAAAAA==" />
<link rel="stylesheet" type="text/css" href="css/future.css" />
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.8.1/css/all.css">
<link rel="stylesheet" type="text/css" href="css/d3_graph.css" /><!-- for d3 diagram maker -->
</head>

<body>
Expand Down Expand Up @@ -120,6 +121,12 @@ <h2><i class="fa fa-lock fa-2x"></i></h2>
<label for="databases">Databases in {{selected_app.name}}</label>
<div style="max-width:100vw">
<div style="overflow-x:auto">
<div>
</br>
<h2>Model graphs</h2>
<div id="vis"></div><!-- d3 database diagrams-->
</br>
</div>
<table>
<tbody v-for="db in databases">
<tr v-for="table in db.tables">
Expand Down Expand Up @@ -264,5 +271,7 @@ <h2>{{modal.title}}</h2>
<script>
T.languages = [[= languages]];
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.9.0/d3.min.js"></script><!-- d3 library for database diagrams-->
<script src="js/d3_graph.js"></script><!-- d3 diagram maker -->
<script src="js/index.js"></script>
</html>