@@ -9,6 +9,11 @@ const { run, get, inject } = Ember;
99
1010const 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
0 commit comments