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