Assets/vega.force.directed.layout.json

{
  "data": [
    {
      "name": "link-data-raw",
      "values": []
    },
    {"name": "link-data", "source": "link-data-raw"},
    {
      "name": "node-data",
      "values": [],
      "transform": [
        {
          "type": "force",
          "iterations": 300,
          "restart": {"signal": "restart"},
          "signal": "force",
          "forces": [
            {"force": "center", "x": {"signal": "cx"}, "y": {"signal": "cy"}},
            {
              "force": "collide",
              "radius": {"signal": "2 * nodeRadius"},
              "iterations": 1,
              "strength": 0.7
            },
            {"force": "nbody", "strength": {"signal": "nodeCharge"}},
            {
              "force": "link",
              "links": "link-data-raw",
              "distance": {"signal": "linkDistance"},
              "id": "index"
            }
          ]
        },
        {
          "type": "formula",
          "as": "fx",
          "expr": "fix[0]!=null && node==datum.index ?invert('xscale',fix[0]):null"
        },
        {
          "type": "formula",
          "as": "fy",
          "expr": "fix[1]!=null && node==datum.index ?invert('yscale',fix[1]):null"
        }
      ]
    },
    {
      "name": "adj-nodes",
      "transform": [
        {
          "type": "filter",
          "expr": "datum.src === hoverIndex || datum.tgt === hoverIndex"
        }
      ],
      "source": "link-data"
    },
    {
      "name": "adjacentIndices",
      "transform": [
        {
          "type": "formula",
          "as": "adj",
          "expr": "datum.src === hoverIndex ? datum.tgt : datum.src"
        },
        {"fields": ["adj"], "type": "project", "as": ["adj"]}
      ],
      "source": "adj-nodes"
    }
  ],
  "marks": [
    {
      "name": "links",
      "from": {"data": "link-data"},
      "type": "path",
      "encode": {
        "update": {
          "stroke": {"value": "#ccc"},
          "strokeWidth": {
            "signal": "datum.src === hoverIndex || datum.tgt === hoverIndex ? 2 : 0.5"
          }
        }
      },
      "interactive": false,
      "transform": [
        {
          "type": "linkpath",
          "require": {"signal": "force"},
          "shape": "line",
          "sourceX": {"expr": "scale('xscale', datum.datum.source.x)"},
          "sourceY": {"expr": "scale('yscale', datum.datum.source.y)"},
          "targetX": {"expr": "scale('xscale', datum.datum.target.x)"},
          "targetY": {"expr": "scale('yscale', datum.datum.target.y)"}
        }
      ]
    },
    {
      "from": {"data": "node-data"},
      "type": "symbol",
      "encode": {
        "enter": {
          "fill": {"scale": "color", "field": "group"},
          "stroke": {"value": "white"}
        },
        "update": {
          "cursor": {"value": "pointer"},
          "fill": {
            "signal": "hoverIndex === datum.index || indata('adjacentIndices', 'adj', datum.index) ? 'red' : scale('color', datum.group)"
          },
          "size": {
            "signal": "(hoverIndex === datum.index || indata('adjacentIndices', 'adj', datum.index)) ? 2.5 * nodeRadius * nodeRadius : 2 * nodeRadius * nodeRadius"
          },
          "zindex": {
            "signal": "(hoverIndex === datum.index || indata('adjacentIndices', 'adj', datum.index)) ? 10 : 1"
          },
          "x": {
            "signal": "fix[0]!=null && node===datum.index ?fix[0]:scale('xscale', datum.x)"
          },
          "y": {
            "signal": "fix[1]!=null && node===datum.index ?fix[1]:scale('yscale', datum.y)"
          },
          "tooltip": {"signal": "{ name: datum.name }"}
        }
      },
      "name": "nodes",
      "zindex": 1
    }
  ],
  "scales": [
    {
      "domain": {"data": "node-data", "field": "group"},
      "name": "color",
      "type": "ordinal",
      "range": {"scheme": "category10"}
    },
    {
      "name": "xscale",
      "zero": false,
      "domain": {"signal": "xdom"},
      "range": {"signal": "xrange"}
    },
    {
      "name": "yscale",
      "zero": false,
      "domain": {"signal": "ydom"},
      "range": {"signal": "yrange"}
    }
  ],
  "signals": [
    {"name": "xrange", "update": "[0, width]"},
    {"name": "yrange", "update": "[height, 0]"},
    {"name": "xext", "update": "[0, width]"},
    {"name": "yext", "update": "[height, 0]"},
    {
      "name": "down",
      "value": null,
      "on": [
        {"events": "mouseup,touchend", "update": "null"},
        {"events": "mousedown, touchstart", "update": "xy()"},
        {"events": "symbol:mousedown, symbol:touchstart", "update": "null"}
      ]
    },
    {
      "name": "xcur",
      "value": null,
      "on": [{"events": "mousedown, touchstart, touchend", "update": "xdom"}]
    },
    {
      "name": "ycur",
      "value": null,
      "on": [{"events": "mousedown, touchstart, touchend", "update": "ydom"}]
    },
    {
      "name": "delta",
      "value": [0, 0],
      "on": [
        {
          "events": [
            {
              "source": "window",
              "type": "mousemove",
              "consume": true,
              "between": [
                {"type": "mousedown"},
                {"source": "window", "type": "mouseup"}
              ]
            },
            {
              "type": "touchmove",
              "consume": true,
              "filter": "event.touches.length === 1"
            }
          ],
          "update": "down ? [down[0]-x(), y()-down[1]] : [0,0]"
        }
      ]
    },
    {
      "name": "anchor",
      "value": [0, 0],
      "on": [
        {
          "events": "wheel",
          "update": "[invert('xscale', x()), invert('yscale', y())]"
        },
        {
          "events": {
            "type": "touchstart",
            "filter": "event.touches.length===2"
          },
          "update": "[(xdom[0] + xdom[1]) / 2, (ydom[0] + ydom[1]) / 2]"
        }
      ]
    },
    {
      "name": "zoom",
      "value": 1,
      "on": [
        {
          "events": "wheel!",
          "force": true,
          "update": "pow(1.001, event.deltaY * pow(16, event.deltaMode))"
        },
        {
          "events": {"signal": "dist2"},
          "force": true,
          "update": "dist1 / dist2"
        },
        {"events": [{"source": "view", "type": "dblclick"}], "update": "1"}
      ]
    },
    {
      "name": "dist1",
      "value": 0,
      "on": [
        {
          "events": {
            "type": "touchstart",
            "filter": "event.touches.length===2"
          },
          "update": "pinchDistance(event)"
        },
        {"events": {"signal": "dist2"}, "update": "dist2"}
      ]
    },
    {
      "name": "dist2",
      "value": 0,
      "on": [
        {
          "events": {
            "type": "touchmove",
            "consume": true,
            "filter": "event.touches.length===2"
          },
          "update": "pinchDistance(event)"
        }
      ]
    },
    {
      "name": "xdom",
      "update": "xext",
      "on": [
        {
          "events": {"signal": "delta"},
          "update": "[xcur[0] + span(xcur) * delta[0] / width, xcur[1] + span(xcur) * delta[0] / width]"
        },
        {
          "events": {"signal": "zoom"},
          "update": "[anchor[0] + (xdom[0] - anchor[0]) * zoom, anchor[0] + (xdom[1] - anchor[0]) * zoom]"
        },
        {"events": [{"source": "view", "type": "dblclick"}], "update": "xrange"}
      ]
    },
    {
      "name": "ydom",
      "update": "yext",
      "on": [
        {
          "events": {"signal": "delta"},
          "update": "[ycur[0] + span(ycur) * delta[1] / height, ycur[1] + span(ycur) * delta[1] / height]"
        },
        {
          "events": {"signal": "zoom"},
          "update": "[anchor[1] + (ydom[0] - anchor[1]) * zoom, anchor[1] + (ydom[1] - anchor[1]) * zoom]"
        },
        {"events": [{"source": "view", "type": "dblclick"}], "update": "yrange"}
      ]
    },
    {"name": "size", "update": "clamp(20 / span(xdom), 1, 1000)"},
    {
      "name": "cx",
      "update": "width / 2",
      "on": [
        {
          "events": "[symbol:mousedown, window:mouseup] > window:mousemove",
          "update": " cx==width/2?cx+0.001:width/2"
        }
      ]
    },
    {"name": "cy", "update": "height / 2"},
    {"name": "w", "value": 1200},
    {"name": "h", "value": 800},
    {
      "name": "hoverIndex",
      "on": [
        {"events": "symbol:mouseover", "update": "datum.index"},
        {"events": "symbol:mouseout", "update": "-1"}
      ],
      "value": -1
    },
    {
      "name": "nodeRadius",
      "bind": {"input": "range", "max": 50, "min": 1, "step": 1},
      "value": 5
    },
    {
      "name": "nodeCharge",
      "bind": {"input": "range", "max": 10, "min": -100, "step": 1},
      "value": -30
    },
    {
      "name": "linkDistance",
      "bind": {"input": "range", "max": 200, "min": 5, "step": 1},
      "value": 30
    },
    {"name": "static", "value": false},
    {
      "description": "State variable for active node fix status.",
      "name": "fix",
      "value": false,
      "on": [
        {
          "events": "symbol:mouseout[!event.buttons], window:mouseup",
          "update": "false"
        },
        {"events": "symbol:mouseover", "update": "fix || true", "force": true},
        {
          "events": "[symbol:mousedown, window:mouseup] > window:mousemove!",
          "update": "xy()",
          "force": true
        }
      ]
    },
    {
      "description": "Graph node most recently interacted with.",
      "name": "node",
      "value": null,
      "on": [
        {
          "events": "symbol:mouseover",
          "update": "fix === true ? datum.index : node"
        }
      ]
    },
    {
      "description": "Flag to restart Force simulation upon data changes.",
      "name": "restart",
      "value": false,
      "on": [{"events": {"signal": "fix"}, "update": "fix && fix.length"}]
    }
  ],
  "$schema": "https://vega.github.io/schema/vega/v6.json",
  "autosize": {"type": "none"},
  "description": "A node-link diagram with force-directed layout",
  "height": {"signal": "h"},
  "width": {"signal": "w"}
}