More graphics

More graphics

Julia logos

A couple of functions in Luxor provide you with instant access to the Julia logo, and the three colored circles:

cells = Table([300], [350, 350])

@layer begin
    translate(cells[1])
    translate(-165, -114)
    rulers()
    julialogo()
end

@layer begin
    translate(cells[2])
    translate(-165, -114)
    rulers()
    julialogo(action=:clip)
    for i in 1:500
        @layer begin
            translate(rand(0:400), rand(0:250))
            juliacircles(10)
        end
    end
    clipreset()
    end

get path

Function.
julialogo(;
    action=:fill,
    color=true,
    bodycolor=colorant"black")

Draw the Julia logo. The default action is to fill the logo and use the colors:

julialogo()

If color is false, the bodycolor color is used for the logo..

The logo can be difficult to position well due to its asymmetric design. The 0/0 point is at the top left corner, the total width is 315pt, the total height is 214pts. The optical center is somewhere between 163-180pts in x, and 96-114pts in y. The gap to the left edge of "j"s descender is 16; the distance between the left edge of the "j" (not the descender) and the right edge of the "a" is at 268pts.

So, to place the logo by locating its center at a point, do this:

gsave()
# scale() first if required
translate(-165, -114) # anything between (x: -163 to -180, y: -96 to -114)
julialogo()
grestore()

To use the logo as a clipping mask:

julialogo(action=:clip)

(In this case the color setting is automatically ignored.)

Luxor.juliacirclesFunction.
juliacircles(radius=100)

Draw the three Julia circles ("dots") in color centered at the origin.

The distance of the centers of each circle from the origin is radius.

The optional keyword argument outercircleratio (default 0.75) determines the radius of each circle relative to the main radius. So the default is to draw circles of radius 75 points around a larger circle of radius 100.

Return the three centerpoints.

The innercircleratio (default 0.65) no longer does anything useful (it used to draw the smaller circles) and will be deprecated.

Hypotrochoids

hypotrochoid() makes hypotrochoids. The result is a polygon. You can either draw it directly, or pass it on for further polygon fun, as here, which uses offsetpoly() to trace round it a few times.

origin()
background("grey15")
sethue("antiquewhite")
setline(1)
p = hypotrochoid(100, 25, 55, :stroke, stepby=0.01, vertices=true)
for i in 0:3:15
    poly(offsetpoly(p, i), :stroke, close=true)
end

hypotrochoid

There's a matching epitrochoid() function.

Luxor.hypotrochoidFunction.
hypotrochoid(R, r, d, action=:none;
        stepby=0.01,
        period=0.0,
        vertices=false)

Make a hypotrochoid with short line segments. (Like a Spirograph.) The curve is traced by a point attached to a circle of radius r rolling around the inside of a fixed circle of radius R, where the point is a distance d from the center of the interior circle. Things get interesting if you supply non-integral values.

Special cases include the hypocycloid, if d = r, and an ellipse, if R = 2r.

stepby, the angular step value, controls the amount of detail, ie the smoothness of the polygon,

If period is not supplied, or 0, the lowest period is calculated for you.

The function can return a polygon (a list of points), or draw the points directly using the supplied action. If the points are drawn, the function returns a tuple showing how many points were drawn and what the period was (as a multiple of pi).

Luxor.epitrochoidFunction.
epitrochoid(R, r, d, action=:none;
        stepby=0.01,
        period=0,
        vertices=false)

Make a epitrochoid with short line segments. (Like a Spirograph.) The curve is traced by a point attached to a circle of radius r rolling around the outside of a fixed circle of radius R, where the point is a distance d from the center of the circle. Things get interesting if you supply non-integral values.

stepby, the angular step value, controls the amount of detail, ie the smoothness of the polygon.

If period is not supplied, or 0, the lowest period is calculated for you.

The function can return a polygon (a list of points), or draw the points directly using the supplied action. If the points are drawn, the function returns a tuple showing how many points were drawn and what the period was (as a multiple of pi).

Cropmarks

If you want cropmarks (aka trim marks), use the cropmarks() function, supplying the centerpoint, followed by the width and height:

cropmarks(O, 1200, 1600)
cropmarks(O, paper_sizes["A0"]...)
sethue("red")
box(O, 150, 150, :stroke)
cropmarks(O, 150, 150)

cropmarks

Luxor.cropmarksFunction.
cropmarks(center, width, height)

Draw cropmarks (also known as trim marks).

Dimensioning

Simple dimensioning graphics can be generated with dimension(). To convert from the default unit (PostScript points), or to modify the dimensioning text, supply a function to the format keyword argument.

dimensioning

setline(0.75)
sethue("purple")
pentagon = ngonside(O, 120, 5, vertices=true)
poly(pentagon, :stroke, close=true)
circle.(pentagon, 2, :fill)
fontsize(6)
label.(split("12345", ""), :NE, pentagon)
fontface("Menlo")
fontsize(10)
sethue("grey30")

dimension(O, pentagon[4],
    fromextension = [0, 0])

dimension(pentagon[1], pentagon[2],
    offset        = -60,
    fromextension = [20, 50],
    toextension   = [20, 50],
    textrotation  = 2π/5,
    textgap       = 20,
    format        = (d) -> string(round(d, digits=4), "pts"))

dimension(pentagon[2], pentagon[3],
     offset        = -40,
     format        =  string)

dimension(pentagon[5], Point(pentagon[5].x, pentagon[4].y),
    offset        = 60,
    format        = (d) -> string("approximately ",round(d, digits=4)),
    fromextension = [5, 5],
    toextension   = [80, 5])

dimension(pentagon[1], midpoint(pentagon[1], pentagon[5]),
    offset               = 70,
    fromextension        = [65, -5],
    toextension          = [65, -5],
    texthorizontaloffset = -5,
    arrowheadlength      = 5,
    format               = (d) ->
        begin
            if isapprox(d, 60.0)
                string("exactly ", round(d, digits=4), "pts")
            else
                string("≈ ", round(d, digits=4), "pts")
            end
        end)

dimension(pentagon[1], pentagon[5],
    offset               = 120,
    fromextension        = [5, 5],
    toextension          = [115, 5],
    textverticaloffset   = 0.5,
    texthorizontaloffset = 0,
    textgap              = 5)
Luxor.dimensionFunction.
dimension(p1::Point, p2::Point;
    format::Function   = (d) -> string(d), # process the measured value into a string
    offset             = 0.0,              # left/right, parallel with x axis
    fromextension      = (10.0, 10.0),     # length of extensions lines left and right
    toextension        = (10.0, 10.0),     #
    textverticaloffset = 0.0,              # range 1.0 (top) to -1.0 (bottom)
    texthorizontaloffset = 0.0,            # range 1.0 (top) to -1.0 (bottom)
    textgap            = 5,                # gap between start of each arrow (≈ fontsize?)
    textrotation       = 0.0,
    arrowlinewidth     = 1.0,
    arrowheadlength    = 10,
    arrowheadangle     = π/8)

Calculate and draw dimensioning graphics for the distance between p1 and p2. The value can be formatted with function format.

p1 is the lower on the page (ie probably the higher y value) point, p2 is the higher on the page (ie probably lower y) point.

offset is to the left (-x) when negative.

Dimension graphics will be rotated to align with a line between p1 and p2.

In textverticaloffset, "vertical" and "horizontal" are best understood by "looking" along the line from the first point to the second. textverticaloffset ranges from -1 to 1, texthorizontaloffset in default units.

        toextension
        [5  ,  5]
       <---> <--->
                             to
       -----------            +
            ^
            |

           -50

            |
            v
       ----------            +
                            from
       <---> <--->
         [5 , 5]
       fromextension

            <---------------->
                  offset

Returns the measured distance and the text.

Bars

For simple bars, use the bars() function, supplying an array of numbers:

fontsize(7)
sethue("black")
v = rand(-100:100, 25)
bars(v)

bars

To change the way the bars and labels are drawn, define some functions and pass them as keyword arguments to bars():

function mybarfunction(low::Point, high::Point, value;
    extremes=[0, 1], barnumber=0, bartotal=0)
    @layer begin
        sethue(Colors.HSB(rescale(value, extremes[1], extremes[2], 0, 360), 1.0, 0.5))
        csize = rescale(value, extremes[1], extremes[2], 5, 25)
        circle(high, csize, :fill)
        setline(1)
        sethue("blue")
        line(Point(low.x, 0), high + (0, csize), :stroke)
        sethue("white")
        text(string(value), high, halign=:center, valign=:middle)
    end
end

function mylabelfunction(low::Point, high::Point, value;
    extremes=[0, 1], barnumber=0, bartotal=0)
    @layer begin
        translate(low)
        text(string(value), O + (0, 10), halign=:center, valign=:middle)
    end
end

v = rand(1:100, 25)
bars(v, xwidth=25, barfunction=mybarfunction, labelfunction=mylabelfunction)

bars 1

Luxor.barsFunction.
bars(values::Array;
        yheight = 200,
        xwidth = 25,
        labels = true,
        barfunction = f,
        labelfunction = f,
    )

Draw some bars where each bar is the height of a value in the array. The bars will fit in a box yheight high (even if there are negative values).

To control the drawing of the text and bars, define functions that process the end points:

mybarfunction(bottom::Point, top::Point, value; extremes=[a, b], barnumber=0, bartotal=0)

mylabelfunction(bottom::Point, top::Point, value; extremes=[a, b], barnumber=0, bartotal=0)

and pass them like this:

bars(v, yheight=10, xwidth=10, barfunction=mybarfunction)
bars(v, xwidth=15, yheight=10, labelfunction=mylabelfunction)

or:

bars(v, labelfunction = (args...; extremes=[], barnumber=0, bartotal=0) ->  setgray(rand()))

To suppress the text labels, use optional keyword labels=false.

Box maps

The boxmap() function divides a rectangular area into a sorted arrangement of smaller boxes or tiles based on the values of elements in an array.

This example uses the Fibonacci sequence to determine the area of the boxes. Notice that the values are sorted in reverse, and are scaled to fit in the available area.

fib = [1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144]

# make a boxmap and store the tiles
tiles = boxmap(fib, BoundingBox()[1], 800, 450)

for (n, t) in enumerate(tiles)
    randomhue()
    bb = BoundingBox(t)
    sethue("black")
    box(bb - 5, :stroke)

    randomhue()
    box(bb - 8, :fill)

    # text labels
    sethue("white")

    # rescale text to fit better
    fontsize(boxwidth(bb) > boxheight(bb) ? boxheight(bb)/4 : boxwidth(bb)/4)
    text(string(sort(fib, rev=true)[n]),
        midpoint(bb[1], bb[2]),
        halign=:center,
            valign=:middle)
end

boxmap

Luxor.boxmapFunction.
boxmap(A::Array, pt, w, h)

Build a box map of the values in A with one corner at pt and width w and height h. There are length(A) boxes. The areas of the boxes are proportional to the original values, scaled as necessary.

The return value is an array of BoxmapTiles. For example:

[BoxmapTile(0.0, 0.0, 10.0, 20.0)
 BoxmapTile(10.0, 0.0, 10.0, 13.3333)
 BoxmapTile(10.0, 13.3333, 10.0, 6.66667)]

with each tile containing (x, y, w, h). box() and BoundingBox() can work with BoxmapTiles as well.

Example

using Luxor
@svg begin
    fontsize(16)
    fontface("HelveticaBold")
    pt = Point(-200, -200)
    a = rand(10:200, 15)
    tiles = boxmap(a, Point(-200, -200), 400, 400)
    for (n, t) in enumerate(tiles)
        randomhue()
        bb = BoundingBox(t)
        box(bb - 2, :stroke)
        box(bb - 5, :fill)
        sethue("white")
        text(string(n), midpoint(bb[1], bb[2]), halign=:center)
    end
end 400 400 "/tmp/boxmap.svg"