Functions

This section describes the drawing functions that are the main purpose of bytefield-svg. They are designed to flexibly draw the elements you might need in a byte field diagram. But they are also intended to be combined with the functions in the Clojure core library, and you can dive down to the Analemma SVG building functions used by these functions if you need to draw something unique.

append-svg

Most people will not need to use this function.

Adds an element to the end of the SVG document being created.

Arguments
[element]

This function is only needed if you are using the lower-level Analemma SVG functions to draw custom SVG content. Call this function with the Clojure data structure returned by the SVG function and it will become part of the diagram.

It’s beyond the current scope of these instructions to try to explain Analemma, so you will need to study its own documentation and source if you want to engage in this kind of low-level drawing. But as a small example, you could draw a circle at coordinates (20, 10) with radius 5 by calling:

(append-svg (svg/circle 20 10 5))

The analemma.svg functions available in bytefield-svg are all organized under the svg namespace alias, and include: svg/add-style, svg/animate, svg/animate-color, svg/animate-motion, svg/animate-transform, svg/circle, svg/defs, svg/draw, svg/ellipse, svg/group, svg/image, svg/line, svg/parse-inline-css, svg/path, svg/polygon, svg/rect, svg/rgb, svg/rotate, svg/style, svg/style-map, svg/svg, svg/text, svg/text-path, svg/transform, svg/translate, svg/translate-value, svg/tref, and svg/tspan.

You can also manipulate the hiccup-like structures returned by these functions using the even-lower level Analemma XML functions. These are organized under the xml namespace alias, and include: xml/add-attrs, xml/add-content, xml/emit, xml/emit-attrs, xml/emit-tag, xml/get-attrs, xml/get-content, xml/get-name, xml/has-attrs?, xml/has-content?, xml/merge-attrs, xml/set-attrs, xml/set-content, and xml/update-attrs.

If you understand the structures built and used by Analemma, you can also build them directly yourself and pass them to append-svg.

defattrs

Register a attribute map for later use in your diagram.

Arguments
[k m]
By convention, in Clojure arguments, k means “a keyword” and m means “a map”.

To add a new named attribute or update one of the predefined attributes, pass the keyword you want to define as the first argument, and the map of attributes that you want that keyword to represent as the second argument.

For example, if you want to have some of your byte boxes have a green background, you could make :bg-green a named attribute that achieves that by calling:

(defattrs :bg-green {:fill "#a0ffa0"})

From then on, you can use :bg-green in any attribute expression to stand in for this fill color.

draw-bottom

Close the bottom of a gap drawn by draw-gap.

Arguments

none

This function isn’t needed if you are continuing to draw enough boxes after your gap to span an entire row, because the top borders of those boxes will draw the bottom of the gap. But if your diagram ends after the gap, or after a partial row after the gap, you will want to call (draw-bottom) to draw the line across the bottom of the gap.

This isn’t done automatically because some diagrams want the gap to extend into some of the boxes of the line after the gap, which can be achieved by setting those boxes to not have a top border, but that only works if the gap doesn’t close itself.

draw-box

This is probably the most-used function in bytefield-svg. It draws the next cell in your byte field diagram.

If the previous box completed a row, drawing this new box will advance to the start of the next row, and draw the row header.

Arguments
[label]
[label attr-spec]

If you call draw-box with just a label, it will draw a box with a default set of attributes that contains that label. If you want to change the way the box is drawn, you can pass an attribute expression as the second argument (see below).

Label Styles

If you don’t want a label in the box, you can pass nil for label.

If you pass a string, it is rendered as text, as if you had passed the result of calling (text label).

If you pass a number, it is rendered in hexadecimal, with enough digits to represent all the bytes spanned by your box (see the discussion of the :span attribute below).

If you need a label with more complex structure or styling you can build it by calling text or hex-text yourself and passing the results as label.

Or you can draw arbitrary SVG content in the box by passing a custom label function as label. Your function will be called with four arguments, the left and top coordinates of the box, and its width and height. (This is one situation where you might use append-svg.)

This example uses a custom label function to draw two lines in the box, from the top left to the bottom right, and the top right to the bottom left:

(draw-box (fn [left top width height]
            (draw-line left top (+ left width) (+ top height))
            (draw-line left (+ top height) (+ left width) top)))

And here’s what it looks like repeated over a four-box row:

0123

Box Attributes

You can modify the box that is drawn by passing in the following attributes:

Attribute Default Value Meaning

:borders

#{:left :right :top :bottom}

Controls which box borders are drawn, and optionally, their individual attributes. See below for more details.

:fill

nil

The fill color to use as the box background.

:height

row-height

If you pass a value here you can override the height of the box. Normally it is controlled by the predefined value row-height.

:span

1

The number of bytes (columns) this box will occupy. You can supply a :span value ranging from 1 to the remaining columns in the row. If you try to go beyond the end of the row, an exception will be thrown.

Here are some sample boxes:

(draw-box 1)
(draw-box "two" {:span 2})
(draw-box nil {:fill "#a0ffa0"})
01two

And as a concrete example of how we can use defattrs to set up a named attribute making it concise to use later:

(defattrs :bg-blue {:fill "#80a0ff"})
(draw-box "b" :bg-blue)
b

When the keyword :bg-blue is found as a standalone attribute expression, it is looked up in the named attribtues, and the fill that we set up with defattrs is found and used.

Box Borders

As noted above, by default a box is drawn with all four borders (left, right, top, and bottom). To change that, you can pass a Clojure set containing a subset of the keywords :left, :right, :top, and :bottom, and only the borders that you include will be drawn.

If you want even more control, rather than a set you can pass a :map, whose keys are the keywords identifying the borders that you want to draw, and whose values are attribute expressions containing the SVG attributes that describe the color and style of line that you want that border to be drawn with. There are predefined attributes that can be useful here. Individual borders can be assigned line styles :border-unrelated (the default) :dotted, and :border-related.

The entire border style of the box can be assigned more compactly using the predefined styles :box-first, :box-related, :box-last, :box-above, :box-above-related, or :box-below. Or of course you can make up your own completely original line styles and border maps.

Here’s a look at the three line styles (with no bottom border):

(draw-box "borders"
          {:span    4
           :borders {:top   :dotted
                     :left  :border-related
                     :right :border-unrelated}})
borders

The same result could have been achieved by using the style map {:stroke-dasharray "1,1"} instead of the predefined attribute :dotted (that is its value), and {:stroke-dasharray "1,3"} instead of the predefined attribute :related, but the short keywords are both easier to type than the full maps, and easier to read and understand than the raw SVG attributes.

And here’s an example of using the predefined attributes that specify entire box border styles (notice how we can use the attribute expression mini-language to combine the border style named attributes with our own :span attribute):

(draw-box "first" [:box-first {:span 3}])
(draw-box "related" [:box-related {:span 3}])
(draw-box "last" [:box-related {:span 3}])
firstrelatedlast

For situations where you’re drawing a lot of related boxes with the same attributes (but different content), even if they span multiple rows, you can use draw-boxes as described in the next section. If you want the boxes to be drawn as a related group, like in the example above, you can use draw-related-boxes.

draw-boxes

This is a shortcut for drawing mutiple labels with the same attributes for each. It calls draw-box for each value in labels.

Arguments
[labels]
[labels attr-spec]

If you pass attr-spec it will be used when calling draw-box for each value in labels. See the draw-box documentation for details about how labels and attributes are used to control the drawing of each box.

draw-column-headers

Draws the row of byte offsets at the top of the diagram, making it easy to visually determine the addresses of boxes below. This is not done until you ask for it, to give you an opportunity to first adjust predefined values that will affect the result.

Arguments
[]
[attr-spec]

If you supply attr-spec, it is parsed as an attribute expression that you can use to further customize the column headers (in ways that don’t affect the structure of the rest of the diagram):

Column Header Attributes

Attribute Default Value Meaning

:font-family

"Courier New, monospace"

The typeface used to draw the column headers.

:font-size

11

Controls the size of the column headers.

:height

14

The amount of vertical space allocated to the column headers.

:labels

column-labels

A sequence whose elements are used as the actual text of each column header in order. You might want to change this if you are drawing a bit field, where the high order bits come first, as shown in the examples below.

With no redefinitions of predefined values and no attribute expression, this generates headers for a row of sixteen bytes as hexadecimal digits:

(draw-column-headers)
0123456789abcdef

If you are dealing with big-endian values, you can reverse the column-labels predefined value that is used to generate the labels, and pass it in as the :labels attribute:

(draw-column-headers {:labels (reverse column-labels)})
fedcba9876543210

If you want to draw a diagram with fewer columns, redefine boxes-per-row before calling this:

(def boxes-per-row 8)
(draw-column-headers)
01234567

But note that if you want to both reduce the number of columns and reverse the headers, you need to do a little more than combining these two steps, because that simple approach results in the following headers:

(def boxes-per-row 8)
(draw-column-headers {:labels (reverse column-labels)})
fedcba98

…​Which makes sense, if you think about it: there are sixteen values in column-labels, so reversing it gives you the top eight. Luckily the solution is straightforward, just use the Clojure’s take function to get the first eight labels before calling reverse:

(def boxes-per-row 8)
(draw-column-headers {:labels (reverse (take 8 column-labels))})
76543210

draw-gap

Draws an indication of discontinuity. Takes a full row (consuming the rest of the current row first, if there have been any boxes drawn in it). Also optionally labels the contents of the gap.

Arguments
[]
[label]
[label attr-spec]

If label is provided, draws it to identify the content of the gap. If there are at least :min-label-columns (which defaults to 8, see the attributes below) remaining on the current row, will center the label in the remaining space on that row before drawing the gap. Otherwise it will advance to the next row, draw the label centered on the entire row, then draw the gap. label is passed to draw-box, so it is interpreted in the same way.

When finishing off the previous row, a box is drawn in the predefined box-above style. You can change that by passing different attributes under the :box-above-style key in your attribute expression (the optional second argument). For example, use {:box-above-style :box-above-related} if the gap relates to the preceding box.

Gap Attributes

Attribute Default Value Meaning

:edge

15

The height of the sections before and after the gap drawing, which just draw the left and right edges of the diagram.

:height

70

The height of the gap context, which is sandwiched bewteen the edges and affects the slope of the gap within it, drawn from the top left of this section to the bottom right.

:gap

10

The height of the gap itself, the unenclosed space between the diagonal lines of the gap.

:gap-style

:dotted

The line style to use in drawing the diagonal lines on either side of the actual gap.

:box-above-style

:box-above

The box style to use when drawing a box to finish of a partial row before the gap, as described above.

:min-label-columns

8

As described above, the number of columns that must be available in the current row if the label is to be drawn in it.

In order to allow you to draw boxes that connect to the bottom of the gap, no bottom border is drawn. If you have a full row of boxes after it this doesn’t matter, as their top borders will close it off. But if the diagram ends at the gap, or with an incomplete row after it, you need to call (draw-bottom) after you draw the gap.
(draw-box "Stuff" {:span 4})
(draw-gap "A gap")
(draw-box "More stuff")
(draw-bottom)
StuffA gap0010i+00More stuff

After a gap, the actual addresses of subsequent rows are not known, since the gap can vary in length. To reflect that, row headers after that point are reset to i+00 (meaning zero bytes past the end of the gap) and grow from there. If you would like a different labeling scheme you can replace the row-header-fn predefined value.

draw-line

Adds a line to the SVG being built up. This is used extensively by the other functions here to draw the diagram, but you can use it yourself to draw your own custom content, either in your diagram itself, or as a part of a custom label function in draw-box.

Arguments
[x1 y1 x2 y2]
[x1 y1 x2 y2 attr-spec]

The four required arguments are the coordinates of the endpoints of the line segment to be drawn. If those are the only arguments you supply, the line will be drawn with a :stroke-width of 1 and a :stroke of #000000 (black). But you can override those (and other SVG attributes) by passing an attribute expression as the fifth argument.

This is a shortcut for drawing mutiple labels with the same basic attributes for each, which are a related group, so the internal borders between boxes inside the group are rendered differently than the borders with boxes outside the group (as illustrated in the example at the end of the draw-box discussion. It calls draw-box for each value in labels, merging any attributes you supply with appropriate border styles on whether this is the first, a middle, or the final box.

Arguments
[labels]
[labels attr-spec]

If you pass attr-spec it will be used when calling draw-box for each value in labels. See the draw-box documentation for details about how labels and attributes are used to control the drawing of each box, but keep in mind that the :borders attribute is controlled by this function so that borders between related cells are drawn with the line style specified by the :border-related predefined attribute, and borders with unrelated cells are drawn with the line style specified by :border-unrelated.

The default definitions of those line styles result in a solid line for borders with unrelated cells, and a light dashed line between related cells. You can change those defaults using defattrs to redefine :border-related and :border-unrelated.

(draw-box "before" {:span 2})
(draw-related-boxes (range 48))
(draw-box "after" {:span 2})
before000102030405060708090a0b0c0d00100e0f101112131415161718191a1b1c1d201e1f202122232425262728292a2b2c2d302e2fafter

draw-row-header

Generates the label in the left margin which identifies the starting address of a row.

You generally don’t need to call this yourself, because it is called automatically whenever boxes you are drawing wrap onto a new row. But you can call it if you are drawing a single-row diagram and still want the row header to be present.
Arguments
[labels]
[labels attr-spec]

Defaults to a :font-size of 11 and :font-family of "Courier New, monospace" but these can be overridden, and other SVG text attributes) can be supplied, through an attribute expression in attr-spec.

In the most common case, label is a string and the SVG text object is constructed as described above. If you need to draw a more complex structure, you can pass in your own SVG text object (with potentially nested tspan objects), and it will simply be positioned.

eval-attribute-spec

This is the function that evaluates attribute expressions. It accepts the mini-language described in that section, and boils it down to a single map of attributes. It’s available for use in your own code so that helper functions you write are able to accept attribute expressions just like the built-in functions do.

hex-text

Creates an SVG text object suitable for use as a box label representing a hexadecimal value. This is the function used internally when you pass a number as the label argument to draw-box.

Arguments
[n]
[n length]
[n length attr-spec]

If you just pass a number in n it is formatted as a two-digit hexadecimal value, using the text styles specified in the predefined attribute :hex. You can specify the number of digits by also passing length, and you can override or augment the SVG text attributes) by passing an attribute expression in attr-spec.

next-row

Advances drawing to the next row of boxes.

You don’t need to call this when drawing boxes, because they will auto-advance as needed, generating the row headers as they do. But you can use it when you want to draw other informational rows that are not part of the box grid.
Arguments
[]
[height]

The height of the row defaults to the predefined value :row-height but can be changed by passing height.

text

Builds an SVG text object for drawing. This is used by hex-text and by draw-box when you pass it a string. If you need to do something more complicated with styling (including nested tspan objects with different styles), this function lets you.

Arguments
[label]
[label attr-spec & content]

If you just pass a single argument, it is rendered as a text string with the styles specified by the predefined attribute :plain. The optional second argument is an attribute expression you can use to pick your own SVG text attributes).

Any arguments after attr-spec are additional text content to be rendered, but if they are vectors they are given special treatment and rendered as a nested tspan object. The first element of the vector is parsed as an attribute expression for the styles to apply to that tspan, and the remaining elements are rendered as its content. (And even here you can embed new tspan objects with new styling by embedding more vectors.)

(draw-box (text "v" :math [:sub "max"]))
(draw-box (text "Some " {} [{:font-style "italic"} "formatted"] " text!")
          {:span 5})
vmaxSome formatted text!

The first example shows a common pattern in my own diagrams: the main text is styled using the :math predefined attributes to look like an equation, and it is followed by a vector representing a nested tspan that uses the :sub predefined attributes to be positioned as a subscript.

The second example has a lot going on: The first text is rendered in the default style, which we have to make explicit by passing {} as the attribute expression so that we can move on to the nested content arguments (using nil would have worked as well).

That content has multiple values this time. The first is again a vector representing a nested tspan object, this time using an explicit attribute map to style its text as italics, and the second is just more text, so it gets styled the same way as the original text.

Following the end of the text function invocation, which makes up the label argument of the draw-box function, we have the attribute expression for the box itself. We use that to make it wide enough to hold the text we’re drawing.

tspan

Builds an SVG tspan object with attributes parsed as an attribute expression.

Arguments
[attr-spec content]
You generally don’t need to call this directly, as text will call it for you when it finds a list or vector in its content.

Any lists or vectors in the content will be recursively parsed as nested tspan objects with their own attribute expressions as the first element.

Deep Symmetry logo Copyright © 2020 Deep Symmetry, LLC