block by steveharoz 28a33114d81a3fac9c6e497811661768

Vega-force testing ground

Full Screen

A Vega port of the force testing ground I originally made in D3.

Features I can’t figure out how to port to Vega:

index.html

<!DOCTYPE html>
<html>
<head>
    <script src="https://vega.github.io/vega/assets/promise.min.js"></script>
    <script src="https://vega.github.io/vega/vega.js"></script>
</head>
<body>
<style>
/* HTML styles */
html{ width: 100%; }
body{ 
  width: 100%; 
  margin: 0; padding: 0; 
  display: flex; 
  font-family: sans-serif; font-size: 75%; }
.controls {
  flex-basis: 200px;
  padding: 0 5px;
}
.controls .force {
  background-color:#eee;
  border-radius: 3px;
  padding: 5px;
  margin: 5px 0;
}
.controls .force p label { margin-right: .5em; font-size: 120%; font-weight: bold;}
.controls .force p { margin-top: 0;}
.controls .force label { display: inline-block; }
.controls input[type="checkbox"] { transform: scale(1.2, 1.2); }
.controls input[type="range"] { margin: 0 5% 0.5em 5%; width: 90%; }
/* for now, hide vega labels */
.controls .force .vega-bind { display: inline-flex; }
.controls .force .vega-bind .vega-bind-name { order: 0; }
.controls .force .vega-bind label { order: 1; }
.controls .force .vega-bind input { order: 2; }
.controls .force .vega-bind-name { display: none; }
/*.controls .force p label .vega-bind { display: inline; }*/
/* alpha viewer */
.controls .alpha p { margin-bottom: .25em; }
.controls .alpha .alpha_bar { height: .5em; border: 1px #777 solid; border-radius: 2px; padding: 1px; display: flex; }
.controls .alpha .alpha_bar #alpha_value { background-color: #555; border-radius: 1px; flex-basis: 100% }
.controls .alpha .alpha_bar:hover { border-width: 2px; margin:-1px; }
.controls .alpha .alpha_bar:active #alpha_value { background-color: #222 }

/* SVG styles */
#chart, svg {
  flex-basis: 100%;
  min-width: 200px;
  height: 100%;
}
.links line {
  stroke: #aaa;
}
.nodes circle {
  pointer-events: all;
}

</style>
</head>
<body>
<div class="controls">
  <div class="force alpha">
    <p><label>alpha</label> Simulation activity</p>
    <div class="alpha_bar" onclick="updateAll();"><div id="alpha_value"></div></div>
  </div>
  <div class="force">
    <p><label>center</label> Shifts the view, so the graph is centered at this location.</p>
    <label>
      x: <span id="centerX"></span>
    </label>
    <label>
      y: <span id="centerY"></span>
    </label>
  </div>

  <div class="force">
    <p><label><span id="chargeEnabled"></span> charge</label> Attracts (+) or repels (-) nodes to/from each other.</p>
    <label title="Negative strength repels nodes. Positive strength attracts nodes.">
      strength: <span id="chargeStrength"></span>
    </label>
    <label title="Minimum distance where force is applied">
      distanceMin: <span id="chargeDistanceMin"></span>
    </label>
    <label title="Maximum distance where force is applied">
      distanceMax: <span id="chargeDistanceMax"></span>
    </label>
  </div>

  <div class="force">
    <p><label><span id="collideEnabled"></span> collide</label> Prevents nodes from overlapping</p>
    <label>
      strength: <span id="collideStrength"></span>
    </label>
    <label title="Size of nodes">
      radius: <span id="collideRadius"></span>
    </label>
    <label title="Higher values increase rigidity of the nodes (WARNING: high values are computationally expensive)">
      iterations: <span id="collideIterations"></span>
    </label>
  </div>

  <div class="force">
    <p><label><span id="forceXEnabled"></span> forceX</label> Acts like gravity. Pulls all points towards an X location.</p>
    <label>
      strength: <span id="forceX_Strength"></span>
    </label>
    <label title="The X location that the force will push the nodes to (NOTE: This demo multiplies by the svg width)">
      x: <span id="forceX_X"></span>
    </label>
  </div>

  <div class="force">
    <p><label><span id="forceYEnabled"></span> forceY</label> Acts like gravity. Pulls all points towards a Y location.</p>
    <label>
      strength: <span id="forceY_Strength"></span>
    </label>
    <label title="The Y location that the force will push the nodes to (NOTE: This demo multiplies by the svg height)">
      y: <span id="forceY_Y"></span>
    </label>
  </div>

  <div class="force">
    <p><label><span id="linkEnabled"></span> link</label> Sets link length</p>
    <label title="The force will push/pull nodes to make links this long">
      distance: <span id="linkDistance"></span>
    </label>
    <label title="Higher values increase rigidity of the links (WARNING: high values are computationally expensive)">
      iterations: <span id="linkIterations"></span>
    </label>
  </div>
</div>
<div id="chart"></div>







<script src="spec.js"></script>
<script>

var view = new vega.View(vega.parse(spec), {
  loader: vega.loader({baseURL: 'https://vega.github.io/vega/'}),
  logLevel: vega.Warn,
  renderer: 'svg'
}).initialize('#chart').hover().run();

</script>

spec.js

var networkFile = 'https://vega.github.io/new-editor/app/data/miserables.json';
var spec = {
  "$schema": "https://vega.github.io/schema/vega/v3.0.json",
  "width": 500,
  "height": 500,
  "autosize": {"type": "fit", "resize": true},

  "signals": [
    // center force
    { "name": "cx", "value": "0.5",
      "bind": {"input": "range", "element": "#centerX", "min": 0, "max": 1, "step": 0.01} },
    { "name": "cy", "value": "0.5",
      "bind": {"input": "range", "element": "#centerY", "min": 0, "max": 1, "step": 0.01} },

    // charge force
    { "name": "chargeEnabled", "value": true,
      "bind": {"input": "checkbox", "element": "#chargeEnabled"} },
    { "name": "chargeStrength", "value": -30,
      "bind": {"input": "range", "element": "#chargeStrength", "min": -100, "max": 10, "step": 1} },
    { "name": "chargeDistanceMin", "value": 1,
      "bind": {"input": "range", "element": "#chargeDistanceMin", "min":0, "max": 50, "step": 0.1} },
    { "name": "chargeDistanceMax", "value": 2000,
      "bind": {"input": "range", "element": "#chargeDistanceMax", "min":0, "max": 2000, "step": 0.1} },

    // collide force
    { "name": "collideEnabled", "value": true,
      "bind": {"input": "checkbox", "element": "#collideEnabled"} },
    { "name": "collideStrength", "value": 0.7,
      "bind": {"input": "range", "element": "#collideStrength", "min":0, "max": 2, "step": 0.1} },
    { "name": "collideRadius", "value": 5,
      "bind": {"input": "range", "element": "#collideRadius", "min":0, "max": 100, "step": 1} },
    { "name": "collideIterations", "value": 1,
      "bind": {"input": "range", "element": "#collideIterations", "min":1, "max": 10, "step": 1} },

    // X force
    { "name": "forceXEnabled", "value": false,
      "bind": {"input": "checkbox", "element": "#forceXEnabled"} },
    { "name": "forceX_Strength", "value": 0,
      "bind": {"input": "range", "element": "#forceX_Strength", "min":0, "max": 1, "step": 0.01} },
    { "name": "forceX_X", "value": .5,
      "bind": {"input": "range", "element": "#forceX_X", "min":0, "max": 1, "step": .01} },
      
    // Y force
    { "name": "forceYEnabled", "value": false,
      "bind": {"input": "checkbox", "element": "#forceYEnabled"} },
    { "name": "forceY_Strength", "value": .1,
      "bind": {"input": "range", "element": "#forceY_Strength", "min":0, "max": 1, "step": 0.01} },
    { "name": "forceY_Y", "value": .5,
      "bind": {"input": "range", "element": "#forceY_Y", "min":0, "max": 1, "step": 0.01} },

    // link force
    { "name": "linkEnabled", "value": true,
      "bind": {"input": "checkbox", "element": "#linkEnabled"} },
    { "name": "linkDistance", "value": 30,
      "bind": {"input": "range", "element": "#linkDistance", "min": 5, "max": 100, "step": 1} },
    { "name": "linkIterations", "value": 1,
      "bind": {"input": "range", "element": "#linkIterations", "min":1, "max": 10, "step": 1} },
    
    // other parameters
    { "name": "static", "value": false },
    
    {
      "description": "State variable for active node fix status.",
      "name": "fix", "value": 0,
      "on": [
        {
          "events": "symbol:mouseout[!event.buttons], window:mouseup",
          "update": "0"
        },
        {
          "events": "symbol:mouseover",
          "update": "fix || 1"
        },
        {
          "events": "[symbol:mousedown, window:mouseup] > window:mousemove!",
          "update": "2", "force": true
        }
      ]
    },
    {
      "description": "Graph node most recently interacted with.",
      "name": "node", "value": null,
      "on": [
        {
          "events": "symbol:mouseover",
          "update": "fix === 1 ? item() : node"
        }
      ]
    },
    {
      "description": "Flag to restart Force simulation upon data changes.",
      "name": "restart", "value": false,
      "on": [
        {"events": {"signal": "fix"}, "update": "fix > 1"}
      ]
    }
  ],

  "data": [
    {
      "name": "node-data",
      "url": networkFile,
      "format": {"type": "json", "property": "nodes"}
    },
    {
      "name": "linkData",
      "url": networkFile,
      "format": {"type": "json", "property": "links"}
    }
  ],

  "marks": [
    {
      "name": "nodes",
      "type": "symbol",
      "zindex": 1,

      "from": {"data": "node-data"},
      "on": [
        {
          "trigger": "fix",
          "modify": "node",
          "values": "fix === 1 ? {fx:node.x, fy:node.y} : {fx:x(), fy:y()}"
        },
        {
          "trigger": "!fix",
          "modify": "node", "values": "{fx: null, fy: null}"
        }
      ],

      "encode": {
        "enter": {
          "fill": {"value": "black"}
        },
        "update": {
          "cursor": {"value": "pointer"},
          "size": {"signal": "collideRadius * collideRadius * 4"},
          "stroke": {"signal": "chargeStrength > 0 ? 'blue' : 'red'"},
          "strokeWidth": {"signal": "abs(chargeStrength) / 15"}
        }
      },

      "transform": [
        {
          "type": "force",
          "iterations": 300,
          "restart": {"signal": "restart"},
          "static": {"signal": "static"},
          "forces": [
            {"force": "center", "x": {"signal": "width * cx"}, "y": {"signal": "height * cy"}},
            {"force": "nbody", "strength": {"signal": "chargeStrength * chargeEnabled"}, "distanceMin": {"signal": "chargeDistanceMin"}, "distanceMax": {"signal": "chargeDistanceMax"}},
            {"force": "collide", "strength": {"signal": "collideStrength * collideEnabled"}, "radius": {"signal": "collideRadius"}, "iterations": {"signal": "collideIterations"}},
            // {"force": "x", "strength": {"expr": "forceX_Strength * forceXEnabled"}, "x": {"expr": "width * forceX_X"}}, // blanks screen if enabled
            {"force": "link", "links": "linkData", "distance": {"signal": "linkDistance"}, "iterations": {"signal": "linkIterations"}}
            // {"force": "link", "links": {"signal": "linkEnabled ? 'linkData' : []"}, "distance": {"signal": "linkDistance"}, "iterations": {"signal": "linkIterations"}}
          ]
        }
      ]
    },
    {
      "type": "path",
      "from": {"data": "linkData"},
      "interactive": false,
      "encode": {
        "update": {
          "stroke": {"value": "#ccc"},
          "strokeWidth": {"value": 0.5}
        }
      },
      "transform": [
        {
          "type": "linkpath", "shape": "line",
          "sourceX": "datum.source.x", "sourceY": "datum.source.y",
          "targetX": "datum.target.x", "targetY": "datum.target.y"
        }
      ]
    }
  ]
};