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.
[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 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
.
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 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 |
|
|
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"})
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
.
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-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-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-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
.
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.
Copyright © 2020 Deep Symmetry, LLC