View Source Scenic.Graph (Scenic v0.11.2)

Please see Graph Overview for a high-level description.

what-is-a-graph

What is a Graph

There are many types of graphs in the field 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 Scenic.Primitive.Group.

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. The first line builds an empty graph with only one group as the root node.

@graph Scenic.Graph.build()

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. You could also do this at runtime, in your init function for example.

The empty graph that is returned from build() is then passed to

|> text( "This is some text", translate: {20, 20} )

This adds a text primitive to the root group. The graph returned from that call is then passed again into

|> 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)

This creates a new group which is filled with several other primitives.

Notice that the anonymous group "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 (see Scenic.Primitive.Style.TextAlign) are added to it. These properties are inherited by the primitives in the group.

inheritance

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.

Styles, however, are NOT inherited by components even though transforms are.

modifying-a-graph

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))

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 .

accessing-primitives

Accessing Primitives

When using a Graph, it is extremely common to access and modify primitives. The 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.

Add to a specified group in a graph.

Compute the bounding box that contains the graph.

Builds and returns an empty graph.

Compile a graph into a script.

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 list of primitives from a graph with a specific id.

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

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

@type bounds() ::
  {left :: number(), top :: number(), right :: number(), bottom :: number()}
@type deferred() :: (t() -> t())
@type t() :: %Scenic.Graph{
  add_to: non_neg_integer(),
  animations: term(),
  ids: map(),
  next_uid: pos_integer(),
  primitives: map()
}

Link to this section Functions

@spec 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 a new Group primitive is added 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 correct place in the new Group.

Note: 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
@spec 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 a new Group primitive is added 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 correct place in the new Group.

Note: All primitives added to a group are appended to the draw order.

Link to this function

add_to(graph, id, builder)

View Source

Add to a specified group in a graph.

Similar to adding a group during graph construction, the add_to function accepts a builder function that adds to a graph under the identified group.

Primitives with the id that are not groups are ignored.

If multiple groups have the given id, then the builder is run against each of them.

@spec bounds(graph :: t()) :: bounds() | nil

Compute the bounding box that contains the graph.

Returns {left, top, right, bottom} or nil if the graph is empty.

@spec 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.

@spec compile(graph :: t()) :: {:ok, Scenic.Script.t()}

Compile a graph into a script.

@spec 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.

@spec count(graph :: t(), id :: any()) :: integer()

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

@spec 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.

@spec find(graph :: t(), (any() -> as_boolean(term()))) :: [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 primitives.

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

@spec get(graph :: t(), id :: any()) :: [Scenic.Primitive.t()]

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

@spec 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.

@spec 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.

@spec 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
@spec 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
@spec 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 how to navigate the tree structure of a Graph.

Link to this function

reduce(graph, id, acc, action)

View Source
@spec 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 how to navigate the tree structure of a Graph.