You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@taverna.apache.org by st...@apache.org on 2016/06/23 08:53:16 UTC
[02/14] incubator-taverna-databundle-viewer git commit: Add files via
upload
Add files via upload
Project: http://git-wip-us.apache.org/repos/asf/incubator-taverna-databundle-viewer/repo
Commit: http://git-wip-us.apache.org/repos/asf/incubator-taverna-databundle-viewer/commit/7dddbeb4
Tree: http://git-wip-us.apache.org/repos/asf/incubator-taverna-databundle-viewer/tree/7dddbeb4
Diff: http://git-wip-us.apache.org/repos/asf/incubator-taverna-databundle-viewer/diff/7dddbeb4
Branch: refs/heads/master
Commit: 7dddbeb48ca8d9a651cf2377e590417e0fa11f17
Parents: 94ddf9b
Author: PCStefan <pa...@gmail.com>
Authored: Mon Jun 6 12:51:05 2016 +0100
Committer: PCStefan <pa...@gmail.com>
Committed: Mon Jun 6 12:51:05 2016 +0100
----------------------------------------------------------------------
app/assets/javascripts/application.coffee | 5 +-
app/assets/javascripts/data_bundle.coffee | 951 +++++++++++++++++++++++--
app/assets/javascripts/sankey.js | 577 +++++++++++++++
app/assets/javascripts/vertical_sankey.js | 292 ++++++++
4 files changed, 1777 insertions(+), 48 deletions(-)
----------------------------------------------------------------------
http://git-wip-us.apache.org/repos/asf/incubator-taverna-databundle-viewer/blob/7dddbeb4/app/assets/javascripts/application.coffee
----------------------------------------------------------------------
diff --git a/app/assets/javascripts/application.coffee b/app/assets/javascripts/application.coffee
index 529d3f7..0cbd92a 100644
--- a/app/assets/javascripts/application.coffee
+++ b/app/assets/javascripts/application.coffee
@@ -32,8 +32,11 @@
#= require jquery_ujs
#= require bootstrap_theme/bootstrap/js/bootstrap.min
#= require d3/d3.min
+#= require d3/d3.js
+#= require sankey.js
+#= require vertical_sankey.js
#= require data_bundle
$ ->
- draw_workflow()
+ draw()
return
http://git-wip-us.apache.org/repos/asf/incubator-taverna-databundle-viewer/blob/7dddbeb4/app/assets/javascripts/data_bundle.coffee
----------------------------------------------------------------------
diff --git a/app/assets/javascripts/data_bundle.coffee b/app/assets/javascripts/data_bundle.coffee
index c45201a..ab00c91 100644
--- a/app/assets/javascripts/data_bundle.coffee
+++ b/app/assets/javascripts/data_bundle.coffee
@@ -17,55 +17,912 @@
# under the License.
#
-@draw_workflow = ->
- if $('svg#graphContainer').length > 0
- d3.json $('#data_bundle').attr('data-url'), (error, links) ->
- tick = ->
- path.attr 'd', (d) ->
- dx = d.target.x - (d.source.x)
- dy = d.target.y - (d.source.y)
- dr = Math.sqrt(dx * dx + dy * dy)
- 'M' + d.source.x + ',' + d.source.y + 'A' + dr + ',' + dr + ' 0 0,1 ' + d.target.x + ',' + d.target.y
- node.attr 'transform', (d) ->
- 'translate(' + d.x + ',' + d.y + ')'
+## sankey diagram : http://bl.ocks.org/d3noob/5028304
+## concur. matrix : https://bost.ocks.org/mike/miserables/
+
+# for every type of activity call draw_provenance
+$(document).ready ->
+ $('#diagramType li a').click ->
+ draw_provenance($(this).text())
+ return
+
+@isEnabled = false #is the mouse wheel scroll disabled or enabled
+
+$('#enableZooming').click ->
+
+ isEnabled = undefined
+
+ if $("#enableZooming span").html() == 'Enable Zooming'
+ isEnabled = true
+ $("#enableZooming span").html('Disable Zooming')
+ else
+ isEnabled = false
+ $("#enableZooming span").html('Enable Zooming')
+
+ disableMidMouse(isEnabled)
+
+ return
+
+@disableMidMouse = (status) ->
+ # enable/disable middle mouse button scrolling
+ wheelEnable = (event) ->
+ event.preventDefault()
+ event.returnValue = true
+ return
+
+ wheelDisable = (event) ->
+ event.preventDefault()
+ event.returnValue = false
+ return
+
+ setisEnabled(status)
+
+ if window.addEventListener
+ if status
+ window.addEventListener('DOMMouseScroll', wheelDisable, false)
+ window.onmousewheel = document.onmousewheel = wheelDisable
+ else
+ window.addEventListener('DOMMouseScroll', wheelEnable, false)
+ window.onmousewheel = document.onmousewheel = wheelEnable
+
+ return
+
+# distingush between single click and double click
+# see http://bl.ocks.org/couchand/6394506
+@clickCancel = ->
+ event = d3.dispatch('click', 'dblclick')
+
+ cc = (selection) ->
+ down = undefined
+ tolerance = 5
+ last = undefined
+ wait = null
+ # euclidean distance
+
+ dist = (a, b) ->
+ Math.sqrt (a[0] - (b[0])) ** 2, (a[1] - (b[1])) ** 2
+
+ selection.on('mousedown', ->
+ down = d3.mouse(document.body)
+ last = +new Date
+ return
+ )
+ selection.on('mouseup', ->
+ if dist(down, d3.mouse(document.body)) > tolerance
return
+ else
+ if wait
+ window.clearTimeout wait
+ wait = null
+ event.dblclick d3.event
+ else
+ wait = window.setTimeout(((e) ->
+ ->
+ event.click e
+ wait = null
+ return
+ )(d3.event), 300)
+ return
+ )
+ return
+ d3.rebind(cc, event, 'on')
+
+# Here start diagrams func
+
+@glob_width = 0
+@dashLine = '\n---------------------------------------------------------------\n'
+@graph = {}
+@tempgraph = {}
+
+# set the width
+@setGLWidth =(reqWidth) ->
+ @glob_width = reqWidth
+ return
+
+@setisEnabled =(status) ->
+ @isEnabled = status
+ return
+
+
+# set a color for a node
+@getColorHex =(source) ->
+ color = d3.scale.category20()
+ colorType = undefined
+ switch source
+ when 'Workflow Run' then colorType = '#0eff7f'
+ when 'Process Run' then colorType = '#258fda'
+ when 'Artifact' then colorType = '#ff7f0e'
+ when 'Dictionary' then colorType = '#7f0eff'
+ else colorType = color(stringTextForColor.replace(RegExp(' .*'), ''))
+ colorType
+
+@getColorTransitionTypeHex =(value) ->
+
+ color = d3.scale.category20()
+ colorType = undefined
+ switch value
+ when 11 then colorType = '#004d24' # wfprov:wasPartOfWorkflow
+ when 12 then colorType = '#c3e221' # this case should not exist
+ when 13 then colorType = '#009947' # wfprov:usedInputArtifact
+ when 14 then colorType = '#003318' # ----- // ------Dictionary
+ when 21 then colorType = '#258fda' # wasPartOfWorkflow
+ when 23 then colorType = '#7cbce9' # usedInputArtifact
+ when 24 then colorType = '#12476d' # usedInputDictionary
+ when 31 then colorType = '#ffc999' # wasoutputFromWf
+ when 32 then colorType = '#ff7f0e' # wasOutputFromProcess
+ when 34 then colorType = '#663000' # insertInList
+ when 41 then colorType = '#bb80ff' # output from wf
+ when 42 then colorType = '#7f0eff' #outputFromProcess
+ when 43 then colorType = '#3c0080' #split
+ when 44 then colorType = '#990000' # insert into another funct
+ else '#c3e221'
+
+@createGroupType =(type) ->
+ group = -1
+ switch type
+ when 'Workflow Run' then group = 1
+ when 'Process Run' then group = 2
+ when 'Artifact' then group = 3
+ when 'Dictionary' then group = 4
+ else group = 0
+ group
+
+# limit a string to maxChar
+@shortenString =(temp, maxChar) ->
+ if temp.length > maxChar
+ temp = temp.substring(0, maxChar) + '..'
+ temp
+
+# limit a string to 32 chars : "{15 chars}..{15 chars}"
+@shortenStringNoMiddle =(temp) ->
+ if temp.length > 32
+ temp = temp.substring(0, 15) + '..' + temp.substring(temp.length - 15, temp.length)
+ temp
+
+@getTimes =(d) ->
+ startTime = new Date()
+ endTime = new Date()
+ nodeTime = 0
+ if(d.hasOwnProperty("startedAtTime"))
+ startTime = new Date(d.startedAtTime)
+ endTime = new Date(d.endedAtTime)
+ nodeTime = 1
+
+ elapsedTime = endTime - startTime
+
+ date_format_iso =(date) ->
+ date.toISOString().replace( /[T]/g, ' ').slice(0, -1)
+
+ hms =(ms) ->
+ date = new Date(ms);
+ str = '';
+ if date.getUTCDate()-1 > 0
+ str += date.getUTCDate()-1 + " days, ";
+ if date.getUTCHours > 0
+ str += date.getUTCHours() + " hours, ";
+ if date.getUTCMinutes() > 0
+ str += date.getUTCMinutes() + " minutes, ";
+ if date.getUTCSeconds() > 0
+ str += date.getUTCSeconds() + " seconds, ";
+ str += date.getUTCMilliseconds() + " millis";
+ str
+
+ if nodeTime == 1
+ 'Start Time: ' + date_format_iso(startTime) + '\nEnd Time: ' + date_format_iso(endTime) + '\nElapsed Time: ' + hms(elapsedTime)
+ else
+ ''
+
+# create function that to split the text into multiple lines for the svg-text
+# cannot find something like this online
+@wrap = (text) ->
+ text.each ->
+ text = d3.select(this)
+ labels = text.text().split("\\n")
+ text.text(null)
+
+ line = []
+
+ lineNumber = 1
+ if(labels.length != 0)
+ lineNumber = (-1) * (Math.floor(labels.length / 2) - 1)
+
+ lineHeight = 1.1
+ for temp in labels
+ temp = temp.substring(temp.lastIndexOf(' '))
+ text.append('tspan').attr('x', text.attr('x')).attr('y', text.attr('y')).attr('dy', lineNumber * lineHeight + 'em' ).text(temp).filter((d) ->
+ d.x < glob_width / 5
+ ).attr('x', "22")
+ lineNumber++
+
+ return
+ return
+# create function that to split the text into multiple lines for the svg-text
+# cannot find something like this online
+@wrapNoNewLine = (text) ->
+ text.each ->
+ text = d3.select(this)
+ labels = text.text().split("\\n")
+ text.text(null)
+
+ line = []
+
+ lineNumber = 1
+ if(labels.length != 0)
+ lineNumber = (-1) * (Math.floor(labels.length / 2) - 1)
+
+ lineHeight = 0.15
+ final = ''
+ for temp in labels
+ final += temp.substring(temp.lastIndexOf(' ')) + ', '
+
+ final = final[0...-2]
+ text.append('tspan').attr('x', text.attr('x')).attr('y', text.attr('y')).attr('dy', lineHeight + 'em' ).text(final).filter((d) ->
+ d.x < glob_width / 5
+ ).attr('x', "22")
+
+ return
+ return
+
+
+# some local functions for zooming in/out and for walking around
+@zoomed = ->
+ if isEnabled
+ d3.select('g#zoomContainer').attr 'transform', 'translate(' + d3.event.translate + ')scale(' + d3.event.scale + ')'
+ else
+ null
+ return
+
+@draw = ->
+ d3.json $('#data_bundle').attr('data-url'), (error, data) ->
+ @tempgraph = $.extend(true, {}, data)
+
+ if(Object.keys(tempgraph).length)
+ hasBeenDrawn = draw_workflow(hasBeenDrawn)
+ draw_provenance()
+
+ return
+ return
+
+@clone = (obj) ->
+ return obj if obj is null or typeof (obj) isnt "object"
+ temp = new obj.constructor()
+ for key of obj
+ temp[key] = clone(obj[key])
+ temp
+
+@draw_workflow =(draw) ->
+ data = clone(@tempgraph.workflow)
+ if !draw?
+ width = 960
+ height = 650
+ opacity = 0.7
+
+ color = d3.scale.category20()
+
+ $('canvas#canvasWF').attr
+ 'width': (width + 150)
+ 'height': (width)
+
+ svgContainer = d3.select('svg#graphContainer').attr('width', width+150).attr('height', width).append('g').attr('transform', (d) ->
+ "translate("+ (width) + ", 0) rotate (90)"
+ )
+
+ verticalSankey = d3.vertical_sankey().nodeWidth(25).nodePadding(20).size([width-128, height])
+
+ path = verticalSankey.link()
+
+ verticalSankey.nodes(data.nodes).links(data.links).layout(32)
+
+ link = svgContainer.append('g').selectAll('.link').data(data.links).enter().append('path').attr('class', 'link').attr('d', path).style('stroke-width', (d) ->
+ Math.max 1, d.dy
+ ).style('stroke', (d) ->
+ d.source.color = color(d.source.name.replace(RegExp(' .*'), ''))
+ ).sort((a, b) ->
+ b.dx - (a.dx)
+ )
+
+ link.attr('opacity', opacity)
+
+ link.append('title').text((d) ->
+ d.source.name + '\n\u2192\n' + d.target.name
+ )
+
+ node = svgContainer.append('g').selectAll('.node').data(data.nodes).enter().append('g').attr('class', 'node').attr('transform', (d) ->
+ 'translate(' + d.x + ',' + d.y + ')'
+ )
+
+ node.append('rect').attr('width', verticalSankey.nodeWidth()).attr('height', (d) ->
+ Math.abs d.dy
+ ).style('fill', (d) ->
+ d.color = color(d.name.replace(RegExp(' .*'), ''))
+ ).style('stroke', (d) ->
+ d3.rgb(d.color).darker 2
+ )
+
+ node.append('text').attr('text-anchor', 'middle').attr('y', (d) ->
+ 12
+ ).attr('x', (d) ->
+ d.dy/-2
+ ).attr('dy', '.35em').attr('transform', (d) ->
+ "translate("+ 0 + ", 0) rotate (270)"
+ ).text((d) ->
+ shortenName =(d) ->
+
+ # convert the text to pixels
+ canvas = document.createElement('canvas')
+ ctx = canvas.getContext("2d")
+ ctx.font = "14px Source Sans Pro"
+ textPX = ctx.measureText(d.name).width
+
+ if textPX > d.dy
+ d.name.substring(0, 9) + '..' + d.name.substring(d.name.length - 9, d.name.length)
+ else
+ d.name
+
+ shortenName(d)
+
+ ).filter (d) ->
+ d.x < width / 2
+
+ return true
+
+@draw_provenance =(diagramType) ->
+ # if diagramType is undefined or null, as default assign the current active
+ # else clear the svg for the diagram
+ if !diagramType?
+ diagramType = $('#diagramType li.active a').text()
+ else
+ d3.select('svg#provContainer').selectAll("*").remove()
+ d3.select('svg#provContainer').remove()
+ d3.select('#mapContainer').append('svg').attr('id','provContainer')
+
+ @graph = clone(@tempgraph)
+ if(diagramType == 'Sankey')
+ draw_sankey()
+ else if(diagramType == 'Co-occurrence')
+ draw_miserables()
+
+
+ return
+
+
+@draw_miserables = ->
+ width = 1580
+ height = 750
+
+ # compute a better width and height for the container
+ nodesCount = Object.keys(graph.provenance.nodes).length
+ linksCount = Object.keys(graph.provenance.links).length
+
+ if nodesCount > 0 or linksCount > 0
+ ratioN = 1.0 * nodesCount * 10
+ width = width + 25
+ height = width
+ divider = 3
+ setGLWidth(width)
+
+ $('canvas#canvasPROV').attr
+ 'width': width
+ 'height': height
+
+
+ x = d3.scale.ordinal().rangeBands([
+ 0
+ (5600 / divider)
+ ])
+ z = d3.scale.linear().domain([
+ 0
+ 4
+ ]).clamp(true)
+
+ color = d3.scale.category20()
+
+ svg = d3.select('svg#provContainer').attr('width', width+602).attr('height', height+535).append('g').attr('transform', 'translate(350,277)')
+
+ # build a [source, target] matrix
+ matrix = []
+ nodes = graph.provenance.nodes
+ n = nodes.length
+
+ row = (row) ->
+ cell = d3.select(this).selectAll('.cell').data(row.filter((d) ->
+ d.z + Math.floor(Math.random() * 10)
+ )).enter().append('rect').attr('class', 'cell').attr('x', (d) ->
+ x d.x
+ ).attr('width', x.rangeBand()).attr('height', x.rangeBand()).style('fill-opacity', (d) ->
+ z d.z
+ ).style('fill', (d) ->
+ console.log(d.x + " .. " + d.y)
+ if d.x == d.y
+ "#123456"
+ else
+ transition = createGroupType(nodes[d.x].type) * 10 + createGroupType(nodes[d.y].type)
+ getColorTransitionTypeHex(transition)
+ ).append('title').text((d) ->
+ str = nodes[d.x].type + ' \u2192 ' + nodes[d.y].type
+ str += dashLine + 'Source: ' + nodes[d.x].name
+
+ if createGroupType(nodes[d.x].type) == 2 # if process
+ str += '\n\n' + getTimes(nodes[d.x])
+ else if createGroupType(nodes[d.x].type) > 2 # if artifact or dictionary
+ if nodes[d.x].content?
+ str += '\n\n' + shortenString(nodes[d.x].content, 500)
+
+ str += dashLine + 'Target: ' + nodes[d.y].name
+ if createGroupType(nodes[d.y].type) == 2 # if process
+ str += '\n\n' + getTimes(nodes[d.y])
+ else if createGroupType(nodes[d.y].type) > 2 # if artifact or dictionary
+ if nodes[d.y].content?
+ str += '\n\n' + shortenString(nodes[d.y].content, 500)
+
+ str
+ ).on('mouseover', mouseover).on('mouseout', mouseout)
+ return
+
+ mouseover = (p) ->
+ d3.selectAll('.row text').classed('active', (d, i) ->
+ i == p.y
+ )
+ d3.selectAll('.column text').classed('active', (d, i) ->
+ i == p.x
+ )
+ return
+
+ mouseout = ->
+ d3.selectAll('text').classed('active', false)
+ return
- nodes = {}
- links.forEach (link) ->
- link.source = nodes[link.source] or (nodes[link.source] =
- name: link.source, file_content: link.file_content)
- link.target = nodes[link.target] or (nodes[link.target] =
- name: link.target, file_content: link.file_content)
- link.value = +link.value
+ order = (value) ->
+ x.domain orders[value]
+ t = svg.transition().duration(1500)
+ t.selectAll('.row').delay((d, i) ->
+ x(i) * 4
+ ).attr('transform', (d, i) ->
+ 'translate(0,' + x(i) + ')'
+ ).selectAll('.cell').delay((d) ->
+ x(d.x) * 4
+ ).attr 'x', (d) ->
+ x d.x
+ t.selectAll('.column').delay((d, i) ->
+ x(i) * 4
+ ).attr 'transform', (d, i) ->
+ 'translate(' + x(i) + ')rotate(-90)'
+ return
+
+
+
+ # Compute index per node.
+ nodes.forEach (node, i) ->
+ node.index = i
+ node.count = 0
+ matrix[i] = d3.range(n).map((j) ->
+ {
+ x: j
+ y: i
+ z: 0
+ }
+ )
+ return
+
+ # Add the legend
+
+ legendCategories = { "category":[{"name":"Workflow Run -wasPartOfWorkflow- Workflow Run", "transition":"11"},
+ {"name":"Workflow Run -usedInput- Artifact", "transition":"13"},
+ {"name":"Workflow Run -usedInput- Dictionary", "transition":"14"},
+ {"name":"Process Run -wasPartOfWorkflow- Workflow Run", "transition":"21"},
+ {"name":"Process Run -usedInput- Artifact", "transition":"23"},
+ {"name":"Process Run -usedInput- Dictionary", "transition":"24"},
+ {"name":"Artifact -wasOutputFrom- Workflow Run", "transition":"31"},
+ {"name":"Artifact -wasOutputFrom- Process Run", "transition":"32"},
+ {"name":"Artifact -isIntegratedIn- Dictionary", "transition":"34"},
+ {"name":"Dictionary -wasOutputFrom- Artifact", "transition":"41"},
+ {"name":"Dictionary -wasOutputFrom- Artifact", "transition":"42"},
+ {"name":"Dictionary -hasMember- Artifact", "transition":"43"},
+ {"name":"Dictionary -isSplit/PushedInto- Dictionary", "transition":"44"}]}
+
+ legend = svg.append('g').attr('class', 'legend').attr('x', 0).attr('y', 0).selectAll('.category').data(legendCategories.category).enter().append('g').attr('class', 'category')
+
+ legendConfig =
+ rectWidth: 20
+ rectHeight: 14
+ xOffset: -350
+ yOffset: -275
+ xOffsetText: 26
+ yOffsetText: -10
+ lineHeight: 10
+ wordApart: 20
+
+ legendConfig.yOffsetText += 20
+ legendConfig.xOffsetText += legendConfig.xOffset
+
+ legend.append('rect').attr('y', (d, i) ->
+ legendConfig.yOffset + i * legendConfig.wordApart
+ ).attr('x', legendConfig.xOffset).attr('height', legendConfig.rectHeight).attr('width', legendConfig.rectWidth).style('fill', (d) ->
+ getColorTransitionTypeHex(parseInt(d.transition))
+ ).style('stroke', '#000000')
+
+ legend.append('text').attr('y', (d, i) ->
+ legendConfig.yOffset + i * legendConfig.wordApart + legendConfig.yOffsetText
+ ).attr('x', legendConfig.xOffsetText).text((d) ->
+ d.name
+ )
+
+ # Convert links to matrix; count character occurrences.
+ graph.provenance.links.forEach (link) ->
+ matrix[link.source][link.target].z = createGroupType(nodes[link.source].type) * 10 + createGroupType(nodes[link.target].type)
+ nodes[link.source].count += link.value
+ nodes[link.target].count += link.value
+ return
+
+ # Precompute the orders.
+ orders =
+ name: d3.range(n).sort((a, b) ->
+ d3.ascending nodes[a].name, nodes[b].name
+ )
+ count: d3.range(n).sort((a, b) ->
+ nodes[b].count - (nodes[a].count)
+ )
+ type: d3.range(n).sort((a, b) ->
+ createGroupType(nodes[b].type) - createGroupType(nodes[a].type)
+ )
+
+ # The default sort order.
+ x.domain orders.name
+
+ #svg.append('rect').attr('class', 'misbackground').attr('width', width/divider).attr('height', height/divider)
+
+ row = svg.selectAll('.row').data(matrix).enter().append('g').attr('class', 'row').attr('transform', (d, i) ->
+ 'translate(0,' + x(i) + ')'
+ ).each(row)
+
+ row.append('line').attr('x2', 5600/divider)
+
+ row.append('text').attr('x', -6).attr('y', x.rangeBand() / 2).attr('dy', '.32em').attr('text-anchor', 'end').text((d, i) ->
+ if nodes[i].hasOwnProperty("label")
+ nodes[i].label
+ else
+ shortenStringNoMiddle(nodes[i].name)
+ ).call(wrapNoNewLine)
+
+ column = svg.selectAll('.column').data(matrix).enter().append('g').attr('class', 'column').attr('transform', (d, i) ->
+ 'translate(' + x(i) + ')rotate(-90)'
+ )
+
+ column.append('line').attr('x1', 5600/divider * (-1))
+
+ column.append('text').attr('x', 6).attr('y', x.rangeBand() / 2).attr('dy', '.32em').attr('text-anchor', 'start').text((d, i) ->
+ if nodes[i].hasOwnProperty("label")
+ nodes[i].label
+ else
+ shortenStringNoMiddle(nodes[i].name)
+ ).call(wrapNoNewLine)
+
+ d3.select('#order').on 'change', ->
+ # clearTimeout timeout
+ order @value
+ return
+
+ return
+
+
+@draw_sankey = ->
+ width = 950
+ height = 750
+ lowOpacity = 0.3
+ hoverOpacity = 0.7
+ highOpacity = 0.9
+
+ # zoom the d3
+ zoom = d3.behavior.zoom().scaleExtent([
+ 0.5
+ 10
+ ]).on('zoom', zoomed)
+
+ # load the svg#sankeyContainer
+ # set the width and height attributes
+ # append a function g that has a tranform process defined by translation
+ svgContainer = d3.select('svg#provContainer')
+
+ # define the sankey object
+ # set the node width to 15
+ # set the node padding to 10
+ sankey = d3.sankey().nodeWidth(20).nodePadding(10)
+
+ # request the sankey path of current sankey
+ path = sankey.reversibleLink()
+
+ # load data to work with
+
+ # compute a better width and height for the container
+ nodesCount = Object.keys(graph.provenance.nodes).length
+ linksCount = Object.keys(graph.provenance.links).length
+
+ if nodesCount > 0 or linksCount > 0
+ ratioLN = linksCount / nodesCount * 100
+ width = width + Math.floor( ratioLN * 3 )
+ height = height + Math.floor( ratioLN )
+ setGLWidth(width)
+
+ $('canvas#canvasPROV').attr
+ 'width': width
+ 'height': height
+
+ svgContainer.attr('width', width+125).attr('height', height+150).append('g')
+
+ rect = svgContainer.append('rect').attr('width', width).attr('height', height).style('fill', 'none').style('pointer-events', 'all')
+
+ sankey = sankey.size([width, height])
+
+
+ svg = svgContainer.append('g').attr("id", "zoomContainer").attr('transform', 'translate(0,' + 75 + ')')
+
+ svgContainer.call(zoom).on("dblclick.zoom", null).on("click.zoom", null).on("mousedown.zoom", null)
+
+ # set the nodes
+ # set the links
+ # set the layout
+ sankey.nodes(graph.provenance.nodes).links(graph.provenance.links)
+ sankey.layout(32)
+
+ legendCategories = { "category":[{"type":"Workflow Run"},{"type":"Process Run"}, {"type":"Artifact"}, {"type":"Dictionary"}] }
+ legend = svgContainer.append('g').attr('class', 'legend').attr('x', 0).attr('y', 0).selectAll('.category').data(legendCategories.category).enter().append('g').attr('class', 'category')
+
+ legendConfig =
+ rectWidth: 20
+ rectHeight: 14
+ xOffset: 625
+ yOffset: 30
+ xOffsetText: 5
+ yOffsetText: 11
+ lineHeight: 10
+ wordApart: 125
+
+ legendConfig.xOffsetText += 20
+ legendConfig.yOffsetText += legendConfig.yOffset
+
+ legend.append('rect').attr('x', (d, i) ->
+ legendConfig.xOffset + i * legendConfig.wordApart
+ )
+ .attr('y', legendConfig.yOffset).attr('height', legendConfig.rectHeight).attr('width', legendConfig.rectWidth).style('fill', (d) ->
+ getColorHex(d.type)
+ ).style('stroke', (d) ->
+ d3.rgb(d.color).darker 1
+ )
+
+ legend.append('text').attr('x', (d, i) ->
+ legendConfig.xOffset + i * legendConfig.wordApart + legendConfig.xOffsetText
+ ).attr('y', legendConfig.yOffsetText).text((d) ->
+ d.type
+ )
+
+
+ # select all the links from the json-data and append them to the Sankey obj in alphabetical order
+ link = svg.append('g').selectAll('.link').data(graph.provenance.links).enter().append('g').attr('class', 'link').attr('id', (d,i) ->
+ d.id = i
+ "link-" + i
+ ).sort((a, b) ->
+ b.dy - (a.dy))
+
+ p0 = link.append("path").attr("d", path(0))
+ p1 = link.append("path").attr("d", path(1))
+ p2 = link.append("path").attr("d", path(2))
+
+ link.attr('fill', (d) ->
+ getColorHex(d.source.type)
+ ).attr('opacity', lowOpacity).on('mouseover', (d) ->
+ if parseFloat(d3.select(this).style('opacity')) != highOpacity
+ d3.select(this).style('opacity', hoverOpacity)
+ ).on('mouseout', (d) ->
+ if parseFloat(d3.select(this).style('opacity')) != highOpacity
+ d3.select(this).style('opacity', lowOpacity)
+ )
+
+ # set the text for the edges
+ link.append('title').text (d) ->
+ dash = '\n-----------------------------------------------------------\n'
+ startText = d.source.type + ' \u2192 ' + d.target.type + dash + 'Source:\nURI: ' + d.source.name
+ endText = 'Target:\nURI: ' + d.target.name
+ startText + dash + endText
+
+ # create the function to drag the node
+ dragmove = (d) ->
+ # uncomment the following to disable x movement (and comment the next line )
+ #d3.select(this).attr('transform', 'translate(' + d.x + ',' + (d.y = Math.max(0, Math.min(height - (d.dy), d3.event.y))) + ')')
+ d3.select(this).attr('transform', 'translate(' + (d.x = Math.max(0, Math.min(width - (d.dx), d3.event.x))) + ',' + (d.y = Math.max(0, Math.min(height - (d.dy), d3.event.y))) + ')')
+ sankey.relayout()
+ p0.attr("d", path(1))
+ p1.attr("d", path(0))
+ p2.attr("d", path(2))
+ return
+
+
+ # select all the nodes from the json-data and append them to the Sankey obj
+ # add behavior : dragmove
+ node = svg.append('g').selectAll('.node').data(graph.provenance.nodes).enter().append('g').attr('class', 'node').attr('transform', (d) ->
+ yValue = Math.min(d.y, height)
+ 'translate(' + d.x + ',' + yValue + ')'
+ ).call(d3.behavior.drag().origin((d) ->
+ d
+ ).on('dragstart', ->
+ @parentNode.appendChild this
+ return
+ ).on('drag', dragmove))
+
+ # choose the form of the node : filled rectangle
+ # set the height of the rectangle to d.dy
+ # set the width of the rectangle to nodeWidth?
+ # set the style to be filled with default color
+ node.append('rect').attr('height', (d) ->
+ Math.max 10, d.dy
+ ).attr('data-clicked', '0').attr('width', sankey.nodeWidth()).style('fill', (d) ->
+ getColorHex(d.type)
+ ).style('stroke', (d) ->
+ d3.rgb(d.color).darker 1
+ ).append('title').text((d) ->
+
+ returnedStr = d.type + dashLine
+ returnedStr += 'URI: ' + d.name + dashLine
+ returnedStr += d.label.split("\\n").join("\n")
+
+ if(d.type == "Process Run")
+ returnedStr += dashLine + getTimes(d)
+ else if(d.type == "Artifact" || d.type == "Dictionary" && d.content)
+ returnedStr += dashLine + "Content :\n" + shortenString(d.content, 500)
+
+ returnedStr
+ )
+
+ #modify the link opacity to the given opacity
+ click_highlight_path_color = (id, opacity) ->
+ d3.select('#link-' + id).style('opacity', opacity)
+
+ click_highlight_path = (node, i) ->
+ # check if the user wants to drag or to click the node
+ # if he wants to drag then the following will be true
+ if (d3.event.defaultPrevented)
+ return
+
+ remainingNodes = []
+ nextNodes = []
+ stroke_opacity = 0
+
+ # if a node has been clicked and then mark it as unclick if clicked again
+ if d3.select(this).attr('data-clicked') == '1'
+ d3.select(this).attr('data-clicked', '0')
+ stroke_opacity = lowOpacity
+ else
+ d3.select(this).attr('data-clicked', '1')
+ stroke_opacity = highOpacity
+
+ # remember all visited nodes and the path
+ # traverse will be a JSON array
+ traverse = [
+ {
+ linkType: 'sourceLinks'
+ nodeType: 'target'
+ }
+ {
+ linkType: 'targetLinks'
+ nodeType: 'source'
+ }
+ ]
+
+ # for each object inside traverse
+ traverse.forEach (step) ->
+ # for each (outgoing,incoming) link
+ node[step.linkType].forEach (link) ->
+ remainingNodes.push(link[step.nodeType])
+ click_highlight_path_color(link.id, stroke_opacity)
+ return
+
+ while remainingNodes.length
+ nextNodes = []
+ remainingNodes.forEach (node) ->
+ node[step.linkType].forEach (link) ->
+ nextNodes.push(link[step.nodeType])
+ click_highlight_path_color(link.id, stroke_opacity)
+ return
+ return
+ remainingNodes = nextNodes
return
+ return
- width = 960
- height = 900
-
- force = d3.layout.force().nodes(d3.values(nodes)).links(links).size([width, height])
- .linkDistance(100).charge(-500).on('tick', tick).start()
- svgContainer = d3.select('svg#graphContainer').attr('width', width).attr('height', height)
- # build the arrow.
- svgContainer.append('svg:defs').selectAll('marker').data(['end']).enter().append('svg:marker').attr('id', String)
- .attr('viewBox', '0 -5 10 10').attr('refX', 15).attr('refY', -1.5).attr('markerWidth', 6)
- .attr('markerHeight', 6).attr('orient', 'auto').append('svg:path').attr 'd', 'M0,-5L10,0L0,5'
- # add the links and the arrows
- path = svgContainer.append('svg:g').selectAll('path').data(force.links()).enter().append('svg:path')
- .attr('class', 'link').attr('marker-end', 'url(#end)')
- # define the nodes
- node = svgContainer.selectAll('.node').data(force.nodes()).enter().append('g').attr('class', 'node')
- .attr('id', (d) -> d.name).call(force.drag)
- # add the nodes
- node.append('circle').attr('r', 5)
- # add the text
- node.append('text').attr('x', 12).attr('dy', '.35em').text (d) ->
- d.name
- node.append('text').attr('class', 'file_content').attr('visibility', 'hidden').text (d) ->
- return d.file_content
-
- node.on 'click', (d) ->
- rect = svgContainer.append('rect').transition().duration(500).attr('width', 250)
- .attr('height', 300).attr('x', 10).attr('y', 10).style('fill', 'white').attr('stroke', 'black')
- text = svgContainer.append('text').text(d.file_content)
- .attr('x', 50).attr('y', 150).attr('fill', 'black')
+
+ cc = clickCancel()
+
+ # show the whole path on single click on nodes
+ # the function highlight_node_links uses Breadth First Search alghorithm to find the reachable nodes
+ # add remove the outgoing edges from current node on dblclick
+ node.call(cc).on('click', click_highlight_path).on('dblclick', (d)->
+ if (d3.event.defaultPrevented)
+ return
+ svg.selectAll('.link').filter((l) ->
+ l.source == d
+ ).attr('display', ->
+ if d3.select(this).attr('display') == 'none'
+ 'inline'
+ else
+ 'none'
+ )
return
+ )
+
+ # set the text of the nodes
+ # set their position
+ # set their font
+ # set the anchor of the text
+ node.append('text').attr('x', (d) ->
+ d.dx/2 - 12
+ ).attr('y', (d) ->
+ d.dy/2 - 10
+ ).attr('text-anchor', 'end')
+ .text((d) ->
+ if d.hasOwnProperty("label")
+ d.label
+ else
+ shortenStringNoMiddle(d.name)
+ ).call(wrap).filter((d) ->
+ d.x < width / 5
+ ).attr('x', "22").attr('text-anchor', 'start')
+
+ # select all the nodes from the json-data and append them to the Sankey obj
+ # add behavior : dragmove
+
+ return
+
+
+#http://techslides.com/save-svg-as-an-image
+d3.select('#saveWF').on('click', ->
+ html = d3.select('svg#graphContainer').attr('version', 1.1).attr('xmlns', 'http://www.w3.org/2000/svg').node().parentNode.innerHTML
+ imgsrc = 'data:image/svg+xml;base64,' + btoa(unescape(encodeURIComponent(html)))
+ img = '<img src="' + imgsrc + '">'
+
+ canvas = document.querySelector('canvas#canvasWF')
+ context = canvas.getContext("2d")
+ image = new Image
+ image.src = imgsrc
+
+ image.onload = ->
+ context.drawImage(image, 0, 0)
+ canvasdata = canvas.toDataURL('image/png')
+ pngimg = '<img src="' + canvasdata + '">'
+
+ now = new Date
+ differential = now.getDate() + "_" + now.getMonth() + "_" + now.getFullYear() + "_" + now.getHours() + "_" + now.getMinutes() + "_" + now.getSeconds()
+
+ a = document.createElement('a')
+ a.download = 'workflow_' + differential + '.png'
+ a.href = canvasdata
+ a.click()
+ return
+
+ return
+)
+
+d3.select('#savePROV').on('click', ->
+ html = d3.select('svg#provContainer').attr('version', 1.1).attr('xmlns', 'http://www.w3.org/2000/svg').node().parentNode.innerHTML
+ imgsrc = 'data:image/svg+xml;base64,' + btoa(unescape(encodeURIComponent(html)))
+ img = '<img src="' + imgsrc + '">'
+
+ canvas = document.querySelector('canvas#canvasPROV')
+ context = canvas.getContext("2d")
+ image = new Image
+ image.src = imgsrc
+
+ image.onload = ->
+ context.drawImage(image, 0, 0)
+ canvasdata = canvas.toDataURL('image/png')
+ pngimg = '<img src="' + canvasdata + '">'
+
+ now = new Date
+ differential = now.getDate() + "_" + now.getMonth() + "_" + now.getFullYear() + "_" + now.getHours() + "_" + now.getMinutes() + "_" + now.getSeconds()
+
+
+ a = document.createElement('a')
+ a.download = 'provenance_' + differential + '.png'
+ a.href = canvasdata
+ a.click()
+ return
+
+ return
+)
\ No newline at end of file
http://git-wip-us.apache.org/repos/asf/incubator-taverna-databundle-viewer/blob/7dddbeb4/app/assets/javascripts/sankey.js
----------------------------------------------------------------------
diff --git a/app/assets/javascripts/sankey.js b/app/assets/javascripts/sankey.js
new file mode 100644
index 0000000..7f3580b
--- /dev/null
+++ b/app/assets/javascripts/sankey.js
@@ -0,0 +1,577 @@
+d3.sankey = function() {
+ var sankey = {},
+ nodeWidth = 24,
+ nodePadding = 8,
+ size = [1, 1],
+ nodes = [],
+ links = [],
+ components = [];
+
+ sankey.nodeWidth = function(_) {
+ if (!arguments.length) return nodeWidth;
+ nodeWidth = +_;
+ return sankey;
+ };
+
+ sankey.nodePadding = function(_) {
+ if (!arguments.length) return nodePadding;
+ nodePadding = +_;
+ return sankey;
+ };
+
+ sankey.nodes = function(_) {
+ if (!arguments.length) return nodes;
+ nodes = _;
+ return sankey;
+ };
+
+ sankey.links = function(_) {
+ if (!arguments.length) return links;
+ links = _;
+ return sankey;
+ };
+
+ sankey.size = function(_) {
+ if (!arguments.length) return size;
+ size = _;
+ return sankey;
+ };
+
+ sankey.layout = function(iterations) {
+ computeNodeLinks();
+ computeNodeValues();
+
+ computeNodeStructure();
+ computeNodeBreadths();
+
+ computeNodeDepths(iterations);
+ computeLinkDepths();
+
+ return sankey;
+ };
+
+ sankey.relayout = function() {
+ computeLinkDepths();
+ return sankey;
+ };
+
+ // A more involved path generator that requires 3 elements to render --
+ // It draws a starting element, intermediate and end element that are useful
+ // while drawing reverse links to get an appropriate fill.
+ //
+ // Each link is now an area and not a basic spline and no longer guarantees
+ // fixed width throughout.
+ //
+ // Sample usage:
+ //
+ // linkNodes = this._svg.append("g").selectAll(".link")
+ // .data(this.links)
+ // .enter().append("g")
+ // .attr("fill", "none")
+ // .attr("class", ".link")
+ // .sort(function(a, b) { return b.dy - a.dy; });
+ //
+ // linkNodePieces = [];
+ // for (var i = 0; i < 3; i++) {
+ // linkNodePieces[i] = linkNodes.append("path")
+ // .attr("class", ".linkPiece")
+ // .attr("d", path(i))
+ // .attr("fill", ...)
+ // }
+ sankey.reversibleLink = function() {
+ var curvature = .5;
+
+ // Used when source is behind target, the first and last paths are simple
+ // lines at the start and end node while the second path is the spline
+ function forwardLink(part, d) {
+ var x0 = d.source.x + d.source.dx,
+ x1 = d.target.x,
+ xi = d3.interpolateNumber(x0, x1),
+ x2 = xi(curvature),
+ x3 = xi(1 - curvature),
+ y0 = d.source.y + d.sy,
+ y1 = d.target.y + d.ty,
+ y2 = d.source.y + d.sy + d.dy,
+ y3 = d.target.y + d.ty + d.dy;
+
+ switch (part) {
+ case 0:
+ return "M" + x0 + "," + y0 + "L" + x0 + "," + (y0 + d.dy);
+
+ case 1:
+ return "M" + x0 + "," + y0
+ + "C" + x2 + "," + y0 + " " + x3 + "," + y1 + " " + x1 + "," + y1
+ + "L" + x1 + "," + y3
+ + "C" + x3 + "," + y3 + " " + x2 + "," + y2 + " " + x0 + "," + y2
+ + "Z";
+
+ case 2:
+ return "M" + x1 + "," + y1 + "L" + x1 + "," + (y1 + d.dy);
+ }
+ }
+
+ // Used for self loops and when the source is actually in front of the
+ // target; the first element is a turning path from the source to the
+ // destination, the second element connects the two twists and the last
+ // twists into the target element.
+ //
+ //
+ // /--Target
+ // \----------------------\
+ // Source--/
+ //
+ function backwardLink(part, d) {
+
+ var curveExtension = 30;
+ var curveDepth = 15;
+
+ function getDir(d) {
+ return d.source.y + d.sy > d.target.y + d.ty ? -1 : 1;
+ }
+
+ function p(x, y) {
+ return x + "," + y + " ";
+ }
+
+ var dt = getDir(d) * curveDepth,
+ x0 = d.source.x + d.source.dx,
+ y0 = d.source.y + d.sy,
+ x1 = d.target.x,
+ y1 = d.target.y + d.ty;
+
+ switch (part) {
+ case 0:
+ return "M" + p(x0, y0) +
+ "C" + p(x0, y0) +
+ p(x0 + curveExtension, y0) +
+ p(x0 + curveExtension, y0 + dt) +
+ "L" + p(x0 + curveExtension, y0 + dt + d.dy) +
+ "C" + p(x0 + curveExtension, y0 + d.dy) +
+ p(x0, y0 + d.dy) +
+ p(x0, y0 + d.dy) +
+ "Z";
+ case 1:
+ return "M" + p(x0 + curveExtension, y0 + dt) +
+ "C" + p(x0 + curveExtension, y0 + 3 * dt) +
+ p(x1 - curveExtension, y1 - 3 * dt) +
+ p(x1 - curveExtension, y1 - dt) +
+ "L" + p(x1 - curveExtension, y1 - dt + d.dy) +
+ "C" + p(x1 - curveExtension, y1 - 3 * dt + d.dy) +
+ p(x0 + curveExtension, y0 + 3 * dt + d.dy) +
+ p(x0 + curveExtension, y0 + dt + d.dy) +
+ "Z";
+
+ case 2:
+ return "M" + p(x1 - curveExtension, y1 - dt) +
+ "C" + p(x1 - curveExtension, y1) +
+ p(x1, y1) +
+ p(x1, y1) +
+ "L" + p(x1, y1 + d.dy) +
+ "C" + p(x1, y1 + d.dy) +
+ p(x1 - curveExtension, y1 + d.dy) +
+ p(x1 - curveExtension, y1 + d.dy - dt) +
+ "Z";
+ }
+ }
+
+ return function(part) {
+ return function(d) {
+ if (d.source.x < d.target.x) {
+ return forwardLink(part, d);
+ } else {
+ return backwardLink(part, d);
+ }
+ }
+ }
+ };
+
+ // The standard link path using a constant width spline that needs a
+ // single path element.
+ sankey.link = function() {
+ var curvature = .5;
+
+ function link(d) {
+ var x0 = d.source.x + d.source.dx,
+ x1 = d.target.x,
+ xi = d3.interpolateNumber(x0, x1),
+ x2 = xi(curvature),
+ x3 = xi(1 - curvature),
+ y0 = d.source.y + d.sy + d.dy / 2,
+ y1 = d.target.y + d.ty + d.dy / 2;
+
+ return "M" + x0 + "," + y0
+ + "C" + x2 + "," + y0
+ + " " + x3 + "," + y1
+ + " " + x1 + "," + y1;
+ }
+
+
+
+ link.curvature = function(_) {
+ if (!arguments.length) return curvature;
+ curvature = +_;
+ return link;
+ };
+
+ return link;
+ };
+
+ // Populate the sourceLinks and targetLinks for each node.
+ // Also, if the source and target are not objects, assume they are indices.
+ function computeNodeLinks() {
+ nodes.forEach(function(node) {
+ node.sourceLinks = [];
+ node.targetLinks = [];
+ });
+
+ links.forEach(function(link) {
+ var source = link.source,
+ target = link.target;
+ if (typeof source === "number") source = link.source = nodes[link.source];
+ if (typeof target === "number") target = link.target = nodes[link.target];
+ source.sourceLinks.push(link);
+ target.targetLinks.push(link);
+ });
+ }
+
+ // Compute the value (size) of each node by summing the associated links.
+ function computeNodeValues() {
+ nodes.forEach(function(node) {
+ if (!(node.value)) //if not already given
+ node.value = Math.max(
+ d3.sum(node.sourceLinks, value),
+ d3.sum(node.targetLinks, value)
+ );
+ });
+ }
+
+ // Take the list of nodes and create a DAG of supervertices, each consisting
+ // of a strongly connected component of the graph
+ //
+ // Based off:
+ // http://en.wikipedia.org/wiki/Tarjan's_strongly_connected_components_algorithm
+ function computeNodeStructure() {
+ var nodeStack = [],
+ index = 0;
+
+ nodes.forEach(function(node) {
+ if (!node.index) {
+ connect(node);
+ }
+
+ });
+
+ function connect(node) {
+ node.index = index++;
+ node.lowIndex = node.index;
+ node.onStack = true;
+ nodeStack.push(node);
+
+ if (node.sourceLinks) {
+ node.sourceLinks.forEach(function(sourceLink){
+ var target = sourceLink.target;
+ if (!target.hasOwnProperty('index')) {
+ connect(target);
+ node.lowIndex = Math.min(node.lowIndex, target.lowIndex);
+ } else if (target.onStack) {
+ node.lowIndex = Math.min(node.lowIndex, target.index);
+ }
+ });
+
+ if (node.lowIndex === node.index) {
+ var component = [], currentNode;
+ do {
+ currentNode = nodeStack.pop()
+ currentNode.onStack = false;
+ component.push(currentNode);
+ } while (currentNode != node);
+ components.push({
+ root: node,
+ scc: component
+ });
+ }
+ }
+ }
+
+ components.forEach(function(component, i){
+ component.index = i;
+ component.scc.forEach(function(node) {
+ node.component = i;
+ });
+ });
+ }
+
+ // Assign the breadth (x-position) for each strongly connected component,
+ // followed by assigning breadth within the component.
+ function computeNodeBreadths() {
+
+ layerComponents();
+
+ components.forEach(function(component, i){
+ bfs(component.root, function(node){
+ var result = node.sourceLinks
+ .filter(function(sourceLink){
+ return sourceLink.target.component == i;
+ })
+ .map(function(sourceLink){
+ return sourceLink.target;
+ });
+ return result;
+ });
+ });
+
+ var max = 0;
+ var componentsByBreadth = d3.nest()
+ .key(function(d) { return d.x; })
+ // .sortKeys(d3.ascending)
+ .entries(components)
+ .map(function(d) { return d.values; });
+
+ var max = -1, nextMax = -1;
+ componentsByBreadth.forEach(function(c){
+ c.forEach(function(component){
+ component.x = max + 1;
+ component.scc.forEach(function(node){
+ if (node.layer)
+ node.x = node.layer;
+ else
+ node.x = component.x + node.x;
+ nextMax = Math.max(nextMax, node.x);
+
+ });
+ });
+ max = nextMax;
+ });
+
+
+ nodes
+ .filter(function(node) {
+ var outLinks = node.sourceLinks.filter(function(link){ return link.source.name != link.target.name; });
+ return (outLinks.length == 0);
+ })
+ .forEach(function(node) { node.x = max; })
+
+ scaleNodeBreadths((size[0] - nodeWidth) / Math.max(max, 1));
+
+ function flatten(a) {
+ return [].concat.apply([], a);
+ }
+
+ function layerComponents() {
+ var remainingComponents = components,
+ nextComponents,
+ visitedIndex,
+ x = 0;
+
+ while (remainingComponents.length) {
+ nextComponents = [];
+ visitedIndex = {};
+
+ remainingComponents.forEach(function(component) {
+ component.x = x;
+
+ component.scc.forEach(function(n) {
+ n.sourceLinks.forEach(function(l) {
+ if (!visitedIndex.hasOwnProperty(l.target.component) &&
+ l.target.component != component.index) {
+ nextComponents.push(components[l.target.component]);
+ visitedIndex[l.target.component] = true;
+ }
+ })
+ });
+ });
+
+ remainingComponents = nextComponents;
+ ++x;
+ }
+ }
+
+ function bfs(node, extractTargets) {
+ var queue = [node], currentCount = 1, nextCount = 0;
+ var x = 0;
+
+ while(currentCount > 0) {
+ var currentNode = queue.shift();
+ currentCount--;
+
+ if (!currentNode.hasOwnProperty('x')) {
+ currentNode.x = x;
+ currentNode.dx = nodeWidth;
+
+ var targets = extractTargets(currentNode);
+
+ queue = queue.concat(targets);
+ nextCount += targets.length;
+ }
+
+
+ if (currentCount == 0) { // level change
+ x++;
+ currentCount = nextCount;
+ nextCount = 0;
+ }
+
+ }
+ }
+ }
+
+ function moveSourcesRight() {
+ nodes.forEach(function(node) {
+ if (!node.targetLinks.length) {
+ node.x = d3.min(node.sourceLinks, function(d) { return d.target.x; }) - 1;
+ }
+ });
+ }
+
+ function moveSinksRight(x) {
+ nodes.forEach(function(node) {
+ if (!node.sourceLinks.length) {
+ node.x = x - 1;
+ }
+ });
+ }
+
+ function scaleNodeBreadths(kx) {
+ nodes.forEach(function(node) {
+ node.x *= kx;
+ });
+ }
+
+ function computeNodeDepths(iterations) {
+ var nodesByBreadth = d3.nest()
+ .key(function(d) { return d.x; })
+ .sortKeys(d3.ascending)
+ .entries(nodes)
+ .map(function(d) { return d.values; });
+
+ initializeNodeDepth();
+ resolveCollisions();
+
+ for (var alpha = 1; iterations > 0; --iterations) {
+ relaxRightToLeft(alpha *= .99);
+ resolveCollisions();
+ relaxLeftToRight(alpha);
+ resolveCollisions();
+ }
+
+ function initializeNodeDepth() {
+ var ky = d3.min(nodesByBreadth, function(nodes) {
+ return (size[1] - (nodes.length - 1) * nodePadding) / d3.sum(nodes, value);
+ });
+
+ nodesByBreadth.forEach(function(nodes) {
+ nodes.forEach(function(node, i) {
+ node.y = i;
+ node.dy = node.value * ky;
+ });
+ });
+
+ links.forEach(function(link) {
+ link.dy = link.value * ky;
+ });
+ }
+
+ function relaxLeftToRight(alpha) {
+ nodesByBreadth.forEach(function(nodes, breadth) {
+ nodes.forEach(function(node) {
+ if (node.targetLinks.length) {
+ var y = d3.sum(node.targetLinks, weightedSource) / d3.sum(node.targetLinks, value);
+ node.y += (y - center(node)) * alpha;
+ }
+ });
+ });
+
+ function weightedSource(link) {
+ return center(link.source) * link.value;
+ }
+ }
+
+ function relaxRightToLeft(alpha) {
+ nodesByBreadth.slice().reverse().forEach(function(nodes) {
+ nodes.forEach(function(node) {
+ if (node.sourceLinks.length) {
+ var y = d3.sum(node.sourceLinks, weightedTarget) / d3.sum(node.sourceLinks, value);
+ node.y += (y - center(node)) * alpha;
+ }
+ });
+ });
+
+ function weightedTarget(link) {
+ return center(link.target) * link.value;
+ }
+ }
+
+ function resolveCollisions() {
+ nodesByBreadth.forEach(function(nodes) {
+ var node,
+ dy,
+ y0 = 0,
+ n = nodes.length,
+ i;
+
+ // Push any overlapping nodes down.
+ nodes.sort(ascendingDepth);
+ for (i = 0; i < n; ++i) {
+ node = nodes[i];
+ dy = y0 - node.y;
+ if (dy > 0) node.y += dy;
+ y0 = node.y + node.dy + nodePadding;
+ }
+
+ // If the bottommost node goes outside the bounds, push it back up.
+ dy = y0 - nodePadding - size[1];
+ if (dy > 0) {
+ y0 = node.y -= dy;
+
+ // Push any overlapping nodes back up.
+ for (i = n - 2; i >= 0; --i) {
+ node = nodes[i];
+ dy = node.y + node.dy + nodePadding - y0;
+ if (dy > 0) node.y -= dy;
+ y0 = node.y;
+ }
+ }
+ });
+ }
+
+ function ascendingDepth(a, b) {
+ return a.y - b.y;
+ }
+ }
+
+ function computeLinkDepths() {
+ nodes.forEach(function(node) {
+ node.sourceLinks.sort(ascendingTargetDepth);
+ node.targetLinks.sort(ascendingSourceDepth);
+ });
+ nodes.forEach(function(node) {
+ var sy = 0, ty = 0;
+ node.sourceLinks.forEach(function(link) {
+ link.sy = sy;
+ sy += link.dy;
+ });
+ node.targetLinks.forEach(function(link) {
+ link.ty = ty;
+ ty += link.dy;
+ });
+ });
+
+ function ascendingSourceDepth(a, b) {
+ return a.source.y - b.source.y;
+ }
+
+ function ascendingTargetDepth(a, b) {
+ return a.target.y - b.target.y;
+ }
+ }
+
+ function center(node) {
+ return node.y + node.dy / 2;
+ }
+
+ function value(link) {
+ return link.value;
+ }
+
+ return sankey;
+};
http://git-wip-us.apache.org/repos/asf/incubator-taverna-databundle-viewer/blob/7dddbeb4/app/assets/javascripts/vertical_sankey.js
----------------------------------------------------------------------
diff --git a/app/assets/javascripts/vertical_sankey.js b/app/assets/javascripts/vertical_sankey.js
new file mode 100644
index 0000000..67fa517
--- /dev/null
+++ b/app/assets/javascripts/vertical_sankey.js
@@ -0,0 +1,292 @@
+d3.vertical_sankey = function() {
+ var sankey = {},
+ nodeWidth = 24,
+ nodePadding = 8,
+ size = [1, 1],
+ nodes = [],
+ links = [];
+
+ sankey.nodeWidth = function(_) {
+ if (!arguments.length) return nodeWidth;
+ nodeWidth = +_;
+ return sankey;
+ };
+
+ sankey.nodePadding = function(_) {
+ if (!arguments.length) return nodePadding;
+ nodePadding = +_;
+ return sankey;
+ };
+
+ sankey.nodes = function(_) {
+ if (!arguments.length) return nodes;
+ nodes = _;
+ return sankey;
+ };
+
+ sankey.links = function(_) {
+ if (!arguments.length) return links;
+ links = _;
+ return sankey;
+ };
+
+ sankey.size = function(_) {
+ if (!arguments.length) return size;
+ size = _;
+ return sankey;
+ };
+
+ sankey.layout = function(iterations) {
+ computeNodeLinks();
+ computeNodeValues();
+ computeNodeBreadths();
+ computeNodeDepths(iterations);
+ computeLinkDepths();
+ return sankey;
+ };
+
+ sankey.relayout = function() {
+ computeLinkDepths();
+ return sankey;
+ };
+
+ sankey.link = function() {
+ var curvature = .5;
+
+ function link(d) {
+ var x0 = d.source.x + d.source.dx,
+ x1 = d.target.x,
+ xi = d3.interpolateNumber(x0, x1),
+ x2 = xi(curvature),
+ x3 = xi(1 - curvature),
+ y0 = d.source.y + d.sy + d.dy / 2,
+ y1 = d.target.y + d.ty + d.dy / 2;
+ return "M" + x0 + "," + y0
+ + "C" + x2 + "," + y0
+ + " " + x3 + "," + y1
+ + " " + x1 + "," + y1;
+ }
+
+ link.curvature = function(_) {
+ if (!arguments.length) return curvature;
+ curvature = +_;
+ return link;
+ };
+
+ return link;
+ };
+
+ // Populate the sourceLinks and targetLinks for each node.
+ // Also, if the source and target are not objects, assume they are indices.
+ function computeNodeLinks() {
+ nodes.forEach(function(node) {
+ node.sourceLinks = [];
+ node.targetLinks = [];
+ });
+ links.forEach(function(link) {
+ var source = link.source,
+ target = link.target;
+ if (typeof source === "number") source = link.source = nodes[link.source];
+ if (typeof target === "number") target = link.target = nodes[link.target];
+ source.sourceLinks.push(link);
+ target.targetLinks.push(link);
+ });
+ }
+
+ // Compute the value (size) of each node by summing the associated links.
+ function computeNodeValues() {
+ nodes.forEach(function(node) {
+ node.value = Math.max(
+ d3.sum(node.sourceLinks, value),
+ d3.sum(node.targetLinks, value)
+ );
+ });
+ }
+
+ // Iteratively assign the breadth (x-position) for each node.
+ // Nodes are assigned the maximum breadth of incoming neighbors plus one;
+ // nodes with no incoming links are assigned breadth zero, while
+ // nodes with no outgoing links are assigned the maximum breadth.
+ function computeNodeBreadths() {
+ var remainingNodes = nodes,
+ nextNodes,
+ x = 0;
+
+ while (remainingNodes.length) {
+ nextNodes = [];
+ remainingNodes.forEach(function(node) {
+ node.x = x;
+ node.dx = nodeWidth;
+ node.sourceLinks.forEach(function(link) {
+ nextNodes.push(link.target);
+ });
+ });
+ remainingNodes = nextNodes;
+ ++x;
+ }
+
+ //
+ moveSinksRight(x);
+ scaleNodeBreadths((size[0] - nodeWidth) / (x - 1));
+ }
+
+ function moveSourcesRight() {
+ nodes.forEach(function(node) {
+ if (!node.targetLinks.length) {
+ node.x = d3.min(node.sourceLinks, function(d) { return d.target.x; }) - 1;
+ }
+ });
+ }
+
+ function moveSinksRight(x) {
+ nodes.forEach(function(node) {
+ if (!node.sourceLinks.length) {
+ node.x = x - 1;
+ }
+ });
+ }
+
+ function scaleNodeBreadths(kx) {
+ nodes.forEach(function(node) {
+ node.x *= kx;
+ });
+ }
+
+ function computeNodeDepths(iterations) {
+ var nodesByBreadth = d3.nest()
+ .key(function(d) { return d.x; })
+ .sortKeys(d3.ascending)
+ .entries(nodes)
+ .map(function(d) { return d.values; });
+
+ //
+ initializeNodeDepth();
+ resolveCollisions();
+ for (var alpha = 1; iterations > 0; --iterations) {
+ relaxRightToLeft(alpha *= .99);
+ resolveCollisions();
+ relaxLeftToRight(alpha);
+ resolveCollisions();
+ }
+
+ function initializeNodeDepth() {
+ var ky = d3.min(nodesByBreadth, function(nodes) {
+ return (size[1] - (nodes.length - 1) * nodePadding) / d3.sum(nodes, value);
+ });
+
+ nodesByBreadth.forEach(function(nodes) {
+ nodes.forEach(function(node, i) {
+ node.y = i;
+ node.dy = node.value * ky;
+ });
+ });
+
+ links.forEach(function(link) {
+ link.dy = link.value * ky;
+ });
+ }
+
+ function relaxLeftToRight(alpha) {
+ nodesByBreadth.forEach(function(nodes, breadth) {
+ nodes.forEach(function(node) {
+ if (node.targetLinks.length) {
+ var y = d3.sum(node.targetLinks, weightedSource) / d3.sum(node.targetLinks, value);
+ node.y += (y - center(node)) * alpha;
+ }
+ });
+ });
+
+ function weightedSource(link) {
+ return center(link.source) * link.value;
+ }
+ }
+
+ function relaxRightToLeft(alpha) {
+ nodesByBreadth.slice().reverse().forEach(function(nodes) {
+ nodes.forEach(function(node) {
+ if (node.sourceLinks.length) {
+ var y = d3.sum(node.sourceLinks, weightedTarget) / d3.sum(node.sourceLinks, value);
+ node.y += (y - center(node)) * alpha;
+ }
+ });
+ });
+
+ function weightedTarget(link) {
+ return center(link.target) * link.value;
+ }
+ }
+
+ function resolveCollisions() {
+ nodesByBreadth.forEach(function(nodes) {
+ var node,
+ dy,
+ y0 = 0,
+ n = nodes.length,
+ i;
+
+ // Push any overlapping nodes down.
+ nodes.sort(ascendingDepth);
+ for (i = 0; i < n; ++i) {
+ node = nodes[i];
+ dy = y0 - node.y;
+ if (dy > 0) node.y += dy;
+ y0 = node.y + node.dy + nodePadding;
+ }
+
+ // If the bottommost node goes outside the bounds, push it back up.
+ dy = y0 - nodePadding - size[1];
+ if (dy > 0) {
+ y0 = node.y -= dy;
+
+ // Push any overlapping nodes back up.
+ for (i = n - 2; i >= 0; --i) {
+ node = nodes[i];
+ dy = node.y + node.dy + nodePadding - y0;
+ if (dy > 0) node.y -= dy;
+ y0 = node.y;
+ }
+ }
+ });
+ }
+
+ function ascendingDepth(a, b) {
+ return a.y - b.y;
+ }
+ }
+
+ function computeLinkDepths() {
+ nodes.forEach(function(node) {
+ node.sourceLinks.sort(ascendingTargetDepth);
+ node.targetLinks.sort(ascendingSourceDepth);
+ });
+ nodes.forEach(function(node) {
+ var sy = 0, ty = 0;
+ node.sourceLinks.forEach(function(link) {
+ link.sy = sy;
+ sy += link.dy;
+ });
+ node.targetLinks.forEach(function(link) {
+ link.ty = ty;
+ ty += link.dy;
+ });
+ });
+
+ function ascendingSourceDepth(a, b) {
+ return a.source.y - b.source.y;
+ }
+
+ function ascendingTargetDepth(a, b) {
+ return a.target.y - b.target.y;
+ }
+ }
+
+ function center(node) {
+ return node.y + node.dy / 2;
+ }
+
+ function value(link) {
+ return link.value;
+ }
+
+ return sankey;
+};