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 ClojureScript core library, and you can dive down to the Analemma SVG building functions used by many of the functions below if you need to draw something unique.

For your coding convenience, the ClojureScript standard library namespaces clojure.set and clojure.string are also made available, so you can call functions from them if needed.

The JavaScript String and Math objects are also available. These allow you to perform additional operations using JavaScript interop syntax. For example here’s how you would calculate :

(Math/pow 2 8)

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.

char->int

Get the UTF-16 code unit of a character.

Arguments
[char]

This is equivalent to e.g. (int \H) in Clojure.

Example:

(draw-related-boxes (number-as-bits (char->int \H) 8))
01001000

defattrs

Register a attribute map for later use in your diagram.

Arguments
[k spec]
By convention, in Clojure arguments, k means “a keyword”. Here spec is an attribute specification expression that will be processed by eval-attribute-spec.

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 attribute expression 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.

If you want to build on an existing predefined attribute, your attribute expression can combine attributes concisely. The vertical text example for draw-box takes advantage of this approach.

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 pass a boolean (true or false), it is rendered as a single-digit bit value (1 or 0) in the same style as hexadecimal numbers.

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.

:next-row-height

n/a

If you pass a value here and this box is the first box to be drawn on a new row, it will set row-height to the specified value after finishing off the old row and before starting the new row. This is needed to coordinate row height changes when drawing vertical text, with the row headers still ending up in the right places.

: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"})
(draw-box false)
01two0

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.

Vertical Text

If you have long values you want to put in your boxes, and you still want to fit a lot of boxes in a row (for example, you are drawing a wide bit field diagram where each bit has a particular meaning) you can rotate the text so it is drawn vertically. Although bytefield-svg does not have any special support for this, you can use SVG’s built in support for CSS to achieve it.

This example, from the Dysentery project’s analysis of the Pioneer Pro DJ Link protocol, actually fits fine horizontally, but can still demonstrate the technique. Here is the horizontal text version:

(def boxes-per-row 8)
(def box-width 70)
(def left-margin 1)
(draw-column-headers {:labels (str/split "7,6,5,4,3,2,1,0" #",")})
(draw-box nil)
(draw-box "Play")
(draw-box "Master")
(draw-box "Sync")
(draw-box "On-Air")
(draw-box nil)
(draw-box "BPM")
(draw-box nil)
76543210PlayMasterSyncOn-AirBPM

And here it is with vertical text. The CSS we need is to set the writing-mode attribute to "vertical-rl". We also want to make row-height higher to fit the rotated labels, instead of widening box-width:

(def boxes-per-row 8)
(def left-margin 1)
(defattrs :vertical [:plain {:writing-mode "vertical-rl"}])  (1)
(draw-column-headers {:labels (str/split "7,6,5,4,3,2,1,0" #",")})
(def row-height 80)  (2)
(draw-box nil)
(draw-box (text "Play" :vertical))  (3)
(draw-box (text "Master" :vertical))
(draw-box (text "Sync" :vertical))
(draw-box (text "On-Air" :vertical))
(draw-box nil)
(draw-box (text "BPM" :vertical))
(draw-box nil)
1 Since there is a bit of code here to define the attributes we use to render rotated text, and we’ll use it multiple times, we define it as a new named attribute set. We want to combine the new CSS style attribute map {:writing-mode "vertical-rl"} with the predefined attributes named :plain, which are what draw-box normally uses. The easiest way to do that is by combining attributes in an attribute expression.
2 At the start of the row where we’re using rotated text, we increase the row height to accommodate it. If you are going to follow this with non-rotated rows, you’ll want to set it back down after your rotated row. We need a bit more height than we needed width because WebKit-based browsers like Chrome and Safari don’t quite center the rotated text vertically, although Firefox seems to get it right.
3 Because we want to apply our attributes to the text, rather than the box, we call text explicitly instead of letting draw-box do it for us, and then we can pass our new named attributes to text.

Putting that all together yields this result:

76543210PlayMasterSyncOn-AirBPM

If you are going to draw another row of boxes after this one with a different height, for example because it doesn’t use any rotated text, when you call draw-box for the first box on the next row pass a :next-row-height attribute to establish the new height as described above.

(draw-box "Label" {:span 2 :next-row-height 30})

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-gap-inline

Draws an indication of discontinuity for a single-row diagram. Takes the space of a single box in the row.

It does not make sense to use this in conjunction with either row or column headers because they will be incorrect.
Arguments
[]
[attr-spec]

Inline Gap Attributes

Attribute Default Value Meaning

:gap

5

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

:gap-style

:border-related

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

:height

row-height

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

:width

15

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

If you want to label the inline gap, draw an open-ended box on either side of it and label that:

(draw-box "name" {:span 2 :borders #{:left :top :bottom}})
(draw-gap-inline)
(draw-box "port" {:span 2})
nameport

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.

draw-padding

Draws enough related boxes to reach the specified memory address (useful if you know where the next chunk of useful information in the diagram occurs, and you don’t want to calculate how many boxes it will take to get there). The address is the value shown in the row header, plus the column header. It is either relative to the start of the diagram, or if a gap has been drawn, to the end of the most recent gap.

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

If no label is supplied, draws a zero byte in each box. If attr-spec is supplied, it is passed along to draw-related-boxes along with each copy of the label.

(draw-column-headers)
(draw-box "start" {:span 2})
(draw-padding 8)
(draw-box "more" {:span 2})
(draw-padding 0x12 nil)
(draw-box "end")
0123456789abcdefstart000000000000more0010end

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-address

Calculates the memory address corresponding to the next box that will be drawn. (If a gap has been drawn, this will be relative to the end of the gap.)

This will only be needed when you are writing fairly sophisticated drawing functions. For example, it is used by draw-padding.
Arguments
_none_

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.

normalize-bit

You probably don’t need to call this, it’s used by number-as-bits below, but it is available in case it might be helpful in writing your own bit drawing functions.

Converts a value to either true or false. All non-zero numbers become true, zero becomes false. Other values are tested for truthiness (in Clojurescript all values other than false and nil are truthy) and translated to true or false accordingly.

Arguments
[value]

Returns a value which when passed as a label to draw-box will be drawn as either 0 or 1 in the hex style.

number-as-bits

Takes a a number and transforms it into a sequence of boolean bit values of the specified length.

Thanks to Swiftb0y for this idea, and for being the first outside user of bytefield-svg, thereby helping to flesh it out.

Arguments
[number length]

This can be used to explode a number into the corresponding bit field by passing the result to draw-boxes or draw-related-boxes.

(def left-margin 1)
(def boxes-per-row 8)
(draw-column-headers {:labels (reverse (take 8 column-labels))})
(draw-related-boxes (number-as-bits 0xd3 8))
7654321011010011

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.

This is a macro that nests any drawing commands in its body inside an a element, turning the nested drawings into a hyperlink with the specified href.

If the form immediately following the href is a map, it is parsed as an attribute expression and establishes the attributes for the a element. (The most common use would be to have the link open in a new window by passing {:target "_blank"}).

Arguments
[href & body]
[href attr-spec & body]

The body can include any number of drawing function calls and other Clojure expressions. Notice that in the example below, the text and lines (including the separately-drawn bottom line of the gap) are hyperlinks, and the Clojure one opens in a new window.

(wrap-link "https://deepsymmetry.org"
  (draw-box (text "length" [:math] [:sub 1]) {:span 4}))
(wrap-link "https://clojure.org" {:target "_blank"}
  (draw-gap "Clojure byte code")
  (draw-bottom))

wrap-svg

This is a macro that nests any drawing commands in its body inside an arbitrary SVG element, which you specify as a vector containing a keyword that identifies the desired element and a map of its attributes and values.

This allows you create any SVG structure you need, even if bytefield-svg doesn’t provide special support for it.
Arguments
[wrapper & body]

The body can include any number of drawing function calls and other Clojure expressions. Here is how we could create the wrap-link example above using this lower-level approach, although wrap-link is more convenient:

(wrap-svg [:a {:href "https://deepsymmetry.org"}]
  (draw-box (text "length" [:math] [:sub 1]) {:span 4}))
(wrap-svg [:a {:href "https://clojure.org" :target "_blank"}]
  (draw-gap "Clojure byte code")
  (draw-bottom))