Skip to content

Commit a2ec657

Browse files
Reformatting nodes and links to use lines instead of curves between nodes,
to offset links so there is a consistent distance away from the node, use common separation between nodes vertically, and re-format the display of each node into a box.
1 parent de497a8 commit a2ec657

File tree

2 files changed

+140
-27
lines changed

2 files changed

+140
-27
lines changed

app/components/basic-tree.js

Lines changed: 135 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ const { run, get, inject } = Ember;
99

1010
const DURATION = 500;
1111

12+
// The offset amount (in px) from the left or right side of a node
13+
// box to offset lines between nodes, so the lines don't come right
14+
// up to the edge of the box.
15+
const NODE_OFFSET_SIZE = 50;
16+
1217
// copied these functions temporarily from `broccoli-viz` here:
1318
// https://github.com/ember-cli/broccoli-viz/blob/master/lib/node-by-id.js
1419

@@ -49,6 +54,25 @@ export default Ember.Component.extend({
4954

5055
let g = svg.append("g");
5156

57+
// Compute the width of a line of text. For now we'll fake it
58+
// by assuming a constant char width. Add 20 for 'padding'.
59+
// TODO: convert to the real line size based on the real characters.
60+
function computeLineWidth(str) {
61+
const CHAR_WIDTH = 5;
62+
let val = str.length * CHAR_WIDTH + 20;
63+
return val;
64+
}
65+
66+
// Given a node, compute the width of the box needed to hold
67+
// the text of the element, by computing the max of the widths
68+
// of all the text lines.
69+
function computeNodeWidth(d) {
70+
return Math.max(computeLineWidth(d.data.label.name),
71+
computeLineWidth(`total: ${(d.value / 1000000).toFixed(2)}`),
72+
computeLineWidth(`self: ${(d.data._stats.time.self / 1000000).toFixed(2)}`),
73+
computeLineWidth(`node id: ${d.data._id}`));
74+
}
75+
5276
let root = hierarchy(graphNode, node => {
5377
let children = [];
5478
for (let child of node.adjacentIterator()) {
@@ -61,14 +85,47 @@ export default Ember.Component.extend({
6185

6286
return children;
6387
})
64-
.sum(d => d._stats.time.self);
88+
.sum(d => d._stats.time.self)
89+
.each(d => d.computedWidth = computeNodeWidth(d));
90+
91+
// For each node height (distance above leaves, which are height = 0)
92+
// keep track of the maximum cell width at that height and then use that
93+
// to compute the desired X position for all the nodes at that height.
94+
let nodeHeightData = [];
95+
96+
root.each((d) => {
97+
let heightData = nodeHeightData[d.height];
98+
if (heightData === undefined) {
99+
heightData = {maxWidth: d.computedWidth, x: 0 }
100+
nodeHeightData[d.height] = heightData;
101+
} else if (heightData.maxWidth < d.computedWidth) {
102+
heightData.maxWidth = d.computedWidth;
103+
}
104+
});
105+
106+
// Now that we have the maxWidth data for all the heights, compute
107+
// the X position for all the cells at each height.
108+
// Each height except the root will have NODE_OFFSET_SIZE on the front.
109+
// Each height except the leaves (height=0) will have NODE_OFFSET_SIZE after it.
110+
// We have to iterate through the list in reverse, since height 0
111+
// has its X value calculated last.
112+
let currX = 0;
113+
114+
for (let i = nodeHeightData.length - 1; i >= 0; i--) {
115+
let item = nodeHeightData[i];
116+
item.x = currX;
117+
currX = currX + item.maxWidth + (2 * NODE_OFFSET_SIZE);
118+
}
65119

66120
// for debugging
67121
self.root = root;
68122

123+
// Create the graph. The nodeSize() is [8,280] (width, height) because we
124+
// want to change the orientation of the graph from top-down to left-right.
125+
// To do that we reverse X and Y for calculations and translations.
69126
let graph = cluster()
70-
.separation((a,b) => a.parent == b.parent ? 4 : 8)
71-
.nodeSize([8, 180]);
127+
.separation((a,b) => 8)
128+
.nodeSize([9, 280]);
72129

73130
function update(source) {
74131
graph(root);
@@ -78,6 +135,18 @@ export default Ember.Component.extend({
78135
.selectAll(".node")
79136
.data(nodes, d => d.data.id);
80137

138+
// The graph is laid out by graph() as vertically oriented
139+
// (the root is at the top). We want to show it as horizontally
140+
// oriented (the root is at the left). In addition, we want
141+
// each 'row' of nodes to show up as a column with the cells
142+
// aligned on their left edge at the cell's 0 point.
143+
// To do all this, we'll flip the d.x and d.y values when translating
144+
// the node to its position.
145+
root.each(d => d.y = nodeHeightData[d.height].x);
146+
147+
// For the 'enter' set, create a node for each entry.
148+
// Move the node to the computed node point (remembering
149+
// that X and Y are reversed so we get a horizontal graph).
81150
let nodeEnter = node
82151
.enter()
83152
.append("g")
@@ -95,42 +164,69 @@ export default Ember.Component.extend({
95164
update(d);
96165
});
97166

98-
// we want to wrap the next few text lines in a rect
99-
// but alignment is annoying, punting for now...
100-
//
101-
// nodeEnter.append("rect")
102-
// .attr('x', '-75')
103-
// .attr('y', '-1.5em')
104-
// .attr('width', '75px')
105-
// .attr('height', "3em")
106-
// .attr('stroke', "black")
107-
// .attr('stroke-width', 1)
108-
// .style('fill', "#fff");
167+
// Draw the node in a box
168+
nodeEnter.append("rect")
169+
.attr('x', 0)
170+
.attr('y', '-2em')
171+
.attr('width', function(d) {
172+
return d.computedWidth;
173+
})
174+
.attr('height', "4em")
175+
.attr('stroke', "black")
176+
.attr('stroke-width', 1)
177+
.style('fill', "#fff");
178+
179+
// Draw a box in a separate color for the first line as
180+
// a 'title'.
181+
nodeEnter.append("rect")
182+
.attr('x', 0)
183+
.attr('y', '-2em')
184+
.attr('width', function(d) {
185+
return d.computedWidth;
186+
})
187+
.attr('height', "1em")
188+
.attr('stroke', "black")
189+
.attr('stroke-width', 1)
190+
.style('fill', "#000000");
109191

110192
nodeEnter
111193
.append("text")
112-
.attr("dy", '0.0em')
113-
.style("text-anchor", function(d) { return d.children ? "end" : "start"; })
194+
.attr('text-anchor', 'middle')
195+
.attr("x", d => d.computedWidth/2)
196+
.attr("y", '-1.7em')
197+
.attr("class", "nodetitle")
198+
.attr("font-weight", "bold")
114199
.text(function(d) {
115-
return `${d.data.label.name} (${d.data._id})`;
200+
return `${d.data.label.name}`;
116201
});
117202

118203
nodeEnter
119204
.append("text")
120-
.attr("dy", '1.1em')
121-
.style("text-anchor", function(d) { return d.children ? "end" : "start"; })
205+
.attr('text-anchor', 'middle')
206+
.attr("x", d => d.computedWidth/2)
207+
.attr("y", '-0.4em')
122208
.text(function(d) {
123209
return `total: ${(d.value / 1000000).toFixed(2)}`;
124210
});
125211

126212
nodeEnter
127213
.append("text")
128-
.attr("dy", '2.1em')
129-
.style("text-anchor", function(d) { return d.children ? "end" : "start"; })
214+
.attr('text-anchor', 'middle')
215+
.attr("x", d => d.computedWidth/2)
216+
.attr("y", '0.8em')
130217
.text(function(d) {
131218
return `self: ${(d.data._stats.time.self / 1000000).toFixed(2)}`;
132219
});
133220

221+
nodeEnter
222+
.append("text")
223+
.attr('text-anchor', 'middle')
224+
.attr("x", d => d.computedWidth/2)
225+
.attr("y", '2.0em')
226+
.text(function(d) {
227+
return `node id: ${d.data._id}`;
228+
});
229+
134230
// update exiting node locations
135231
node
136232
.transition()
@@ -147,6 +243,10 @@ export default Ember.Component.extend({
147243
})
148244
.remove();
149245

246+
// Create all the links between the various nodes. Each node
247+
// will have the link from an earlier node (higher height)
248+
// come into the 0 point for the node, and the links to lower
249+
// height nodes start at the right edge of the node (+ NODE_OFFSET_SIZE).
150250
let link = g
151251
.selectAll(".link")
152252
.data(links, d => d.target.data.id);
@@ -156,20 +256,28 @@ export default Ember.Component.extend({
156256
.append("path")
157257
.attr("class", "link")
158258
.attr("d", function(d) {
259+
let sourceExitY = d.source.y + d.source.computedWidth + NODE_OFFSET_SIZE;
260+
let targetEntranceY = d.target.y - NODE_OFFSET_SIZE;
261+
159262
return "M" + d.target.y + "," + d.target.x
160-
+ "C" + (d.source.y + 50) + "," + d.target.x
161-
+ " " + (d.source.y + 50) + "," + d.source.x
162-
+ " " + d.source.y + "," + d.source.x;
263+
+ "L" + targetEntranceY + "," + d.target.x
264+
+ " " + sourceExitY + "," + d.target.x
265+
+ " " + sourceExitY + "," + d.source.x
266+
+ " " + (sourceExitY - NODE_OFFSET_SIZE) + "," + d.source.x;
163267
});
164268

165269
link
166270
.transition()
167271
.duration(DURATION)
168272
.attr("d", function(d) {
273+
let sourceExitY = d.source.y + d.source.computedWidth + NODE_OFFSET_SIZE;
274+
let targetEntranceY = d.target.y - NODE_OFFSET_SIZE;
275+
169276
return "M" + d.target.y + "," + d.target.x
170-
+ "C" + (d.source.y + 50) + "," + d.target.x
171-
+ " " + (d.source.y + 50) + "," + d.source.x
172-
+ " " + d.source.y + "," + d.source.x;
277+
+ "L" + targetEntranceY + "," + d.target.x
278+
+ " " + sourceExitY + "," + d.target.x
279+
+ " " + sourceExitY + "," + d.source.x
280+
+ " " + (sourceExitY - NODE_OFFSET_SIZE) + "," + d.source.x;
173281
});
174282

175283
// update exiting link locations

app/styles/app.css

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,11 @@ body > [class="ember-view"],
3838
font: 10px sans-serif;
3939
}
4040

41+
.nodetitle {
42+
fill: white;
43+
font-weight: bold !important;
44+
}
45+
4146
.link {
4247
fill: none;
4348
stroke: #555;

0 commit comments

Comments
 (0)