PageSourceSearch

https://embed-goodfire-attr-graph.netlify.app/attribution_graph/init-cg-subgraph.js

js embed-goodfire-attr-graph.netlify.app collected 2026-10-03 10:41:51 UTC 22,173 bytes, 617 lines download raw bytes

1window.initCgSubgraph = function ({visState, renderAll, data, cgSel}) {
2  var subgraphSel = cgSel.select('.subgraph')
3  subgraphSel.datum().resizeFn = renderSubgraph
4
5  var nodeIdToNode = {}
6  var sgNodes = []
7  var sgLinks = []
8
9  let nodeSel = null
10  let memberNodeSel = null
11  let simulation = null
12
13  var nodeWidth = 75
14  var nodeHeight = 25
15
16  function supernodesToUrl() {
17    // util.params.set('supernodes', JSON.stringify(subgraphState.supernodes))
18  }
19
20  var subgraphState = visState.subgraph = visState.subgraph || {
21    sticky: true,
22    dagrefy: true,
23    supernodes: visState.supernodes || [],
24    activeGrouping: {
25      isActive: false,
26      selectedNodeIds: new Set(),
27    }
28  }
29  
30  d3.select('body')
31    .on('keydown.grouping' + data.metadata.slug, ev => {
32      if (ev.repeat) return
33      if (!visState.isEditMode || ev.key != 'g') return
34      subgraphState.activeGrouping.isActive = true
35      styleNodes()
36      
37      subgraphSel.classed('is-grouping', true)
38    })
39    .on('keyup.grouping' + data.metadata.slug, ev => {
40      if (!visState.isEditMode || ev.key != 'g') return
41      if (subgraphState.activeGrouping.selectedNodeIds.size > 1){
42        var allSelectedIds = []
43        var prevSupernodeLabel = ''
44        subgraphState.activeGrouping.selectedNodeIds.forEach(id => {
45          var node = nodeIdToNode[id]
46          if (!node?.memberNodeIds) return allSelectedIds.push(id)
47          prevSupernodeLabel = node.ppClerp
48
49          // if a supernode is selected, remove the previous super node
50          subgraphState.supernodes = subgraphState.supernodes.filter(([label, ...nodeIds]) =>
51            !nodeIds.every(d => node.memberNodeIds.includes(d))
52          )
53          // and adds its member nodes to selection
54          node.memberNodeIds.forEach(id => allSelectedIds.push(id))
55        })
56
57        var label = prevSupernodeLabel || allSelectedIds
58          .map(id => nodeIdToNode[id]?.ppClerp)
59          .find(d => d) || 'supernode'
60        subgraphState.supernodes.push([label, ...new Set(allSelectedIds)])
61        supernodesToUrl()
62      }
63      subgraphState.activeGrouping.isActive = false
64      subgraphState.activeGrouping.selectedNodeIds.clear()
65      renderSubgraph()
66      
67      subgraphSel.classed('is-grouping', false)
68    })
69
70  let {nodes, links} = data
71  
72  function renderSubgraph() {
73    var c = d3.conventions({
74      sel: subgraphSel.html(''),
75      margin: {top: 26, bottom: 5, left: visState.isHideLayer ? 0 : 30},
76      layers: 'sd',
77    })
78    // subgraphSel.st({borderTop: '1px solid #eee'})
79    
80    c.svg.append('text.section-title').text('Subgraph').translate(-16, 1)
81    c.svg.append('g.border-path').append('path')
82      .at({stroke: '#eee', d: 'M 0 -10 H ' + c.width})
83
84
85    var [svg, div] = c.layers
86
87    // // set up arrowheads
88    // svg.appendMany('marker', [{id: 'mid-negative', color: '#40004b'},{id: 'mid-positive', color: '#00441b'}])
89    //   .at({id: d => d.id, orient: 'auto', refX: .1, refY: 1}) // marker-height/marker-width?
90    //   .append('path')
91    //   .at({d: 'M0,0 V2 L1,1 Z', fill: d => d.color})
92
93
94    // pick out the subgraph and do supernode surgery
95    nodes.forEach(d => d.supernodeId = null)
96    var pinnedIds = visState.pinnedIds.slice(0, 200) // max of 200 nodes
97    var pinnedNodes = nodes.filter(d => pinnedIds.includes(d.nodeId))
98
99    // create supernodes and mark their children
100    nodeIdToNode = Object.fromEntries(pinnedNodes.map(d => [d.nodeId, d]))
101    var supernodes = subgraphState.supernodes
102      .map(([label, ...nodeIds], i) => {
103        var nodeId = nodeIdToNode[label] ? `supernode-${i}` : label
104        var memberNodes = nodeIds.map(id => nodeIdToNode[id]).filter(d => d)
105        memberNodes.forEach(d => d.supernodeId = nodeId)
106  
107        var rv = {
108          nodeId,
109          featureId: `supernode-${i}`,
110          ppClerp: label,
111          layer: d3.mean(memberNodes, d => +d.layer),
112          ctx_idx: d3.mean(memberNodes, d => d.ctx_idx),
113          ppLayer: d3.extent(memberNodes, d => +d.layer).join('—'),
114          streamIdx: d3.mean(memberNodes, d => d.streamIdx),
115          memberNodeIds: nodeIds,
116          memberNodes,
117          isSuperNode: true,
118        }
119        nodeIdToNode[rv.nodeId] = rv
120  
121        return rv
122      })
123      .filter(d => d.memberNodes.length)
124    
125    // update clerps — fragile hack if hClerpUpdate changes
126    nodes.forEach(d => d.ppClerp = d.clerp)
127    supernodes.forEach(({ppClerp, memberNodes}) => {
128      if (memberNodes.length == 1 && ppClerp == memberNodes[0].ppClerp) return
129      
130      memberNodes.forEach(d => {
131        d.ppClerp = `[${ppClerp}]` + (ppClerp != d.ppClerp ? ' ' + d.ppClerp : '')
132      })
133    })
134    
135    // inputAbsSumExternalSn: the abs sum of input links from outside the supernode
136    pinnedNodes.forEach(d => {
137      d.inputAbsSumExternalSn = d3.sum(d.sourceLinks, e => {
138        if (!e.sourceNode.supernodeId) return Math.abs(e.weight)
139        return e.sourceNode.supernodeId == d.supernodeId ? 0 : Math.abs(e.weight)
140      })
141      d.sgSnInputWeighting = d.inputAbsSumExternalSn/d.inputAbsSum
142    })
143
144    // subgraph plots pinnedNodes not in a supernode and supernodes
145    sgNodes = pinnedNodes.filter(d => !d.supernodeId).concat(supernodes)
146    sgNodes.forEach(d => {
147      // for supernodes, sum up values from member nodes
148      if (d.isSuperNode) {
149        d.inputAbsSum = d3.sum(d.memberNodes, e => e.inputAbsSum)
150        d.inputAbsSumExternalSn = d3.sum(d.memberNodes, e => e.inputAbsSumExternalSn)
151      } else {
152        d.memberNodes = [d]
153      }
154
155      var sum = d3.sum(d.memberNodes, e => e.sgSnInputWeighting)
156      d.memberNodes.forEach(e => e.sgSnInputWeighting = e.sgSnInputWeighting/sum)
157    })
158
159    // select subgraph links
160    sgLinks = links
161      .filter(d => nodeIdToNode[d.sourceNode.nodeId] && nodeIdToNode[d.targetNode.nodeId])
162      .map(d => ({
163        source: d.sourceNode.nodeId,
164        target: d.targetNode.nodeId,
165        weight: d.weight,
166        color: d.pctInputColor,
167        ogLink: d,
168      }))
169
170    // then remap source/target to supernodes
171    sgLinks.forEach(link => {
172      if (nodeIdToNode[link.source]?.supernodeId) link.source = nodeIdToNode[link.source].supernodeId
173      if (nodeIdToNode[link.target]?.supernodeId) link.target = nodeIdToNode[link.target].supernodeId
174    })
175
176    // finally combine parallel links and remove self-links
177    sgLinks = d3.nestBy(sgLinks, d => d.source + '-' + d.target)
178      .map(links => {
179        var weight = d3.sum(links, link => {
180          var {inputAbsSumExternalSn, sgSnInputWeighting} = link.ogLink.targetNode
181          return link.weight/inputAbsSumExternalSn*sgSnInputWeighting
182        })
183
184        return {
185          source: links[0].source,
186          target: links[0].target,
187          weight,
188          color: utilCg.pctInputColorFn(weight),
189          pctInput: weight,
190          pctInputColor: utilCg.pctInputColorFn(weight),
191          ogLinks: links
192        }
193      })
194      .filter(d => d.source !== d.target)
195    sgLinks = d3.sort(sgLinks, d => Math.abs(d.weight))
196
197    let xScale = d3.scaleLinear()
198      .domain(d3.extent(sgNodes.map(d => d.ctx_idx)))
199      .range([0, c.width*3/4])
200    let yScale = d3.scaleLinear()
201      .domain(d3.extent(sgNodes.map(d => d.streamIdx)).toReversed())
202      .range([0, c.height - nodeHeight])
203
204    // d3.force is impure, need copy
205    // Also want to persist these positions across node changes
206    const existingNodes = window.selForceNodes && new Map(window.selForceNodes.map(n => [n.node.nodeId, n]))
207    window.selForceNodes = sgNodes.map(node => {
208      const existing = existingNodes?.get(node.nodeId)
209      return {
210        x: existing ? existing.x : xScale(node.ctx_idx),
211        y: existing ? existing.y : yScale(node.streamIdx),
212        fx: existing?.fx,
213        fy: existing?.fy,
214        nodeId: node.nodeId, // for addFeatureEvents
215        featureId: node.featureId, // for addFeatureEvents
216        node,
217        sortedSlug: d3.sort(node.memberNodes.map(d => d.featureIndex).join(' ')),
218      }
219    })
220    
221
222    var selForceNodes = window.selForceNodes = d3.sort(window.selForceNodes, d => d.sortedSlug)
223    window._exportSubgraphPos = function(){
224      return selForceNodes.map(d => [d.x/c.width*1000, d.y/c.height*1000].map(Math.round)).flat().join(' ')
225    }
226
227    if (simulation) simulation.stop()
228    simulation = d3.forceSimulation(selForceNodes)
229      .force('link', d3.forceLink(sgLinks).id(d => d.node.nodeId))
230      .force('charge', d3.forceManyBody())
231      .force('collide', d3.forceCollide(Math.sqrt(nodeHeight ** 2 + nodeWidth ** 2) / 2))
232      .force('container', forceContainer([[-10, 0], [c.width - nodeHeight, c.height - nodeHeight]]))
233      .force('x', d3.forceX(d => xScale(d.node.ctx_idx)).strength(.1))
234      .force('y', d3.forceY(d => yScale(d.node.streamIdx)).strength(2))
235
236    var svgPaths = svg.appendMany('path.link-path', sgLinks).at({
237      fill: 'none',
238      markerMid: d => d.weight > 0 ? 'url(#mid-positive)' : 'url(#mid-negative)',
239      strokeWidth: d => Math.abs(d.weight)*15,
240      stroke: d => d.color,
241      opacity: 0.8,
242      strokeLinecap: 'round',
243    })
244
245    var edgeLabels = svg.appendMany('text.weight-label', sgLinks)
246      // .text(d => d3.format('+.2f')(d.weight))
247
248    simulation.on('tick', renderForce)
249
250    var drag = d3.drag()
251      .on('drag', (ev) => {
252        // Only when actually dragging, mark as no longer dagre positioned and restart sim
253        ev.subject.dagrePositioned = false
254        if (!ev.active) simulation.alphaTarget(0.3).restart()
255        ev.subject.fx = ev.subject.x = ev.x
256        ev.subject.fy = ev.subject.y = ev.y
257        renderForce()
258      })
259      .on('end', (ev) => {
260        if (!ev.active) simulation.alphaTarget(0)
261        if (!subgraphState.sticky && !ev.subject.dagrePositioned){
262          ev.subject.fx = null
263          ev.subject.fy = null
264        }
265      })
266
267    nodeSel = div
268      .appendMany('div.supernode-container', selForceNodes)
269      .translate(d => [d.x, d.y])
270      .st({width: nodeWidth, height: nodeHeight})
271      .call(utilCg.addFeatureEvents(visState, renderAll, ev => ev.shiftKey))
272      .on('click.group', (ev, d) => {
273        var {isActive, selectedNodeIds} = subgraphState.activeGrouping
274        if (!isActive) return
275
276        // If it's a child node, use its parent supernode's ID instead
277        var nodeId = d.supernodeId || d.nodeId
278        selectedNodeIds.has(nodeId) ? selectedNodeIds.delete(nodeId) : selectedNodeIds.add(nodeId)
279        
280        styleNodes()
281        ev.stopPropagation()
282        ev.preventDefault()
283      })
284      .call(drag)
285
286    selForceNodes.forEach(d => {
287      if (!d.node.memberNodes) d.node.memberNodes = [d.node]
288    })
289
290    var supernodeSel = nodeSel//.filter(d => d.node.isSuperNode)
291      .classed('is-supernode', true)
292      .st({height: nodeHeight + 12})
293
294    memberNodeSel = supernodeSel.append('div.member-circles')
295      .st({
296        width: d => d.node.memberNodes.length <= 4 ? 'auto' : 'calc(32px + 12px)', 
297        gap: d => d.node.memberNodes.length <= 4 ? 4 : 0,
298      })
299      .appendMany('div.member-circle', d => d.node.memberNodes)
300      .classed('not-clt-feature', d => d.feature_type != 'cross layer transcoder')
301      .st({marginLeft: function(d, i) {
302          var n = this.parentNode.childNodes.length  
303          return n <= 4 ? 0 : i == 0 ? 0 : -((n - 4)*8)/(n - 1)
304      }})
305      .call(utilCg.addFeatureEvents(visState, renderAll, ev => ev.shiftKey))
306      .on('click.stop-parent', ev => {
307        if (!subgraphState.activeGrouping.isActive) ev.stopPropagation()
308      })  
309      .on('mouseover.stop-parent', ev => ev.stopPropagation())
310      .at({title: d => d.ppClerp})
311
312    if (visState.isEditMode) {
313      // TODO: enable
314      supernodeSel.select('.member-circles')
315        .filter(d => d.node.isSuperNode)
316        .append('div.ungroup-btn')
317        .text('×').st({top: 2, left: -15, position: 'absolute'})
318        .on('click', (ev, d) => {
319          ev.stopPropagation()
320          
321          subgraphState.supernodes = subgraphState.supernodes.filter(([label, ...nodeIds]) =>
322            !nodeIds.every(id => d.node.memberNodeIds.includes(id))
323          )
324          supernodesToUrl()
325          renderSubgraph()
326        })
327    }
328
329    var nodeTextSel = nodeSel.append('div.node-text-container')
330    nodeTextSel.append('span')
331      .text(d => d.node.ppClerp)
332      .on('click', (ev, d) => {
333        if (!visState.isEditMode) return
334        if (!d.node.isSuperNode) return
335        // TODO: enable?
336        return
337        ev.stopPropagation()
338
339        var spanSel = d3.select(ev.target).st({display: 'none'})
340        var input = d3.select(spanSel.node().parentNode).append('input')
341          .at({class: 'temp-edit', value: spanSel.text()})
342          .on('blur', save)
343          .on('keydown', ev => {
344            if (ev.key === 'Enter'){
345              save()
346              input.node().blur()
347            }
348            ev.stopPropagation()
349          })
350
351        input.node().focus()
352
353        function save(){
354          var idx = subgraphState.supernodes.findIndex(([label, ...nodeIds]) =>
355            nodeIds.every(id => d.node.memberNodeIds.includes(id))
356          )
357          if (idx >= 0){
358            subgraphState.supernodes[idx][0] = input.node().value || 'supernode'
359            supernodesToUrl()
360            renderSubgraph()
361          }
362        }
363      })
364
365
366    nodeTextSel.each(function(d) {
367      d.textHeight = this.getBoundingClientRect().height || -8
368    })
369
370    nodeSel.append('div.clicked-weight.source')
371    nodeSel.append('div.clicked-weight.target')
372    styleNodes()
373
374
375    var checkboxes = Object.entries({
376      sticky: () => {
377        // simulation.alphaTarget(0.3).restart()
378        if (!subgraphState.sticky) unsticky()
379      },
380      dagrefy: () => {
381        subgraphState.dagrefy ? dagrefy() : selForceNodes.forEac
381h(d => d.dagrePositioned = null)
382      },
383    }).map(([key, fn]) => ({key, fn}))
384
385
386    if (visState.isEditMode && false) {
387      div.append('div.checkbox-container').translate([-c.margin.left, c.margin.bottom])
388        .appendMany('label', checkboxes).append('input')
389        .at({type: 'checkbox'})
390        .property('checked', d => subgraphState[d.key])
391        .on('change', function(ev, d){
392          subgraphState[d.key] = this.checked
393          d.fn()
394        })
395        .parent().append('span').text(d => d.key)
396    }
397
398    checkboxes.forEach(d => d.fn())
399
400    function unsticky(){
401      selForceNodes.forEach(d => (d.fx = d.fy = null))
402      simulation.alphaTarget(0.3).restart()
403      if (subgraphState.dagrefy) {
404        subgraphState.dagrefy = false
405        d3.select('.checkbox-container').selectAll('input').filter(d => d.key == 'dagrefy').property('checked', 0)
406        checkboxes.find(d => d.key == 'dagrefy').fn()
407      }
408    }
409
410    function dagrefy(){
411      if (visState.sg_pos){
412        var nums = visState.sg_pos.split(' ').map(d => +d)
413        selForceNodes.forEach((d, i) => {
414          d.fx = d.x = nums[i*2 + 0]/1000*c.width
415          d.fy = d.y = nums[i*2 + 1]/1000*c.height
416        })
417
418        nodeSel.translate(d => [d.x, d.y])
419        styleNodes()
420        renderEdges()
421
422        visState.og_sg_pos = visState.sg_pos
423        delete visState.sg_pos
424      }
425      if (visState.og_sg_pos) return
426
427
428      var g = new window.dagre.graphlib.Graph()
429      g.setGraph({rankdir: 'BT', nodesep: 20, ranksep: 20})
430      g.setDefaultEdgeLabel(() => ({}))
431
432      sgLinks.forEach(d =>{
433        if (Math.abs(d.weight) < .003) return
434        // set width and height to make dagre return x and y for edges
435        g.setEdge(d.source.nodeId, d.target.nodeId, {width: 1, height: 1, labelpos: 'c', weight: Math.abs(d.weight)})
436      })
437      sgNodes.forEach(d => {
438        g.setNode(d.nodeId, {width: nodeWidth, height: nodeHeight})
439      })
440
441      window.dagre.layout(g)
442
443      // rescale to fit container
444      var xs = d3.scaleLinear([0, g.graph().width], [0, Math.min(c.width, g.graph().width)])
445      var ys = d3.scaleLinear([0, g.graph().height], [0, Math.min(c.height, g.graph().height)])
446
447      // flip to make ctx_idx left to right and streamIdx bottom to top
448      var w0 = d3.mean(selForceNodes, d =>  g.node(d.nodeId).x*d.node.ctx_idx)
449      var w1 = d3.mean(selForceNodes, d => -g.node(d.nodeId).x*d.node.ctx_idx)
450      if (w0 < w1) xs.range(xs.range().reverse())
451
452      var w0 = d3.mean(selForceNodes, d => g.node(d.nodeId).y*d.node.streamIdx)
453      var w1 = d3.mean(selForceNodes, d => -g.node(d.nodeId).y*d.node.streamIdx)
454      if (w0 < w1) ys.range(ys.range().reverse())
455
456      for (var node of window.selForceNodes) {
457        var pos = g.node(node.nodeId)
458        node.fx = node.x = xs(pos.x) - nodeWidth/2
459        node.fy = node.y = ys(pos.y) - nodeHeight/2
460        node.dagrePositioned = true
461      }
462
463      // var curveFactory = d3.line(d => d.x, d => d.y).curve(d3.curveBasis)
464      // svgPaths.at({d: d => {
465      //   var points = g.edge(d.source.nodeId, d.target.nodeId)?.points
466      //   if (!points) return ''
467      //   return curveFactory(points.map(p => ({x: xs(p.x), y: ys(p.y)})))
468      // }})
469      renderEdges()
470
471      // edgeLabels.translate(d => {
472      //   var pos = g.edge(d.source.nodeId, d.target.nodeId)
473      //   if (!pos) return [-100, -100]
474      //   return [xs(pos.x), ys(pos.y)]
475      // })
476      styleNodes()
477    }
478
479    function renderForce(){
480      nodeSel.translate(d => [d.x, d.y])
481
482      renderEdges()
483
484      edgeLabels
485        .filter(d => !(d.source.dagrePositioned && d.target.dagrePositioned))
486        .translate(d => [
487          (d.source.x + d.target.x) / 2 + nodeWidth / 2,
488          (d.source.y + d.target.y) / 2 + nodeHeight / 2
489        ])
490    }
491    
492    function renderEdges(){
493      
494      // TODO: use actual strokeWidth to spread
495      d3.nestBy(sgLinks, d => d.source).forEach(links => {
496        // if (links[0].source.nodeId == '6_12890134_-0') debugger
497        var numSlots = links[0].source.node.memberNodes.length
498        var totalWidth = (Math.min(4, numSlots))*8
499        d3.sort(links, d => Math.atan2(d.target.y - d.source.y, d.target.x - d.source.x))
500          .forEach((d, i) => d.sourceOffsetX = (i - links.length/2)*totalWidth/links.length)
501      })
502
503      d3.nestBy(sgLinks, d => d.target).forEach(links => {
504        var numSlots = links[0].target.node.memberNodes.length
505        var totalWidth = (Math.min(4, numSlots) + 1)*3
506        d3.sort(links, d => -Math.atan2(d.source.y - d.target.y, d.source.x - d.target.x))
507          .forEach((d, i) => d.targetOffsetX = (i - links.length/2)*totalWidth/links.length)
508      })
509
510      svgPaths.at({
511        d: d => {
512          var x0 = d.source.x + nodeWidth/2 + d.sourceOffsetX
513          var y0 = d.source.y 
514          var x1 = d.target.x + nodeWidth/2 + d.targetOffsetX
515          var y1 = d.target.y + d.target.textHeight + 28
516
517          return `M${x0},${y0} L${x1},${y1}`
518        },
519      })
520    }
521  }
522
523
524  function styleNodes() {
525    if (!nodeSel) return
526
527    nodeSel
528      .classed('clicked', d => d.nodeId == visState.clickedId)
529      .classed('hovered', d => d.featureId == visState.hoveredId)
530      .st({zIndex: d => Math.round(d.x*20 + d.y) + 1000})
531      .classed('grouping-selected', d => subgraphState.activeGrouping.selectedNodeIds.has(d.nodeId))
532
533    memberNodeSel
534      .classed('clicked', d => d.nodeId == visState.clickedId)
535      .classed('hovered', d => d.featureId == visState.hoveredId)
536      .st({
537        background: d => d.tmpClickedLink?.pctInputColor,
538        color: d => utilCg.bgColorToTextColor(d.tmpClickedLink?.pctInputColor)
539      })
540      // .at({title: d => d3.format('.1%')(d.tmpClickedLink?.pctInput)})
541
542
543
544    // style clicked links using supernode adjusted graph when possible
545    sgNodes.forEach(d => {
546      d.tmpClickedSgSource = d.tmpClickedLink?.sourceNode == d ? d.tmpClickedLink : null
547      d.tmpClickedSgTarget = d.tmpClickedLink?.targetNode == d ? d.tmpClickedLink : null
548    })
549
550    if (visState.clickedId) {
551      sgLinks.forEach(d => {
552        if (d.source.nodeId == visState.clickedId) nodeIdToNode[d.target.nodeId].tmpClickedSgTarget = d
553        if (d.target.nodeId == visState.clickedId) nodeIdToNode[d.source.nodeId].tmpClickedSgSource = d
554      })
555    }
556
557    // nodeSel.selectAll('.clicked-weight.source')
558    //   .st({display: d => d.node.tmpClickedSgSource ? '' : 'none'})
559    //   .filter(d => d.node.tmpClickedSgSource)
560    //   .text(d => d3.format('.1%')(d.node.tmpClickedSgSource.pctInput))
561    //   .st({
562    //     background: d => d.node.tmpClickedSgSource.pctInputColor,
563    //     color: d => utilCg.bgColorToTextColor(d.node.tmpClickedSgSource.pctInputColor)
564    //   })
565
566    // nodeSel.selectAll('.clicked-weight.target')
567    //   .st({display: d => d.node.tmpClickedSgTarget ? '' : 'none'})
568    //   .filter(d => d.node.tmpClickedSgTarget)
569    //   .text(d => d3.format('.1%')(d.node.tmpClickedSgTarget.pctInput))
570    //   .st({
571    //     background: d => d.node.tmpClickedSgTarget.pctInputColor,
572    //     color: d => utilCg.bgColorToTextColor(d.node.tmpClickedSgTarget.pctInputColor)
573    //   })
574  }
575
576  renderAll.hClerpUpdate.fns['subgraph'] = renderSubgraph
577  renderAll.pinnedIds.fns['subgraph'] = renderSubgraph
578  renderAll.clickedId.fns['subgraph'] = styleNodes
579  renderAll.hoveredId.fns['subgraph'] = styleNodes
580
581  // https://github.com/1wheel/d3-force-container/blob/master/src/force-container.js
582  function forceContainer(bbox) {
583    var nodes, strength = 1
584
585    function force(alpha) {
586      var i,
587          n = nodes.length,
588          node,
589          x = 0,
590          y = 0
591
592      for (i = 0; i < n; ++i) {
593        node = nodes[i], x = node.x, y = node.y
594
595        if (x < bbox[0][0]) node.vx += (bbox[0][0] - x)*alpha
596        if (y < bbox[0][1]) node.vy += (bbox[0][1] - y)*alpha
597        if (x > bbox[1][0]) node.vx += (bbox[1][0] - x)*alpha
598        if (y > bbox[1][1]) node.vy += (bbox[1][1] - y)*alpha
599      }
600    }
601
602    force.initialize = function(_){
603      nodes = _
604    }
605
606    force.bbox = function(_){
607      return arguments.length ? (bbox = +_, force) : bbox
608    }
609    force.strength = function(_){
610      return arguments.length ? (strength = +_, force) : strength
611    }
612
613    return force
614  }
615}
616
617window.init?.()

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.