Diagrams User Manual

Contents

Preliminaries

Introduction

diagrams is a flexible, powerful embedded domain-specific language (EDSL) for creating vector graphics. It can be used for creating a wide range of graphics, such as ...

  • A cool example or two

About this document

This document attempts to explain all major aspects of using the diagrams core and standard libraries, organized by topic to make it easy to find what you are looking for. It is not, however, a complete reference of every single function in the standard library: for that, see the API documentation listed under Other resources. Most sections contain links to relevant module(s) you can follow to read about other functions not covered in the text.

Module names in the text are typeset like this: Diagrams.Prelude. Click on a module name to visit its documentation. You can also click on any function or operator name in code examples to take you to its documentation. Try it:

> example = circle 2 ||| unitCircle

Mathematical equations are typeset using MathJax. Right-click on any equation to access MathJax options, like displaying the LaTeX source, switching between MathML and HTML/CSS for display, zoom settings, and so on.

This user manual is still under construction. Content that has yet to be written is noted by a light blue box with a "document" icon on the right hand side, like this:

  • Explain zygohistomorphic prepromorphisms

  • Essay on postmodernist critiques of diagrams vis-a-vis Kant

If you see a box like this in the place of something you would really like to know about, please bug the developers (using the #diagrams IRC channel on Freenode, or the diagrams mailing list) so they can prioritize it!

Warnings, "gotchas", and other important asides are in a red box with a "warning" icon, like this:

Diagrams is extremely addictive and may be hazardous to your health!

You would do well to pay special attention to the contents of such boxes.

Other resources

Here are some other resources that may be helpful to you as you learn about diagrams:

  • The API reference documentation for all the diagrams packages is intended to be high-quality and up-to-date. If you find an omission, error, or something confusing, please report it as a bug!

  • The diagrams website has a gallery of examples and links to tutorials, blog posts, and other documentation.

  • The #diagrams IRC channel on Freenode is a friendly place where you can get help from other diagrams developers and users.

  • Consider joining the diagrams mailing list for discussions and announcements about diagrams.

  • See the developer wiki for more specialized documentation and information on planned and ongoing development.

  • See the bug tracker for a list of open tickets. If you find a bug or would like to request a feature, please file a ticket!

Installation

Before installing diagrams, you will need the following:

If you are on a Mac or Windows, GHC itself comes with the Haskell Platform; if you are on Linux, you will have to install GHC first.

Once you have successfully installed the Haskell platform, installing diagrams should be as easy as issuing the command:

cabal install diagrams

Currently this isn't quite true because of difficulty of installing cairo. Make sure we either have an alternate backend in place OR add more information about installing cairo here before releasing.

Getting started

Create a file called TestDiagram.hs (or whatever you like) with the following contents:

{-# LANGUAGE NoMonomorphismRestriction #-}

import Diagrams.Prelude
import Diagrams.Backend.Cairo.CmdLine

main = defaultMain (circle 1)

The first line turns off the evil monomorphism restriction, which is quite important when using diagrams: otherwise you will quickly run into lots of crazy error messages.

Diagrams.Prelude re-exports most everything from the standard library; Diagrams.Backend.Cairo.CmdLine provides a command-line interface to the cairo rendering backend.

To compile your program, type

$ ghc --make TestDiagram

(Note that the $ indicates a command prompt and should not actually be typed.) Then execute TestDiagram with some appropriate options:

$ ./TestDiagram -w 100 -h 100 -o TestDiagram.png

The above will generate a 100x100 PNG that should look like this:

Try typing

$ ./TestDiagram --help

to see the other options that are supported.

  • Link to the tutorial

  • Change the above for whatever the recommended starter backend is, if it changes

Essential concepts

Before we jump into the main content of the manual, this chapter explains a number of general ideas and central concepts that will recur throughought. If you're eager to skip right to the good stuff, feel free to skip this section at first, and come back to it when necessary; there are many links to this chapter from elsewhere in the manual.

Monoids

A monoid consists of

  • A set of elements \(S\)

  • An associative binary operation on the set, that is, some operation

    \[\oplus \colon S \to S \to S\]

    for which

    \[(x \oplus y) \oplus z = x \oplus (y \oplus z).\]
  • An identity element \(i \in S\) which is the identity for \(\oplus\), that is,

    \[x \oplus i = i \oplus x = x.\]

In Haskell, monoids are expressed using the Monoid type class, defined in Data.Monoid:

> class Monoid m where
> mempty :: m
> mappend :: m -> m -> m

The mappend function represents the associative binary operation, and mempty is the identity element. A function

> mconcat :: Monoid m => [m] -> m

is also provided as a shorthand for the common operation of combining a whole list of elements with mappend.

Since mappend is tediously long to write, diagrams provides the operator (<>) as a synonym. (Hopefully this synonym will soon become part of Data.Monoid itself!)

Monoids are used extensively in diagrams: diagrams, transformations, trails, paths, styles, and colors are all instances.

Faking optional named arguments

Many diagram-related operations can be customized in a wide variety of ways. For example, when creating a regular polygon, one can customize the number of sides, the radius, the orientation, and so on. However, to have a single function that takes all of these options as separate arguments is a real pain: it's hard to remember what the arugments are and what order they should go in, and often one wants to use default values for many of the options and only override a few. Some languages (such as Python) support optional, named function arguments, which are ideal for this sort of situation. Sadly, Haskell does not. However, we can fake it!

Any function which should take some optional, named arguments instead takes a single argument which is a record of options. The record type is declared to be an instance of the Default type class:

> class Default d where
> def :: d

That is, types which have a Default instance have some default value called def. For option records, def is declared to be the record containing all the default arguments. The idea is that you can pass def as an argument to a function which takes a record of options, and use record update syntax to override only the fields you want, like this:

foo (def { arg1 = someValue, arg6 = blah })

There are a couple more things to note. First, record update actually binds more tightly than function application, so the parentheses above are actually not necessary. Second, diagrams also defines with as a synonym for def, which makes the syntax a bit more natural. So, instead of the above, you could write

foo with { arg1 = someValue, arg6 = blah }

Vectors and points

Although much of this user manual focuses on constructing two-dimensional diagrams, the definitions in the core library in fact work for any vector space. Vector spaces are defined in the Data.VectorSpace module from the vector-space package.

Many objects (diagrams, paths, backends...) inherently live in some particular vector space. The vector space associated to any type can be computed by the type function V. So, for example, the type

Foo d => V d -> d -> d

is the type of a two-argument function whose first argument is a vector in whatever vector space corresponds to the type d (which must be an instance of Foo).

Each vector space has a type of vectors v and an associated type of scalars, Scalar v. A vector represents a direction and magnitude, whereas a scalar represents only a magnitude. Important operations on vectors and scalars include:

  • Adding and subtracting vectors with (^+^) and (^-^)

  • Multiplying a vector by a scalar with (*^)

See Data.VectorSpace for other functions and operators.

One might think we could also identify points in a space with vectors having one end at the origin. However, this turns out to be a poor idea. There is a very important difference between vectors and points: namely, vectors are translationally invariant whereas points are not. A vector represents a direction and magnitude, not a location. Translating a vector has no effect. Points, on the other hand, represent a specific location. Translating a point results in a different point.

Although it is a bad idea to conflate vectors and points, we can certainly represent points using vectors. diagrams defines a newtype wrapper around vectors called Point. The most important connection between points and vectors is given by (.-.), defined in Data.AffineSpace. If p1 and p2 are points, p2 .-. p1 is the vector giving the direction and distance from p1 to p2. Offsetting a point by a vector (resulting in a new point) is accomplished with (.+^).

Bounding functions and local vector spaces

In order to be able to position diagrams relative to one another, each diagram must keep track of some bounding information. Rather than use a bounding box (which is neither general nor compositional) or even a more general bounding path (which is rather complicated to deal with), each diagram has an associated bounding function. Given some direction (represented by a vector) as input, the bounding function answers the question: "how far in this direction must one go before reaching a perpendicular (hyper)plane that completely encloses the diagram on one side of it?"

That's a bit of a mouthful, so hopefully the below illustration will help clarify things if you found the above description confusing. (For completeness, the code used to generate the illustration is included, although you certainly aren't expected to understand it yet if you are just reading this manual for the first time!)

> illustrateBound v d
> = mconcat
> [ origin ~~ (origin .+^ v)
> # lc black # lw 0.03
> , polygon with { polyType = PolyRegular 3 0.1
> , polyOrient = OrientTo (negateV v)
> }
> # fc black
> # translate v
> , origin ~~ b
> # lc green # lw 0.05
> , p1 ~~ p2
> # lc red # lw 0.02
> ]
> where
> b = boundary v d
> v' = normalized v
> p1 = b .+^ (rotateBy (1/4) v')
> p2 = b .+^ (rotateBy (-1/4) v')
>
> d1 :: Path R2
> d1 = circlePath 1
>
> d2 :: Path R2
> d2 = (pentagon 1 === roundedRect (1.5,0.7) 0.3)
>
> example = (stroke d1 # showOrigin <> illustrateBound (-0.5,0.3) d1)
> ||| (stroke d2 # showOrigin <> illustrateBound (0.5, 0.2) d2)

The black arrows represent inputs to the bounding functions for the two diagrams; the bounding functions' outputs are the distances represented by the thick green lines. The red lines illustrate the enclosing (hyper)planes (which are really to be thought of as extending infinitely to either side): notice how they are as close as possible to the diagrams without intersecting them at all.

Of course, the base point from which the bounding function is measuring matters quite a lot! If there were no base point, questions of the form "how far do you have to go..." would be meaningless -- how far from where? This base point (indicated by the red dots in the diagram above) is called the local origin of a diagram. Every diagram has its own intrinsic local vector space; operations on diagrams are always with respect to their local origin, and you can affect the way diagrams are combined with one another by moving their local origins. The showOrigin function is provided as a quick way of visualizing the local origin of a diagram (also illustrated above).

Postfix transformation

You will often see idiomatic diagrams code that looks like this:

foobar # attr1
       # attr2
       # attr3
       # transform1

There is nothing magical about (#), and it is not required in order to apply attributes or transformations. In fact, it is nothing more than reverse function application with a high precedence (namely, 8):

x # f = f x

(#) is provided simply because it often reads better to first write down what a diagram is, and then afterwards write down attributes and modifications. Additionally, (#) has a high precedence so it can be used to make "local" modifications without using lots of parentheses:

> example =     square 2 # fc red # rotateBy (1/3)
> ||| circle 1 # lc blue # fc green

Note how the modifiers fc red and rotateBy (1/3) apply only to the square, and lc blue and fc green only to the circle ((|||) has a precedence of 6).

Creating 2D diagrams

  • add some fun diagrams here?

The main purpose of diagrams is to construct two-dimensional vector graphics, although it can be used for more general purposes as well. This section explains the building blocks provided by diagrams-core and diagrams-lib for constructing two-dimensional diagrams.

All 2D-specific things can be found in Diagrams.TwoD, which re-exports most of the contents of Diagrams.TwoD.* modules. This section also covers many things which are not specific to two dimensions; later sections will make clear which are which.

Basic 2D types

Diagrams.TwoD.Types defines types for working with two-dimensional Euclidean space and with angles.

Euclidean 2-space

There are three main type synonyms defined for referring to two-dimensional space:

  • R2 is the type of the two-dimensional Euclidean vector space. It is a synonym for (Double, Double). The positive \(x\)-axis extends to the right, and the positive \(y\)-axis extends upwards. This is consistent with standard mathematical practice, but upside-down with respect to many common graphics systems. This is intentional: the goal is to provide an elegant interface which is abstracted as much as possible from implementation details.

    unitX and unitY are unit vectors in the positive \(x\)- and \(y\)-directions, respectively. Their negated counterparts are unit_X and unit_Y.

  • P2 is the type of points in two-dimensional space. It is a synonym for Point R2. The distinction between points and vectors is important; see Vectors and points.

  • T2 is the type of two-dimensional affine transformations. It is a synonym for Transformation R2.

Angles

The Angle type class classifies types which measure two-dimensional angles. Three instances are provided by default (you can, of course, also make your own):

  • CircleFrac represents fractions of a circle. A value of 1 represents a full turn.

  • Rad represents angles measured in radians. A value of tau (that is, \(\tau = 2 \pi\)) represents a full turn. (If you haven't heard of \(\tau\), see The Tau Manifesto.)

  • Deg represents angles measured in degrees. A value of 360 represents a full turn.

The intention is that to pass an argument to a function that expects a value of some Angle type, you can write something like (3 :: Deg) or (3 :: Rad). The convertAngle function is also provided for converting between different angle representations.

The direction function computes the angle of a vector, measured clockwise from the positive \(x\)-axis.

Primitive shapes

diagrams-lib provides many standard two-dimensional shapes for use in constructing diagrams.

Circles and ellipses

Circles can be created with the unitCircle and circle functions, defined in Diagrams.TwoD.Ellipse.

For example,

> example = circle 0.5 <> unitCircle

unitCircle creates a circle of radius 1 centered at the origin; circle takes the desired radius as an argument.

Every ellipse is the image of the unit circle under some affine transformation, so ellipses can be created by appropriately scaling and rotating circles.

> example = unitCircle # scaleX 0.5 # rotateBy (1/6)

For convenience the standard library also provides ellipse, for creating an ellipse with a given eccentricity, and ellipseXY, for creating an axis-aligned ellipse with specified radii in the x and y directions.

Arcs

Diagrams.TwoD.Arc provides a function arc, which constructs a radius-one circular arc starting at a first angle and extending counterclockwise to the second.

> example = arc (tau/4 :: Rad) (4 * tau / 7 :: Rad)

Pre-defined shapes

Diagrams.TwoD.Shapes provides a number of pre-defined polygons and other path-based shapes. For example:

  • eqTriangle constructs an equilateral triangle with sides of a given length.

  • square constructs a square with a given side length; unitSquare constructs a square with sides of length 1.

  • pentagon, hexagon, ..., dodecagon construct other regular polygons with sides of a given length.

  • In general, regPoly constructs a regular polygon with any number of sides.

  • rect constructs a rectangle of a given width and height.

  • roundedRect constructs a rectangle with circular rounded corners.

> example = square 1 ||| rect 0.3 0.5
> ||| eqTriangle 1 ||| roundedRect (0.7,0.4) 0.1

More special polygons will likely be added in future versions of the library.

Completing the hodgepodge in Diagrams.TwoD.Shapes for now, the functions hrule and vrule create horizontal and vertical lines, respectively.

> example = circle 1 ||| hrule 2 ||| circle 1

General polygons

The polygon function from Diagrams.TwoD.Polygons can be used to construct a wide variety of polygons. Its argument is a record of optional arguments that control the generated polygon:

  • polyType specifies one of several methods for determining the vertices of the polygon:

    • PolyRegular indicates a regular polygon with a certain number of sides and a given radius.

    • PolySides specifies the vertices using a list of angles between edges, and a list of edge lengths.

    • PolyPolar specifies the vertices using polar coordinates: a list of central angles between vertices, and a list of vertex radii.

  • polyOrient specifies the PolyOrientation: the polygon can be oriented with an edge parallel to the \(x\)-axis. with an edge parallel to the \(y\)-axis, or with an edge perpendicular to any given vector. You may also specify that no special orientation should be applied, in which case the first vertex of the polygon will be located along the positive \(x\)-axis.

  • Additionally, a center other than the origin can be specified using polyCenter.

> poly1 = polygon with { polyType   = PolyRegular 13 5
> , polyOrient = OrientV }
> poly2 = polygon with { polyType = PolyPolar (repeat (1/40 :: CircleFrac))
> (take 40 $ cycle [2,7,4,6]) }
> example = (poly1 ||| strutX 1 ||| poly2) # lw 0.05

Notice the idiom of using with to construct a record of default options and selectively overriding particular options by name. with is a synonym for def from the type class Default, which specifies a default value for types which are instances. You can read more about this idiom in the section Faking optional named arguments.

Star polygons

A "star polygon" is a polygon where the edges do not connect consecutive vertices; for example:

> example = star (StarSkip 3) (regPoly 13 1) # stroke

Diagrams.TwoD.Polygons provides the star function for creating star polygons of this sort, although it is actually quite a bit more general.

As its second argument, star expects a list of points. One way to generate a list of points is with polygon-generating functions such as polygon or regPoly, or indeed, any function which can output any PathLike type (see the section about PathLike), since a list of points is an instance of the PathLike class. Of course, you are free to construct the list of points using whatever method you like!

As its first argument, star takes a value of type StarOpts, for which there are two possibilities:

  • StarSkip specifies that every math:n th vertex should be connected by an edge.

    > example = stroke (star (StarSkip 2) (regPoly 8 1))
    > ||| strutX 1
    > ||| stroke (star (StarSkip 3) (regPoly 8 1))

    As you can see, star may result in a path with multiple components, if the argument to StarSkip evenly divides the number of vertices.

  • StarFun takes as an argument a function of type (Int -> Int), which specifies which vertices should be connected to which other vertices. Given the function \(f\), vertex \(i\) is connected to vertex \(j\) if and only if \(f(i) \equiv j \pmod n\), where \(n\) is the number of vertices. This can be used as a compact, precise way of specifying how to connect a set of points (or as a fun way to visualize functions in \(Z_n\)!).

    > funs          = map (flip (^)) [2..6]
    > visualize f = stroke' with { vertexNames = [[0 .. 6 :: Int]] }
    > (regPoly 7 1)
    > # lw 0
    > # showLabels
    > # fontSize 0.6
    > <> star (StarFun f) (regPoly 7 1)
    > # stroke # lw 0.05 # lc red
    > example = centerXY . hcat' with {sep = 0.5} $ map visualize funs

You may notice that all the above examples need to call stroke (or stroke'), which converts a path into a diagram. Many functions similar to star are polymorphic in their return type over any PathLike, but star is not. As we have seen, star may need to construct a path with multiple components, which is not supported by the PathLike class.

Composing diagrams

The diagrams framework is fundamentally compositional: complex diagrams are created by combining simpler diagrams in various ways. Many of the combination methods discussed in this section are defined in Diagrams.Combinators.

Superimposing diagrams with atop

The most fundamental way to combine two diagrams is to place one on top of the other with atop. The diagram d1 `atop` d2 is formed by placing d1's local origin on top of d2's local origin; that is, by identifying their local vector spaces.

> example = circle 1 `atop` square (sqrt 2)

As noted before, diagrams form a monoid with composition given by identification of vector spaces. atop is simply a synonym for mappend (or (<>)), specialized to two dimensions.

This also means that a list of diagrams can be stacked with mconcat; that is, mconcat [d1, d2, d3, ...] is the diagram with d1 on top of d2 on top of d3 on top of...

> example = mconcat [ circle 0.1 # fc green
> , eqTriangle 1 # scale 0.4 # fc yellow
> , square 1 # fc blue
> , circle 1 # fc red
> ]

Juxtaposing diagrams

Fundamentally, atop is actually the only way to compose diagrams; however, there are a number of other combining methods (all ultimately implemented in terms of atop) provided for convenience.

Two diagrams can be placed next to each other using beside. The first argument to beside is a vector specifying a direction. The second and third arguments are diagrams, which are placed next to each other so that the vector points from the first diagram to the second.

> example = beside (20,30) (circle 1 # fc orange) (circle 1.5 # fc purple)
> # showOrigin

As can be seen from the above example, the length of the vector makes no difference, only its direction is taken into account. (To place diagrams at a certain fixed distance from each other, see cat'.) As can also be seen, the local origin of the new, combined diagram is at the point of tangency between the two subdiagrams.

To place diagrams next to each other while leaving the local origin of the combined diagram in the same place as the local origin of the first subdiagram, use append instead of beside:

> example = append (20,30) (circle 1 # fc orange) (circle 1.5 # fc purple)
> # showOrigin

Since placing diagrams next to one another horizontally and vertically is quite common, special combinators are provided for convenience. (|||) and (===) are specializations of beside which juxtapose diagrams in the \(x\)- and \(y\)-directions, respectively.

> d1 = circle 1 # fc red
> d2 = square 1 # fc blue
> example = (d1 ||| d2) ||| strutX 3 ||| ( d1
> ===
> d2 )

See Bounding functions and local vector spaces for more information on what "next to" means, or see Bounding functions for precise details.

Concatenating diagrams

We have already seen one way to combine a list of diagrams, using mconcat to stack them. Several other methods for combining lists of diagrams are also provided in Diagrams.Combinators.

The simplest method of combining multiple diagrams is position, which takes a list of diagrams paired with points, and places the local origin of each diagram at the indicated point.

> example = position (zip (map mkPoint [-3, -2.8 .. 3]) (repeat dot))
> where dot = circle 0.2 # fc black
> mkPoint x = P (x,x^2)

cat is like an iterated version of beside, which takes a direction vector and a list of diagrams, laying out the diagrams beside one another in a row. The local origins of the subdiagrams will be placed along a straight line in the direction of the given vector.

> example = cat (2,-1) (map p [3..8]) # showOrigin
> where p n = regPoly n 1 # lw 0.03

Note, however, that the local origin of the final diagram is placed at the local origin of the first diagram in the list.

For more control over the way in which the diagrams are laid out, use cat', a variant of cat which also takes a CatOpts record. See the documentation for cat' and CatOpts to learn about the various possibilities.

> example = cat' (2,-1) with { catMethod = Distrib, sep = 2 } (map p [3..8])
> where p n = regPoly n 1 # lw 0.03
> # scale (1 + fromIntegral n/4)
> # showOrigin

For convenience, Diagrams.TwoD.Combinators also provides hcat, hcat', vcat, and vcat', variants of cat and cat' which concatenate diagrams horizontally and vertically.

Finally, appends is like an iterated variant of append, with the important difference that multiple diagrams are placed next to a single central diagram without reference to one another; simply iterating append causes each of the previously appended diagrams to be taken into account when deciding where to place the next one.

> c        = circle 1 # lw 0.03
> dirs = iterate (rotateBy (1/7)) unitX
> cdirs = zip dirs (replicate 7 c)
> example1 = appends c cdirs
> example2 = foldl (\a (v,b) -> append v a b) c cdirs
> example = example1 ||| strutX 3 ||| example2

Diagrams.Combinators also provides decoratePath and decorateTrail, which are described in Decorating trails and paths.

Modifying diagrams

Attributes and styles

Every diagram has a style which is an arbitrary collection of attributes. This section will describe some of the default attributes which are provided by the diagrams library and recognized by most backends. However, you can easily create your own attributes as well; for details, see Style and attribute internals.

In many examples, you will see attributes applied to diagrams using the (#) operator. However, keep in mind that there is nothing special about this operator as far as attributes are concerned. It is merely backwards function application, which is used for attributes since it often reads better to have the main diagram come first, followed by modifications to its attributes.

In general, inner attributes (that is, attributes applied earlier) override outer ones. Note, however, that this is not a requirement. Each attribute may define its own specific method for combining multiple instances. See Style and attribute internals for more details.

Most of the attributes discussed in this section are defined in Diagrams.Attributes.

Color

Two-dimensional diagrams have two main colors, the color used to stroke the paths in the diagram and the color used to fill them. These can be set, respectively, with the lc (line color) and fc (fill color) functions.

> example = circle 0.2 # lc purple # fc yellow

By default, diagrams use a black line color and a completely transparent fill color.

Colors themselves are handled by the colour package, which provides a large set of predefined color names as well as many more sophisticated color operations; see its documentation for more information. The colour package uses a different type for colors with an alpha channel (i.e. transparency). To make use of transparent colors you can use lcA and fcA.

> import Data.Colour (withOpacity)
>
> colors = map (blue `withOpacity`) [0.1, 0.2 .. 1.0]
> example = hcat' with { catMethod = Distrib, sep = 1 }
> (zipWith fcA colors (repeat (circle 1)))

Transparency can also be tweaked with the Opacity attribute, which sets the opacity/transparency of a diagram as a whole. Applying opacity p to a diagram, where p is a value between 0 and 1, results in a diagram p times as opaque.

> s c     = square 1 # fc c
> reds = (s darkred ||| s red) === (s pink ||| s indianred)
> example = hcat' with { sep = 1 } . take 4 . iterate (opacity 0.7) $ reds

Line width

To alter the width of the lines used to stroke paths, use lw. The default line width is (arbitrarily) 0.01. You can also set the line width to zero if you do not want a path stroked at all.

Line width actually more subtle than you might think. Suppose you create a diagram consisting of a square, and another square twice as large next to it (using scale 2). How should they be drawn? Should the lines be the same width, or should the larger square use a line twice as thick?

In fact, in many situations the lines should actually be the same thickness, so a collection of shapes will be drawn in a uniform way. This is the default in diagrams. Specifically, the argument to lw is measured with respect to the final vector space of a complete, rendered diagram, not with respect to the local vector space at the time the lw function is applied. Put another way, subsequent transformations do not affect the line width. This is perhaps a bit confusing, but trying to get line widths to look reasonable would be a nightmare otherwise.

> example = (square 1
> ||| square 1 # scale 2
> ||| circle 1 # scaleX 3) # lw 0.03

However, occasionally you do want subsequent transformations to affect line width. The freeze function is supplied for this purpose. Once freeze has been applied to a diagram, any subsequent transformations will affect the line width.

> example = (square 1
> ||| square 1 # freeze # scale 2
> ||| circle 1 # freeze # scaleX 3) # lw 0.03

Note that line width does not affect the bounding function of diagrams at all. Future versions of the standard library may provide a function to convert a stroked path into an actual region, which would allow line width to be taken into account.

Other line parameters

Many rendering backends provide some control over the particular way in which lines are drawn. Currently, diagrams provides support for three aspects of line drawing:

> path = fromVertices (map P [(0,0), (1,0.3), (2,0), (2.2,0.3)]) # lw 0.1
> example = centerXY . vcat' with { sep = 0.1 }
> $ map (path #)
> [ lineCap LineCapButt . lineJoin LineJoinMiter
> , lineCap LineCapRound . lineJoin LineJoinRound
> , lineCap LineCapSquare . lineJoin LineJoinBevel
> , dashing [0.1,0.2,0.3,0.1] 0
> ]

The HasStyle class

Functions such as fc, lc, lw, lineCap, and so on, do not actually take only diagrams as arguments. They take any type which is an instance of the HasStyle type class. Of course, diagrams themselves are an instance.

However, the Style type is also an instance. This is useful in writing functions which offer the caller flexible control over the style of generated diagrams. The general pattern is to take a Style (or several) as an argument, then apply it to a diagram along with some default attributes:

> myFun style = d # applyStyle style # lc red # ...
> where d = ...

This way, any attributes provided by the user in the style argument will override the default attributes specified afterwards.

To call myFun, a user can construct a Style by starting with an empty style (mempty, since Style is an instance of Monoid) and applying the desired attributes:

> foo = myFun (mempty # fontSize 10 # lw 0 # fc green)

If the type T is an instance of HasStyle, then [T] is also. This means that you can apply styles uniformly to entire lists of diagrams at once, which occasionally comes in handy. The function type a -> T is also an instance of HasStyle whenever T is, which comes in handy even more occasionally.

2D Transformations

Any diagram can be transformed by applying arbitrary affine transformations to it. Affine transformations include linear transformations (rotation, scaling, reflection, shears --- anything which leaves the origin fixed and sends lines to lines) as well as translations. Diagrams.TwoD.Transform defines a number of common affine transformations in two-dimensional space. (To construct transformations more directly, see Graphics.Rendering.Diagrams.Transform.)

Every transformation comes in two variants, a noun form and a verb form. For example, there are two functions for scaling along the \(x\)-axis, scalingX and scaleX. The noun form constructs a transformation object, which can then be stored in a data structure, passed as an argument, combined with other transformations, etc., and ultimately applied to a diagram with the transform function. The verb form directly applies the transformation to a diagram. The verb form is much more common (and the documentation below will only discuss verb forms), but getting one's hands on a transformation can occasionally be useful.

Transformations in general

Before looking at specific two-dimensional transformations, it's worth saying a bit about transformations in general (a fuller treatment can be found under Transformations). The Transformation type is defined in Graphics.Rendering.Diagrams.Transform, from the diagrams-core package. Transformation is parameterized by the vector space over which it acts; recall that T2 is provided as a synonym for Transformation R2.

Transformation v is a Monoid for any vector space v:

  • mempty is the identity transformation;

  • mappend is composition of transformations: t1 `mappend` t2 (also written t1 <> t2) performs first t2, then t1.

To invert a transformation, use inv. For any transformation t,

t <> inv t == inv t <> t == mempty.

To apply a transformation to a diagram, use transform.

Rotation

Use rotate to rotate a diagram couterclockwise by a given angle about the origin. Since rotate takes an angle, you must specify an angle type, as in rotate (80 :: Deg). In the common case that you wish to rotate by an angle specified as a certain fraction of a circle, like rotate (1/8 :: CircleFrac), you can use rotateBy instead. rotateBy is specialized to only accept fractions of a circle, so in this example you would only have to write rotateBy
(1/8)
.

You can also use rotateAbout in the case that you want to rotate about some point other than the origin.

> eff = text "F" <> square 1 # lw 0
> rs = map rotateBy [1/7, 2/7 .. 6/7]
> example = hcat . map (eff #) $ rs

Scaling and reflection

Scaling by a given factor is accomplished with scale (which scales uniformly in all directions), scaleX (which scales along the \(x\)-axis only), or scaleY (which scales along the \(y\)-axis only). All of these can be used both for enlarging (with a factor greater than one) and shrinking (with a factor less than one). Using a negative factor results in a reflection (in the case of scaleX and scaleY) or a 180-degree rotation (in the case of scale).

> eff = text "F" <> square 1 # lw 0
> ts = [ scale (1/2), id, scale 2, scaleX 2, scaleY 2
> , scale (-1), scaleX (-1), scaleY (-1)
> ]
>
> example = hcat . map (eff #) $ ts

Scaling by zero is forbidden. Let us never speak of it again.

For convenience, reflectX and reflectY perform reflection along the \(x\)- and \(y\)-axes, respectively; but I think you can guess how they are implemented. Their names can be confusing (does reflectX reflect along the \(x\)-axis or across the \(x\)-axis?) but you can just remember that reflectX = scaleX (-1).

To reflect in some line other than an axis, use reflectAbout.

> eff = text "F" <> square 1 # lw 0
> example = eff
> <> reflectAbout (P (0.2,0.2)) (rotateBy (-1/10) unitX) eff

Translation

Translation is achieved with translate, translateX, and translateY, which should be self-explanatory.

Conjugation

Diagrams.Transform exports useful transformation utilities which are not specific to two dimensions. At the moment there are only two: conjugate and under. The first simply performs conjugation: conjugate t1 t2 == inv t1 <> t2 <> t1, that is, performs t1, then t2, then undoes t1.

under performs a transformation using conjugation. It takes as arguments a function to perform some transformation as well as a transformation to conjugate by. For example, scaling by a factor of 2 along the diagonal line \(y = x\) can be accomplished thus:

> eff = text "F" <> square 1 # lw 0
> example = (scaleX 2 `under` rotation (-1/8 :: CircleFrac)) eff

The letter F is first rotated so that the desired scaling axis lies along the \(x\)-axis; then scaleX is performed; then it is rotated back to its original position.

Note that reflectAbout and rotateAbout are implemented using under.

The Transformable class

Transformations can be applied not just to diagrams, but values of any type which is an instance of the Transformable type class. Instances of Transformable include vectors, points, trails, paths, bounding functions, and Transformations themselves. In addition, lists, maps, or sets of Transformable things are also Transformable in the obvious way.

Alignment

Since diagrams are always combined with respect to their local origins, moving a diagram's local origin affects the way it combines with others. The position of a diagram's local origin is referred to as its alignment.

The functions moveOriginBy and moveOriginTo are provided for explicitly moving a diagram's origin, by an absolute amount and to an absolute location, respectively. moveOriginBy and translate are actually dual, in the sense that

moveOriginBy v === translate (negateV v).

This duality comes about since translate moves a diagram with respect to its origin, whereas moveOriginBy moves the origin with respect to the diagram. Both are provided so that you can use whichever one corresponds to the most natural point of view in a given situation, without having to worry about inserting calls to negateV.

Often, however, one wishes to move a diagram's origin with respect to its bounding function. To this end, some general tools are provided in Diagrams.Align, and specialized 2D-specific ones by Diagrams.TwoD.Align.

Functions like alignT (align Top) and alignBR (align Bottom Right) move the local origin to the edge of the bounding region:

> s = square 1 # fc yellow
> x |-| y = x ||| strutX 0.5 ||| y
> example = (s # showOrigin)
> |-| (s # alignT # showOrigin)
> |-| (s # alignBR # showOrigin)

There are two things to note about the above example. First, notice how alignT and alignBR move the local origin of the square in the way you would expect. Second, notice that when placed "next to" each other using the (|||) operator, the squares are placed so that their local origins fall on a horizontal line.

Functions like alignY allow finer control over the alignment. In the below example, the origin is moved to a series of locations interpolating between the bottom and top of the square:

> s = square 1 # fc yellow
> example = hcat . map showOrigin
> $ zipWith alignY [-1, -0.8 .. 1] (repeat s)

Working with paths

Paths are one of the most fundamental tools in diagrams. They can be used not only directly to draw things, but also as guides to help create and position other diagrams.

Segments

The most basic path component is a Segment, which is some sort of primitive path from one point to another. Segments are translationally invariant; that is, they have no inherent location, and applying a translation to a segment has no effect (however, other sorts of transformations, such as rotations and scales, have the effect you would expect). In other words, a segment is not a way to get from point A to point B; it is a way to get from wherever you are to somewhere else.

Currently, diagrams supports two types of segment, defined in Diagrams.Segment:

  • A linear segment is simply a straight line, defined by an offset from its beginning point to its end point; you can construct one using straight.

  • A Bézier segment is a cubic curve defined by an offset from its beginning to its end, along with two control points; you can construct one using bezier3. An example is shown below, with the endpoints shown in red and the control points in blue. Bézier curves always start off from the beginning point heading towards the first control point, and end up at the final point heading away from the last control point. That is, in any drawing of a Bézier curve like the one below, the curve will be tangent to the two dotted lines.

> illustrateBezier c1 c2 p2
> = endpt
> <> endpt # translate p2
> <> ctrlpt # translate c1
> <> ctrlpt # translate c2
> <> l1
> <> l2
> <> fromSegments [bezier3 c1 c2 p2]
> where
> dashed = dashing [0.1,0.1] 0
> endpt = circle 0.05 # fc red # lw 0
> ctrlpt = circle 0.05 # fc blue # lw 0
> l1 = fromOffsets [c1] # dashed
> l2 = fromOffsets [p2 ^-^ c2] # translate c2 # dashed
>
> p2 = (3,-1) :: R2 -- endpoint
> [c1,c2] = [(1,2), (3,0)] -- control points
>
> example = illustrateBezier c1 c2 p2

Diagrams.Segment also provides a few tools for working with segments:

  • atParam for computing points along a segment;

  • segOffset for computing the offset from the start of a segment to its endpoint;

  • splitAtParam for splitting a segment into two smaller segments;

  • arcLength for approximating the arc length of a segment;

  • arcLengthToParam for approximating the parameter corresponding to a given arc length along the segment; and

  • adjustSegment for extending or shrinking a segment.

Trails

Trails, defined in Diagrams.Path, are essentially lists of segments laid end-to-end. Since segments are translationally invariant, so are trails; that is, trails have no inherent starting location, and translating them has no effect.

Trails can also be open or closed: a closed trail is one with an implicit (linear) segment connecting the endpoint of the trail to the starting point.

To construct a Trail, you can use one of the following:

  • fromSegments takes an explicit list of Segments.

  • fromOffsets takes a list of vectors, and turns each one into a linear segment.

  • fromVertices takes a list of vertices, generating linear segments between them.

  • (~~) creates a simple linear trail between two points.

  • cubicSpline creates a smooth curve passing through a given list of points; it is described in more detail in the section on Splines.

If you look at the types of these functions, you will note that they do not, in fact, return just Trails: they actually return any type which is an instance of PathLike, which includes Trails, Paths (to be covered in the next section), Diagrams, and lists of points. See the PathLike section for more on the PathLike class.

Trails form a Monoid with concatenation as the binary operation, and the empty (no-segment) trail as the identity element. The example below creates a two-segment trail called spike and then constructs a starburst path by concatenating a number of rotated copies. strokeT turns a trail into a diagram, with the start of the trail at the local origin.

> spike :: Trail R2
> spike = fromOffsets [(1,3), (1,-3)]
>
> burst = mconcat . take 13 . iterate (rotateBy (-1/13)) $ spike
>
> example = strokeT burst # fc yellow # lw 0.1 # lc orange

For details on the functions provided for manipulating trails, see the documentation for Diagrams.Path. One other function worth mentioning is explodeTrail, which turns each segment in a trail into its own individual Path. This is useful when you want to construct a trail but then do different things with its individual segments. For example, we could construct the same starburst as above but color the edges individually:

> spike :: Trail R2
> spike = fromOffsets [(1,3), (1,-3)]
>
> burst = mconcat . take 13 . iterate (rotateBy (-1/13)) $ spike
>
> colors = cycle [aqua, orange, deeppink, blueviolet, crimson, darkgreen]
>
> example = lw 0.1
> . mconcat
> . zipWith lc colors
> . map stroke . explodeTrail origin
> $ burst

(If we wanted to fill the starburst with yellow as before, we would have to separately draw another copy of the trail with a line width of zero before exploding it; this is left as an exercise for the reader.)

Paths

A Path, also defined in Diagrams.Path, is a (possibly empty) collection of trails, along with an absolute starting location for each trail. Paths of a single trail can be constructed using the same functions described in the previous section: fromSegments, fromOffsets, fromVertices, (~~), and cubicSpline.

Paths also form a Monoid, but the binary operation is superposition (just like that of diagrams). Paths with multiple components can be used, for example, to create shapes with holes:

> ring :: Path R2
> ring = circlePath 3 <> circlePath 2
>
> example = stroke ring # fc purple # fillRule EvenOdd

(See Fill rules for an explanation of the call to fillRule
EvenOdd
.)

stroke turns a path into a diagram, just as strokeT turns a trail into a diagram. (In fact, strokeT really works by first turning the trail into a path and then calling stroke on the result.)

explodePath, similar to explodeTrail, turns the segments of a path into individual paths. Since a path is a collection of trails, each of which is a sequence of segments, explodePath actually returns a list of lists of paths.

For information on other path manipulation functions such as pathFromTrail, pathFromTrailAt, pathVertices, and pathOffsets, see the documentation in Diagrams.Path.

Decorating trails and paths

Paths (and trails) can be used not just to draw certain shapes, but also as tools for positioning other objects. To this end, diagrams provides decoratePath and decorateTrail, which position a list of objects at the vertices of a given path or trail, respectively.

For example, suppose we want to create an equilateral triangular arrangement of dots. One possibility is to create horizontal rows of dots, center them, and stack them vertically. However, this is annoying, because we must manually compute the proper vertical stacking distance between rows. Whether you think this sounds easy or not, it is certainly going to involve the sqrt function, or perhaps some trig, and we'd rather avoid all that.

Fortunately, there's an easier way: after creating the horizontal rows, we create the path corresponding to the left-hand side of the triangle (which can be done using a simple rotation), and then decorate it with the rows.

> dot = circle 1 # fc black
> mkRow n = hcat' with {sep = 0.5} (replicate n dot)
> mkTri n = decoratePath
> (fromOffsets (replicate (n-1) (2.5 *^ unitX))
> # rotateBy (1/6))
> (map mkRow [n, n-1 .. 1])
> example = mkTri 5

The PathLike class

As you may have noticed by now, a large class of functions in the standard library---such as square, polygon, fromVertices, and so on---generate not just diagrams, but any type which is an instance of the PathLike type class.

Currently, the circle function does not return any instance of the PathLike class! It can only return a diagram. To get any PathLike, use the circlePath function instead. If you find this annoying, you are welcome to fix it.

The PathLike type class has only a single method, pathLike:

> pathLike :: Point (V p)
> -> Bool
> -> [Segment (V p)]
> -> p
  • The first argument is a starting point for the path-like thing; path-like things which are translationally invariant (such as Trails) simply ignore this argument.

  • The second argument indicates whether the path-like thing should be closed.

  • The third argument specifies the segments of the path-like thing.

Currently, there are four instances of PathLike:

  • Trail: as noted before, the implementation of pathLike for Trails ignores the first argument, since Trails have no inherent starting location.

  • Path: of course, pathLike can only construct paths of a single component.

  • Diagram b R2: as long as the backend b knows how to render 2D paths, pathLike can construct a diagram by stroking the generated single-component path.

  • [Point v]: this instance generates the vertices of the path.

It is quite convenient to be able to use, say, square 2 as a diagram, path, trail, or list of vertices, whichever suits one's needs. Otherwise, either four different functions would be needed for each primitive (like square, squarePath, squareTrail, and squareVertices, ugh), or else explicit conversion functions would have to be inserted when you wanted something other than what the square function gave you by default.

As an (admittedly contrived) example, the following diagram defines s as an alias for square 2 and then uses it at all four instances of PathLike:

> s = square 2  -- a squarish thing.
>
> blueSquares = decoratePath s {- 1 -}
> (replicate 4 (s {- 2 -} # scale 0.5) # fc blue)
> paths = lc purple . stroke $ star (StarSkip 2) s {- 3 -}
> plus = centerXY . lc green . strokeT
> . mconcat . take 5 . iterate (rotateBy (1/5))
> $ s {- 4 -}
> example = (blueSquares <> plus <> paths) # lw 0.05

Exercise: figure out which occurrence of s has which type. (Answers below.)

At its best, this type-directed behavior results in a "it just works/do what I mean" experience. However, it can occasionally be confusing, and care is needed. The biggest gotcha occurs when combining a number of shapes using (<>) or mconcat: diagrams, paths, trails, and lists of vertices all have Monoid instances, but they are all different, so the combination of shapes has different semantics depending on which type is inferred.

> ts = mconcat . take 3 . iterate (rotateBy (1/9)) $ eqTriangle 1
> example = (ts ||| stroke ts ||| strokeT ts ||| fromVertices ts) # fc red

The above example defines ts by generating three equilateral triangles offset by 1/9 rotations, then combining them with mconcat. The sneaky thing about this is that ts can have the type of any PathLike instance, and it has completely different meanings depending on which type is chosen. The example uses ts at each of the four PathLike types:

  • Since example is a diagram, the first ts, used by itself, is also a diagram; hence it is interpreted as three equilateral triangle diagrams superimposed on one another with atop.

  • stroke turns Paths into diagrams, so the second ts has type Path R2. Hence it is interpreted as three triangular paths superimposed into one three-component path, which is then stroked.

  • strokeT turns Trails into diagrams, so the third occurrence of ts has type Trail R2. It is thus interpreted as three triangular trails (without the implicit closing segments) sequenced end-to-end into one long trail.

  • The last occurrence of ts is a list of points, namely, the concatenation of the vertices of the three triangles. Turning this into a diagram with fromVertices generates a single-component, open path that visits each of the points in turn. The generated diagram looks passingly similar to the one from the second occurrence of ts, but a careful look reveals that they are quite different.

Of course, one way to avoid all this would be to give ts a specific type annotation, if you know which type you would like it to be. Then using it at a different type will result in a type error, rather than confusing semantics.

Answers to the square 2 type inference challenge:

  1. Path R2

  2. Diagram b R2

  3. [P2]

  4. Trail R2

The Closeable class

Creating closed paths can be accomplished with the close method of the Closeable type class. There is also an open method, which does what you would think. Currently, there are only two instances of Closeable: Trail and Path.

Splines

Constructing Bézier segments by hand is tedious. The Diagrams.CubicSpline module provides the cubicSpline function, which, given a list of points, constructs a smooth curved path passing through each point in turn. The first argument to cubicSpline is a boolean value indicating whether the path should be closed.

> pts = map P [(0,0), (2,3), (5,-2), (-4,1), (0,3)]
> dot = circle 0.2 # fc blue # lw 0
> mkPath closed = position (zip pts (repeat dot))
> <> cubicSpline closed pts # lw 0.05
> example = mkPath False ||| strutX 2 ||| mkPath True

For more control over the generation of curved paths, see the diagrams-spiro package.

Fill rules

There are two main algorithms or "rules" used when determining which areas to fill with color when filling the interior of a path: the winding rule and the even-odd rule. The rule used to draw a path-based diagram can be set with fillRule. For simple, non-self-intersecting paths, determining which points are inside is quite simple, and the two algorithms give the same results. However, for self-intersecting paths, they usually result in different regions being filled.

> loopyStar = fc red
> . mconcat . map (cubicSpline True)
> . pathVertices
> . star (StarSkip 3)
> $ regPoly 7 1
> example = loopyStar # fillRule EvenOdd
> ||| strutX 1
> ||| loopyStar # fillRule Winding
  • The even-odd rule specifies that a point is inside the path if a straight line extended from the point off to infinity (in one direction only) crosses the path an odd number of times. Points with an even number of crossings are outside the path. This rule is simple to implement and works perfectly well for non-self-intersecting paths. For self-intersecting paths, however, it results in a funny pattern of alternatingly filled and unfilled regions, as seen in the above example. Sometimes this pattern is desirable for its own sake.

  • The winding rule specifies that a point is inside the path if its winding number is nonzero. The winding number measures how many times the path "winds" around the point, and can be intuitively computed as follows: imagine yourself standing at the given point, facing some point on the path. You hold one end of an (infinitely stretchy) rope; the other end of the rope is attached to a train sitting at the point on the path at which you are looking. Now the train begins traveling around the path. As it goes, you keep hold of your end of the rope while standing fixed in place, not turning at all. After the train has completed one circuit around the path, look at the rope: if it is wrapped around you some number of times, you are inside the path; if it is not wrapped around you, you are outside the path. More generally, we say that the number of times the rope is wrapped around you (positive for one direction and negative for the other) is the point's winding number.

    Draw a picture of you and the train

    For example, if you stand outside a circle looking at a train traveling around it, the rope will move from side to side as the train goes around the circle, but ultimately will return to exactly the state in which it started. If you are standing inside the circle, however, the rope will end up wrapped around you once.

    For paths with multiple components, the winding number is simply the sum of the winding numbers for the individual components. This means, for example, that "holes" can be created in shapes using a path component traveling in the opposite direction from the outer path.

    This rule does a much better job with self-intersecting paths, and it turns out to be (with some clever optimizations) not much more difficult to implement or inefficient than the even-odd rule.

Clipping

With backends that support clipping, paths can be used to clip other diagrams. Only the portion of a clipped diagram falling inside the clipping path will be drawn. Note that the diagram's bounding function is unaffected.

> example = square 3
> # fc green
> # lw 0.05
> # clipBy (square 3.2 # rotateBy (1/10))

Text

Text objects, defined in Diagrams.TwoD.Text, can be created with the text function.

> example = text "Hello world!" <> rect 8 1

The most important thing to keep in mind when working with text objects is that they take up no space; that is, the bounding function for a text object is constantly zero. If we omitted the rectangle from the above example, there would be no output.

Text objects take up no space!

There are two reasons for this. First, computing the size of some text in a given font is rather complicated, and diagrams cannot (yet) do it natively. The only way it would be able to discover the size of a text object is to query some backend (such as cairo) which knows how to compute it, but this would result in the text function being no longer pure.

The second reason is that font size is handled similarly to line width, so the size of a text object cannot be known at the time of its creation anyway! (Future versions of diagrams may include some sort of constraint-solving engine to be able to handle this sort of situation, but don't hold your breath.) Font size is treated similarly to line width for a similar reason: we often want disparate text elements to be the same size, but those text elements may be part of subdiagrams that have been transformed in various ways.

To set the font size, use the fontSize function; the default font size is (arbitrarily) 1. Remember, however, that the font size is measured in the final vector space of the diagram, rather than in the local vector space in effect at the time of the text's creation.

Other attributes of text can be set using font, bold (or, more generally, fontWeight), italic, and oblique (or, more generally, fontSlant). Text is colored with the current fill color (see Color).

> text' s t = text t # fontSize s <> strutY (s * 1.3)
> example = centerXY $
> text' 10 "Hello" # italic
> === text' 5 "there" # bold # font "freeserif"
> === text' 3 "world" # fc green

The current text support is certainly meagre: planned features for future versions of diagrams include better alignment between text objects placed side-by-side, and the ability to convert text objects to paths.

Images

The Diagrams.TwoD.Image module provides basic support for including external images in diagrams. Simply use the image function and specify a file name and size for the image:

> no = (circle 1 <> hrule 2 # rotateBy (1/8))
> # lw 0.2 # lc red
> example = no <> image "static/phone.png" 1.5 1.5

Unfortunately, you must specify both a width and a height for each image. You might hope to be able to specify just a width or just a height, and have the other dimension computed so as to preserve the image's aspect ratio. However, there is no way for diagrams to query an image's aspect ratio until rendering time, but (until such time as a constraint solver is added) it needs to know the size of the image when composing it with other subdiagrams. Hence, both dimensions must be specified, and for the purposes of positioning relative to other diagrams, the image will be assumed to occupy a rectangle of the given dimensions.

However, note that the image's aspect ratio will be preserved: if you specify dimensions that do not match the actual aspect ratio of the image, blank space will be left in one of the two dimensions to compensate. If you wish to alter an image's aspect ratio, you can do so by scaling nonuniformly with scaleX, scaleY, or something similar.

Currently, the cairo backend can only include images in .png format, but hopefully this will be expanded in the future. Other backends may be able to handle other types of external images.

Working with bounds

The Bounds type, defined in Graphics.Rendering.Diagrams.Bounds, encapsulates bounding functions (see Bounding functions and local vector spaces). Things which have an associated bounding function---including diagrams, segments, trails, and paths---are instances of the Boundable type class.

Bounding functions are used implicitly when placing diagrams next to each other (see Juxtaposing diagrams) or when aligning diagrams (see Alignment). There are also

  • strut creates a diagram which produces no output but takes up the same space as a line segment. There are also versions specialized to two dimensions, strutX and strutY. These functions are useful for putting space in between diagrams.

> example = circle 1 ||| strutX 2 ||| square 2
  • pad increases the bounding function of a diagram by a certain factor in all directions.

> surround d = c === (c ||| d ||| c) # centerXY === c
> where c = circle 0.5
>
> example = surround (square 1) ||| strutX 1
> ||| surround (pad 1.2 $ square 1)

However, the behavior of pad often trips up first-time users of diagrams:

pad expands the bounding function relative to the local origin. So if you want the padding to be equal on all sides, use centerXY first.

For example,

> surround d = c === (c ||| d ||| c) # centerXY === c
> where c = circle 0.5
>
> p = strokeT (square 1)
>
> example = surround (pad 1.2 $ p # showOrigin) ||| strutX 1
> ||| surround (pad 1.2 $ p # centerXY # showOrigin)

Named subdiagrams

  • IsName

  • Giving names to diagrams

  • qualifying names

  • withName etc.

  • idiomatic use of withName etc.

Using queries

  • Queries

  • Using queries with different monoids

Bounding boxes

  • mention bounding box library

Tools for backends

  • lots more stuff goes in this section

Diagrams.Segment exports a FixedSegment type, representing segments which do have an inherent starting location. Trails and paths can be "compiled" into lists of FixedSegments with absolute locations using fixTrail and fixPath. This is of interest to authors of rendering backends that do not support relative drawing commands.

Tips and tricks

Deciphering error messages

Core library

This chapter explains the low-level inner workings of diagrams-core. Casual users of diagrams should not need to read this section (although a quick skim may well turn up something interesting). It is intended more for developers and power users who want to learn how diagrams actually works under the hood.

This section may not get written for a while; yell if you'd like to read it. The more people who yell, the faster it will get done. =)

Vector spaces

The V type function

Points and vectors

Transformations

Bounding functions

Queries

Style and attribute internals

Names

UD-Trees

Backends

The Backend class

The Renderable class

The cairo backend

Other backends

  • SVG

  • postscript

  • TikZ

  • povray

  • OpenGL?