Graph style script

Graph Style Script language

This guide will show you how to easily get started with the Graph Style Script language. GSS is a language for customizing the visual display of graphs. For a complete list of available features consult the Style script reference guide.

Graph example

In this guide, we will use an example graph with European countries and cities. The data can be found here. Countries have the label Country, while cities have the label City. All nodes have the property name. Cities have many additional properties, including country (containing country) and drinks_USD (average drink price).

Setting graph labels

We want to label country nodes with country names, and city nodes with city names and containing country names. To achieve that we can use two directives. The first one selects countries and the second one selects cities.

@NodeStyle HasLabel(node, "Country") {
  label: Property(node, "name")
}
 
@NodeStyle HasLabel(node, "City") {
    label: Format("{cityName}, {countryName}",
                  Property(node, "name"),
                  Property(node, "country"))
}

In the case of the Format function, content inside the curly braces is ignored but can be helpful for clarity.

Setting node images

It would be nice to display flags in the country nodes. This can be achieved using URLs of flag images. There is a website that hosts many world flags so we can use images from there (opens in a new tab). Their API expects a country name as a part of the URL path so we will make the following directive.

@NodeStyle HasLabel(node, "Country") {
  image-url: Format("https://cdn.countryflags.com/thumbs/{}/flag-800.png",
                    LowerCase(Property(node, "name")))
}

Unfortunately, this won't work for all countries. Flags for England and Scotland cannot be found on the website because they aren't real countries. So we can get around that by providing custom directives below the general one above.

@NodeStyle Equals(Property(node, "name"), "England") {
   image-url: "https://upload.wikimedia.org/wikipedia/en/thumb/b/be/Flag_of_England.svg/2560px-Flag_of_England.svg.png"
}
 
@NodeStyle Equals(Property(node, "name"), "Scotland") {
  image-url: "https://upload.wikimedia.org/wikipedia/commons/thumb/1/10/Flag_of_Scotland.svg/1200px-Flag_of_Scotland.svg.png"
 }

Also, URLs for a country name with whitespace inside them don't so we also have to provide custom URLs for the Czech Republic and Bosnia and Herzegovina.

@NodeStyle Equals(Property(node, "name"), "Bosnia and Herzegovina") {
   image-url: "https://upload.wikimedia.org/wikipedia/commons/thumb/b/bf/Flag_of_Bosnia_and_Herzegovina.svg/1200px-Flag_of_Bosnia_and_Herzegovina.svg.png"
 }
 
 @NodeStyle Equals(Property(node, "name"), "Czech Republic") {
   image-url: "https://upload.wikimedia.org/wikipedia/commons/thumb/c/cb/Flag_of_the_Czech_Republic.svg/2560px-Flag_of_the_Czech_Republic.svg.png"
 }

Now all the country nodes have their flags displayed.

Highlighting interesting nodes

We can highlight nodes with low drink prices in the following way. We want to use a beer image and a bigger size along with a red shadow.

@NodeStyle And(
     HasLabel(node, "City"),
     Less(Property(node, "drinks_USD"), 5)) {
  size: 50
  image-url: "https://www.sciencenews.org/wp-content/uploads/2020/05/050620_mt_beer_feat-1028x579.jpg"
  shadow-color: red
 }

Caching results for faster performance

To normalize some value, for example, the size or width of all the nodes or relationships in the graph, find the minimum and maximum values of all nodes. For example, a node labled "Person"has the propertyagethat holds the age information of a particular person. We want the node propertysize` to be 5 for the youngest person and 20 for the oldest one in the presented graph. All other node sizes should be normalized within that range.

One of the solutions could look like this:

// Size range min/max variables
Define(MIN_SIZE, 5)
Define(MAX_SIZE, 20)
Define(PROP_NAME, "age")
Define(SIZE_RANGE, Sub(MAX_SIZE, MIN_SIZE))
 
// A set of utility functions
// Create a new array of property values from an array of nodes
Define(GetProperties, Function(nodes, propName,
  Map(nodes, Function(singleNode, Property(singleNode, propName)))
))
// Keep only the numeric values from an array of values
Define(KeepNumericValues, Function(values,
  Filter(values, Function(value, IsNumber(value)))
))
 
// Functions to find min and max value in the input nodes
Define(GetMaxValue, Function(nodes,
  Max(KeepNumericValues(GetProperties(nodes, PROP_NAME)))
))
Define(GetMinValue, Function(nodes,
  Min(KeepNumericValues(GetProperties(nodes, PROP_NAME)))
))
 
// Normalize function that receives two inputs: node (n) and
// graph (g) and returns normalized value into a range
// [MIN_SIZE, MAX_SIZE]
Define(Normalize, Function(n, g,
  Add(
    MIN_SIZE,
    Mul(
      SIZE_RANGE,
      Div(
        Sub(Property(n, PROP_NAME), GetMinValue(Nodes(g))),
        Sub(GetMaxValue(Nodes(g)), GetMinValue(Nodes(g)))
      )
    )
  )
))
 
// For all nodes with the label "Person" and numeric property "age"
@NodeStyle And(HasLabel(node, "Person"), IsNumber(Property(node, PROP_NAME))) {
  color: white
  size: Normalize(node, graph)
  width: Div(Normalize(node, graph), 5)
  label: Format("Age: {}", AsText(Property(node, PROP_NAME)))
}

Using Graph Style Script to style different nodes by its size

The problem with the solution above is slow performance. The Normalize function is called twice for each node in the graph view. Each Normalize call iterates through all nodes three times: two times for GetMinValue and once for GetMaxValue. For small graphs, you won't see a difference in performance but as the number of nodes rises the performance issues will follow.

To solve this issue, cache the results by calculating outside of @NodeStyle and @EdgeStyle directives where the variable graph is also available. Inside the @NodeStyle directive, a local variable can be used to store the normalized value and use it with size and width properties thus calling the Normalize function only once.

Check the improved GSS code below:

// Size range min/max variables
Define(MIN_SIZE, 5)
Define(MAX_SIZE, 20)
Define(PROP_NAME, "age")
Define(SIZE_RANGE, Sub(MAX_SIZE, MIN_SIZE))
 
// A set of utility functions
// Create a new array of property values from an array of nodes
Define(GetProperties, Function(nodes, propName,
  Map(nodes, Function(singleNode, Property(singleNode, propName)))
))
// Keep only the numeric values from an array of values
Define(KeepNumericValues, Function(values,
  Filter(values, Function(value, IsNumber(value)))
))
 
// Variables MAX_VALUE and MIN_VALUE will hold the max and min
// values of all node properties.
// The If statement is used to handle errors when there are no values to calculate
// min and max from.
Define(MAX_VALUE, If(
  Greater(NodeCount(graph), 0),
  Max(KeepNumericValues(GetProperties(Nodes(graph), PROP_NAME))),
  0
))
Define(MIN_VALUE, If(
  Greater(NodeCount(graph), 0),
  Min(KeepNumericValues(GetProperties(Nodes(graph), PROP_NAME))),
  0
))
 
// Normalize function that receives one inputs: node and
// returns normalized value into a range [MIN_SIZE, MAX_SIZE]
Define(Normalize, Function(n,
  Add(
    MIN_SIZE,
    Mul(
      SIZE_RANGE,
      Div(
        Sub(Property(n, PROP_NAME), MIN_VALUE),
        Sub(MAX_VALUE, MIN_VALUE)
      )
    )
  )
))
 
// For all the nodes with label "Person" and numeric property "age"
@NodeStyle And(HasLabel(node, "Person"), IsNumber(Property(node, PROP_NAME))) {
  // Local variable used to cache a result from function Normalize
  Define(NORM, Normalize(node))
 
  color: white
  size: NORM
  width: Div(NORM, 5)
  label: Format("Age: {}", AsText(Property(node, PROP_NAME)))
}

Main building blocks

The main building blocks of Graph Style Script (GSS) are expressions and directives. GSS files are a sequence of expressions and directives.

Expressions

Expressions are used to combine values to create new values using functions. For example, the expression:

Add(2, 5)
  -> 7

creates a new value 7 from values 2 and 5. There are a lot of functions built into Graph Style Script so there are even more ways to combine values. There is even a function to create new functions.

When expressions are evaluated, values are created. There are several types of Graph Style Script values: Boolean, Color, Number, String, Array, Dictionary, Functionand Null.

An expression can be either literal expressions, name expressions or function applications. Literal expressions exist for Colors, Numbers and Strings.

This is a literal expression for Strings.

"Hello"
  -> Hello

It evaluates to the value "Hello" of the type String. The newline character and double quotes can be escaped in strings using \ (backslash).

"In the end he said: \"I am Iron Man!\""
  -> In the end he said: "I am Iron Man!"

These are literal expressions for Numbers.

123
  -> 123
3.14159
  -> 3.14159

Literal expressions for colors are hex strings starting with '#'. This is a literal expression for the color red.

#ff0000
  -> #ff0000

Name expressions are names that can be evaluated if there are values bound to them in the environment (lexical scope). Names can start with any of the lower case or upper case letters of the English alphabet and apart from those can contain digits and the following characters: -, _. Names can be defined using the Define function.

Define(superhero, "Iron Man")
superhero
  -> Iron Man

In the previous example the value "Iron Man" was bound to the name superhero. After that name expression superhero evaluates the value "Iron Man" to type String.

There are many built-in names that are bound to useful values. Most used are boolean values which are bound to True and False and null value which is bound to Null. Also, all the CSS web colors are bound to their names.

dodgerblue
  -> #1e90ff
forestgreen
  -> #228b22

The third type of expressions are function application expressions. A function can be applied to the list of expressions (arguments) in the following way.

Concat("Agents", " ", "of", " ", "S.H.I.E.L.D.")
  -> Agents of S.H.I.E.L.D.

Here the function Concat was applied to the list of string literal expressions to produce their concatenation. Any expression can be an argument.

Not all expressions have to be evaluated. For example, when calling If function one argument will not be evaluated.

Define(mood, "happy")
Define(name, "Happy Hogan")
If(Equals(mood, "happy"),
   Format("{} is happy today.", name),
   Format("{} is not happy today.", name))
  -> Happy Hogan is happy today.

In the previous example expression Format("{} is not happy today", name) will not be evaluated because its value is not needed.

Some other function will not evaluate their arguments because they are interested in their names and not values. For example, when creating a new function argument names aren't evaluated, but are remembered to be later bound to the function arguments when the function is called.

Define(square, Function(x, Mul(x, x)))
square(2)
  -> 4

In the previous example the name x isn't evaluated in the first line, and neither is the expression Mul(x, x). In the second line when the function square is called number 2 will be bound to the name x and only then will Mul(x, x) be evaluated.

Directives

Directives are the second building block of style script. Directive names start with '@'. The name is followed by the optional expression (filter) which is followed by an opening curly brace, directive body and a closing curly brace. The directive body is a list of pairs of property names and expressions. Property names and expressions are separated by a colon and after every expression, a new line must follow. The directive structure is the following.

@<DirectiveName> <expression> {
  <property-name-1>: <expression-1>
  ...
  ...
  <property-name-n>: <expression-n>
}

Like in CSS, directives defined later override properties of the previous directives.

Graph Style Script currently has four directives:

  • @NodeStyle - for defining the visual style of graph nodes.
  • @EdgeStyle - for defining the visual style of graph relationships.
  • @ViewStyle - for defining the general graph style properties.
  • @ViewStyle.Map - for defining the graph style properties when map is in the background.

An example of a directive is @NodeStyle directive which can be used to specify style properties of a graph node.

@NodeStyle {
  border-width: 2
  color: #abcdef
  label: "Hello, World!"
}

@NodeStyle

The @NodeStyle directive is used for defining style properties of a graph node. It is possible to filter the nodes to which the directive applies by providing an optional predicate after the directive name and before the opening curly brace.

Before any expressions are evaluated (including the predicate) the name node is bound to the graph node for which the directive is being evaluated. Graph node is of type Dictionary and has all information about the node (properties, labels).

Here is an example of a @NodeStyle directive that is applied to all graph nodes with the label superhero:

@NodeStyle HasLabel(node, vehicle) {
  label: Format("{}, horsepower: {}",
                Property(node, "model"),
                Property(node, "horsepower"))
}

The predicate can be any expression that returns a value of type Boolean. It should depend on node, because if it doesn't, it will either be applied to all nodes or to no nodes.

@NodeStyle And(HasProperty(node, "name"),
               Equals(Property(node, "name"), "Tony Stark")) {
  color: gold
  shadow-color: red
  label: "You know who I am"
}

Take a look at the GSS @NodeStyle directive properties page to see all node styling possibilities.

@EdgeStyle

The @EdgeStyle directive is used for defining the style properties of a graph relationship. Most things work like the @NodeStyle directive with one exception: the directive will bind the name edge to the relationship for which the directive is being evaluated (@NodeStyle binds the name node).

Take a look at the GSS @EdgeStyle directive properties page to see all relationship styling possibilities.

@ViewStyle

@ViewStyle directive is used for defining style properties of a general graph view: link distance, view, physics, repel force, etc. It is also possible to use a predicate expression which acts as a filter to apply the defined properties to the final directive output.

@ViewStyle <predicate expression> {
  <property-name-1>: <value expression-1>
  ...
  <property-name-n>: <value expression-n>
}

Similar to @NodeStyle and @EdgeStyle, @ViewStyle has a built-in variable graph which can be used for directive filter or property assignment.

An example below shows a general directive style definition and a directive where style properties will only be applied if there are more than 10 nodes in the graph.

@ViewStyle {
  collision-radius: 15
  physics-enabled: True
}
 
@ViewStyle Greater(NodeCount(graph), 10) {
  physics-enabled: False
  repel-force: -300
}

If there are less than 10 nodes in the graph, the final default graph style properties will be:

{
  "collision-radius": 15,
  "physics-enabled": true
}

Otherwise, if there are more than 10 nodes in the graph, the final default graph style properties will be:

{
  "collision-radius": 15,
  "physics-enabled": false,
  "repel-force": -300
}

Take a look at the GSS @ViewStyle directive properties page to see all styling possibilities.

@ViewStyle.Map

@ViewStyle.Map directive is a subset of @ViewStyle because it defines additional style properties for a graph view when there is a map background. The map view will be available only if:

  • @ViewStyle contains a property view set to value "map".
  • There is at least one node with defined latitude and longitude properties

It is also possible to use a predicate expression which acts as a filter to apply the defined properties to the final directive output.

@ViewStyle.Map <predicate expression> {
  <property-name-1>: <value expression-1>
  ...
  <property-name-n>: <value expression-n>
}

Similar to @ViewStyle, @ViewStyle.Map also has a built-in variable graph which can be used for directive filter or property assignment.

An example below shows a general directive style definition and a directive where style properties will be only applied if there are more than 10 nodes in the graph.

@ViewStyle {
  view: "map"
}
 
@ViewStyle.Map {
  tile-layer: "detailed"
}
 
@ViewStyle.Map Greater(NodeCount(graph), 10) {
  tile-layer: "dark"
}

If there are less than 10 nodes in the graph, the final map graph style properties will be:

{
  "tile-layer": "detailed"
}

Otherwise, if there are more than 10 nodes in the graph, the final map graph style properties will be:

{
  "tile-layer": "dark"
}

Take a look at the GSS @ViewStyle.Map directive properties page to see all styling possibilities.

Built-in functions

Graph Style Script has a large number of built-in functions that can help you with achieving the right style for your graph. Take a look at the list of GSS built-in functions.

Built-in colors

Graph Style Script comes with built-in colors that you can use the color's name. Take a look at the list of built-in colors.

Built-in variables

Graph Style Script has a few built-in variables that you can use: node, edge, and graph. Read more about it in the list of built-in variables.

File Structure

Style script files are composed of expressions and directives. All expressions outside directives are evaluated first in the global environment. This is useful for defining names using function Define. After that @NodeStyle and @EdgeStyle directives are evaluated for each node and relationship, respectively. All the names in the global environment are visible while applying the directives so they can be used for defining property values inside directives.

For example:

// These are the global variables
Define(square, Function(x, Mul(x, x)))
Define(maxAllowedDebt, 10000)
 
@NodeStyle HasLabel(node, "BankUser") {
  // This is a local variable
  Define(nodeDebt, Property(node, "debt"))
 
  size: square(nodeDebt)
  color: If(Greater(nodeDebt, maxAllowedDebt),
            red,
            lightblue)
}

Names square and maxAllowedDebt are visible inside @NodeStyle directive.