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
).
I also recommend to install the Source Code Pro fonts (regular and italic),
as I find they look the best among the free options and therefore are used by default.
For more examples see the demo page.
-
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.
You can configure the automatically generated instances like so:
import reftree.core.ToRefTree
case class Tree(size: Int, value: Int, children: List[Tree])
implicit val treeDerivationConfig = (ToRefTree.DerivationConfig[Tree]
.rename("MyTree") // display as “MyTree”
.tweakField("size", _.withName("s")) // label the field “s”, instead of “size”
.tweakField("value", _.withTreeHighlight(true)) // highlight the value
.tweakField("children", _.withoutName)) // do not label the “children” field
implicitly[ToRefTree[Tree]] // auto-derivation will use the configuration above
For something custom, manual derivation is the way to go, for example:
import reftree.core._
implicit def treeInstance: ToRefTree[Tree] = ToRefTree[Tree] { tree =>
RefTree.Ref(tree, Seq(
// display the size as a hexadecimal number (why not?)
RefTree.Val(tree.size).withHint(RefTree.Val.Hex).toField.withName("s"),
// highlight the value
tree.value.refTree.withHighlight(true).toField.withName("value"),
// do not label the children
tree.children.refTree.toField
)).rename("MyTree") // change the name displayed for the class
}
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 += Resolver.bintrayRepo("stanch", "maven")
libraryDependencies += "org.stanch" %% "reftree" % "latest-version"
Contributions are welcome! A few helpful tips:
- To update the documentation or the demo, change the files in
demo/src/main/tut/
and runsbt tutQuick
. Do not update the markdown files at the root of the repository by hand, since they are autogenerated bytut
. - For now the diagrams generated by
sbt tut
andsbt "demo/runMain reftree.demo.All"
are the system tests. Look through them if you are introducing any rendering changes. Note that the animation examples insbt demo/run
are quite demanding in terms of RAM and running time. - On Linux I recommend
eog
andgifview
for viewing PNG and animated GIF respectively.