Working with Graphical Marks
In Vizagrams, every drawing is, in final analysis, just a collection of graphical primitives. These primitives are geometric shapes such as circles, lines, polygons, and so on. A graphical mark is a subtype of the abstract type Mark
for which there exists a function that turns it into a diagram, in other words, a mark is a data type which we can represent as a diagram.
Note that, according to this definition, every primitive shape is itself a mark, since the output diagram is just itself. Hence, when we define a diagram using primitives, this is equivalent to using marks.
1. Introducing Existing Marks
Vizagrams already comes with several graphical marks ready to use. Some of these graphical marks are just the primitive geometric shapes such as circles, lines, polygons, Bezier curves, while others are more complex, such as arrows, faces, and so on.
Let us start by listing all the available marks that come with Vizagrams. Some of them are used internally, hence one does not need to worry about it.
using Vizagrams
using InteractiveUtils
subtypes(Mark)
23-element Vector{Any}:
Area
Arrow
Axis
Bar
BoxPlot
Face
Frame
Grid
Hist
LaTeX
⋮
TextMark
Tick
Title
Trail
Vizagrams.MPrim
Vizagrams.MTDiagram
Vizagrams.TM
XAxis
YAxis
Note that many of the existing marks involve data visualization components, such as ticks, axes, legend and so on. We are not going to use them now, as our goal is not to introduce the data visualization aspects of Vizagram. Let us instead start with some simpler examples:
d = Arrow()
draw(d, height=100)
When we draw an instance of Arrow
, we are drawing a line together with a triangle. The advantage of encapsulating into a mark is that we can specify how to parametrize this mark in order to get the desired behavior. This becomes clearer in the next example:
d = Face() →
Face(eyestyle=S(:fill=>:red)) →
Face(smile=1) →
Face(smilestyle=S(:strokeDasharray=>2),smile=-1) →
Face(headstyle=S(:fill=>:grey))
draw(d, height=100)
Note that the mark Face
has parameters that allows us to modify some of its features, but not everything. The overall "shape" of the face is kept.
We can combine different marks in the same diagram.
angles = 0:π/10:π
d = Face(smile=0.5) +
mapreduce(a->R(a)Arrow(pts=[[1,0],[2,0]],headsize=a/10),+, angles) +
S(:fill=>:grey)T(0,-1.5)*Rectangle(h=1,w=2)
draw(d,height=200)
2. Creating Custom Marks
Besides the marks provided in Vizagrams, users can create their own marks and use them to create diagrams. The process of mark creation can be divided in two steps. First, the user must create a new data type and make it a subtype of Mark
, which is the abstract type provided by Vizagrams. Secondly, the user must define a method for the generic function ζ
(\zeta
). This ζ
method is the one responsible for turning the new mark into a diagram.
import Vizagrams: ζ
struct Tree <: Mark
h #parameter that specifies the height of the tree
end
Tree(;h=2) = Tree(h)
# Specifying how to draw a tree
function ζ(tree::Tree)
# grabbing the height for the tree
height = tree.h
# defining a diagram for the trunk
trunk = S(:fill=>:brown)T(0,height/2)*Rectangle(h=height,w=0.5)
# drawing the leaves
angles = collect(0:0.7:2π)
leaves = S(:fill=>:green)*(
Circle(r=0.5)+
mapreduce(x->T(cos(x)*(0.5),sin(x)*(0.5))Circle(r=0.3),+,angles)
)
# combining the trunk and the leaves to draw the tree
return trunk + T(0,height)leaves
end
d = Tree(h=2) → Tree(h=4)
draw(d)
The power of creating marks is that we can now use them to create other marks. Hence, we can gradually increase the complexity of our marks. Let us use our tree mark to define a forest.
using Random
struct Forest <: Mark
n
end
Forest(;n=2) = Forest(n)
function ζ(m::Forest)
Random.seed!(4)
n = m.n
pos = 2 .* rand(n,2).-1
pos = pos[sortperm(pos[:,2],rev=true),:]
trees = mapreduce(x->T(x...)U(0.1)*Tree(),+, eachrow(pos))
return trees+S(:fillOpacity=>0.1)*(Square(l=2.5))
end
draw(Forest(50))