diff --git a/gui/src/components/entry/properties/WorkflowCard.js b/gui/src/components/entry/properties/WorkflowCard.js
index 1133015d4a3e5c007168bccde10f27b85ecb8146..ccaf6ec04de569cc4617151cb8dbff3f8dccb25e 100644
--- a/gui/src/components/entry/properties/WorkflowCard.js
+++ b/gui/src/components/entry/properties/WorkflowCard.js
@@ -17,950 +17,1219 @@
  */
 import React, { useEffect, useMemo, useRef, useState } from 'react'
 import PropTypes from 'prop-types'
-import { makeStyles, Tooltip, IconButton } from '@material-ui/core'
+import * as d3 from 'd3'
+import { makeStyles, Tooltip, IconButton, TextField, FormControl } from '@material-ui/core'
 import grey from '@material-ui/core/colors/grey'
+import blueGrey from '@material-ui/core/colors/blueGrey'
 import red from '@material-ui/core/colors/red'
+import { Replay, Undo, Label, LabelOff, PlayArrowSharp, StopSharp, Clear } from '@material-ui/icons'
+import { useHistory } from 'react-router-dom'
+import { isPlainObject } from 'lodash'
 import { PropertyCard, PropertyGrid } from './PropertyCard'
 import { resolveNomadUrl, resolveInternalRef, createEntryUrl } from '../../../utils'
 import { useApi } from '../../api'
-import { useHistory } from 'react-router-dom'
-import * as d3 from 'd3'
 import { getUrl } from '../../nav/Routes'
-import { nomadFontFamily, nomadPrimaryColor, nomadSecondaryColor, apiBase } from '../../../config'
-import { Replay, Undo, Redo } from '@material-ui/icons'
+import { nomadFontFamily, apiBase } from '../../../config'
+import { useAsyncError } from '../../../hooks'
 
 const useWorkflowGraphStyles = makeStyles(theme => ({
   root: {
     width: '100%',
     minWidth: 1000,
     '& .link': {
-      strokeWidth: 3,
       fill: 'none',
-      stroke: grey[500],
-      strokeOpacity: 0.5
-    },
-    '& .crosslink': {
-      fill: 'none',
-      stroke: red[500],
       strokeOpacity: 0.5,
-      strokeWidth: 3
+      fillOpacity: 0.5,
+      strokeWidth: 2.5
     },
     '& .text': {
       fontFamily: nomadFontFamily,
       fontSize: 12,
-      fontWeight: 'bold',
       dy: '0.35em',
-      fill: grey[800]
+      textAnchor: 'middle'
     },
-    '& .circle': {
-      stroke: grey[500],
+    '& .icon': {
       strokeWidth: 2,
       strokeOpacity: 0.5
     }
   }
 }))
 
-function addMarkers(svg, nodes, offset) {
-  const markerWidth = 5
+let archives = {}
 
-  const markerIds = []
+const resolveSection = async (source, query) => {
+  if (isPlainObject(source.section)) {
+    // already resolved
+    return source
+  }
 
-  const addIds = (node) => {
-    const nodeLinks = node.data.crossLinks || []
-    nodeLinks.forEach(link => {
-      const nodeTarget = nodes.filter(node => node.data.key === link[0])
-      nodeTarget.forEach(target => {
-        markerIds.push(`${node.id}-${target.id}`)
-      })
-    })
-    if (node._children) {
-      node._children.forEach(child => addIds(child))
+  let path = source.path
+  if (typeof path !== 'string') path = source.section || '/'
+  const pathSegments = path.split('#').filter(p => p)
+
+  let archive = source.archive
+  let baseUrl = createEntryUrl(apiBase, archive?.metadata?.upload_id, archive?.metadata?.entry_id)
+  let section
+  if (pathSegments.length === 1) {
+    // internal reference
+    path = pathSegments[0]
+    try {
+      section = source.section ? source.path : resolveInternalRef(path, archive)
+    } catch (error) {
+      console.error(`Cannot resolve section ${path}`)
+      return
+    }
+  } else {
+    // external reference
+    const url = resolveNomadUrl(path, baseUrl)
+    const {api, required} = query
+    try {
+      archive = archives[url.entryId]
+      if (!archive) {
+        const response = await api.post(`/entries/${url.entryId}/archive/query`, {required: required})
+        archive = response.data.archive
+        archives[url.entryId] = archive
+      }
+      path = pathSegments[1]
+      if (!path.startsWith('/')) path = `/${path}`
+      section = source.section ? source.path : resolveInternalRef(path, archive)
+    } catch (error) {
+      console.error(`Cannot resolve entry ${url.entryId}: ${error}`)
+      return
     }
   }
 
-  nodes.forEach(node => {
-    addIds(node)
-  })
+  if (!section) return
 
-  svg.append('defs').selectAll('marker')
-    .data(markerIds)
-    .enter()
-    .append('marker')
-    .attr('class', d => `marker-${d}`)
-    .attr('id', String)
-    .attr('viewBox', '0 -5 10 10')
-    .attr('refX', offset || 0)
-    .attr('refY', 0)
-    .attr('markerWidth', markerWidth)
-    .attr('markerHeight', markerWidth)
-    .attr('xoverflow', 'visible')
-    .attr('orient', 'auto')
-    .style('fill', red[500])
-    .style('fill-opacity', 0.5)
-    .append('path')
-    .attr('d', 'M0,-5L10,0L0,5')
-}
+  baseUrl = createEntryUrl(apiBase, archive?.metadata?.upload_id, archive?.metadata?.entry_id)
+  const match = path.match('.*/([^/]+)/(\\d+)$')
 
-const ForceDirected = React.memo(({data, layout, setTooltipContent}) => {
-  const classes = useWorkflowGraphStyles()
-  const svgRef = useRef()
-  const history = useHistory()
-  const finalLayout = useMemo(() => {
-    const defaultLayout = {
-     width: 600,
-     margin: {top: 40, bottom: 40, left: 20, right: 20},
-     circleRadius: 17,
-     linkDistance: 40
-    }
-    return {...defaultLayout, ...layout}
-  }, [layout])
+  const sectionKeys = query.sectionKeys || []
+  const nChildren = (section) => (sectionKeys.map(key => (section[key] || []).length)).reduce((a, b) => a + b)
+  if (section.task) {
+    const task = await resolveSection({path: section.task, archive: archive}, query)
+    if (!task) return
+    task.section.inputs = section.inputs || task.section.inputs
+    task.section.outputs = section.outputs || task.section.outputs
+    task.nChildren = nChildren(task.section)
+    return task
+  }
 
-  useEffect(() => {
-    const svg = d3.select(svgRef.current)
-    svg.selectAll('g').remove()
-    const { width, circleRadius, linkDistance, margin } = finalLayout
-    svg
-      .attr('width', width)
-      .attr('height', width)
+  return {
+    name: section.name,
+    section: section,
+    sectionType: match ? match[1] : path.split('/').pop(),
+    path: path,
+    archive: source.section ? null : archive,
+    url: [baseUrl, path].join('#'),
+    entryId: archive?.metadata?.entry_id,
+    color: section.color,
+    nChildren: nChildren(section)
+  }
+}
 
-    const svgGroup = svg.append('g')
+const range = (start, stop) => Array.from(Array(stop - start).fill(start).map((n, i) => n + i))
 
-    const gCrossLink = svgGroup.append('g')
-      .attr('class', 'crosslink')
+const getNodes = async (source, query) => {
+  let {resolveIndices, maxNodes} = query
+  const {sectionKeys} = query
 
-    const gLink = svgGroup.append('g')
-      .attr('class', 'link')
+  let nodes = source.nodes || []
+  if (nodes.length && !resolveIndices) return nodes
 
-    const gNode = svgGroup.append('g')
-      .attr('class', 'node')
-      .attr('cursor', 'pointer')
-      .attr('pointer-events', 'all')
+  if (!maxNodes) maxNodes = 7
 
-    // fix inputs and outputs to edge of frame
-    const mid = width / 2
-    const dy = circleRadius * 10
+  const resolved = await resolveSection(source, query)
+  if (!resolved) return nodes
+  const parent = resolved.section
+  if (!parent) return nodes
 
-    if (!data.children) data.children = []
+  nodes = []
+  const archive = resolved.archive
+  for (const key of sectionKeys) {
+    let children = parent[key] || []
+    const nChildren = children.length
+    if (!Array.isArray(children)) children = [children]
 
-    const inputChildren = data.children.filter(d => d.intent && d.intent.startsWith('input'))
-    let offset = inputChildren.length * dy / 2
-    let fixPoints = inputChildren.map((child, index) => mid + index * dy - offset)
-    for (const [index, child] of inputChildren.entries()) {
-      // vertical configuration, switch fixX and fixY for vertical
-      child.fixX = fixPoints[index]
-      child.fixY = margin.top
+    let sectionIndices = range(0, nChildren)
+    if (resolveIndices && resolveIndices[key]) {
+      sectionIndices = resolveIndices[key] || sectionIndices
+    } else if (maxNodes < nChildren) {
+      const mid = Math.floor(maxNodes / 2)
+      // show first and last few nodes
+      sectionIndices = [...range(0, mid), ...range(nChildren - mid, nChildren)]
     }
-    const outputChildren = data.children.filter(d => d.intent && d.intent.startsWith('output'))
-    offset = outputChildren.length * dy / 2
-    fixPoints = outputChildren.map((child, index) => mid + index * dy - offset)
-    for (const [index, child] of outputChildren.entries()) {
-      // vertical configuration, switch fixX and fixY for vertical
-      child.fixX = fixPoints[index]
-      child.fixY = width - margin.bottom
+
+    for (const index of sectionIndices) {
+      const child = children[index]
+      if (!child) continue
+
+      let path = child
+      if (!child.section) path = `${source.path}/${key}/${index}`
+      const section = await resolveSection({section: child.section, path: path, archive: archive}, query)
+      if (!section) continue
+
+      section.name = child.name
+      section.type = key
+      section.index = index
+      section.parent = source
+      section.total = nChildren
+      nodes.push(section)
     }
+  }
+
+  return nodes
+}
+
+const getLinks = async (source, query) => {
+  if (source.links) return source.links
+
+  const nodes = source.nodes
+  if (!source.nodes) return
 
-    let nodes = []
-    function flatten(node) {
-      node.children = node.children || []
-      node.children.forEach(child => flatten(child))
-      nodes.push(node)
+  const links = []
+  for (const node of nodes) {
+    if (node.type === 'tasks') {
+      node.nodes = await getNodes(node, query)
     }
+  }
 
-    flatten(data)
-
-    // add crosslinks between nodes
-    function addCrossLinksData(data) {
-      const workflows = []
-      const tasks = []
-      const inputs = []
-      const outputs = []
-      data.children.forEach(child => {
-        if (child.type.startsWith('workflow')) workflows.push(child)
-        else if (child.type.startsWith('task')) tasks.push(child)
-        else if (child.intent && child.intent.startsWith('input')) inputs.push(child)
-        else if (child.intent && child.intent.startsWith('output')) outputs.push(child)
-      })
+  const isLinked = (source, target) => {
+    if (source.url === target.url) return false
 
-      data.children = [...inputs, ...outputs, ...workflows, ...tasks]
+    const outputs = []
+    if (source.type === 'tasks' && source.nodes) {
+      outputs.push(...source.nodes.filter(node => node.type === 'outputs').map(node => node.url))
+    } else {
+      outputs.push(source.url)
+    }
 
-      const addLink = (link, node, dash = '3,3') => {
-        link.push(dash)
-        nodes.forEach(d => {
-          d.crossLinks = d.crossLinks || []
-          if (d.key === node.key) {
-            // apply link to duplicate nodes
-            const links = d.crossLinks.map(l => l[0])
-            if (link[0] && !links.includes(link[0])) d.crossLinks.push(link)
-          }
-          if (d.key === link[0]) {
-            node.crossLinks = node.crossLinks || []
-            const links = node.crossLinks.map(l => l[0])
-            if (link[0] && !links.includes(link[0])) node.crossLinks.push(link)
-          }
-        })
-      }
+    const inputs = []
+    if (target.type === 'tasks' && target.nodes) {
+      inputs.push(...target.nodes.filter(node => node.type && node.type.startsWith('inputs')).map(node => node.url))
+    } else {
+      inputs.push(target.url)
+    }
 
-      const allTasks = [...workflows, ...tasks]
-
-      // add link from inputs to workflow
-      inputs.forEach(input => addLink([data.key, 'Input'], input))
-      // add link from workflow to outputs
-      outputs.forEach(output => addLink([output.key, 'Output'], data))
-      // add link from from input to first task
-      if (tasks.length > 0) {
-        // add link from input to first task
-        inputs.forEach(input => {
-          addLink([tasks[0].key, 'Input'], input)
-        })
-        // add link from last task to output
-        outputs.forEach(output => {
-          // add link only if no task is connected to output
-          let linked = false
-          for (const node of allTasks) {
-            const links = node.crossLinks.map(l => l[0])
-            if (links.includes(output.key)) {
-              linked = true
-              break
-            }
-          }
-          if (!linked) addLink([output.key, 'Output'], tasks[tasks.length - 1])
-        })
+    let linked = false
+    for (const output of outputs) {
+      if (!output) continue
+      if (inputs.includes(output)) {
+        linked = true
+        break
       }
+    }
+    return linked
+  }
 
-      // add links between tasks if inputs/outputs are connected
-      allTasks.forEach(task1 => {
-        allTasks.forEach(task2 => {
-          if (task1.key !== task2.key) {
-            const outputs = task1.children
-              .filter(child => child.intent && child.intent.startsWith('output'))
-              .map(child => child.key)
-            const inputs = task2.children
-              .filter(child => child.intent && child.intent.startsWith('input'))
-              .map(child => child.key)
-            let linked = false
-            for (const input of inputs) {
-              if (outputs.includes(input)) {
-                linked = true
-                break
-              }
+  // links from inputs to source
+  const inputs = nodes.filter(node => node.type && node.type.startsWith('inputs') && node.url)
+  inputs.forEach(input => {
+    links.push({source: input, target: source, label: 'Input'})
+  })
+  // links from source to outputs
+  const outputs = nodes.filter(node => node.type === 'outputs' && node.url)
+  outputs.forEach(output => {
+    links.push({source: source, target: output, label: 'Output'})
+  })
+
+  // source and target are linked if any of the outputs of source or source itself is any of the
+  // inputs of target or target itself
+  for (const source of ['inputs', 'tasks']) {
+    for (const target of ['outputs', 'tasks']) {
+      const sourceNodes = nodes.filter(node => node.type && node.type.startsWith(source) && node.url)
+      const targetNodes = nodes.filter(node => node.type === target && node.url)
+      sourceNodes.forEach(sourceNode => {
+        targetNodes.forEach(targetNode => {
+          if (isLinked(sourceNode, targetNode)) {
+            let label = ''
+            if (source === 'inputs') {
+              label = 'Input'
+            } else if (target === 'outputs') {
+              label = 'Output'
+            } else {
+              label = 'Click to see how tasks are linked'
             }
-            if (linked) addLink([task2.key, 'Sequential tasks'], task1)
+            links.push({source: sourceNode, target: targetNode, label: label})
           }
         })
       })
-
-      // iterate over all tasks
-      workflows.forEach(workflow => addCrossLinksData(workflow))
-      tasks.forEach(task => addCrossLinksData(task))
     }
+  }
 
-    addCrossLinksData(data)
+  return links
+}
 
-    // pack layout for the task quantities
-    const pack = d3.pack()
-      .size([circleRadius * 2, circleRadius * 2])
-      .padding(5)
+const Graph = React.memo(({
+  source,
+  query,
+  layout,
+  setTooltipContent,
+  setCurrentNode,
+  setShowLegend,
+  setEnableForce
+  }) => {
+  const classes = useWorkflowGraphStyles()
+  const svgRef = useRef()
+  const history = useHistory()
+  const asyncError = useAsyncError()
+  const finalLayout = useMemo(() => {
+    const defaultLayout = {
+      width: 700,
+      margin: {top: 60, bottom: 60, left: 40, right: 40},
+      circleRadius: 20,
+      markerWidth: 4,
+      scaling: 0.35,
+      color: {
+        text: grey[800],
+        link: red[800],
+        outline: blueGrey[800],
+        workflow: '#192E86',
+        task: '#00AC7C',
+        input: '#A59FFF',
+        output: '#005A35'
+      },
+      shape: {
+        input: 'circle',
+        workflow: 'rect',
+        task: 'rect',
+        output: 'circle'
+      }
+    }
+    return {...defaultLayout, ...layout}
+  }, [layout])
+  archives = {}
 
-    // define transition of nodes
-    // const transition = svg.transition()
-    //   .duration(250)
+  useEffect(() => {
+    const {width, markerWidth, scaling, color} = finalLayout
+    const nodeShape = finalLayout.shape
+    const whRatio = 1.6
+    const legendSize = 12
+    const height = width / whRatio
 
-    function addSubGraph(node) {
-      if (!node._children || !node._children.length) return
+    const svg = d3.select(svgRef.current)
 
-      const d = {...node.data}
-      d.children = (node._children || []).map(child => {
-        return {...child.data, crossLinks: []}
-      })
-      d.crossLinks = []
-      d.children.forEach(child => {
-        if (child.intent === 'output') d.crossLinks.push([`${child.key}_subgraph`, ''])
-        if (child.intent === 'input') child.crossLinks.push([`${d.key}_subgraph`, ''])
-      })
+    const inOutColor = d3.interpolateRgb(color.input, color.output)(0.5)
+
+    let nodes, node, line
+    let id = 0
+    let view
+    let focus
+    let previousNode = 'root'
+    let root
+    const dquery = {...query, resolveIndices: {}}
+    let currentNode = root
+    let hasError = false
+    let enableForce = false
+    let dragged = false
+    let zoomTransform = d3.zoomIdentity
+
+    const isWorkflow = (d) => {
+      if (d.sectionType && d.sectionType.startsWith('workflow')) return true
+      const tasks = (d.nodes || []).filter(n => n.type === 'tasks')
+      return tasks.length > 0
+    }
 
-      const dRoot = d3.hierarchy(d)
-      dRoot.sum(d => d.size).sort((a, b) => a.data.index - b.data.index)
-      dRoot.descendants().forEach((node, i) => {
-        node.data.key = `${node.data.key}_subgraph`
-        node.id = i + nodes.length + subGraphNodes.length
-      })
-      pack(dRoot)
-
-      const crossLinks = []
-      dRoot.children = dRoot.children || []
-      dRoot.children.forEach(child => {
-        let label = ''
-        if (child.data.intent === 'output') {
-          for (const link of node.data.crossLinks) {
-            if (child.data.key.startsWith(link[0])) {
-              label = link[1]
-              break
-            }
-          }
-          crossLinks.push({source: dRoot, target: child, id: `${dRoot.id}-${child.id}`, label: label})
-        }
-        if (child.data.intent === 'input') {
-          let label = ''
-          for (const nodeChild of node._children) {
-            for (const link of nodeChild.data.crossLinks) {
-              if (dRoot.data.key.startsWith(link[0])) {
-                label = link[1]
-                break
-              }
-            }
-            if (label) break
-          }
-          crossLinks.push({source: child, target: dRoot, id: `${child.id}-${dRoot.id}`, label: label})
-        }
-      })
-      dRoot._children = dRoot.children
-      node.subGraph = {root: dRoot, crossLinks: crossLinks}
-      subGraphNodes.push(...dRoot.descendants())
+    const nodeColor = (d) => {
+      if (d.color) return d.color
+      if (d.type === 'link') return '#ffffff'
+      if (isWorkflow(d)) return color.workflow
+      if (d.type === 'tasks') return color.task
+      if (d.type === 'inputs-outputs') return inOutColor
+      return d.type === 'inputs' ? color.input : color.output
     }
 
-    // TODO using hierarchy is not required
-    const root = d3.hierarchy(data)
-    nodes = root.descendants()
+    const trimName = (name) => name && name.length > 25 ? `${name.substring(0, 22)}...` : name
 
-    const subGraphNodes = []
+    svg.selectAll('g').remove()
+    svg.attr('width', width)
+      .attr('height', height)
 
-    const crossLinkMap = {}
+    const svgGroup = svg.append('g')
 
-    nodes.forEach((d, i) => {
-      // initialize all nodes at the center
-      d.x = width / 2
-      d.y = width / 2
-      d.id = i
-      d._children = d.children
-      if (d.depth) d.children = null
-      if (d.id) if (!crossLinkMap[d.data.key]) crossLinkMap[d.data.key] = d
-      if (d.data.type && d.data.type.startsWith('task')) addSubGraph(d)
-    })
-    addMarkers(svg, nodes, circleRadius)
+    const defs = svg.append('defs')
+
+    const addLinkMarkers = (links) => {
+      defs.selectAll('marker')
+        .exit().remove()
+        .data(links.map(link => link.id))
+        .enter()
+        .append('marker')
+        .attr('class', d => `marker-${d}`)
+        .attr('id', String)
+        .attr('viewBox', '0 -5 10 10')
+        .attr('refX', 0)
+        .attr('refY', 0)
+        .attr('markerWidth', markerWidth)
+        .attr('markerHeight', markerWidth)
+        .attr('xoverflow', 'visible')
+        .attr('orient', 'auto')
+        .style('fill', color.link)
+        .attr('fill-opacity', 0.5)
+        .append('path')
+        .attr('d', 'M0,-5L10,0L0,5')
+    }
+
+    const legend = svg.append('g')
+      .attr('class', 'legend')
+      .attr('visibility', 'visible')
+
+    const addLegend = (label, index) => {
+      const gLegend = legend.append('g')
+        .attr('cursor', 'pointer')
+        .attr('pointer-events', 'all')
+      const shape = nodeShape[label]
+      const icon = gLegend.append(shape)
+      const dx = width / 8
+      const x = width / 2 + dx * index - dx * 1.5
+      const y = height - legendSize * 2
+
+      if (shape === 'rect') {
+        icon.attr('width', legendSize * whRatio)
+          .attr('height', legendSize)
+          .attr('rx', legendSize * 0.1)
+          .attr('x', x - legendSize * whRatio / 2)
+          .attr('y', y - legendSize / 2)
+      } else {
+        icon.attr('r', legendSize / 2)
+          .attr('cx', x)
+          .attr('cy', y)
+        }
+      icon
+        .attr('fill', nodeColor({type: label + 's', sectionType: label + 's'}))
+
+      gLegend.append('text')
+        .attr('class', 'text')
+        .text(label)
+        .style('text-anchor', 'middle')
+        .attr('x', x)
+        .attr('y', y + legendSize * 1.2)
+        .style('alignment-baseline', 'middle')
+
+      gLegend
+        .on('mouseover', () => {
+          let tooltip = ''
+          if (label === 'input') {
+            tooltip = <p>
+              Input to a task or workflow.
+            </p>
+          } else if (label === 'output') {
+            tooltip = <p>
+              Output from a task or workflow.
+            </p>
+          } else if (label === 'workflow') {
+            tooltip = <p>
+              Task containing further sub-tasks.
+            </p>
+          } else if (label === 'task') {
+            tooltip = <p>
+              Elementary task with inputs and outputs.
+            </p>
+          }
+          setTooltipContent(tooltip)
+        })
+        .on('mouseout', () => {
+          setTooltipContent('')
+        })
+    }
 
-    addMarkers(svg, subGraphNodes)
+    // add legend
+    legend.append('path')
+      .attr('d', () => {
+        const line = d3.line().x(d => d[0]).y(d => d[1])
+        return line([
+          [width / 4 + 70, height - legendSize * 3],
+          [3 * width / 4, height - legendSize * 3],
+          [3 * width / 4, height],
+          [width / 4, height],
+          [width / 4, height - legendSize * 3],
+          [width / 4 + 10, height - legendSize * 3]
+        ])
+      })
+      .attr('stroke', grey[900])
+      .attr('stroke-width', 0.5)
+      .attr('fill', 'none')
+    legend.append('text')
+      .text('Legend')
+      .attr('class', 'text')
+      .attr('font-weight', 'bold')
+      .attr('x', width / 4 + 40)
+      .attr('y', height - legendSize * 2.8)
+
+    const legendLabels = Object.keys(nodeShape)
+    legendLabels.forEach((label, index) => addLegend(label, index))
 
     // add zoom
     const zoomBehaviors = d3.zoom()
-    .on('zoom', () => {
-      svgGroup.attr('transform', d3.event.transform)
-    })
+      .on('zoom', () => {
+        zoomTransform = d3.event.transform
+        svgGroup.attr('transform', zoomTransform)
+      })
 
     svg.call(zoomBehaviors)
 
+    // set zoom factor to <1 inorder to see the inputs and outputs
+    const zoomF = (r) => width * scaling / r
+
+    // add force directed behavior for tasks inside box
     const simulation = d3.forceSimulation()
-      .force('crosslink', d3.forceLink().id(d => d.id).distance(linkDistance).strength(1.0))
-      .force('link', d3.forceLink().id(d => d.id).distance(linkDistance).strength(0.5))
-      .force('charge', d3.forceManyBody().strength(-50.0))
+      .force('link', d3.forceLink().id(d => d.id))
+      .force('charge', d3.forceManyBody())
       .velocityDecay(0.2)
       .alphaDecay(0.2)
-      .force('x', d3.forceX(d => d.data.fixX || 0).strength(d => d.data.fixX === undefined ? 0 : 3))
-      .force('y', d3.forceY(d => d.data.fixY || 0).strength(d => d.data.fixY === undefined ? 0 : 3))
-      .force('collision', d3.forceCollide().radius(circleRadius * 2))
-
-    function drag(simulation) {
-      function dragStart(d) {
-        if (!d3.event.active) simulation.alphaTarget(0.7).restart()
-        d.fx = d.x
-        d.fy = d.y
-      }
-
-      function dragged(d) {
-        d.fx = d3.event.x
-        d.fy = d3.event.y
-      }
 
-      function dragEnd(d) {
-        if (!d3.event.active) simulation.alphaTarget(0)
+    // add drag
+    const dragBehaviors = d3.drag()
+      .on('drag', d => {
+        if (!d.parent || d.url === nodes[0].url) return
+        const event = d3.event.sourceEvent
+        const k = zoomF(view[2])
+        const x = ((event.offsetX - zoomTransform.x) / zoomTransform.k - width / 2) / k + view[0]
+        const y = ((event.offsetY - zoomTransform.y) / zoomTransform.k - height / 2) / k + view[1]
+        setTooltipContent('')
+        if (enableForce) {
+          d.fx = x
+          d.fy = y
+        } else {
+          d.x = x
+          d.y = y
+          zoomTo(view)
+        }
+      })
+      .on('start', d => {
+        if (!enableForce) return
+        simulation.alphaTarget(0.7).restart()
+      })
+      .on('end', d => {
+        dragged = true
+        if (!enableForce) return
+        simulation.alphaTarget(0)
         d.fx = null
         d.fy = null
-      }
-
-      return d3.drag()
-        .on('start', dragStart)
-        .on('drag', dragged)
-        .on('end', dragEnd)
-    }
-
-    let node, link, crossLink
-
-    function ticked() {
-      link
-        .attr('x1', d => d.source.x)
-        .attr('y1', d => d.source.y)
-        .attr('x2', d => d.target.x)
-        .attr('y2', d => d.target.y)
-
-      crossLink
-        .attr('x1', d => d.source.x)
-        .attr('y1', d => d.source.y)
-        .attr('x2', d => d.target.x)
-        .attr('y2', d => d.target.y)
-
-      node
-        .attr('transform', d => `translate(${d.x},${d.y})`)
-    }
-
-    let nodesHistory = []
-    let currentNodesIndex = 0
-    const nodesMap = {}
-    nodes.forEach(node => {
-      nodesMap[node.id] = node
-    })
-
-    const pushNodes = (nodes) => {
-      nodesHistory.push(nodes.map(node => node.id))
-      currentNodesIndex = currentNodesIndex + 1
-    }
+      })
 
-    const rootNodes = root.leaves()
-    pushNodes(rootNodes)
+    const gNode = svgGroup.append('g')
+      .attr('class', 'node')
+      .attr('cursor', 'pointer')
+      .attr('pointer-events', 'all')
 
-    d3.select('#backbutton')
-      .on('click', () => {
-        currentNodesIndex = currentNodesIndex - 1
-        currentNodesIndex = Math.max(currentNodesIndex, 0)
-        update(nodesHistory[currentNodesIndex].map(id => nodesMap[id]))
-        simulation.restart()
+    const gLink = svgGroup.append('g')
+      .attr('class', 'link')
+      .attr('cursor', 'default')
+      .attr('pointer-events', 'all')
+      .attr('stroke', color.link)
+
+    const setNodesPosition = (root) => {
+      // layout root and its nodes horizontally from inputs (left) root and tasks (middle)
+      // and outputs (right)
+      if (!root.nodes || root.children) return
+
+      const circleRadius = root.r / 12
+      const dx = circleRadius * 4
+      const fx = 1.4
+      const tasks = root.nodes.filter(node => node.type === 'tasks')
+      root.children = tasks
+      // tasks are arranged in a circle inside the parent workflow
+      tasks.forEach((node, index) => {
+        const theta = (2 * Math.PI * index / tasks.length) - Math.PI / 4
+        node.r = Math.sqrt(node.size || 1) * circleRadius * 1.5
+        const r = root.r - node.r
+        node.x = -r * 0.95 * Math.cos(theta) * whRatio + root.x
+        node.y = -r * (theta < 0 || theta > Math.PI ? 0.95 : 0.85) * Math.sin(theta) + root.y
       })
 
-    d3.select('#forwardbutton')
-      .on('click', () => {
-        currentNodesIndex = currentNodesIndex + 1
-        currentNodesIndex = Math.min(currentNodesIndex, nodesHistory.length - 1)
-        update(nodesHistory[currentNodesIndex].map(id => nodesMap[id]))
-        simulation.restart()
+      // inputs left
+      const inputs = root.nodes.filter(node => node.type === 'inputs')
+      let offsetX = (inputs.length - 1) * dx / 2
+      inputs.forEach((node, index) => {
+        node.y = index * dx - offsetX + root.y
+        node.x = -root.r * fx * whRatio + root.x
+        node.r = circleRadius
       })
 
-    d3.select('#resetbutton')
-      .on('click', () => {
-      currentNodesIndex = 0
-      nodesHistory = []
-      pushNodes(rootNodes)
-      update(rootNodes)
-      simulation.restart()
-    })
-
-    function update(nodes) {
-      const links = []
-      const crossLinks = []
-      nodes.forEach(node => {
-        const nodeLinks = node.data.crossLinks || []
-        nodeLinks.forEach(link => {
-          const nodeTarget = crossLinkMap[link[0]]
-          if (node && nodes.includes(nodeTarget)) {
-            crossLinks.push({
-              source: node.id,
-              target: nodeTarget.id,
-              label: link[1],
-              id: `${node.id}-${nodeTarget.id}`,
-              dash: link[2],
-              marker: link[3] === undefined
-            })
-          }
-        })
+      // inputs-outputs top
+      const inouts = root.nodes.filter(node => node.type === 'inputs-outputs')
+      offsetX = (inouts.length - 1) * dx
+      inouts.forEach((node, index) => {
+        node.x = index * dx * 2 - offsetX + root.x
+        node.y = -root.r * fx + root.y
+        node.r = circleRadius
       })
 
-      //  update nodes
-      node = gNode.selectAll('g')
-        .data(nodes, d => d.id)
-
-      node.exit().remove()
+      // outputs right
+      const outputs = root.nodes.filter(node => node.type === 'outputs')
+      offsetX = (outputs.length - 1) * dx / 2
+      outputs.forEach((node, index) => {
+        node.y = index * dx - offsetX + root.y
+        node.x = root.r * whRatio * fx + root.x
+        node.r = circleRadius
+      })
+      root.nodes = [...inputs, ...inouts, ...root.children, ...outputs]
+    }
 
-      const handleMouseOverText = d => {
-        d3.select(`#text-${d.id}`).style('fill', red[800])
-        const text = d.data.type.includes('workflow') ? 'overview page' : 'archive section'
-        if (d.data.entryId) {
-          setTooltipContent(`Go to ${text} for entry ${d.data.entryId}`)
-        }
+    const fLink = (source, target) => {
+      let vx = target.x - source.x
+      let vy = target.y - source.y
+      const vr = Math.sqrt(vx * vx + vy * vy)
+      const sin = vy / vr
+      const cos = vx / vr
+      const sx = vy ? Math.max(-1, Math.min(cos / sin, 1)) * Math.sign(sin) : Math.sign(vx)
+      const sy = vx ? Math.max(-1, Math.min(sin / cos, 1)) * Math.sign(cos) : Math.sign(vy)
+      const targetR = (f) => Math.abs(target.r) * f
+      let offsetxt, offsetyt, offsetxs, offsetys
+      if (source.r > 0) {
+        // offset from edge of circle
+        offsetxs = cos * source.r
+        offsetys = sin * source.r
+      } else {
+        // offset from edge of square
+        offsetxs = -sx * source.r * whRatio
+        offsetys = -sy * source.r
       }
-
-      const handleMouseOutText = d => {
-        setTooltipContent('')
-        d3.select(`#text-${d.id}`).style('fill', grey[800])
+      if (target.r > 0) {
+        // offset to edge of circle
+        offsetxt = cos * targetR(1)
+        offsetyt = sin * targetR(1)
+      } else {
+        // offset to edge of square
+        offsetxt = sx * targetR(whRatio)
+        offsetyt = sy * targetR(1)
       }
+      source = {x: source.x + offsetxs, y: source.y + offsetys}
+      target = {x: target.x - offsetxt, y: target.y - offsetyt}
+      vx = target.x - source.x
+      vy = target.y - source.y
+      const line = d3.line().x(d => d[0]).y(d => d[1])
+        .curve(d3.curveBasis)
+      const points = [[source.x, source.y]]
+      if (Math.abs(vx) > Math.abs(vy)) {
+        target.x = target.x - markerWidth * 2 * Math.sign(vx)
+        // points.push([source.x + vx / 2, source.y])
+        // points.push([source.x + vx / 2, target.y])
+      } else {
+        target.y = target.y - markerWidth * 2 * Math.sign(vy)
+        // points.push([source.x, source.y + vy / 2])
+        // points.push([target.x, source.y + vy / 2])
+      }
+      points.push([target.x, target.y])
+      return line(points)
+    }
 
-      const handleClickText = d => {
-        if (d.id !== 0) {
-          let path = `entry/id/${d.data.entryId}`
-          const sectionPath = d.data.path ? d.data.path.replace(/\/(?=\d)/g, ':') : null
-          path = d.data.type.startsWith('workflow') ? path : sectionPath ? `${path}/data${sectionPath}` : path
-          const url = getUrl(path)
-          history.push(url)
+    const zoomTo = (v) => {
+      // for elementary tasks, set radius to 1/6
+      const k = zoomF(v[2])
+      const rk = (node) => {
+        return node.url === focus.url && !isWorkflow(node) ? 1 / 6 : 1
+      }
+      const bound = (d) => {
+        let x = (d.x - v[0]) * k + width / 2
+        let y = (d.y - v[1]) * k + height / 2
+        const s = 0.8 * scaling
+        if (d.type === 'tasks' && d.url !== source.url) {
+          x = Math.min(Math.max(x, s * width), (1 - s) * width)
+          y = Math.min(Math.max(y, s * height), (1 - s) * height)
         }
+        return [x, y]
       }
 
-      const handleMouseOverCircle = d => {
-        d3.select(`#circle-${d.id}`).style('stroke-opacity', 1)
-        const children = d.children || []
-        children.forEach(child => {
-          d3.select(`#link-${d.id}-${child.id}`).style('stroke-opacity', 1)
-          d3.select(`#circle-${child.id}`).style('stroke-opacity', 1)
+      view = v
+      node
+        .attr('transform', d => {
+          const [x, y] = bound(d)
+          return `translate(${x},${y})`
         })
-      }
+      node.selectAll('circle').attr('r', d => d.r * rk(d) * k)
+      node.selectAll('rect')
+        .attr('x', d => -d.r * rk(d) * k * whRatio)
+        .attr('y', d => -d.r * rk(d) * k)
+        .attr('rx', d => d.r * rk(d) * k * 0.05)
+        .attr('width', d => d.r * 2 * rk(d) * k * whRatio)
+        .attr('height', d => d.r * 2 * rk(d) * k)
+      line.attr('d', d => {
+        const translate = (node) => {
+          const isCircle = ['inputs', 'outputs', 'inputs-outputs'].includes(node.type)
+          const dr = node.r * rk(node) * k
+          const [x, y] = bound(node)
+          return {
+            x: x,
+            y: y,
+            r: isCircle ? dr : -dr
+          }
+        }
+        return fLink(translate(d.source), translate(d.target))
+      })
+      node.selectAll('text').attr('y', d => -1.2 * d.r * rk(d) * k)
+    }
 
-      const handleMouseOutCircle = d => {
-        d3.select(`#circle-${d.id}`).style('stroke-opacity', 0.5)
-        const children = d.children || []
-        children.forEach(child => {
-          d3.select(`#link-${d.id}-${child.id}`).style('stroke-opacity', 0.5)
-          d3.select(`#circle-${child.id}`).style('stroke-opacity', 0.5)
+    const zoom = (d) => {
+      focus = d
+      if (!focus) return
+      const transition = svg.transition()
+        .duration(500)
+        .tween('zoom', d => {
+          const i = d3.interpolateZoom(view, [focus.x, focus.y, focus.r * 2])
+          return t => zoomTo(i(t))
         })
-      }
 
-      const handleMouseOverCrossLink = d => {
-        d3.select(`#crosslink-${d.id}`).style('stroke-opacity', 1.0)
-        d3.select(`.marker-${d.id}`).style('fill-opacity', 1.0)
-        d3.select(`#circle-${d.source.id}`).style('stroke', red[500]).style('stroke-opacity', 1.0)
-        d3.select(`#circle-${d.target.id}`).style('stroke', red[500]).style('stroke-opacity', 1.0)
-        setTooltipContent(d.label)
-      }
-      const handleMouseOutCrossLink = d => {
-        d3.select(`#crosslink-${d.id}`).style('stroke-opacity', 0.5)
-        d3.select(`.marker-${d.id}`).style('fill-opacity', 0.5)
-        d3.select(`#circle-${d.source.id}`).style('stroke', grey[500]).style('stroke-opacity', 0.5)
-        d3.select(`#circle-${d.target.id}`).style('stroke', grey[500]).style('stroke-opacity', 0.5)
-        setTooltipContent('')
-      }
+      node.selectAll('text')
+        .transition(transition)
+    }
 
-      const nodeEnter = node.enter().append('g')
-        .attr('id', d => `node-${d.id}`)
-        .on('click', d => {
-          if (d3.event.defaultPrevented) return
-
-          if (d.data.type.startsWith('task') && d.subGraph) {
-            const k = width / (circleRadius * 4)
-            const dNodes = d.subGraph.root.descendants()
-
-            gNode.selectAll('g').selectAll('.subgraph')
-              .attr('visibility', g => g.id === d.id ? 'visible' : 'hidden')
-
-            const node = d3.select(`#node-${d.id}`)
-              .raise()
-
-            node.select('.subgraph')
-              .attr('visibility', 'visible')
-
-            const dNode = node.select('.subgraph')
-              .raise()
-
-            const subNode = dNode.selectAll('circle')
-              .data(d.subGraph.root.children ? dNodes : [], d => d.id)
-
-            const subNodeEnter = subNode.enter()
-              .append('circle')
-              .attr('class', 'circle')
-              .attr('id', d => `circle-${d.id}`)
-              .attr('r', di => di.r * k)
-              .attr('fill', d => d.depth ? nomadPrimaryColor.light : nomadSecondaryColor.dark)
-              .attr('transform', di => `translate(${(di.x - dNodes[0].x) * k},${(di.y - dNodes[0].y) * k})`)
-              .on('mouseover', handleMouseOverCircle)
-              .on('mouseout', handleMouseOutCircle)
-
-            subNode.exit().remove()
-
-            subNodeEnter.merge(subNode)
-
-            const dCrossLinks = d.subGraph.root.children ? d.subGraph.crossLinks : []
-            const subCrossLink = dNode.select('.crosslink').raise().selectAll('path')
-              .data(dCrossLinks, d => d.id)
-
-            const subCrossLinkEnter = subCrossLink.enter()
-              .append('path')
-              .attr('class', 'crosslink')
-              .attr('id', d => `crosslink-${d.id}`)
-              .attr('marker-end', d => `url(#${d.id})`)
-              .attr('d', d => {
-                  const fcrossLink = (source, target) => {
-                    const path = d3.path()
-                    path.moveTo(source.x, source.y)
-                    path.lineTo(target.x, target.y)
-                    return path
-                  }
-                  const translate = (node) => {
-                    return {x: (node.x - dNodes[0].x) * k, y: (node.y - dNodes[0].y) * k}
-                  }
-                  return fcrossLink(translate(d.source), translate(d.target))
-              })
-              .on('mouseover', handleMouseOverCrossLink)
-              .on('mouseout', handleMouseOutCrossLink)
-
-            subCrossLink.exit().remove()
-
-            subCrossLinkEnter.merge(subCrossLink)
-
-            const subLabel = dNode.selectAll('text')
-              .data(d.subGraph.root.children ? dNodes : [], d => d.id)
-
-            const subLabelEnter = subLabel.enter()
-              .append('text')
-              .attr('class', 'text')
-              .attr('id', d => `text-${d.id}`)
-              .style('font-size', d => d.parent ? 12 : 18)
-              .attr('y', d => d.parent ? -10 : -d.r * k * 0.8)
-              .attr('text-anchor', 'middle')
-              .text(d => d.data.name)
-              .attr('transform', di => `translate(${(di.x - dNodes[0].x) * k},${(di.y - dNodes[0].y) * k})`)
-              .on('mouseover', handleMouseOverText)
-              .on('mouseout', handleMouseOutText)
-              .on('click', handleClickText)
-
-            subLabel.exit().remove()
-
-            subLabelEnter.merge(subLabel)
-
-            d.subGraph.root.children = d.subGraph.root.children ? null : d.subGraph.root._children
-          } else if (d._children) {
-            if (d.children) {
-              d.children = null
+    const update = (source) => {
+      if (!source) return
+
+      if (!source.nodes || !source.nodes.length) return
+
+      dragged = false
+
+      const setShowNodes = (inputValue) => {
+        const value = []
+        if (hasError) setCurrentNode(currentNode)
+        let sourceNode = currentNode
+        if (['inputs', 'outputs'].includes(currentNode.type)) {
+          sourceNode = currentNode.parent
+        }
+        if (!sourceNode) return
+        let total = 1
+        const nodes = sourceNode.nodes.filter(node => node.type === (currentNode.type || 'tasks'))
+        if (nodes.length) total = nodes[0].total
+        const toNumber = (value) => {
+          if (value.includes('%')) return Math.floor(parseFloat(value.replace('%')) * total / 100)
+          if (value.includes('.')) return Math.floor(parseFloat(value.replace('%')) * total)
+          value = parseInt(value)
+          if (value < 0) value = total + value
+          return value
+        }
+        for (const query of inputValue.split(',')) {
+          try {
+            const q = query.split(':')
+            if (q.length === 2) {
+              value.push(...range(...q.map((qi, n) => qi ? toNumber(qi) : n === 0 ? 0 : total)))
             } else {
-              const tasks = d._children.filter(child => {
-                return child.data.type && (child.data.type.startsWith('workflow') || child.data.type.startsWith('task'))
-              })
-              if (tasks.length > 0) nodes = nodes.filter(node => node.id !== d.id)
-              const keys = nodes.map(node => node.data.key)
-              d._children.forEach(child => {
-                if (!keys.includes(child.data.key)) nodes.push(child)
-              })
-              d.children = d._children
+              value.push(toNumber(q[0]))
             }
-            // save snapshots of nodes
-            pushNodes(nodes)
-            update(nodes)
+          } catch (error) {
+            console.error(error)
+            hasError = true
+            setCurrentNode(null)
+            break
           }
+        }
+        if (!value.length) return
+        dquery['resolveIndices'][currentNode.type || 'tasks'] = [...new Set(value)]
+        source.nodes = source.nodes.filter(node => node.url !== currentNode.url)
+        // set children and links to recalculate node positions and links
+        const d = source
+        d.children = undefined
+        d.links = undefined
+        d.parent = source.parent
+        resolveSection(d, query).then(d => {
+          if (!d) return
+          getNodes(d, dquery).then(nodes => {
+            d.nodes = nodes
+            getLinks(d, query).then(links => {
+              d.links = links
+              update(d)
+              zoom(d)
+            }).catch(asyncError)
+          }).catch(asyncError)
+          .catch(asyncError)
         })
-        .call(drag(simulation))
-
-      const subGraph = nodeEnter.append('g')
-        .attr('class', 'subgraph')
-
-      subGraph.append('g')
-        .attr('class', 'crosslink')
-
-      nodeEnter.append('circle')
-        .attr('class', 'circle')
-        .attr('id', d => `circle-${d.id}`)
-        .attr('r', circleRadius)
-        .attr('fill', d => {
-          if (d.data.type.startsWith('workflow')) return nomadPrimaryColor.dark
-          if (d.data.type.startsWith('task')) return nomadSecondaryColor.dark
-          return nomadPrimaryColor.light
-        })
-        .on('mouseover', handleMouseOverCircle)
-        .on('mouseout', handleMouseOutCircle)
-
-      nodeEnter.append('text')
-        .attr('class', 'text')
-        .attr('id', d => `text-${d.id}`)
-        .text(d => d.data.name)
-        .style('font-size', d => d.depth ? 12 : 18)
-        .attr('text-anchor', 'middle')
-        .attr('y', -circleRadius - 5)
-        .on('click', handleClickText)
-        .on('mouseover', handleMouseOverText)
-        .on('mouseout', handleMouseOutText)
-
-      // update the text
-      node.selectAll('text')
-        .text(d => d.data.name)
+      }
 
-      node = nodeEnter.merge(node)
+      d3.select('#nodes-filter-clear')
+        .on('click', () => {
+          const mid = (query.maxNodes || 6) / 2
+          setShowNodes(`:${mid},${-mid}:`)
+      })
 
-      link = gLink.selectAll('line')
-        .data(links, d => d.target.id)
+      d3.select('#nodes-filter-enter')
+        .on('keydown', () => {
+          const inputValue = d3.event.target.value
+          if (d3.event.key === 'Enter' && inputValue) setShowNodes(inputValue)
+        })
 
-      link.exit().remove()
+      d3.select('#backbutton')
+        .on('click', () => {
+          previousNode = source.parent
+          handleClickIcon(source)
+        })
 
-      const linkEnter = link.enter().append('line')
-        .attr('id', d => `link-${d.source.id}-${d.target.id}`)
+      d3.select('#resetbutton')
+        .on('click', () => {
+          previousNode = 'root'
+          root.nodes = null
+          root.links = null
+          root.children = null
+          dquery.resolveIndices = {}
+          resolveSection(root, query).then(d => {
+            if (!d) return
+            getNodes(d, query).then(nodes => {
+              d.nodes = nodes
+              getLinks(d, query).then(links => {
+                d.links = links
+                update(d)
+                zoom(d)
+              }).catch(asyncError)
+            }).catch(asyncError)
+            .catch(asyncError)
+          })
+          svg.call(zoomBehaviors.transform, d3.zoomIdentity)
+          if (enableForce) simulation.alphaTarget(0)
+        })
 
-      link = linkEnter.merge(link)
+      d3.select('#legendtogglebutton')
+        .on('click', () => {
+          const visibility = legend.attr('visibility') === 'visible' ? 'hidden' : 'visible'
+          legend.attr('visibility', visibility)
+          setShowLegend(visibility === 'visible')
+        })
 
-      crossLink = gCrossLink.selectAll('line')
-        .data(crossLinks, d => d.id)
+      d3.select('#forcetogglebutton')
+        .on('click', () => {
+          enableForce = !enableForce
+          setEnableForce(enableForce)
+          update(source)
+          zoom(source)
+          if (enableForce) {
+            simulation.alphaTarget(0.7).restart()
+          } else {
+            simulation.alphaTarget(0)
+          }
+        })
 
-      crossLink.exit().remove()
+      // set ids and size
+      const maxLength = Math.max(...source.nodes
+        .filter(node => node.type === 'tasks').map(node => node.nChildren || 0))
+      source.size = source.nChildren / maxLength
+      source.id = id
+      id = id + 1
+      source.nodes.forEach((node) => {
+        node.size = isWorkflow(node) ? node.nChildren / maxLength : 1
+        // put a lower limit on size so node will not get too small
+        node.size = Math.max(node.size, 0.1)
+        node.id = id
+        id = id + 1
+      })
 
-      const crossLinkEnter = crossLink.enter().append('line')
-        .attr('class', 'crosslink')
-        .style('stroke-dasharray', d => d.dash)
-        .attr('marker-end', d => d.marker ? `url(#${d.id})` : null)
-        .attr('id', d => `crosslink-${d.id}`)
-        .on('mouseover', handleMouseOverCrossLink)
-        .on('mouseout', handleMouseOutCrossLink)
+      setNodesPosition(source)
 
-      crossLink = crossLinkEnter.merge(crossLink)
+      nodes = [source, ...source.nodes]
+      const links = source.links
 
-      simulation
-        .nodes(nodes)
-        .on('tick', ticked)
+      links.forEach((link) => {
+        link.id = `${link.source.id}-${link.target.id}`
+      })
 
-      // disabled links links
-      // simulation
-      //   .force('link')
-      //   .links(links)
+      addLinkMarkers(links)
 
-      simulation
-        .force('crosslink')
-        .links(crossLinks)
-    }
+      if (enableForce) {
+        const k = zoomF(source.r * 2)
+        simulation
+          .nodes(source.nodes.filter(node => node.type === 'tasks'))
+          .on('tick', () => zoomTo(view))
 
-    update(rootNodes)
-  }, [data, svgRef, history, finalLayout, setTooltipContent])
-  return <svg className={classes.root} ref={svgRef}></svg>
-})
+        simulation
+          .force('charge')
+          .strength(-10 / k ** 2)
 
-ForceDirected.propTypes = {
-  data: PropTypes.object.isRequired,
-  layout: PropTypes.object,
-  setTooltipContent: PropTypes.any
-}
+        simulation
+          .force('link')
+          .strength(0.005 / k)
+          .distance(source.r)
+          .links(source.links.filter(link => link.source.type === 'tasks' && link.target.type === 'tasks'))
 
-const WorkflowCard = React.memo(({archive}) => {
-  const [data, setData] = useState()
-  const {api} = useApi()
-  const [tooltipContent, setTooltipContent] = useState('')
-  const [tooltipPosition, setTooltipPosition] = useState({x: undefined, y: undefined})
-
-  useEffect(() => {
-    const crossLinks = {}
+        simulation
+          .force('center', d3.forceCenter(source.x, source.y))
 
-    const addCrossLink = (key, link) => {
-      if (key in crossLinks) {
-        crossLinks[key].push(link)
-      } else {
-        crossLinks[key] = [link]
+        simulation
+          .force('collide', d3.forceCollide(source.r / 3).strength(2))
       }
-    }
 
-    const createHierarchy = async function(section, archive, name, type, index, reference) {
-      const baseUrl = createEntryUrl(apiBase, archive?.metadata?.upload_id, archive?.metadata?.entry_id)
-
-      if (typeof section === 'string') {
-        const match = section.match('.*/(\\w+)/(\\d+)$')
-        if (match) {
-          type = type || match[1]
-          index = match.length > 2 ? parseInt(match[2]) : 0
-        } else {
-          type = type || section.split('/').pop()
+      const handleMouseOverIcon = (d) => {
+        d3.select(`#icon-${d.id}`).style('stroke-opacity', 1)
+        if (d.url === source.url) {
+          if (!previousNode || previousNode === 'root') return
+          setTooltipContent(<p>Click to go back up</p>)
+          return
         }
-        reference = section
+        if (['inputs', 'outputs'].includes(d.type)) {
+          setTooltipContent(<p>Click to switch {d.type} filter</p>)
+        }
+        const sectionType = d.sectionType === 'tasks' ? 'task' : 'workflow'
+        if (d.type === 'tasks') setTooltipContent(<p>Click to expand {sectionType}</p>)
       }
 
-      // resolve section from reference path
-      const resolved = await (async (path) => {
-        if (!(typeof path === 'string')) {
-          return [section, archive, path, baseUrl]
-        }
-        if (path.startsWith('#')) path = path.slice(1)
-        if (!path.includes('#/')) {
-          try {
-            return [resolveInternalRef(path, archive), archive, path, baseUrl]
-          } catch (error) {
-            return [null, archive, path, baseUrl]
-          }
-        }
-        const url = resolveNomadUrl(path, baseUrl)
-        const query = {'workflow2': '*', 'metadata': '*'}
-        try {
-          const response = await api.post(`/entries/${url.entryId}/archive/query`, {required: query})
-          let sectionPath = path.split('#').pop()
-          if (!sectionPath.startsWith('/')) sectionPath = `/${sectionPath}`
-          const archive = response.data.archive
-          const section = type !== 'section' ? resolveInternalRef(sectionPath, archive) : sectionPath
-          const baseUrl = createEntryUrl(apiBase, archive?.metadata?.upload_id, archive?.metadata?.entry_id)
-          return [section, archive, sectionPath, baseUrl]
-        } catch (error) {
-          console.error(`Cannot resolve entry ${url.entryId}: ${error}`)
-          return [null, archive, path, baseUrl]
+      const handleMouseOutIcon = (d) => {
+        setTooltipContent('')
+        d3.select(`#icon-${d.id}`).style('stroke-opacity', 0.5)
+      }
+
+      const handleClickIcon = (d) => {
+        if (dragged) d.children = null
+        d3.event.stopPropagation()
+        setCurrentNode(d)
+        dquery['resolveIndices'][d.type || 'tasks'] = null
+        currentNode = d
+        if (!d.nodes || !d.nodes.length) {
+          return
         }
-      })(reference)
-
-      // get data for the current section
-      const sectionData = await (async (section, archive, path, baseUrl) => {
-        const sectionKey = [baseUrl, path].join('#')
-        const children = []
-        const maxNodes = 6
-        const start = Math.floor(maxNodes / 2)
-        if (typeof section === 'string' || !section) {
-          return {
-            name: name,
-            key: sectionKey,
-            type: type,
-            size: 20,
-            children: null,
-            path: path.split('#').pop(),
-            entryId: archive?.metadata?.entry_id,
-            index: index
-          }
+        if (d.url === source.url) {
+          if (!previousNode || previousNode === 'root') return
+          setCurrentNode(previousNode)
+          currentNode = previousNode
+          update(previousNode)
+          zoom(previousNode)
+          previousNode = previousNode.parent
+          return
         }
-        if (Array.isArray(section)) {
-          for (const [index, ref] of section.entries()) {
-            const child = await createHierarchy(ref, archive, ref.name, type, index, `${path}/${index}`)
-            children.push(child)
-          }
-        } else {
-          const taskInputs = []
-          const taskOutputs = []
-          if (section.section) {
-            const child = await createHierarchy(section.section, archive, section.name, 'section')
-            children.push(child)
-          }
-          if (section.inputs) {
-            let parent
-            const end = section.inputs.length - start
-            for (const [index, ref] of section.inputs.entries()) {
-              if (index < start || index >= end || start + 1 === end) {
-                parent = await createHierarchy(ref, archive, ref.name || 'input', 'inputs', index, `${path}/inputs/${index}`)
-                if (!parent) continue
-                const child = parent.children ? parent.children[0] : null
-                if (child) {
-                  addCrossLink(child.key, [sectionKey, ref.name || ''])
-                  child.intent = 'input'
-                  taskInputs.push(child)
-                }
-              }
-            }
-            if (section.inputs.length > maxNodes + 1) {
-              const otherInputs = {
-                name: `Inputs ${start + 1} - ${end} not shown`,
-                type: taskInputs[start - 1].type,
-                path: taskInputs[start - 1].path,
-                entryId: taskInputs[start - 1].entryId,
-                key: `${taskInputs[start - 1].key}.invisible.input`,
-                intent: 'input',
-                size: 20
-              }
-              taskInputs.splice(start, 0, otherInputs)
-              addCrossLink(taskInputs[start - 1].key, [otherInputs.key, '', null, false])
-              addCrossLink(taskInputs[start + 1].key, [otherInputs.key, '', null, false])
-            }
-            children.push(...taskInputs)
+        d.parent = source
+        previousNode = d.parent
+        resolveSection(d, query).then(d => {
+          if (!d) return
+          getNodes(d, query).then(nodes => {
+            d.nodes = nodes
+            getLinks(d, query).then(links => {
+              d.links = links
+              update(d)
+              zoom(d)
+            }).catch(asyncError)
+          }).catch(asyncError)
+          .catch(asyncError)
+        })
+      }
+
+      node = gNode.selectAll('g')
+        .attr('visibility', 'hidden')
+        .attr('pointer-events', 'none')
+        .exit().remove()
+        .data(nodes, d => d.id)
+        .attr('cursor', d => d.nodes ? null : 'pointer')
+        .join('g')
+        .call(dragBehaviors)
+
+      node
+        .filter(d => d.type === 'tasks' || d.url === nodes[0].url)
+        .append('rect')
+        .attr('class', 'icon')
+        .attr('id', d => `icon-${d.id}`)
+        .attr('stroke', color.outline)
+        .attr('fill', d => nodeColor(d))
+        .attr('fill-opacity', d => {
+          if (d.type === 'link') return 0
+          if (!d.nodes || !d.nodes.filter(node => node.type === 'tasks').length) return 1
+          if (d.url === source.url) return 0.2
+          return 1
+        })
+        .on('mouseover', handleMouseOverIcon)
+        .on('mouseout', handleMouseOutIcon)
+        .on('click', handleClickIcon)
+
+      node
+        .filter(d => d.type === 'inputs' || d.type === 'outputs' || d.type === 'inputs-outputs')
+        .append('circle')
+        .attr('class', 'icon')
+        .attr('id', d => `icon-${d.id}`)
+        .attr('stroke', color.outline)
+        .attr('fill', d => nodeColor(d))
+        .on('mouseover', handleMouseOverIcon)
+        .on('mouseout', handleMouseOutIcon)
+        .on('click', handleClickIcon)
+
+      node.append('text')
+        .attr('class', 'text')
+        .attr('fill', color.text)
+        .attr('font-weight', d => d.url === source.url ? 'bold' : 'none')
+        .attr('id', d => `text-${d.id}`)
+        .text(d => trimName(d.name))
+        .style('font-size', d => d.url === nodes[0].url ? 18 : 14)
+        .on('click', d => {
+          d3.event.stopPropagation()
+          if (!d.entryId || !d.parent) return
+          let path = `entry/id/${d.entryId}`
+          const sectionPath = d.path ? d.path.replace(/\/(?=\d)/g, ':') : null
+          path = d.sectionType.startsWith('workflow') ? path : sectionPath ? `${path}/data${sectionPath}` : path
+          const url = getUrl(path)
+          history.push(url)
+        })
+        .on('mouseover', d => {
+          if (!d.type || !d.parent) return
+          if (!d.sectionType) return
+          d3.select(`#text-${d.id}`).style('font-weight', 'bold')
+            .text(d.name)
+          const text = d.sectionType.includes('workflow') ? 'overview page' : 'archive section'
+          if (d.entryId) {
+            setTooltipContent(<p>Click to go to {text} for entry<br/>{d.entryId}</p>)
           }
-          if (section.outputs) {
-            const end = section.outputs.length - start
-            for (const [index, ref] of section.outputs.entries()) {
-              if (index < start || index >= end || start + 1 === end) {
-                const parent = await createHierarchy(ref, archive, ref.name || 'output', 'outputs', index, `${path}/outputs/${index}`)
-                if (!parent) continue
-                const child = parent.children ? parent.children[0] : null
-                if (child) {
-                  addCrossLink(sectionKey, [child.key, ref.name || ''])
-                  child.intent = 'output'
-                  taskOutputs.push(child)
-                }
-              }
+        })
+        .on('mouseout', d => {
+          setTooltipContent('')
+          d3.select(`#text-${d.id}`).style('font-weight', null)
+            .text(d => trimName(d.name))
+        })
+
+      const link = gLink.selectAll('path')
+        .attr('visibility', 'hidden')
+        .attr('pointer-events', 'none')
+        .exit().remove()
+        .data(links)
+
+      line = link.enter()
+        .append('path')
+        .attr('pointer-events', 'all')
+        .attr('id', d => `link-${d.id}`)
+        .attr('marker-end', d => `url(#${d.id})`)
+        .attr('visibility', 'visible')
+        .on('click', d => {
+          d3.event.stopPropagation()
+          const parent = d.source.parent || d.target.parent
+          if (!parent) return
+          // use the parent node to containt source and target nodes
+          const linkNode = parent
+          const sourceNode = d.source
+          const targetNode = d.target
+          if (sourceNode.type !== 'tasks' || targetNode.type !== 'tasks') return
+
+          const store = (node, recursive) => {
+            if (typeof node !== 'object') return
+            node._name = node.name
+            node._type = node.type
+            node.children = null
+            if (node.links) {
+              node._links = [...node.links]
+              node.links = null
             }
-            if (section.outputs.length > maxNodes + 1) {
-              const otherOutputs = {
-                name: `Outputs ${start + 1} - ${end} not shown`,
-                type: taskOutputs[start - 1].type,
-                path: taskOutputs[start - 1].path,
-                entryId: taskOutputs[start - 1].entryId,
-                key: `${taskOutputs[start - 1].key}.invisible.output`,
-                intent: 'output',
-                size: 20
+            if (node.nodes) {
+              node._nodes = [...node.nodes]
+              if (recursive) {
+                node.nodes.forEach(n => {
+                  store(n)
+                })
               }
-              taskOutputs.splice(start, 0, otherOutputs)
-              addCrossLink(taskOutputs[start - 1].key, [otherOutputs.key, '', null, false])
-              addCrossLink(taskOutputs[start + 1].key, [otherOutputs.key, '', null, false])
             }
-            children.push(...taskOutputs)
           }
-          if (section.task) {
-            const task = await createHierarchy(section.task, archive)
-            task.name = section.name || task.name
-            task.children = task.children || []
-            const taskKeys = task.children.map(child => child.key)
-            taskInputs.forEach(child => {
-              if (!taskKeys.includes(child.key)) task.children.push(child)
-            })
-            taskOutputs.forEach(child => {
-              if (!taskKeys.includes(child.key)) task.children.push(child)
-            })
-            children.push(task)
-            return task
+
+          const restore = (node) => {
+            if (typeof node !== 'object' || !node._nodes) return
+            node.nodes = node._nodes
+            node.links = node._links
+            node.children = null
+            node.name = node._name || node.name
+            node.type = node._type || node.type
           }
-          if (section.tasks) {
-            type = 'workflow'
-            // resolve only several first and last tasks
-            const end = section.tasks.length - start
-            const sectionChildren = []
-            for (const [index, ref] of section.tasks.entries()) {
-              if (index < start || index >= end || start + 1 === end) {
-                const task = await createHierarchy(ref, archive, ref.name || 'task', 'tasks', index, `${path}/tasks/${index}`)
-                if (task) sectionChildren.push(task)
-              }
+
+          // store the original nodes info
+          store(linkNode)
+          store(sourceNode)
+          store(targetNode)
+
+          // include into sourceNode the tagetNode inputs
+          const inputs = [...targetNode.nodes.filter(node => node.type === 'inputs')]
+          let url = {}
+          sourceNode.nodes.forEach(node => {
+            url[node.url] = node.name
+          })
+          inputs.forEach(input => {
+            if (Object.keys(url).includes(input.url)) {
+              sourceNode.nodes.push(input)
+              input._name = input.name
+              const sourceName = url[input.url]
+              if (sourceName !== input.name) input.name = `${input.name}/${url[input.url]}`
+              input._type = input.type
+              input.type = 'inputs-outputs'
             }
-            if (section.tasks.length > maxNodes + 1) {
-              const otherTasks = {
-                name: `Tasks ${start + 1} - ${end} not shown`,
-                type: sectionChildren[start - 1].type,
-                path: sectionChildren[start - 1].path,
-                entryId: sectionChildren[start - 1].entryId,
-                key: `${sectionChildren[start - 1].key}.invisible.task`,
-                size: 20
+          })
+
+          linkNode.type = 'link'
+          // parent nodes should contain the source, target
+          linkNode.nodes = [sourceNode, targetNode]
+          // and the targetNode inputs
+          linkNode.nodes.push(...inputs)
+
+          // and the sourceNode outputs
+          url = inputs.map(node => node.url)
+          const outputs = sourceNode.nodes.filter(node => node.type === 'outputs' && !url.includes(node.url))
+          linkNode.nodes.push(...outputs)
+
+          // and the tagetNode outputs
+          linkNode.nodes.push(...targetNode.nodes.filter(node => node.type === 'outputs'))
+
+          // generate links for this configuration
+          getLinks(linkNode, query).then(links => {
+            for (const [index, link] of links.entries()) {
+              if (link.target.url === sourceNode.url) {
+                // flip source and target since node is output of sourceNode
+                links[index] = {source: link.target, target: link.source, id: link.id, label: 'Output'}
+              } else if (link.target.url === linkNode.url || link.source.url === linkNode.url) {
+                // remove link to and from rootNode
+                links[index] = null
+              } else if (link.source.url === sourceNode.url && link.target.url === targetNode.url) {
+                // remove link from sourceNode to targetNode, unnecessary
+                links[index] = null
               }
-              sectionChildren.splice(start, 0, otherTasks)
-              addCrossLink(sectionChildren[start - 1].key, [otherTasks.key, '', null, false])
-              addCrossLink(sectionChildren[start + 1].key, [otherTasks.key, '', null, false])
             }
-            children.push(...sectionChildren)
-          }
-        }
+            linkNode.links = links.filter(link => link)
+            linkNode.name = null
+            // linkNode.type = 'link'
+            update(linkNode)
+            zoom(linkNode)
+            previousNode = linkNode
+            // reset original nodes info
+            inputs.forEach(input => {
+              input.type = 'inputs'
+              input.name = input._name || input.name
+              input.children = null
+            })
+            linkNode.type = linkNode._type
+            restore(linkNode)
+            restore(sourceNode)
+            restore(targetNode)
+            setNodesPosition(linkNode)
+        })
+        })
+        .on('mouseover', d => {
+          d3.select(`#link-${d.id}`).style('stroke-opacity', 1.0)
+          svg.select(`.marker-${d.id}`).attr('fill-opacity', 1.0)
+          d3.select(`#icon-${d.source.id}`).style('stroke', color.link).style('stroke-opacity', 1.0)
+          d3.select(`#icon-${d.target.id}`).style('stroke', color.link).style('stroke-opacity', 1.0)
+          setTooltipContent(<p>{d.label}</p>)
+        })
+        .on('mouseout', d => {
+          d3.select(`#link-${d.id}`).style('stroke-opacity', 0.5)
+          svg.select(`.marker-${d.id}`).attr('fill-opacity', 0.5)
+          d3.select(`#icon-${d.source.id}`).style('stroke', color.outline).style('stroke-opacity', 0.5)
+          d3.select(`#icon-${d.target.id}`).style('stroke', color.outline).style('stroke-opacity', 0.5)
+          setTooltipContent('')
+        })
+    }
 
-        return {
-          name: section.name || name || type,
-          key: sectionKey,
-          type: type,
-          size: 20,
-          children: children.length === 0 ? null : children,
-          path: path,
-          entryId: archive?.metadata?.entry_id,
-          index: index
-        }
-      })(resolved[0], resolved[1], resolved[2], resolved[3])
+    resolveSection(source, query).then(source => {
+      if (!source) return
+      getNodes(source, query).then(nodes => {
+        source.nodes = nodes
+        source.x = 0
+        source.y = 0
+        source.r = width / 2
+        getLinks(source, query).then(links => {
+          source.links = links
+          focus = source
+          root = source
+          setCurrentNode(source)
+          currentNode = source
+          update(source)
+          zoomTo([source.x, source.y, source.r * 2])
+        }).catch(asyncError)
+      }).catch(asyncError)
+      .catch(asyncError)
+    })
+  }, [
+    history,
+    setTooltipContent,
+    setCurrentNode,
+    setShowLegend,
+    setEnableForce,
+    query,
+    source,
+    svgRef,
+    finalLayout,
+    asyncError
+  ])
 
-      return sectionData
-    }
+  return <svg className={classes.root} ref={svgRef}></svg>
+})
 
-    const workflow = archive?.workflow2
-    if (workflow) {
-      createHierarchy('/workflow2', archive, 'Workflow').then(result => {
-        // save the cross links to each section
-        function getLinks(section) {
-          section.crossLinks = crossLinks[section.key]
-          if (section.children) {
-            section.children.forEach(child => {
-            getLinks(child)
-            })
-          }
-        }
-        getLinks(result)
-        setData(result)
-      })
-    }
-  }, [archive, api])
+Graph.propTypes = {
+  source: PropTypes.object.isRequired,
+  query: PropTypes.object.isRequired,
+  layout: PropTypes.object,
+  setTooltipContent: PropTypes.any,
+  setCurrentNode: PropTypes.any,
+  setShowLegend: PropTypes.any,
+  setEnableForce: PropTypes.any
+}
+
+const WorkflowCard = React.memo(({archive}) => {
+  const {api} = useApi()
+  const [tooltipContent, setTooltipContent] = useState('')
+  const [tooltipPosition, setTooltipPosition] = useState({x: undefined, y: undefined})
+  const [showLegend, setShowLegend] = useState(true)
+  const [enableForce, setEnableForce] = useState(false)
+  const [currentNode, setCurrentNode] = useState({type: 'tasks'})
+  const [inputValue, setInputValue] = useState('')
+  const query = useMemo(() => ({
+    api: api,
+    required: { 'workflow2': '*', 'metadata': '*' },
+    sectionKeys: ['inputs', 'tasks', 'outputs'],
+    maxNodes: 6
+  }), [api])
 
   const graph = useMemo(() => {
-    if (!data) {
-      return ''
+    if (!archive || !archive.workflow2) return ''
+
+    const source = {
+      path: '/workflow2',
+      archive: archive
     }
 
-    return <ForceDirected data={data} setTooltipContent={setTooltipContent}></ForceDirected>
-  }, [data])
+    return <Graph
+      source={source}
+      query={query}
+      setTooltipContent={setTooltipContent}
+      setCurrentNode={setCurrentNode}
+      setShowLegend={setShowLegend}
+      setEnableForce={setEnableForce}
+    ></Graph>
+  }, [archive, query])
+
+  let nodeType = 'tasks'
+  let nodesCount = 0
+  if (currentNode) {
+    nodeType = currentNode.type || 'tasks'
+    let sourceNode = currentNode
+    if (['inputs', 'outputs'].includes(nodeType)) sourceNode = currentNode.parent || currentNode
+    const nodes = sourceNode.nodes ? sourceNode.nodes.filter(node => node.type === nodeType) : []
+    if (nodes.length) nodesCount = nodes[0].total
+  }
+
+  let label = `No ${nodeType.slice(0, -1)} to show`
+  if (nodesCount) label = `Filter ${nodeType} (N=${nodesCount})`
 
   const actions = <div>
+    <FormControl margin='none'>
+      <TextField
+        error={!currentNode}
+        size='medium'
+        id='nodes-filter-enter'
+        label={currentNode ? label : 'Invalid input'}
+        disabled={!nodesCount || !currentNode}
+        placeholder={'Enter range: 5, 4:-1, :50%'}
+        variant='outlined'
+        value={inputValue}
+        onChange={(event) => setInputValue(event.target.value)}
+        InputProps={{
+          endAdornment: (
+            <IconButton id='nodes-filter-clear' onClick={() => setInputValue('')} size='medium'>
+              <Clear />
+            </IconButton>
+          )
+        }}
+      ></TextField>
+    </FormControl>
+    <IconButton id='forcetogglebutton'>
+      <Tooltip title={`${enableForce ? 'Disable' : 'Enable'} force simulation`}>
+        {enableForce ? <StopSharp /> : <PlayArrowSharp />}
+      </Tooltip>
+    </IconButton>
+    <IconButton id='legendtogglebutton'>
+      <Tooltip title={showLegend ? 'Hide legend' : 'Show legend'}>
+        {showLegend ? <LabelOff /> : <Label />}
+      </Tooltip>
+    </IconButton>
     <IconButton id='backbutton'>
       <Tooltip title="Back">
         <Undo />
       </Tooltip>
     </IconButton>
-    <IconButton id='forwardbutton'>
-      <Tooltip title="Forward">
-        <Redo />
-      </Tooltip>
-    </IconButton>
     <IconButton id='resetbutton'>
       <Tooltip title="Reset">
         <Replay />
@@ -972,9 +1241,6 @@ const WorkflowCard = React.memo(({archive}) => {
     <PropertyGrid>
       <Tooltip title={tooltipContent}
         onMouseMove={event => setTooltipPosition({x: event.pageX, y: event.pageY})}
-        enterNextDelay={700}
-        leaveDelay={700}
-        arrow
         PopperProps={
           {anchorEl: {
             clientHeight: 0,
@@ -990,10 +1256,10 @@ const WorkflowCard = React.memo(({archive}) => {
           }}
         }
         >
-          <div>
-            {graph}
-          </div>
-        </Tooltip>
+        <div>
+          {graph}
+        </div>
+      </Tooltip>
     </PropertyGrid>
   </PropertyCard>
 })