Scenic v0.9.0 Scenic.Graph View Source

Please see Graph Overview for a high-level description.

What is a Graph

There are many types of graphs in the world of computer science. There are graphs that show data to a user. There are graphs that give access to databases. Graphs that link people to a social network.

In Scenic, a Graph is a graph in same way that the DOM in HTML is a graph. It is a hierarchical tree of data that describes a set of things to draw on the screen.

You build a graph by appending primitives (individual things to draw) to the current node in the tree. Nodes are represented by the Group primitive in Scenic.

The following example builds a simple graph that displays some text, creates a group, then adds more text and a rounded rectangle to it.

@graph  Scenic.Graph.build()
|> text( "This is some text", translate: {20, 20} )
|> group( fn(graph) ->
  graph
  |> text( "This text is in a group", translate: {200, 24} )
  |> rounded_rectangle( {400, 30}, stroke: {2, :blue})
end, translate: {20, 100}, text_align: :center)

There is a fair amount going on in the example above, we we will break it down. The first line

@graph  Scenic.Graph.build()

builds an empty graph with only one group as the root node. By assigning it to the compile directive @group, we know that this group will be built once at compile time and will be very fast to access later during runtime.

The empty graph that is returned from build() is then passed to text(...), which adds a text primitive to the root group. The resulting graph from that call is then passed again into the group(...) call. This creates a new group and has calls an anonymous function that you can use to add primitives to the newly created group.

Notice that the anonymous “builder” function receives a graph as its only parameter. This is the same graph that we are building, except that it has a marker indicating that new primitives added to it are inserted into the new group instead of the root of the graph.

Finally, when the group is finished, a translation matrix and a :text_align style are added to it. These properties are inherited by the primitives in the group.

Inheritance

An important concept to understand is that both styles and transforms are inherited down the graph. This means that if you apply a style or transform to any group (including the root), then all primitives contained by that group will have those properties applied to them too. This is true even if the primitive is nested in several groups at the same time.

@graph  Scenic.Graph.build(font: :roboto_mono)
|> text( "This text inherits the font", translate: {20, 20} )
|> group( fn(graph) ->
  graph
  |> text( "This text also inherits the font", translate: {200, 24} )
  |> text( "This text overrides the font", font: :roboto )
end, translate: {20, 100}, text_align: :center)

Transforms, such as translate, rotate, scale, also inherit down the graph, but do so slightly differently than the styles. With a style, when you set a specific value on a primitive, that overrides the inherited value of the same type.

With a transform, the values multiply together. This allows you to position items within a group relative to the origin {0,0}, then move the group as a whole, keeping the interior positions unchanged.

Modifying a Graph

Scenic was written specifically for Erlang/Elixir, which is a functional programming model with immutable data.

As such, once you make a graph, it stays in memory unchanged - until you transform it via Graph.modify/3. Technically you never change it (that’s the immutable part), instead Graph.modify returns a new graph with different data in it.

Graph.modify/3 is the single Graph function that you will use the most.

For example, lets go back to our graph with the two text items in it.

@graph Graph.build(font: :roboto, font_size: 24, rotate: 0.4)
  |> text("Hello World", translate: {300, 300}, id: :small_text)
  |> text("Bigger Hello", font_size: 40, scale: 1.5, id: :big_text)

This time, we’ve assigned ids to both of the text primitives. This makes it easy to find and modify that primitive in the graph.

graph =
  @graph
  |> Graph.modify( :small_text, &text(&1, "Smaller Hello", font_size: 16))
  |> Graph.modify( :big_text, &text(&1, "Bigger Hello", font_size: 60))
  |> push_graph()

Notice that the graph is modified multiple times in the pipeline. The push_graph/1 function is relatively heavy when the graph references other scenes. The recommended pattern is to make multiple changes to the graph and then push once at the end.

Accessing Primitives

When using a Graph, it is extremely common to access and modify primitives. They way you do this is by putting an id on the primitives you care about in a graph.

@graph Graph.build()
  |> text("small text", id: :small_text)
  |> text("bit text", id: :big_text)

When you get primitives, or modify a graph, you specify them by id. This happens quickly, but at a cost of using a little memory. If you aren’t going to access a primitive, then don’t assign an id to them.

One interesting note: There is nothing saying you have to use an atom as the id. In fact you can use any Erlang term you want. This can be very powerful, especially when used to identify components…

Link to this section Summary

Functions

Add a pre-built primitive to the current group in the graph

Build and add a primitive to the current group in the graph

Builds and returns an empty graph

Returns a count of all the primitives in a graph

Returns a count of all the primitives in a graph with a specific id

Permanently delete a primitive from a group by id

Find one or more primitives in a graph via a filter function

Returns a single primitive from a graph with a specific id

Returns a list of primitives from a graph with a specific id

get_root(graph) deprecated

Returns the root group of a graph as a primitive. Deprecated. Use Graph.get!(graph, :_root_) instead

Map all primitives in a graph into a new graph

Map all primitives in a graph that match a specified id into a new graph

Modify one or more primitives in a graph

Invokes action for each primitive in the graph with the accumulator

Invokes action for each primitive that matches an id in the graph with the accumulator

Link to this section Types

Link to this type t() View Source
t() :: %Scenic.Graph{
  add_to: non_neg_integer(),
  ids: map(),
  next_uid: pos_integer(),
  primitives: map()
}

Link to this section Functions

Link to this function add(graph, primitive) View Source
add(graph :: t(), primitive :: Scenic.Primitive.t()) :: t()

Add a pre-built primitive to the current group in the graph.

This is usually called during graph construction. When add a new Group primitive to a Graph, it marks the new group as the current one before calling the group’s builder function. This is what allows you to add primitives to the right place in the new Group.

Note that all primitives added to a group are appended to the draw order.

Link to this function add(graph, primitive_module, primitive_data, opts \\ []) View Source
add(graph :: t(), module :: atom(), data :: any(), opts :: keyword()) ::
  t()

Build and add a primitive to the current group in the graph.

This is usually called during graph construction. When add a new Group primitive to a Graph, it marks the new group as the current one before calling the group’s builder function. This is what allows you to add primitives to the right place in the new Group.

Note that all primitives added to a group are appended to the draw order.

Link to this function build(opts \\ []) View Source
build(opts :: keyword()) :: t()

Builds and returns an empty graph.

Just like any primitive, you can pass in an option list of styles and transforms. These will be applied to the otherwise empty root group in the new graph.

Link to this function count(graph) View Source
count(graph :: t()) :: integer()

Returns a count of all the primitives in a graph.

The root Group counts as a primitive, so an empty graph should have a count of 1.

Link to this function count(graph, id) View Source
count(graph :: t(), id :: any()) :: integer()

Returns a count of all the primitives in a graph with a specific id.

Link to this function delete(graph, id) View Source
delete(graph :: t(), id :: any()) :: t()

Permanently delete a primitive from a group by id.

This will remove a primitive (or many if they have the same id) from a graph. It then returns the modified graph.

If you delete a group from a graph, then all primitives contained by that group are deleted as well.

Link to this function find(graph, finder) View Source
find(graph :: t(), (any() -> as_boolean(term()))) :: [
  {any(), Scenic.Primitive.t()}
]

Find one or more primitives in a graph via a filter function.

Pass in a function that accepts a primitive and returns a boolean.

Returns a list of tuples containing the matching id at the primitive.

[{id, primitive}]

Warning: This function crawls the entire graph and is thus slower than accessing items via a fully-specified id.

Link to this function get!(graph, id) View Source
get!(graph :: t(), id :: any()) :: Scenic.Primitive.t()

Returns a single primitive from a graph with a specific id.

This will raise an error if either none or multiple primitives are found with the specified id.

Link to this function get(graph, id) View Source
get(graph :: t(), id :: any()) :: [Scenic.Primitive.t()]

Returns a list of primitives from a graph with a specific id.

Link to this function get_root(graph) View Source
get_root(graph :: t()) :: Scenic.Primitive.t()
This function is deprecated. Use Graph.get!(graph, :_root_) instead.

Returns the root group of a graph as a primitive. Deprecated. Use Graph.get!(graph, :_root_) instead.

Link to this function map(graph, action) View Source
map(graph :: t(), action :: function()) :: t()

Map all primitives in a graph into a new graph.

Crawls through the entire graph, passing each primitive to the callback function. The result of the callback replaces that primitive in the graph. The updated graph is returned.

Link to this function map(graph, id, action) View Source
map(graph :: t(), id :: any(), action :: function()) :: t()

Map all primitives in a graph that match a specified id into a new graph.

Crawls through the entire graph, passing each primitive to the callback function. The result of the callback replaces that primitive in the graph. The updated graph is returned.

This is so similar to the modify function that it may be deprecated in the future. For now I recommend you use Graph.modify/3 instead of this.

Link to this function modify(graph, id, action) View Source
modify(
  graph :: t(),
  id :: any() | (any() -> as_boolean(term())),
  action :: (any() -> Scenic.Primitive.t())
) :: t()

Modify one or more primitives in a graph.

Retrieves the primitive (or primitives) specified by id and passes them to a callback function. The result of the callback function is stored as the new version of that primitive in the graph.

If multiple primitives match the specified id, then each is passed, in turn, to the callback function.

The id can be either

  • a term to match against (fast)
  • a filter function that returns a boolean (slower)

Examples:

graph
|> Graph.modify( :explicit_id, &text("Updated Text 1") )
|> Graph.modify( {:id, 123}, &text("Updated Text 2") )
|> Graph.modify( &match?({:id,_},&1), &text("Updated Text 3") )
Link to this function reduce(graph, acc, action) View Source
reduce(graph :: t(), acc :: any(), action :: function()) :: any()

Invokes action for each primitive in the graph with the accumulator.

Iterates over all primitives in a graph, passing each into the callback function with an accumulator. The return value of the callback is the new accumulator.

This is extremely similar in behaviour to Elixir’s Enum.reduce function, except that it understands now to navigate the tree structure of a Graph.

Link to this function reduce(graph, id, acc, action) View Source
reduce(graph :: t(), id :: any(), acc :: any(), action :: function()) ::
  any()

Invokes action for each primitive that matches an id in the graph with the accumulator.

Iterates over all primitives that match a specified id, passing each into the callback function with an accumulator.

This is extremely similar in behaviour to Elixir’s Enum.reduce function, except that it understands now to navigate the tree structure of a Graph.