ch/directed-graph/app.js
// update graph (called when needed)
update graph (called when needed)
update graph ( called when needed )
function restart() {
  path = path.data(links);
  path.classed('selected', (d) => d === selectedLink)
    .style('marker-start', (d) => d.left ? 'url(#start-arrow)' : '')
    .style('marker-end', (d) => d.right ? 'url(#end-arrow)' : '');
  path.exit().remove();
  path = path.enter().append('svg:path')
    .attr('class', 'link')
    .classed('selected', (d) => d === selectedLink)
    .style('marker-start', (d) => d.left ? 'url(#start-arrow)' : '')
    .style('marker-end', (d) => d.right ? 'url(#end-arrow)' : '')
    .on('mousedown', (d) => {
      if (d3.event.ctrlKey) return;
      mousedownLink = d;
      selectedLink = (mousedownLink === selectedLink) ? null : mousedownLink;
      selectedNode = null;
      restart();
    })
    .merge(path);
  circle = circle.data(nodes, (d) => d.id);
  circle.selectAll('circle')
    .style('fill', (d) => (d === selectedNode) ? d3.rgb(colors(d.id)).brighter().toString() : colors(d.id))
    .classed('reflexive', (d) => d.reflexive);
  circle.exit().remove();
  function removeEdge(d, i){
    links.splice(links.indexOf(d), 1);
    d3.event.preventDefault();
    restart();
    showGraphLatex();
  }
  const g = circle.enter().append('svg:g');
  g.append('svg:circle')
    .attr('class', 'node')
    .attr('r', 12)
    .style('fill', (d) => (d === selectedNode) ? d3.rgb(colors(d.id)).brighter().toString() : colors(d.id))
    .style('stroke', (d) => d3.rgb(colors(d.id)).darker().toString())
    .classed('reflexive', (d) => d.reflexive)
    .on('mouseover', function (d) {
      if (!mousedownNode || d === mousedownNode) return;
      d3.select(this).attr('transform', 'scale(1.1)');
    })
    .on('mouseout', function (d) {
      if (!mousedownNode || d === mousedownNode) return;
      d3.select(this).attr('transform', '');
    })
    .on('mousedown', (d) => {
      if (d3.event.ctrlKey) return;
      mousedownNode = d;
      selectedNode = (mousedownNode === selectedNode) ? null : mousedownNode;
      selectedLink = null;
      dragLine
        .style('marker-end', 'url(#end-arrow)')
        .classed('hidden', false)
        .attr('d', `M${mousedownNode.x},${mousedownNode.y}L${mousedownNode.x},${mousedownNode.y}`);
      restart();
    })
    .on('mouseup', function (d) {
      if (!mousedownNode) return;
      dragLine
        .classed('hidden', true)
        .style('marker-end', '');
      mouseupNode = d;
      if (mouseupNode === mousedownNode) {
        resetMouseVars();
        return;
      }
      d3.select(this).attr('transform', '');
      const isRight = mousedownNode.id < mouseupNode.id;
      const source = isRight ? mousedownNode : mouseupNode;
      const target = isRight ? mouseupNode : mousedownNode;
      const link = links.filter((l) => l.source === source && l.target === target)[0];
      if (link) {
        link[isRight ? 'right' : 'left'] = true;
      } else {
        links.push({ source, target, left: !isRight, right: isRight });
      }
      selectedLink = link;
      selectedNode = null;
      restart();
      showGraphLatex();
    })
    .on('contextmenu', removeNode);
  g.append('svg:text')
    .attr('x', 0)
    .attr('y', 4)
    .attr('class', 'id')
    .text((d) => d.id);
  circle = g.merge(circle);
  force
    .nodes(nodes)
    .force('link').links(links);
  force.alphaTarget(0.3).restart();
}
function restart ( ) { path = path . data ( links ) ; path . classed ( 'selected' , ( d ) => d === selectedLink ) . style ( 'marker-start' , ( d ) => d . left ? 'url(#start-arrow)' : '' ) . style ( 'marker-end' , ( d ) => d . right ? 'url(#end-arrow)' : '' ) ; path . exit ( ) . remove ( ) ; path = path . enter ( ) . append ( 'svg:path' ) . attr ( 'class' , 'link' ) . classed ( 'selected' , ( d ) => d === selectedLink ) . style ( 'marker-start' , ( d ) => d . left ? 'url(#start-arrow)' : '' ) . style ( 'marker-end' , ( d ) => d . right ? 'url(#end-arrow)' : '' ) . on ( 'mousedown' , ( d ) => { if ( d3 . event . ctrlKey ) return ; mousedownLink = d ; selectedLink = ( mousedownLink === selectedLink ) ? null : mousedownLink ; selectedNode = null ; restart ( ) ; } ) . merge ( path ) ; circle = circle . data ( nodes , ( d ) => d . id ) ; circle . selectAll ( 'circle' ) . style ( 'fill' , ( d ) => ( d === selectedNode ) ? d3 . rgb ( colors ( d . id ) ) . brighter ( ) . toString ( ) : colors ( d . id ) ) . classed ( 'reflexive' , ( d ) => d . reflexive ) ; circle . exit ( ) . remove ( ) ; function removeEdge ( d , i ) { links . splice ( links . indexOf ( d ) , 1 ) ; d3 . event . preventDefault ( ) ; restart ( ) ; showGraphLatex ( ) ; } const g = circle . enter ( ) . append ( 'svg:g' ) ; g . append ( 'svg:circle' ) . attr ( 'class' , 'node' ) . attr ( 'r' , 12 ) . style ( 'fill' , ( d ) => ( d === selectedNode ) ? d3 . rgb ( colors ( d . id ) ) . brighter ( ) . toString ( ) : colors ( d . id ) ) . style ( 'stroke' , ( d ) => d3 . rgb ( colors ( d . id ) ) . darker ( ) . toString ( ) ) . classed ( 'reflexive' , ( d ) => d . reflexive ) . on ( 'mouseover' , function ( d ) { if ( ! mousedownNode || d === mousedownNode ) return ; d3 . select ( this ) . attr ( 'transform' , 'scale(1.1)' ) ; } ) . on ( 'mouseout' , function ( d ) { if ( ! mousedownNode || d === mousedownNode ) return ; d3 . select ( this ) . attr ( 'transform' , '' ) ; } ) . on ( 'mousedown' , ( d ) => { if ( d3 . event . ctrlKey ) return ; mousedownNode = d ; selectedNode = ( mousedownNode === selectedNode ) ? null : mousedownNode ; selectedLink = null ; dragLine . style ( 'marker-end' , 'url(#end-arrow)' ) . classed ( 'hidden' , false ) . attr ( 'd' , ` ${ mousedownNode . x } ${ mousedownNode . y } ${ mousedownNode . x } ${ mousedownNode . y } ` ) ; restart ( ) ; } ) . on ( 'mouseup' , function ( d ) { if ( ! mousedownNode ) return ; dragLine . classed ( 'hidden' , true ) . style ( 'marker-end' , '' ) ; mouseupNode = d ; if ( mouseupNode === mousedownNode ) { resetMouseVars ( ) ; return ; } d3 . select ( this ) . attr ( 'transform' , '' ) ; const isRight = mousedownNode . id < mouseupNode . id ; const source = isRight ? mousedownNode : mouseupNode ; const target = isRight ? mouseupNode : mousedownNode ; const link = links . filter ( ( l ) => l . source === source && l . target === target ) [ 0 ] ; if ( link ) { link [ isRight ? 'right' : 'left' ] = true ; } else { links . push ( { source , target , left : ! isRight , right : isRight } ) ; } selectedLink = link ; selectedNode = null ; restart ( ) ; showGraphLatex ( ) ; } ) . on ( 'contextmenu' , removeNode ) ; g . append ( 'svg:text' ) . attr ( 'x' , 0 ) . attr ( 'y' , 4 ) . attr ( 'class' , 'id' ) . text ( ( d ) => d . id ) ; circle = g . merge ( circle ) ; force . nodes ( nodes ) . force ( 'link' ) . links ( links ) ; force . alphaTarget ( 0.3 ) . restart ( ) ; }
update graph (called when needed)
update graph ( called when needed )
// path (link) group // update existing links // remove old links // add new links // select link // circle (node) group // NB: the function arg is crucial here! nodes are known by id, not by index! // update existing nodes (reflexive & selected visual states) // remove old nodes // add new nodes // enlarge target node // unenlarge target node // select node // reposition drag line // needed by FF // check for drag-to-self // unenlarge target node // add link to graph (update if exists) // NB: links are strictly source < target; arrows separately specified by booleans // select new link // show node IDs // set the graph in motion