Skip to content

Instantly share code, notes, and snippets.

@steveharoz
Last active April 26, 2017 01:30
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save steveharoz/28a33114d81a3fac9c6e497811661768 to your computer and use it in GitHub Desktop.
Save steveharoz/28a33114d81a3fac9c6e497811661768 to your computer and use it in GitHub Desktop.
Vega-force testing ground
license: gpl-3.0
height: 1030
scrolling: yes

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

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

  • Need "actualWidth" and "actualHeight" signals for autosized SVG
  • Position signal control as text label <br/> control (would need feature to specify label's parent element or specify descriptive text)
  • forceX and forceY don't seem to behave correctly
  • Can't disable/enable link force (may need "enabled" parameter for force)
  • Get simulation alpha (likely not possible with vega)
  • circle radius = collideRadius (problem with using path instead of circle?) (thanks Jeff Heer)
  • circle stroke color doesn't like ternary operator (thanks Jeff Heer)
<!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>
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"
}
]
}
]
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment