Skip to content

Commit f0af852

Browse files
committed
d3 model diagrams for dashboard
1 parent b540e88 commit f0af852

File tree

5 files changed

+297
-8
lines changed

5 files changed

+297
-8
lines changed

apps/_dashboard/__init__.py

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -351,23 +351,58 @@ def url(*args):
351351
]
352352
if len(args) == 1:
353353

354+
# for model d3 graphs
355+
nodes = list()
356+
links = list()
357+
354358
def tables(name):
355359
db = getattr(module, name)
356360
make_safe(db)
357-
return [
358-
{
361+
tablelist = list()
362+
for t in getattr(module, name):
363+
364+
# add links and nodes for the db graph
365+
# code from web2py appadmin
366+
fields = []
367+
for field in t:
368+
f_type = field.type
369+
if not isinstance(f_type, str):
370+
disp = ' '
371+
elif f_type == 'string':
372+
disp = field.length
373+
elif f_type == 'id':
374+
disp = 'PK'
375+
elif f_type.startswith('reference') or \
376+
f_type.startswith('list:reference'):
377+
disp = 'FK'
378+
else:
379+
disp = ' '
380+
fields.append(dict(name=field.name, type=field.type, disp=disp))
381+
382+
if isinstance(f_type, str) and (
383+
f_type.startswith('reference') or
384+
f_type.startswith('list:reference')):
385+
referenced_table = f_type.split()[1].split('.')[0]
386+
387+
links.append(dict(source=t._tablename, target = referenced_table))
388+
389+
nodes.append(dict(name=t._tablename, type='table', fields = fields))
390+
# end of code for d3 graphs
391+
392+
tablelist.append({
359393
"name": t._tablename,
360394
"fields": t.fields,
361395
"link": url(name, t._tablename) + "?model=true",
362-
}
363-
for t in getattr(module, name)
364-
]
396+
})
397+
return tablelist
365398

366399
return {
367400
"databases": [
368401
{"name": name, "tables": tables(name)} for name in databases
369-
]
370-
}
402+
],
403+
"links": links,
404+
"nodes": nodes}
405+
371406
elif len(args) > 2 and args[1] in databases:
372407
db = getattr(module, args[1])
373408
make_safe(db)
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
.node {fill: steelblue;
2+
stroke: #636363;
3+
stroke-width: 1px;}
4+
5+
.auth {fill: lightgrey;}
6+
7+
.table {r: 10;}
8+
9+
.link {stroke: #bbbbbb;
10+
stroke-width: 2px;}
11+
td {padding: 4px;}
12+
13+
div.tooltip {
14+
position: absolute;
15+
text-align: left;
16+
/* width: 140px; */
17+
/* height: 28px; */
18+
padding: 0px 5px 0px 5px;
19+
padding-top: 0px;
20+
font: 12px sans-serif;
21+
background: #fff7bc;
22+
border: solid 1px #aaa;
23+
border-radius: 6px;
24+
pointer-events: none;}
25+
26+
h5 { font: 14px sans-serif;
27+
background : #ec7014;
28+
color: #ffffe5;
29+
padding: 5px 2px 5px 2px;
30+
margin-top: 1px;}
31+
path {
32+
fill: #aaaaaa;}
33+

apps/_dashboard/static/js/d3_graph.js

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
// Some reference links:
2+
// How to get link ids instead of index
3+
// http://stackoverflow.com/questions/23986466/d3-force-layout-linking-nodes-by-name-instead-of-index
4+
// embedding web2py in d3
5+
// http://stackoverflow.com/questions/34326343/embedding-d3-js-graph-in-a-web2py-bootstrap-page
6+
7+
// nodes and links are retrieved by init.js from rest service
8+
var links = Array();
9+
var nodes = Array();
10+
11+
function populateNodes(data){
12+
nodes.splice(0, nodes.length);
13+
data.forEach(function(e){
14+
nodes.push(e);
15+
});
16+
}
17+
18+
function populateLinks(data){
19+
links.splice(0, links.length);
20+
data.forEach(function(e){
21+
links.push(e);
22+
});
23+
}
24+
25+
function d3_graph(nodes, links) {
26+
// code to flush the d3 object
27+
var myvisdiv = document.getElementById("vis");
28+
myvisdiv.innerHTML = "";
29+
30+
// erease old and get the new tables and relations
31+
populateNodes(nodes);
32+
populateLinks(links);
33+
34+
if (nodes.length == 0){
35+
myvisdiv.innerHTML = "No diagrams to draw";
36+
return
37+
}
38+
39+
var edges = [];
40+
41+
links.forEach(function(e) {
42+
var sourceNode = nodes.filter(function(n) {
43+
return n.name === e.source;
44+
})[0],
45+
targetNode = nodes.filter(function(n) {
46+
return n.name === e.target;
47+
})[0];
48+
49+
edges.push({
50+
source: sourceNode,
51+
target: targetNode,
52+
value: 1});
53+
54+
});
55+
56+
edges.forEach(function(e) {
57+
58+
if (!e.source["linkcount"]) e.source["linkcount"] = 0;
59+
if (!e.target["linkcount"]) e.target["linkcount"] = 0;
60+
61+
e.source["linkcount"]++;
62+
e.target["linkcount"]++;
63+
});
64+
65+
var width = window.innerWidth, height = window.innerHeight/3;
66+
// var height = window.innerHeight|| docEl.clientHeight|| bodyEl.clientHeight;
67+
// var width = window.innerWidth || docEl.clientWidth || bodyEl.clientWidth;
68+
var svg = d3.select("#vis").append("svg")
69+
.attr("width", width)
70+
.attr("height", height);
71+
72+
// updated for d3 v4.
73+
var simulation = d3.forceSimulation()
74+
.force("link", d3.forceLink().id(function(d) { return d.id; }))
75+
.force("charge", d3.forceManyBody().strength(strength))
76+
.force("center", d3.forceCenter(width / 2, height / 2))
77+
.force("collision", d3.forceCollide(35));
78+
79+
// Node charge strength. Repel strength greater for less links.
80+
//function strength(d) { return -50/d["linkcount"] ; }
81+
function strength(d) { return -25 ; }
82+
83+
// Link distance. Distance increases with number of links at source and target
84+
function distance(d) { return (60 + (d.source["linkcount"] * d.target["linkcount"])) ; }
85+
86+
// Link strength. Strength is less for highly connected nodes (move towards target dist)
87+
function strengthl(d) { return 5/(d.source["linkcount"] + d.target["linkcount"]) ; }
88+
89+
simulation
90+
.nodes(nodes)
91+
.on("tick", tick);
92+
93+
simulation.force("link")
94+
.links(edges)
95+
.distance(distance)
96+
.strength(strengthl);
97+
98+
// build the arrow.
99+
svg.append("svg:defs").selectAll("marker")
100+
.data(["end"]) // Different link/path types can be defined here
101+
.enter().append("svg:marker") // This section adds in the arrows
102+
.attr("id", String)
103+
.attr("viewBox", "0 -5 10 10")
104+
.attr("refX", 25) // Moves the arrow head out, allow for radius
105+
.attr("refY", 0) // -1.5
106+
.attr("markerWidth", 6)
107+
.attr("markerHeight", 6)
108+
.attr("orient", "auto")
109+
.append("svg:path")
110+
.attr("d", "M0,-5L10,0L0,5");
111+
112+
var link = svg.selectAll('.link')
113+
.data(edges)
114+
.enter().append('line')
115+
.attr("class", "link")
116+
.attr("marker-end", "url(#end)");
117+
118+
var node = svg.selectAll(".node")
119+
.data(nodes)
120+
.enter().append("g")
121+
.attr("class", function(d) { return "node " + d.type;})
122+
.attr('transform', function(d) {
123+
return "translate(" + d.x + "," + d.y + ")"})
124+
.classed("auth", function(d) { return (d.name.startsWith("auth") ? true : false);});
125+
126+
node.call(d3.drag()
127+
.on("start", dragstarted)
128+
.on("drag", dragged)
129+
.on("end", dragended));
130+
131+
// add the nodes
132+
node.append('circle')
133+
.attr('r', 16)
134+
;
135+
136+
// add text
137+
node.append("text")
138+
.attr("x", 12)
139+
.attr("dy", "-1.1em")
140+
.text(function(d) {return d.name;});
141+
142+
node.on("mouseover", function(e, d) {
143+
144+
var g = d3.select(this); // the node (table)
145+
146+
// tooltip
147+
var fields = d.fields;
148+
var fieldformat = "<TABLE>";
149+
fields.forEach(function(d) {
150+
fieldformat += "<TR><TD><B>"+ d.name+"</B></TD><TD>"+ d.type+"</TD><TD>"+ d.disp+"</TD></TR>";
151+
});
152+
fieldformat += "</TABLE>";
153+
var tiplength = d.fields.length;
154+
155+
// Define 'div' for tooltips
156+
var div = d3.select("body").append("div") // declare the tooltip div
157+
.attr("class", "tooltip") // apply the 'tooltip' class
158+
.style("opacity", 0)
159+
.html('<h5>' + d.name + '</h5>' + fieldformat)
160+
.style("left", 20 + (e.pageX) + "px")// or just (d.x + 50 + "px")
161+
.style("top", tooltop(e, tiplength))// or ...
162+
.transition()
163+
.duration(800)
164+
.style("opacity", 0.9);
165+
});
166+
167+
function tooltop(e, tiplength) {
168+
//aim to ensure tooltip is fully visible whenver possible
169+
return (Math.max(e.pageY - 20 - (tiplength * 14),0)) + "px"
170+
}
171+
172+
node.on("mouseout", function(e, d) {
173+
d3.select("body").select('div.tooltip').remove();
174+
});
175+
176+
// instead of waiting for force to end with : force.on('end', function()
177+
// use .on("tick", instead. Here is the tick function
178+
function tick() {
179+
node.attr('transform', function(d) {
180+
d.x = Math.max(30, Math.min(width - 16, d.x));
181+
d.y = Math.max(30, Math.min(height - 16, d.y));
182+
return "translate(" + d.x + "," + d.y + ")"; });
183+
184+
link.attr('x1', function(d) {return d.source.x;})
185+
.attr('y1', function(d) {return d.source.y;})
186+
.attr('x2', function(d) {return d.target.x;})
187+
.attr('y2', function(d) {return d.target.y;});
188+
};
189+
190+
function dragstarted(e, d) {
191+
if (!e.active) simulation.alphaTarget(0.3).restart();
192+
d.fx = d.x;
193+
d.fy = d.y;
194+
};
195+
196+
function dragged(e, d) {
197+
d.fx = e.x;
198+
d.fy = e.y;
199+
};
200+
201+
function dragended(e, d) {
202+
if (!e.active) simulation.alphaTarget(0);
203+
d.fx = null;
204+
d.fy = null;
205+
};
206+
207+
};

apps/_dashboard/static/js/index.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,12 @@ let init = (app) => {
204204
app.vue.walk = [];
205205
var name = app.vue.selected_app.name;
206206
Q.get('../walk/'+name).then(r=>{app.vue.walk=r.json().payload;});
207-
Q.get('../rest/'+name).then(r=>{app.vue.databases=r.json().databases;});
207+
Q.get('../rest/'+name).then(r=>{
208+
var restpayload = r.json();
209+
app.vue.databases=restpayload.databases;
210+
// d3 database graphs
211+
d3_graph(restpayload.nodes, restpayload.links);
212+
});
208213
app.vue.selected_filename = null;
209214
}
210215
app.clear_tickets = () => {

apps/_dashboard/templates/index.html

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
<link rel="shortcut icon" href="data:image/x-icon;base64,AAABAAEAAQEAAAEAIAAwAAAAFgAAACgAAAABAAAAAgAAAAEAIAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAPAAAAAA==" />
66
<link rel="stylesheet" type="text/css" href="css/future.css" />
77
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.8.1/css/all.css">
8+
<link rel="stylesheet" type="text/css" href="css/d3_graph.css" /><!-- for d3 diagram maker -->
89
</head>
910

1011
<body>
@@ -120,6 +121,12 @@ <h2><i class="fa fa-lock fa-2x"></i></h2>
120121
<label for="databases">Databases in {{selected_app.name}}</label>
121122
<div style="max-width:100vw">
122123
<div style="overflow-x:auto">
124+
<div>
125+
</br>
126+
<h2>Model graphs</h2>
127+
<div id="vis"></div><!-- d3 database diagrams-->
128+
</br>
129+
</div>
123130
<table>
124131
<tbody v-for="db in databases">
125132
<tr v-for="table in db.tables">
@@ -264,5 +271,7 @@ <h2>{{modal.title}}</h2>
264271
<script>
265272
T.languages = [[= languages]];
266273
</script>
274+
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.9.0/d3.min.js"></script><!-- d3 library for database diagrams-->
275+
<script src="js/d3_graph.js"></script><!-- d3 diagram maker -->
267276
<script src="js/index.js"></script>
268277
</html>

0 commit comments

Comments
 (0)