Skip to content

Commit 0ad7402

Browse files
authored
Merge pull request #15 from dcombslinkedin/master
Reformatting nodes and links to use lines instead of curves between n…
2 parents 4fde225 + 50afebf commit 0ad7402

File tree

4 files changed

+251
-50
lines changed

4 files changed

+251
-50
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(() => 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/components/slow-node-times.js

Lines changed: 66 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,31 @@ function selfTime(node) {
1313
return value;
1414
}
1515
}
16+
return 0;
1617
}
1718

18-
function nodeTime(node) {
19-
let nodeTotal = 0;
20-
for (let childNode of node.dfsIterator((n) => n.label.broccoliNode)) {
21-
nodeTotal += selfTime(childNode);
19+
// Given a node, compute the total time taken by the node
20+
// and its children (by summing the total time of the children
21+
// and adding the self time of the node). Return that value,
22+
// and assign it to the _stats.time.plugin attribute of the node.
23+
// Note: we skip the non-broccoliNodes except at the beginning
24+
// (the root of the tree is not a broccoliNode, but we want to
25+
// proceed to its children
26+
function computeNodeTimes(node) {
27+
var total = selfTime(node);
28+
29+
for (let childNode of node.adjacentIterator()) {
30+
if (childNode.label.broccoliNode) {
31+
total += computeNodeTimes(childNode);
32+
}
2233
}
2334

24-
return nodeTotal;
35+
Ember.set(node._stats.time, 'plugin', total);
36+
37+
return total;
2538
}
2639

40+
2741
export default Ember.Component.extend({
2842
graph: inject.service(),
2943

@@ -34,29 +48,32 @@ export default Ember.Component.extend({
3448

3549
nodes: computed('data', 'filter', 'pluginNameFilter', 'groupByPluginName', function() {
3650
let data = this.get('data');
51+
3752
let nodes = [];
3853

39-
if (!data) { return nodes; }
54+
if (!data) {
55+
return nodes;
56+
}
57+
58+
computeNodeTimes(data); // start at root node of tree (which is not a broccoliNode)
4059

4160
for (let node of data.dfsIterator()) {
4261
if (node.label.broccoliNode) {
4362
nodes.push(node);
44-
if (!node._stats.time.plugin) {
45-
node._stats.time.plugin = nodeTime(node);
46-
}
4763
}
4864
}
4965

5066
let pluginNameFilter = this.get('pluginNameFilter');
5167
if (pluginNameFilter) {
5268
nodes = nodes.filter((node) => {
53-
if (!node.label.broccoliNode) { return false; }
54-
if (node.label.broccoliPluginName !== pluginNameFilter) { return false; }
55-
56-
return true;
69+
return (node.label.broccoliNode &&
70+
(pluginNameFilter === node.label.broccoliPluginName ||
71+
pluginNameFilter === 'undefined' && node.label.broccoliPluginName === undefined));
5772
});
5873
}
5974

75+
// Note: the following is also gathering stats for the items that
76+
// have no broccoliPluginName (the 'name' is undefined).
6077
let groupByPluginName = this.get('groupByPluginName');
6178
if (groupByPluginName) {
6279
let pluginNameMap = nodes.reduce((memo, node) => {
@@ -65,9 +82,10 @@ export default Ember.Component.extend({
6582
memo[pluginName].time += node._stats.time.plugin;
6683
memo[pluginName].count++;
6784
return memo;
68-
}, {})
85+
}, {});
6986

7087
nodes = [];
88+
7189
for (let pluginName in pluginNameMap) {
7290
nodes.push({
7391
groupedByPluginName: true,
@@ -82,6 +100,36 @@ export default Ember.Component.extend({
82100
return nodes;
83101
}).readOnly(),
84102

103+
pluginNames: computed('nodes', function() {
104+
let nodes = this.get('nodes');
105+
106+
if (!nodes || nodes.length === 0) {
107+
return [];
108+
}
109+
110+
// If the first item in the list is an object with
111+
// 'groupedByPluginName' = true, we just need to pull
112+
// off the label as the plugin name. If not, we need
113+
// to create a map of the plugin names and return that.
114+
let pluginNames = [];
115+
116+
if (nodes[0].groupedByPluginName === true) {
117+
pluginNames = nodes.map(node => node.label.name);
118+
} else {
119+
let pluginNameMap = nodes.reduce((memo, node) => {
120+
let pluginName = node.label.broccoliPluginName;
121+
memo[pluginName] = pluginName;
122+
return memo;
123+
}, {});
124+
125+
pluginNames = Object.keys(pluginNameMap);
126+
}
127+
128+
pluginNames.sort();
129+
130+
return pluginNames;
131+
}).readOnly(),
132+
85133
sortedNodes: computed('nodes', 'sortDescending', function() {
86134
let sortDescending = this.get('sortDescending');
87135
return this.get('nodes').sort((a, b) => {
@@ -118,6 +166,10 @@ export default Ember.Component.extend({
118166

119167
toggleTime() {
120168
this.toggleProperty('sortDescending');
169+
},
170+
171+
selectFilter(value) {
172+
this.set('pluginNameFilter', (value === 'clearFilter' ? undefined : value));
121173
}
122174
}
123175
});

0 commit comments

Comments
 (0)