Tutorial/Graphs
It lived. It served. It died.
This article is dedicated to the late graph extension.
Fly high, sweet angel... ![]()
![]()
Graphs are these crazy little things that can create javascript canvas, which makes them much more powerful than HTML or CSS or modules.
Introduction
Graphs are created using JSON text inside of a <graph> tag, using the Vega 2.0 data visualization language.
Overview
There are three types of properties that are most important for making a graph.
Marks are objects drawn on the graph.
Signals take user input to make the graph interactive.
Data can be used to store information, akin to variables.
Expand the text for a more detailed overview.
Marks
- Properties - to draw marks, they must be given properties such as color, position, etc. When specifying a property, you should say whether it will be given to the object when it's first drawn (enter), recalculated every time it's drawn (update), when the mark is hovered over (hover), or when the mark is removed (exit).
Signals
- Streams - short for "event streams" pick up input like mouse clicks, keyboard presses, etc.
- Expressions - the value of the signal is set by an expression. Expressions use
Data
- Data Transform - data can be altered by applying different transforms to it, like filtering out certain values, sorting data, performing math on the data, etc.
- Streaming Data - data can also be modified by inserting and removing data points, or toggling whether they are including in a data set. The modified points are determined by a signal.
Example: Rectangle
Let's draw a rectangle. Here is the starting point.
<graph>
{
"width":200,
"height":200
}
</graph>"key":"value"The height and width here determine the size of the graph.
<graph>
{
"width":200,
"height":200,
"marks":[
{
}
]
}
</graph>Now we add a key "marks" with an array value. Each entry of the array will be an object providing instructions on how to draw something to the screen. Also remember to add the comma to the "height" line now, since it's no longer the end of the list!
<graph>
{
"width":200,
"height":200,
"marks":[
{
"name":"square",
"type":"rect"
}
]
}
</graph>These are more key/value pairs, inside the first object of the marks array. We set the name of the object to "square," and declare that it should be a rectangle. Other types of mark include symbols, paths, arcs, areas, lines, rules, text, and groups. Now we need to describe how to draw the rectangle, using a number of "properties."
<graph>
{
"width":200,
"height":200,
"marks":[
{
"name":"square",
"type":"rect",
"properties":{
"enter":{
},
"update":{
}
}
}
]
}
</graph>Since graphs are interactive, we need to say immediately whether a property of the rectangle will change and have to be updated. These will go in the "update" section. Others can go in "enter," and get looked at when the rectangle is first drawn, but not ever again.
<graph>
{
"width":200,
"height":200,
"marks":[
{
"name":"square",
"type":"rect",
"properties":{
"enter":{
"width":{"value":50}
},
"update":{
}
}
}
]
}
</graph>First we'd like to set the width of the rectangle. This is done with the key "width" and a value which is another object. This object is called a value reference and has a key named "value" with the value 50. If you simply set "width":50 then the rectangle won't render. You need the value reference.
<graph>
{
"width":200,
"height":200,
"marks":[
{
"name":"square",
"type":"rect",
"properties":{
"enter":{
"width":{"value":50},
"height":{"value":50},
"x":{"value":20},
"y":{"value":30}
},
"update":{
}
}
}
]
}
</graph>Now we set the height and the x-/y-position of the rectangle. Remember that the x- and y- coordinates start counting from the top left corner, unlike regular Cartesian coordinates. The x-value increases going to the right, but the y-value increases going down.
<graph>
{
"width":200,
"height":200,
"marks":[
{
"name":"square",
"type":"rect",
"properties":{
"enter":{
"width":{"value":50},
"height":{"value":50},
"x":{"value":20},
"y":{"value":30}
},
"update":{
"fill":{"value":"steelblue"}
}
}
}
]
}
</graph>The last step is to set the fill color of the rectangle. Check out your graph! If it's not showing up, double check for missing/extra commas, and square brackets that should be braces (or vice versa). <graph> {
"width":200,
"height":200,
"marks":[
{
"name":"square",
"type":"rect",
"properties":{
"enter":{
"width":{"value":50},
"height":{"value":50},
"x":{"value":20},
"y":{"value":30}
},
"update":{
"fill":{"value":"steelblue"}
}
}
}
]
} </graph>
Example: Clicking
To add interactivity to our graph, we're going to introduce signals. A signal is a variable that responds to user input. If another value is set equal to a signal, then it updates whenever the signal updates. We're going to set the fill color of our rectangle equal to a signal so that when we click, it turns red.
<graph>
{
"width":200,
"height":200,
"signals":[
{
}
]
}
</graph>It starts out the same way, but instead of marks we're going to define an array of signals.
<graph>
{
"width":200,
"height":200,
"signals":[
{
"name":"clicked",
"streams":[
{
}
]
}
]
}
</graph>We'll give the signal a name, and then create a streams array. Streams are what will allow us to capture the mouse clicking. Each object in the array will need to have a "type" which describes what input to record, and an "expr" expression to determine what its updated value should be.
<graph>
{
"width":200,
"height":200,
"signals":[
{
"name":"clicked",
"streams":[
{
"type":"window:mousedown"
}
]
}
]
}
</graph>The type is given in two parts: the first describes which object will watch for events, and the second describes what events to watch out for. "window:mousedown" records any time the mouse is pressed down anywhere on the page (window). Besides mousedown, there are other events for keyboard presses, mouse dragging, scrolling, double clicking, as well touch screen events for mobile devices. Besides window, other objects could be all marks of a certain type e.g. "rect:mousedown" or a mark with a specific name e.g. "@square:mousedown" (the @ is required for names). It could even be a CSS selector for an element on the page, e.g. "#header:mousedown" for an HTML element with id "header." If the first half is omitted entirely, then it tracks events anywhere in the graph window e.g. "mousedown."
<graph>
{
"width":200,
"height":200,
"signals":[
{
"name":"clicked",
"streams":[
{
"type":"window:mousedown",
"expr":"\"red\""
}
]
}
]
}
</graph>If the mouse is pressed down, the "clicked" signal is set equal to \"red.\"
Since the expression is wrapped in quotes, and we want the value to be "red" (a string), then we need to escape the inner quotes. This can also be done with single quotes, i.e. "expr":"'red'"
<graph>
{
"width":200,
"height":200,
"signals":[
{
"name":"clicked",
"streams":[
{
"type":"window:mousedown",
"expr":"\"red\""
},
{
"type":"window:mouseup",
"expr":"\"steelblue\""
}
]
}
]
}
</graph>We'll add another object to our set of streams. This one will watch for any time the mouse is released anywhere on the website, and it sets "clicked" equal to "steelblue."
<graph>
{
"width":200,
"height":200,
"signals":[
{
"name":"clicked",
"streams":[
{
"type":"window:mousedown",
"expr":"\"red\""
},
{
"type":"window:mouseup",
"expr":"\"steelblue\""
}
]
}
],
"marks":[
{
"name":"square",
"type":"rect",
"properties":{
"enter":{
"width":{"value":50},
"height":{"value":50},
"x":{"value":20},
"y":{"value":30}
},
"update":{
"fill":{"value":"steelblue"}
}
}
}
]
}
</graph>Now we add back our rectangle.
<graph>
{
"width":200,
"height":200,
"signals":[
{
"name":"clicked",
"streams":[
{
"type":"window:mousedown",
"expr":"\"red\""
},
{
"type":"window:mouseup",
"expr":"\"steelblue\""
}
]
}
],
"marks":[
{
"name":"square",
"type":"rect",
"properties":{
"enter":{
"width":{"value":50},
"height":{"value":50},
"x":{"value":20},
"y":{"value":30}
},
"update":{
"fill":{"signal":"clicked"}
}
}
}
]
}
</graph>Now we set the value of our rectangle's "fill" to be the "clicked" signal. Whenever a mouse is pressed or released on the window, "clicked" will update, and so will the color of the rectangle. The {"signal":"name"} object is another kind of value reference.
However, we still have a problem. When the program is first run, the signal "clicked" isn't set to anything. It only updates when the status of the mouse updates, and it has no idea whether the mouse is up or down when the graph starts.
<graph>
{
"width":200,
"height":200,
"signals":[
{
"name":"clicked",
"init":"steelblue",
"streams":[
{
"type":"window:mousedown",
"expr":"\"red\""
},
{
"type":"window:mouseup",
"expr":"\"steelblue\""
}
]
}
],
"marks":[
{
"name":"square",
"type":"rect",
"properties":{
"enter":{
"width":{"value":50},
"height":{"value":50},
"x":{"value":20},
"y":{"value":30}
},
"update":{
"fill":{"signal":"clicked"}
}
}
}
]
}
</graph>The last step is to create an "init" property of the stream, which sets signal initially. The value isn't an expression, so it doesn't need to have escaped quotes.
<graph> {
"width":200,
"height":200,
"signals":[
{
"name":"clicked",
"init":"steelblue",
"streams":[
{
"type":"window:mousedown",
"expr":"\"red\""
},
{
"type":"window:mouseup",
"expr":"\"steelblue\""
}
]
}
],
"marks":[
{
"name":"square",
"type":"rect",
"properties":{
"enter":{
"width":{"value":50},
"height":{"value":50},
"x":{"value":20},
"y":{"value":30}
},
"update":{
"fill":{"signal":"clicked"}
}
}
}
]
} </graph>
Example 3: Data
Let's add some data...
<graph>
{
"width":200,
"height":200,
"data":[
{
}
]
}
</graph>Data is another one of these top-level properties which is an array of objects.
<graph>
{
"width":200,
"height":200,
"data":[
{
"name":"table",
"values":[5, 10, 15, 25, 50]
}
]
}
</graph>We can give the data a name, as well as some values. Here's another way to provide values.
<graph>
{
"width":200,
"height":200,
"data":[
{
"name":"table",
"values":[{"hi":5},{"hi":"word"},{"hi":10}]
}
]
}
</graph>This makes a list of 3 data points (each one can also be called a datum, a row, or a record). They are each an object, and each has a key called "hi." This is called the field, or column. The value of that key could be a number, string, or another data set. All data in Vega comes as a list of objects (rows), and each row has a key/value pair for every field (column). When we created the data list before from just numbers, the program automatically assigned these numbers to a field named "data"
<graph>
{
"width":200,
"height":200,
"data":[
{
"name":"table",
"values":[
{"hi":5, "lala":1},
{"hi":"word", "lala":5},
{"hi":{"bye":4, "bye":3, "bye":"word"}, "lala":8}
]
}
]
}
</graph>Here's another example of a data set. This time, each data point has two fields: "hi" and "lala." The value of "hi" in the last row is another data set with 3 rows and a field called "bye." Also note that Vega randomly assigns each data point an ID, in a field called "_id" which is automatically generated and different each time the program runs.
<graph>
{
"width":200,
"height":200,
"data":[
{
"name":"table",
"values":[
{"x":5, "y":5, "message":"we"},
{"x":25, "y":30, "message":"seals"},
{"x":50, "y":90, "message":"three"}
],
"transform":[
]
}
]
}
</graph>Here's the data we'll actually be using. It has three fields called "x," "y," and "message."
We also create an array of data transforms. These describe operations that can be performed on data.
<graph>
{
"width":200,
"height":200,
"data":[
{
"name":"table",
"values":[
{"x":5, "y":5, "message":"we"},
{"x":25, "y":30, "message":"seals"},
{"x":50, "y":90, "message":"three"}
],
"transform":[
{
"type":"formula",
"field":"x",
"expr":"datum.x + 5"
}
]
}
]
}
</graph>There are many types of data transforms. The most useful is probably formula, which will allow us to use expressions to change data. Other useful transforms include filter, which removes any data that doesn't satisfy a test condition, and lookup, which imports data points from another data set based on matching data.
Here, the type of data transform is formula, and it will update the values of the field "x." The expression will return the datum's "x" field added to 5. The object "datum" stores the key/value pairs for each field in the current data point. For example, if we kept the type and field the same, but changed the expr to "datum.message + ' hello'" then the data would look like this after the transform:
{"x":"we hello", "y":5, "message":"we"},
{"x":"seals hello", "y":30, "message":"seals"},
{"x":"three hello", "y":90, "message":"three"}Expressions also have access to the names of signals. To see this, let's add back our signal from the last example.
<graph>
{
"width":200,
"height":200,
"signals":[
{
"name":"clicked",
"init":"steelblue",
"streams":[
{
"type":"window:mousedown",
"expr":"\"red\""
},
{
"type":"window:mouseup",
"expr":"\"steelblue\""
}
]
}
],
"data":[
{
"name":"table",
"values":[
{"x":5, "y":5, "message":"we"},
{"x":25, "y":30, "message":"seals"},
{"x":50, "y":90, "message":"three"}
],
"transform":[
{
"type":"formula",
"field":"x",
"expr":"datum.x + 5"
}
]
}
]
}
</graph>Now we're going to create another formula transform.
<graph>
{
"width":200,
"height":200,
"signals":[
{
"name":"clicked",
"init":"steelblue",
"streams":[
{
"type":"window:mousedown",
"expr":"\"red\""
},
{
"type":"window:mouseup",
"expr":"\"steelblue\""
}
]
}
],
"data":[
{
"name":"table",
"values":[
{"x":5, "y":5, "message":"we"},
{"x":25, "y":30, "message":"seals"},
{"x":50, "y":90, "message":"three"}
],
"transform":[
{
"type":"formula",
"field":"x",
"expr":"datum.x + 5"
},
{
"type":"formula",
"field:"color",
"expr":"clicked"
}
]
}
]
}
</graph>This formula creates a new field called "color" and for each data point sets it to the value of the signal "clicked." Every time "clicked" updates, this formula will be recalculated.
<graph>
{
"width":200,
"height":200,
"signals":[
{
"name":"clicked",
"init":"steelblue",
"streams":[
{
"type":"window:mousedown",
"expr":"\"red\""
},
{
"type":"window:mouseup",
"expr":"\"steelblue\""
}
]
}
],
"data":[
{
"name":"table",
"values":[
{"x":5, "y":5, "message":"we"},
{"x":25, "y":30, "message":"seals"},
{"x":50, "y":90, "message":"three"}
],
"transform":[
{
"type":"formula",
"field":"x",
"expr":"datum.x + 5"
}
]
}
]
}
</graph>Now we're going to create another formula transform.
<graph>
{
"width":200,
"height":200,
"signals":[
{
"name":"clicked",
"init":"steelblue",
"streams":[
{
"type":"window:mousedown",
"expr":"\"red\""
},
{
"type":"window:mouseup",
"expr":"\"steelblue\""
}
]
}
],
"data":[
{
"name":"table",
"values":[
{"x":5, "y":5, "message":"we"},
{"x":25, "y":30, "message":"seals"},
{"x":50, "y":90, "message":"three"}
],
"transform":[
{
"type":"formula",
"field":"x",
"expr":"datum.x + 5"
},
{
"type":"formula",
"field":"color",
"expr":"clicked"
}
]
}
],
"marks":[
{
"name":"messages",
"type":"text"
}
]
}
</graph>We're going to create a new kind of mark this time, for displaying text. The text will be drawn using data from our data set, so we need to tell it where our data is.
<graph>
{
"width":200,
"height":200,
"signals":[
{
"name":"clicked",
"init":"steelblue",
"streams":[
{
"type":"window:mousedown",
"expr":"\"red\""
},
{
"type":"window:mouseup",
"expr":"\"steelblue\""
}
]
}
],
"data":[
{
"name":"table",
"values":[
{"x":5, "y":5, "message":"we"},
{"x":25, "y":30, "message":"seals"},
{"x":50, "y":90, "message":"three"}
],
"transform":[
{
"type":"formula",
"field":"x",
"expr":"datum.x + 5"
},
{
"type":"formula",
"field":"color",
"expr":"clicked"
}
]
}
],
"marks":[
{
"name":"messages",
"type":"text",
"from":{"data":"table"}
}
]
}
</graph>The "from" property is used to do this, and its value is a reference to our data named "table."
<graph>
{
"width":200,
"height":200,
"signals":[
{
"name":"clicked",
"init":"steelblue",
"streams":[
{
"type":"window:mousedown",
"expr":"\"red\""
},
{
"type":"window:mouseup",
"expr":"\"steelblue\""
}
]
}
],
"data":[
{
"name":"table",
"values":[
{"x":5, "y":5, "message":"we"},
{"x":25, "y":30, "message":"seals"},
{"x":50, "y":90, "message":"three"}
],
"transform":[
{
"type":"formula",
"field":"x",
"expr":"datum.x + 5"
},
{
"type":"formula",
"field":"color",
"expr":"clicked"
}
]
}
],
"marks":[
{
"name":"messages",
"type":"text",
"from":{"data":"table"},
"properties":{
"enter":{
"x":{"field":"x"}
},
"update":{
}
}
}
]
}
</graph>We start filling in the properties for each text object. Instead of specifying an x-value using a direct value like {"value":5}, we use a different kind of value reference. {"field":"x"} tells the program to use the "x" field in the "table" data set we gave earlier. A new "text" mark will be created for every row in the "table" data set, and its "x" value will be the "x" value of the corresponding row.
<graph>
{
"width":200,
"height":200,
"signals":[
{
"name":"clicked",
"init":"steelblue",
"streams":[
{
"type":"window:mousedown",
"expr":"\"red\""
},
{
"type":"window:mouseup",
"expr":"\"steelblue\""
}
]
}
],
"data":[
{
"name":"table",
"values":[
{"x":5, "y":5, "message":"we"},
{"x":25, "y":30, "message":"seals"},
{"x":50, "y":90, "message":"three"}
],
"transform":[
{
"type":"formula",
"field":"x",
"expr":"datum.x + 5"
},
{
"type":"formula",
"field":"color",
"expr":"clicked"
}
]
}
],
"marks":[
{
"name":"messages",
"type":"text",
"from":{"data":"table"},
"properties":{
"enter":{
"x":{"field":"x"},
"y":{"field":"y"},
"text":{"field":"message"}
},
"update":{
"fill":{"field":"color"}
}
}
}
]
}
</graph>We fill in the other properties for our text. The "text" property determines what the text will say. Note that "height" and "width" aren't required, since they're determined by the text. The "fill" property is placed in the "update" section, since the "clicked" signal will change the value of the "color" field. <graph> {
"width":200,
"height":200,
"signals":[
{
"name":"clicked",
"init":"steelblue",
"streams":[
{
"type":"window:mousedown",
"expr":"\"red\""
},
{
"type":"window:mouseup",
"expr":"\"steelblue\""
}
]
}
],
"data":[
{
"name":"table",
"values":[
{"x":5, "y":5, "message":"we"},
{"x":25, "y":30, "message":"seals"},
{"x":50, "y":90, "message":"three"}
],
"transform":[
{
"type":"formula",
"field":"x",
"expr":"datum.x + 5"
},
{
"type":"formula",
"field":"color",
"expr":"clicked"
}
]
}
],
"marks":[
{
"name":"messages",
"type":"text",
"from":{"data":"table"},
"properties":{
"enter":{
"x":{"field":"x"},
"y":{"field":"y"},
"text":{"field":"message"}
},
"update":{
"fill":{"field":"color"}
}
}
}
]
} </graph>
Animations
When making games with graphs, it'd be nice to have the graph update graphics even when no user input is being given. For example, if we were making Snake, we'd want the snake to continue heading straight when no keys are being pressed. The problem is that, as far as I can tell, Vega only updates the graph when a signal updates. In the current version of Vega (version 5) this isn't a problem, because it has a timer signal, but this doesn't exist in Vega 2.0 (what the wiki graph extension uses). Here are some attempts to get animations to work.
STRATEGY 1 - MOUSEOVER
One of the events you can use in streams is called "mouseover" and activates when the mouse first moves on top of an element (the page, the graph, a symbol, etc.) The opposite of "mouseover" is "mouseout," when the mouse leaves the element.
One idea is to have a symbol record mouseover and mouseout events. Then, shrink or move the symbol when mouseover happens so that the mouse isn't over it anymore, and undo the change on mouseout. This would cause the mouse to alternate being on/off the symbol every frame, producing a signal that updates every frame. This signal could then be used as a clock to update every other part of the graph game.
The problem is that even if the mouse changes its status of being on/off the element, "mouseover" and "mouseout" events only happen when the mouse actually moves. If the mouse is stationary neither gets triggered.
STRATEGY 2 - SIGNAL LOOP
Remember that a signal is set by an expression, and that expressions can use signals in their evaluation. What if there was a signal evaluated itself in order to set its own value? Or if two signals alternately set the value of the other? This would again produce a signal clock which could be used in expressions elsewhere in the program.
Warning. The following code will cause an infinite loop. You can try with the Vega editor in a private browsing window and the tab will freeze. See troubleshooting for more info.
{
"signals":[
{
"name":"clock",
"expr":"clock ? false : true"
}
}Signals can be set by an expression even with no "streams" array. A stream defined this way won't update unless there's a signal in the expression definition. Here, the expression uses the value of the signal itself. The expression takes the form of a ternary operator. In a ternary operator, the part before the question mark is tested to see if it's ture or flase. If it's true, the value of "clock" is set to the part after the question mark but before the colon. If it's false, then the value of "clock" is set to the part after the colon.
So, the clock signal starts out with no value. Javascript evaluates this as "false", and sets "clock" equal to the part after the colon, which is "true." Since the "clock" signal just updated, any expression using that signal is run again, including the definition of "clock." This time, "clock" is false , so it's set equal to the part before the colon, "false." It loops forever, like that paradox about "this sentence is false." If we had other code in the graph, it would never be run.
Warning: infinite loop.
{
"signals":[
{
"name":"clock1",
"expr":"clock2 ? false : true"
},
{
"name":"clock2"
"expr":"clock1"
}
}Here's another attempt at the same thing. The thinking is that using two signals relying on each other would avoid the infinite loop problem. For example, "clock1" might run first and be set to "true," since "clock2" isn't defined. Then "clock2" would be set to "true." Then the program might continue to run the rest of the graph code, and when it comes to "clock1" on the next pass, it gets set to "false." And so on...
This is not what happens. There is no "next pass," since graph code will only run once unless a signal updates. In our example, when "clock2" is set to "true," it immediately updates all expressions containing "clock2." This means it goes back to the "clock1" definition and updates "clock1." Which updates "clock2" which updates "clock1" which updates...
Here's a fact about signals. If after an update they have the same value as before, then they don't cause expressions that use them to reevaluate (if you want them to anyway, you can add "verbose":"true" to the signal definition). Can this be used to make a clock? I can't think of a way myself, mostly because of the following problem.
{
"signals":[
{
"name":"clock1",
"expr":"clock2 ? false : true"
},
{
"name":"clock2"
"expr":"clock1 ? false : true"
}
}This code runs fine! The values update like this:
clock1: null -> true
clock2: null -> false
clock1: true -> trueIn the last step, clock1 doesn't change its value, so it doesn't update clock2. So, there aren't any infinite loops. However, this means the graph only makes one pass through the code. After the clock signals settle on their values, they never update again and so the rest of the program never updates, either.
STRATEGY 3: DATA LOOP
Expressions are able to use a number of functions. Here's a list. These even include time functions! For example, now() returns the unix timestamp up to milliseconds. This is so frustrating, because they will only run once on each update, so I don't know how to make use of them. The function I want to focus on is indata(), which searches to see if a value is in a data table. For example, indata("table",5,"numbers") will search the data set "table" for the value 5 in the "numbers" field. If it finds a match, it returns true.
Now recall that some data transforms are able to use expressions to modify data ("formula," for example). The idea for this strategy is to make another clock. One half is a signal, which will update a data set. The other half is the data set, which will update the signal.
{
"signals":[
{
"name":"clock",
"init":0,
"expr":"indata('table',0,'data') ? 1 : 0"
}
],
"data":[
{
"name":"table",
"values":[1],
"transform":[
{
"type":"formula",
"field":"data",
"expr":"clock"
}
]
}
]
}We start with a "clock" signal equal to 0, and a "table" data set with a single value 1 in the field "data" (set automatically by Vega). The idea is that when the formula transform runs, the single data value will be set to "clock," which is 0. Then the signal will check whether there's a 0 in the table, which there is now. So "clock" gets set to the 1 part of the ternary operator. Then the data value gets set to "clock," which is 1 this time. It'll be another clock loop. Maybe if we use the "data" section this time, the "marks" code will be able to run before the "signal" is updated, and we'll get a loop that doesn't freeze everything.
Well it turns out that a data table doesn't behave like a signal, because it doesn't update every expression that calls "indata()" on it. So this code only runs once. "clock" gets set to 0 and the data value gets set to "clock," and then that's it.
If reference another signal in the definition of the "clock" signal, we could get "clock" to run again on the updated table. But this presents the same problem as in the previous section, with the signal loops. If we use "clock" in its own definition, then "clock" will recurse on itself forever, and if we use a new signal, then either both loop each other forever, or they both run only once.
STRATEGY 4: FORCE TRANSFORM
Have you gone through the example graphs in the Vega editor? One of them is called force_drag and shows an interactive web of nodes. When you click on a node, you can drag it and the rest of the web will react in a realistic way. When you release the node, it will settle back into place. The graph will continue to animate it settling even when you're not interacting with any of the nodes or otherwise giving it input.
This effect is possible with the use of a data transform called "force", which takes two data sets: a set of nodes and a set of edges. It runs a physics simulation and outputs two extra fields onto the nodes set, which are the x- and y-positions of each node. These can be used to draw corresponding marks onto the graph. If the setting "interactive":"true" is set on the data transform, then Vega will continue to update the x- and y-positions every frame. So far, this is the only example I know of in Vega 2.0 where things will update without a signal changing.
In non-interactive mode, the graph will run the simulation for a certain number of iterations, set by the user. Only after it's finished will it run the rest of the graph code. In interactive mode, the simulation and the graphics run at the same time, however it has a maximum amount of time it will spend running the simulation, which cannot be changed. I think it's around a second.
The transform also takes an "active" property. This selects a node in the data set whose position can be adjusted, which will optionally reset the simulation timer.
How can we use this information to create a clock that lasts forever? I don't know, really. Here are some strange things I've noticed, though.
- If I put a formula transform after the force transform that sets all the layout_x coordinates to 0, it doesn't do anything
- It's possible to define data transforms in the marks section by describing them in the "from" property. I had a symbol in marks to draw the nodes, and it updated its position every frame to match the nodes data set. I applied a transform that halved every layout_x value, and it worked, but only on the first frame. The symbols were fixed in their first positions, only with half x-coordinates, while all the other marks continued to animate (it's normal for data transforms made for a specific mark not to affect the original data set used for another mark, although I don't know why the symbols I tested stopped receiving updates when I did a transform.)
- The debug screen shows a single, unchanging set of values for the nodes array. These values are the positions of the nodes after the simulation has finished running.
OTHER STRATEGIES
These are just some ideas I've tried. I don't know what the actual solution is, I just think it'd be crazy if there wasn't one! Some other ideas...
The documentation for marks describes a "delay" property. "The translation delay, in milliseconds, for mark updates. The delay can be set in conjunction with the backing data (possibly through a scale transform) to provide staggered animations." I haven't gotten this to work, but it sounds pretty good, although a little strange. Animations would be calculated in advance, and only shown after a delay. If this were used in the snake game example, you would have it so that game animates the snake going along based on the last input it received, only animating a little forward movement at a time. Internally, the game would have calculated where you would run into and die if you didn't give any further inputs. It would delay showing this until either you died that way, or you made some input to turn and it would calculate a new death trajectory. Cool!
Expressions just use javascript, although you're restricted in what features you can use. If it were possible to execute arbitrary javascript code in graphs, this would probably be the way to do it. I don't know how realistic that is.
I think I have already missed a lot. I wanted to type this up and was hoping that putting ideas into words would help me find something I overlooked. I hope when you read this you're doing it aggressively, looking for loopholes and things I got wrong. That's all for now, thanks for reading.
Troubleshooting
The online Vega editor is kinda annoying because it doesn't tell you what you did wrong besides small typos. Here is a list of things to check if your graph isn't working.
Simple typos.
- Every item in a list must have a comma after it.
- The last item in a list can't have a comma after it.
- Are any colons placed inside strings?
"width:"50will give an error. - Strings inside of strings should be placed inside single quotes, not double quotes.
- Be careful with terms that are written with square brackets, with braces around individual items. For example,
"data":[{...}]instead of"data":{{...}}or"data":[...] - Pay attention to whether important terms are singular or plural. Plural: "data," "marks," "signals," "streams." Singular: "transform," "modify."
Other.
- Are there any mark properties given directly? For example, putting
"width":50will produce an error, when it should be"width":{"value":50} - If marks use data, make sure to set "from" correctly. If I'm importing a data set named "table" then use I'll use "from":{"data":"table"} rather than just "from":"table"
- Make sure that the variables used in an expression exist. For example, when the program first starts running, some signals may not have a value (usually if they don't have an "init"), so expressions may behave strangely.
- If you need your mark to respond to input, any properties that change must be put in the "update" section instead of "enter!"
- The editor saves your code (I think when you run it), and runs it when you first load the website. So if your program gets caught in an infinite loop, you need to clear cookies on the site to use it again. This will remove all your code. If you think your program might loop, you should save it in another document first.
- Make sure to test your program often!
Additionally, the vega editor has a debug screen. I don't know all the details yet, but I think it doesn't appear unless your program has marks that use a data field (AKA they use "from").