Graph Style Script language
Graph Style Script (GSS) is a language created by Memgraph for customizing graph visualization. It is composed of two main elements: expressions and directives. Each directive includes a set of configurable properties, along with their expected data types.
A GSS file is essentially a sequence of the main building blocks and it includes several built-in elements to help define styles:
The following example uses a graph of European countries and cities to illustrate GSS basics.
You can find the dataset here.
- Countries are labeled
Country
, and cities are labeledCity
. - All nodes have a
name
property. - City nodes also include properties like
country
(the country they belong to) anddrinks_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. 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"
}
URLs with country names containing spaces don’t work, so we 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:
@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
}
The code above will use a beer image and a bigger size along with a red shadow.
Normalizing values
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 labeled “Person”has the property
agethat holds the age information of a particular person. We want the node property
size` 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)))
}
Caching results for faster performance
The problem with the example for normalizing values 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)))
}