@@ -9,6 +9,11 @@ const { run, get, inject } = Ember;
9
9
10
10
const DURATION = 500 ;
11
11
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
+
12
17
// copied these functions temporarily from `broccoli-viz` here:
13
18
// https://github.com/ember-cli/broccoli-viz/blob/master/lib/node-by-id.js
14
19
@@ -49,6 +54,25 @@ export default Ember.Component.extend({
49
54
50
55
let g = svg . append ( "g" ) ;
51
56
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
+
52
76
let root = hierarchy ( graphNode , node => {
53
77
let children = [ ] ;
54
78
for ( let child of node . adjacentIterator ( ) ) {
@@ -61,14 +85,47 @@ export default Ember.Component.extend({
61
85
62
86
return children ;
63
87
} )
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
+ }
65
119
66
120
// for debugging
67
121
self . root = root ;
68
122
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.
69
126
let graph = cluster ( )
70
- . separation ( ( a , b ) => a . parent == b . parent ? 4 : 8 )
71
- . nodeSize ( [ 8 , 180 ] ) ;
127
+ . separation ( ( ) => 8 )
128
+ . nodeSize ( [ 9 , 280 ] ) ;
72
129
73
130
function update ( source ) {
74
131
graph ( root ) ;
@@ -78,6 +135,18 @@ export default Ember.Component.extend({
78
135
. selectAll ( ".node" )
79
136
. data ( nodes , d => d . data . id ) ;
80
137
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).
81
150
let nodeEnter = node
82
151
. enter ( )
83
152
. append ( "g" )
@@ -95,42 +164,69 @@ export default Ember.Component.extend({
95
164
update ( d ) ;
96
165
} ) ;
97
166
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" ) ;
109
191
110
192
nodeEnter
111
193
. 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" )
114
199
. text ( function ( d ) {
115
- return `${ d . data . label . name } ( ${ d . data . _id } ) ` ;
200
+ return `${ d . data . label . name } ` ;
116
201
} ) ;
117
202
118
203
nodeEnter
119
204
. 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' )
122
208
. text ( function ( d ) {
123
209
return `total: ${ ( d . value / 1000000 ) . toFixed ( 2 ) } ` ;
124
210
} ) ;
125
211
126
212
nodeEnter
127
213
. 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' )
130
217
. text ( function ( d ) {
131
218
return `self: ${ ( d . data . _stats . time . self / 1000000 ) . toFixed ( 2 ) } ` ;
132
219
} ) ;
133
220
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
+
134
230
// update exiting node locations
135
231
node
136
232
. transition ( )
@@ -147,6 +243,10 @@ export default Ember.Component.extend({
147
243
} )
148
244
. remove ( ) ;
149
245
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).
150
250
let link = g
151
251
. selectAll ( ".link" )
152
252
. data ( links , d => d . target . data . id ) ;
@@ -156,20 +256,28 @@ export default Ember.Component.extend({
156
256
. append ( "path" )
157
257
. attr ( "class" , "link" )
158
258
. 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
+
159
262
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 ;
163
267
} ) ;
164
268
165
269
link
166
270
. transition ( )
167
271
. duration ( DURATION )
168
272
. 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
+
169
276
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 ;
173
281
} ) ;
174
282
175
283
// update exiting link locations
0 commit comments