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
Math
object is also available, so you can use it to perform mathematical
computations 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.
[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.
[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
.
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.
[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:
Box Attributes
You can modify the box that is drawn by passing in the following attributes:
Attribute | Default Value | Meaning |
---|---|---|
|
|
Controls which box borders are drawn, and optionally, their individual attributes. See below for more details. |
|
|
The fill color to use as the box background. |
|
|
If you pass a value here you can override the
height of the box. Normally it is controlled by the
predefined value |
|
|
If you pass a value here and this box is the
first box to be drawn on a new row, it will set |
|
|
The number of bytes (columns) this box will occupy. You can
supply a |
Here are some sample boxes:
(draw-box 1)
(draw-box "two" {:span 2})
(draw-box nil {:fill "#a0ffa0"})
(draw-box false)
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)
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}})
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}])
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)
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:
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-boxes
This is a shortcut for drawing mutiple labels with the same attributes
for each. It calls draw-box
for each value in labels
.
[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.
[]
[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 |
---|---|---|
|
|
The typeface used to draw the column headers. |
|
|
Controls the size of the column headers. |
|
|
The amount of vertical space allocated to the column headers. |
|
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)
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)})
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)
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)})
…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))})
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.
[]
[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 |
---|---|---|
|
|
The height of the sections before and after the gap drawing, which just draw the left and right edges of the diagram. |
|
|
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. |
|
|
The height of the gap itself, the unenclosed space between the diagonal lines of the gap. |
|
|
The line style to use in drawing the diagonal lines on either side of the actual gap. |
|
|
The box style to use when drawing a box to finish of a partial row before the gap, as described above. |
|
|
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)
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. |
[]
[attr-spec]
Inline Gap Attributes
Attribute | Default Value | Meaning |
---|---|---|
|
|
The width of the gap itself, the unenclosed space between the diagonal lines of the gap. |
|
|
The line style to use in drawing the diagonal lines on either side of the actual gap. |
|
|
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 |
|
|
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})
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
.
[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.
[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")
draw-related-boxes
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.
[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})
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. |
[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
.
[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 .
|
_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. |
[]
[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.
[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.
[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))
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.
[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})
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.
[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.
wrap-link
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"}
).
[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. |
[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))