mirror of
https://github.com/kevingruesser/bootstrap-vz.git
synced 2025-08-22 18:00:35 +00:00
185 lines
6.5 KiB
CoffeeScript
185 lines
6.5 KiB
CoffeeScript
class window.TaskOverview
|
|
viewBoxHeight = 800
|
|
viewBoxWidth = 800
|
|
margins =
|
|
top: 200
|
|
left: 50
|
|
bottom: 200
|
|
right: 50
|
|
gravity =
|
|
lateral: .1
|
|
longitudinal: .2
|
|
|
|
length = ([x,y]) -> Math.sqrt(x*x + y*y)
|
|
sum = ([x1,y1], [x2,y2]) -> [x1+x2, y1+y2]
|
|
diff = ([x1,y1], [x2,y2]) -> [x1-x2, y1-y2]
|
|
prod = ([x,y], scalar) -> [x*scalar, y*scalar]
|
|
div = ([x,y], scalar) -> [x/scalar, y/scalar]
|
|
unit = (vector) -> div(vector, length(vector))
|
|
scale = (vector, scalar) -> prod(unit(vector), scalar)
|
|
position = (coord, vector) -> [coord, sum(coord, vector)]
|
|
|
|
free = ([coord1, coord2]) -> diff(coord2, coord1)
|
|
pmult = (pvector=[coord1, _], scalar) -> position(coord1, prod(free(pvector), scalar))
|
|
pdiv = (pvector=[coord1, _], scalar) -> position(coord1, div(free(pvector), scalar))
|
|
|
|
constructor: ({@selector}) ->
|
|
@svg = d3.select(@selector)
|
|
.attr('viewBox', "0 0 #{viewBoxWidth} #{viewBoxHeight}")
|
|
d3.json '_static/graph.json', @buildGraph
|
|
|
|
buildGraph: (error, @data) =>
|
|
@createDefinitions()
|
|
taskLayout = @createNodes()
|
|
taskLayout.start()
|
|
|
|
createDefinitions: () ->
|
|
definitions = @svg.append 'defs'
|
|
arrow = definitions.append('marker')
|
|
arrow.attr('id', 'right-arrowhead')
|
|
.attr('refX', arrowHeight = 4)
|
|
.attr('refY', (arrowWidth = 6) / 2)
|
|
.attr('markerWidth', arrowHeight)
|
|
.attr('markerHeight', arrowWidth)
|
|
.attr('orient', 'auto')
|
|
.append('path').attr('d', "M0,0 V#{arrowWidth} L#{arrowHeight},#{arrowWidth/2} Z")
|
|
|
|
partitionKey = 'phase'
|
|
nodeColorKey = 'module'
|
|
|
|
keyMap:
|
|
phase: 'phases'
|
|
module: 'modules'
|
|
partition: (key, idx) ->
|
|
return @data[@keyMap[key]]
|
|
|
|
nodeRadius = 10
|
|
nodePadding = 10
|
|
createNodes: () ->
|
|
options =
|
|
gravity: 0
|
|
linkDistance: 50
|
|
linkStrength: .8
|
|
charge: -130
|
|
size: [viewBoxWidth, viewBoxHeight]
|
|
|
|
layout = d3.layout.force()
|
|
layout[option](value) for option, value of options
|
|
|
|
array_sum = (list) -> list.reduce(((a,b) -> a + b), 0)
|
|
partitioning =
|
|
nonLinear: (groupCounts, range, k=2) ->
|
|
ratios = (Math.pow(count, 1/k) for count in groupCounts)
|
|
fraction = range / (array_sum ratios)
|
|
return (fraction * ratio for ratio in ratios)
|
|
linear: (groups, range) ->
|
|
fraction = range / groups
|
|
return (fraction for _ in [0..groups])
|
|
offset: (ranges, i) -> (array_sum ranges.slice 0, i) + ranges[i] / 2
|
|
|
|
layout.nodes @data.nodes
|
|
layout.links @data.links
|
|
|
|
grouping = d3.nest().key((d) -> d[partitionKey]).sortKeys(d3.ascending)
|
|
|
|
widths = partitioning.nonLinear((d.values for d in grouping.rollup((d) -> d.length).entries(@data.nodes)),
|
|
viewBoxWidth - margins.left - margins.right)
|
|
heights = partitioning.nonLinear((d.values for d in grouping.rollup((d) -> d.length).entries(@data.nodes)),
|
|
viewBoxHeight - margins.top - margins.bottom, 4)
|
|
for node in @data.nodes
|
|
# node.cx = margins.left + partitioning.offset(widths, node[partitionKey])
|
|
# node.cy = viewBoxHeight / 2 + margins.top
|
|
node.cx = viewBoxWidth / 2 + margins.left
|
|
node.cy = margins.top + partitioning.offset(heights, node[partitionKey])
|
|
node.radius = nodeRadius
|
|
|
|
|
|
groups = d3.nest().key((d) -> d[partitionKey])
|
|
.sortKeys(d3.ascending)
|
|
.entries(layout.nodes())
|
|
|
|
hullColors = d3.scale.category20()
|
|
nodeColors = d3.scale.category20c()
|
|
|
|
hulls = @svg.append('g').attr('class', 'hulls')
|
|
.selectAll('path').data(groups).enter()
|
|
.append('path').attr('id', (d) -> "hull-#{d.key}")
|
|
.style
|
|
'fill': (d, i) -> hullColors(i)
|
|
'stroke': (d, i) -> hullColors(i)
|
|
|
|
hullLabels = @svg.append('g').attr('class', 'hull-labels')
|
|
.selectAll('text').data(groups).enter()
|
|
.append('text')
|
|
hullLabels.append('textPath').attr('xlink:href', (d) -> "#hull-#{d.key}")
|
|
.text((d) => @partition(partitionKey)[d.key].name)
|
|
|
|
links = @svg.append('g').attr('class', 'links')
|
|
.selectAll('line').data(layout.links()).enter()
|
|
.append('line').attr('marker-end', 'url(#right-arrowhead)')
|
|
|
|
mouseOver = (d) ->
|
|
labels.classed 'hover', (l) -> d is l
|
|
nodes.classed 'highlight', (n) -> d.module is n.module
|
|
|
|
mouseOut = (d) ->
|
|
labels.classed 'hover', no
|
|
nodes.classed 'highlight', no
|
|
|
|
nodes = @svg.append('g').attr('class', 'nodes')
|
|
.selectAll('g.partition').data(groups).enter()
|
|
.append('g').attr('class', 'partition')
|
|
.selectAll('circle').data((d) -> d.values).enter()
|
|
.append('circle').attr('r', (d) -> d.radius)
|
|
.style('fill', (d) -> nodeColors(d[nodeColorKey]))
|
|
.call(layout.drag)
|
|
.on('mouseover', mouseOver)
|
|
.on('mouseout', mouseOut)
|
|
|
|
labels = @svg.append('g').attr('class', 'node-labels')
|
|
.selectAll('g.partition').data(groups).enter()
|
|
.append('g').attr('class', 'partition')
|
|
.selectAll('text').data((d) -> d.values).enter()
|
|
.append('text').text((d) -> d.name)
|
|
.attr('transform', (d) -> offset=-(d.radius + 5); "translate(0,#{offset})")
|
|
|
|
rotate = (x, n) ->
|
|
n = n % x.length
|
|
x.slice(0,-n).reverse().concat(x.slice(-n).reverse()).reverse()
|
|
circle_coords = (parts) ->
|
|
partSize = 2*Math.PI/parts
|
|
return (for i in [0..parts]
|
|
theta = partSize*i
|
|
[(Math.cos theta), (Math.sin theta)])
|
|
hullPointMatrix = (prod(v, nodeRadius*2) for v in circle_coords(16))
|
|
hullBoundaries = (d) ->
|
|
nodePoints = d.values.map (i) -> [i.x, i.y]
|
|
padded_points = []
|
|
padded_points.push sum(p, v) for v in hullPointMatrix for p in nodePoints
|
|
points = d3.geom.hull(padded_points)
|
|
points = rotate points, Math.floor -points.length / 2.5
|
|
"M#{points.join('L')}Z"
|
|
|
|
gravity_fn = (alpha) =>
|
|
(d) ->
|
|
d.x += (d.cx - d.x) * alpha * gravity.lateral
|
|
d.y += (d.cy - d.y) * alpha * gravity.longitudinal
|
|
|
|
layout.on 'tick', (e) =>
|
|
hulls.attr('d', hullBoundaries)
|
|
nodes.each gravity_fn(e.alpha)
|
|
nodes.attr
|
|
cx: ({x}) -> x
|
|
cy: ({y}) -> y
|
|
labels.each gravity_fn(e.alpha)
|
|
labels.attr
|
|
x: ({x}) -> x
|
|
y: ({y}) -> y
|
|
links.each ({source:{x:x1,y:y1},target:{x:x2,y:y2}}, i) ->
|
|
[x,y] = scale(free([[x1,y1], [x2,y2]]), nodeRadius)
|
|
@setAttribute 'x1', x1 + x
|
|
@setAttribute 'y1', y1 + y
|
|
@setAttribute 'x2', x2 - x
|
|
@setAttribute 'y2', y2 - y
|
|
|
|
return layout
|