This project aims to provide visualizations for common functional data structures used in Scala.
The visualizations are generated automatically from code, which allows to use them in an interactive fashion.
To use this library you will need to have GraphViz installed (and have dot
on your PATH
).
For more examples see the materials for my talk “Unzipping Immutability”.
- Pre-made visualizations of many standard collections.
- Automatic visualization of case classes (using shapeless).
case class Employee(
name: String,
salary: Long
)
case class Startup(
name: String,
founder: Employee,
team: List[Employee]
)
- Static images as well as animations can be generated.
- Hassle-free captions (using sourcecode).
This library renders diagrams based on a simple data representation called
RefTree
.
Essentially, a RefTree
denotes either an object (AnyRef
) with a number of fields,
or a primitive (AnyVal
).
To render a value of type A
, you will need an implicit instance of ToRefTree[A]
.
For many Scala collections, as well as case classes, no extra work is needed,
as these instances are readily available or generated on the fly.
For examples of manual instance derivation, see the
contrib
package.
To render diagrams and animations, you will need a Renderer
:
import reftree.render._
import reftree.diagram._
import java.nio.file.Paths
import scala.collection.immutable.Queue
val renderer = Renderer(
renderingOptions = RenderingOptions(density = 75),
directory = Paths.get("images", "usage")
)
You can also pass a format
parameter as a String to the Renderer
constructor
to specify the format you require. The default is png
. You can specify any
file type supported by dot -T
.
There are two ways to use it:
// using the `render` method
renderer.render("queue", Diagram(Queue(1)))
// using syntactic sugar
import renderer._
Diagram(Queue(1)).render("queue")
There are various rendering options you can set, for example:
// using the `render` method
renderer.tweakRendering(_.withVerticalSpacing(2)).render("queue", Diagram(Queue(1)))
// using syntactic sugar
Diagram(Queue(1)).render("queue", _.withVerticalSpacing(2))
Diagrams can be created and combined into bigger diagrams using the following API:
// no caption
Diagram(Queue(1)).render("caption-none")
// automatically set caption to "Queue(1) :+ 2"
Diagram.sourceCodeCaption(Queue(1) :+ 2).render("caption-source")
// use toString to get the caption, i.e. "Queue(1, 2)"
Diagram.toStringCaption(Queue(1) :+ 2).render("caption-tostring")
// merge two diagrams, set captions manually
(Diagram(Queue(1)).withCaption("one") + Diagram(Queue(2)).withCaption("two")).render("one-two")
// isolate each diagram in its own namespace (graph nodes will not be shared across them)
(Diagram(Queue(1)).toNamespace("one") + Diagram(Queue(2)).toNamespace("two")).render("namespaced")
Animation is essentially a sequence of diagrams, which can be rendered to an animated GIF. The simplest way to create an animation is to use the builder API:
(Animation
.startWith(Queue(1))
.iterateWithIndex(2)((queue, i) ⇒ queue :+ (i + 1))
.build()
.render("animation-simple"))
You can also configure how the diagram for each frame is produced:
(Animation
.startWith(Queue(1))
.iterateWithIndex(2)((queue, i) ⇒ queue :+ (i + 1))
.build(Diagram(_).withCaption("My Queue").withColor(2))
.render("animation-captioned-red"))
Note that by default the library will try to reduce the average movement of
all tree nodes across animation frames. Sometimes you want to “anchor”
the root of the data structure instead, to force it to stay still
while everything else is moving. You can achieve this via withAnchor
method:
(Animation
.startWith(Queue(1))
.iterateWithIndex(2)((queue, i) ⇒ queue :+ (i + 1))
.build(Diagram(_).withAnchor("queue").withCaption("This node is anchored!"))
.render("animation-anchored"))
Finally, animations can be combined in sequence or in parallel, for example:
val queue1 = (Animation
.startWith(Queue(1))
.iterateWithIndex(2)((queue, i) ⇒ queue :+ (i + 1))
.build()
.toNamespace("one"))
val queue2 = (Animation
.startWith(Queue(10))
.iterateWithIndex(2)((queue, i) ⇒ queue :+ (10 * (i + 1)))
.build()
.toNamespace("two"))
(queue1 + queue2).render("animation-parallel")
See the materials for my talk “Unzipping Immutability” for more inspiration!
This project is intended for educational purposes and therefore is licensed under GPL 3.0.
To try it interactively:
$ sbt demo
@ render(List(1, 2, 3))
// display diagram.png with your favorite image viewer
You can depend on the library by adding these lines to your build.sbt
(the latest version can be found here:
):
resolvers ++= Seq(
Resolver.bintrayRepo("stanch", "maven"),
Resolver.bintrayRepo("drdozer", "maven")
)
libraryDependencies += "org.stanch" %% "reftree" % "latest-version"