creating-an-svg-device-03.Rmd
This series of vignettes will walk through the process of creating a very basic SVG output device using devout
.
The only code to be written is R code. All the tricky C/C++ code is taken care of within devout
.
Vignettes in this series:
In the previous vignettes we create a callback function (svg_callback()
) and implemented handlers for device_call = "open"
and "close"
.
Now we will add handlers for the device calls which want to draw a graphics primitive e.g. rect
, line
, circle
.
The svg_open()
and svg_close()
functions are adapted to only write the lead-in and lead-out parts of an SVG document. The size for the SVG canvas is found in the device settings i.e. state$dd
. For a list of all the possible device settings, please see devinfo$dd
.
devinfo$dd %>%
knitr::kable(caption = "Device settings")
name | type | length | description | default |
---|---|---|---|---|
left | numeric | 1 | left raster coordinate | 0 |
top | numeric | 1 | top raster coordinate | 0 |
right | numeric | 1 | right raster coordinate | width * 72 |
bottom | numeric | 1 | bottom raster coordinate | height * 72 |
clipLeft | numeric | 1 | rectangular clipping extents | 0 |
clipTop | numeric | 1 | rectangular clipping extents | 0 |
clipRight | numeric | 1 | rectangular clipping extents | width * 72 |
clipBottom | numeric | 1 | rectangular clipping extents | height * 72 |
xcharOffset | numeric | 1 | x character addressing offset - unused | 0.49 |
yCharOffset | numeric | 1 | y character addressing offset | 0.3333 |
yLineBias | numeric | 1 | 1/2 interline space as frac of line height | 0.2 |
ipr | numeric | 2 | Inches per raster c(x, y) | c(1/72, 1/72) |
cra | numeric | 2 | Character size in rasters c(x, y) | c(0.9 * startps, 1.2 * startps) |
gamma | numeric | 1 | Device Gamma Correction | 1 |
canClip | logical | 1 | Device-level clipping | TRUE |
canHAdj | integer | 1 | Can do at least some horiz adjust of text: 0 = none, 1 = {0, 0.5, 1}, 2 = [0, 1] | 0L |
canChangeGamma | logical | 1 | can the gamma factor be modified? | FALSE |
displayListOn | logical | 1 | toggle for initial display list status | FALSE |
haveTransparency | integer | 1 | 1 = no, 2 = yes | 2L |
haveTransparentBg | integer | 1 | 1 = no, 2 = fully, 3 = semi | 2L |
haveRaster | integer | 1 | 1 = no, 2 = yes, 3 = except for missing values | 2L |
haveCapture | integer | 1 | 1 = no, 2 = yes | 2L |
haveLocator | integer | 1 | 1 = no, 2 = yes | 2L |
startfill | integer | 4 | sets par(bg) and gpar(fill) | c(255L, 255L, 255L, 255L) |
startcol | integer | 4 | sets par(fg), par(col) and gpar(col) | c(0L, 0L, 0L, 255L) |
startps | numeric | 1 | initial pointsize | 12 |
startlty | integer | 1 | initial linetype | 0L |
startfont | integer | 1 | initial font | 1L |
startgamma | numeric | 1 | initial gamma correction | 1 |
wantSymbolUTF8 | logical | 1 | NFI | TRUE |
hasTextUTF8 | logical | 1 | Device supports UTF8 text | TRUE |
useRotatedTextInContour | logical | 1 | Is rotated text good enough to be preferable to Hershey in contour label | TRUE |
canGenMouseDown | logical | 1 | can the device generate mousedown events | FALSE |
canGenMouseMove | logical | 1 | can the device generate mousemove events | FALSE |
canGenMouseUp | logical | 1 | can the device generate mouseup events | FALSE |
canGenKeybd | logical | 1 | can the device generate keyboard events | FALSE |
canGenIdle | logical | 1 | can the device generate idle events | FALSE |
gettingEvent | logical | 1 | This is set while getGraphicsEvent is actively looking for events | FALSE |
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# When opening a device
# - create a "canvas". For svg, the canvas is just a text string of SVG
# commands that we'll keep adding to with each device call
# - add the canvas to the 'state$rdata' list
# - always return the state so we keep the canvas across different device calls
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
svg_open <- function(args, state) {
state$rdata$svg <- glue::glue('<svg height="{state$dd$bottom/72}" width="{state$dd$right/72}">')
state
}
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# When the device is closed, add the closing svg tag and output to file
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
svg_close <- function(args, state) {
state$rdata$svg <- paste(state$rdata$svg, "</svg>", sep = "\n")
writeLines(state$rdata$svg, state$rdata$filename)
state
}
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Add a circle to the SVG
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
svg_circle <- function(args, state) {
state$rdata$svg <- paste(
state$rdata$svg,
glue::glue('<circle cx="{args$x/72}" cy="{args$y/72}" r="{args$r}" stroke="black" fill="black" />'),
sep = "\n"
)
state
}
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Add a line to the SVG
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
svg_polyline <- function(args, state) {
state$rdata$svg <- paste(
state$rdata$svg,
glue::glue('<polyline points="{paste(args$x/72, args$y/72, sep=",", collapse = " ")}" stroke="black" fill = "none" />'),
sep = "\n"
)
state
}
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Add a polyline to the SVG
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
svg_line <- function(args, state) {
state$rdata$svg <- paste(
state$rdata$svg,
glue::glue('<line x1="{args$x1/72}" y1="{args$y1/72}" x2="{args$x2/72}" y2="{args$y2/72}" stroke="black" fill = "none" />'),
sep = "\n"
)
state
}
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Add text to the SVG
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
svg_text <- function(args, state) {
state$rdata$svg <- paste(
state$rdata$svg,
glue::glue('<text x="{args$x/72}" y="{args$y/72}" transform="rotate({-args$rot}, {args$x/72}, {args$y/72})" fill="black">{args$str}</text>'),
sep = "\n"
)
state
}
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Call a different function to match each of the device calls we handle.
# Always return the state
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
svg_callback <- function(device_call, args, state) {
switch(
device_call,
"open" = svg_open (args, state),
"close" = svg_close (args, state),
"circle" = svg_circle (args, state),
"line" = svg_line (args, state),
"polyline" = svg_polyline (args, state),
"textUTF8" = svg_text (args, state),
state
)
}
rdevice(svg_callback, filename = "svg/test-draw.svg")
ggplot(mtcars) + geom_point(aes(mpg, wt))
invisible(dev.off())
View the SVG text output
cat(paste(readLines("svg/test-draw.svg"), collapse = "\n"))
#> <svg height="8" width="10">
#> <polyline points="0.714315068493151,7.20447156512392 9.92389649923896,7.20447156512392" stroke="black" fill = "none" />
#> <polyline points="0.714315068493151,5.47409614441951 9.92389649923896,5.47409614441951" stroke="black" fill = "none" />
#> <polyline points="0.714315068493151,3.74372072371509 9.92389649923896,3.74372072371509" stroke="black" fill = "none" />
#> <polyline points="0.714315068493151,2.01334530301067 9.92389649923896,2.01334530301067" stroke="black" fill = "none" />
#> <polyline points="0.714315068493151,0.282969882306249 9.92389649923896,0.282969882306249" stroke="black" fill = "none" />
#> <polyline points="1.88109956310408,7.52035159817352 1.88109956310408,0.0761035007610344" stroke="black" fill = "none" />
#> <polyline points="3.66244993655588,7.52035159817352 3.66244993655588,0.0761035007610344" stroke="black" fill = "none" />
#> <polyline points="5.44380031000768,7.52035159817352 5.44380031000768,0.0761035007610344" stroke="black" fill = "none" />
#> <polyline points="7.22515068345949,7.52035159817352 7.22515068345949,0.0761035007610344" stroke="black" fill = "none" />
#> <polyline points="9.00650105691129,7.52035159817352 9.00650105691129,0.0761035007610344" stroke="black" fill = "none" />
#> <polyline points="0.714315068493151,6.33928385477171 9.92389649923896,6.33928385477171" stroke="black" fill = "none" />
#> <polyline points="0.714315068493151,4.6089084340673 9.92389649923896,4.6089084340673" stroke="black" fill = "none" />
#> <polyline points="0.714315068493151,2.87853301336288 9.92389649923896,2.87853301336288" stroke="black" fill = "none" />
#> <polyline points="0.714315068493151,1.14815759265846 9.92389649923896,1.14815759265846" stroke="black" fill = "none" />
#> <polyline points="0.99042437637818,7.52035159817352 0.99042437637818,0.0761035007610344" stroke="black" fill = "none" />
#> <polyline points="2.77177474982998,7.52035159817352 2.77177474982998,0.0761035007610344" stroke="black" fill = "none" />
#> <polyline points="4.55312512328178,7.52035159817352 4.55312512328178,0.0761035007610344" stroke="black" fill = "none" />
#> <polyline points="6.33447549673359,7.52035159817352 6.33447549673359,0.0761035007610344" stroke="black" fill = "none" />
#> <polyline points="8.11582587018539,7.52035159817352 8.11582587018539,0.0761035007610344" stroke="black" fill = "none" />
#> <polyline points="9.89717624363719,7.52035159817352 9.89717624363719,0.0761035007610344" stroke="black" fill = "none" />
#> <circle cx="4.90939519797214" cy="5.26645109393497" r="1.95479822834646" stroke="black" fill="black" />
#> <circle cx="4.90939519797214" cy="4.82520536165535" r="1.95479822834646" stroke="black" fill="black" />
#> <circle cx="5.55068133241479" cy="5.7855637201463" r="1.95479822834646" stroke="black" fill="black" />
#> <circle cx="5.05190322784829" cy="4.23687771861585" r="1.95479822834646" stroke="black" fill="black" />
#> <circle cx="4.08997402618431" cy="3.84754324895735" r="1.95479822834646" stroke="black" fill="black" />
#> <circle cx="3.8762119813701" cy="3.81293574054326" r="1.95479822834646" stroke="black" fill="black" />
#> <circle cx="2.52238569754673" cy="3.62259444426578" r="1.95479822834646" stroke="black" fill="black" />
#> <circle cx="6.12071345191937" cy="4.28013710413346" r="1.95479822834646" stroke="black" fill="black" />
#> <circle cx="5.55068133241479" cy="4.34935212096163" r="1.95479822834646" stroke="black" fill="black" />
#> <circle cx="4.2681090635295" cy="3.84754324895735" r="1.95479822834646" stroke="black" fill="black" />
#> <circle cx="3.76933095896299" cy="3.84754324895735" r="1.95479822834646" stroke="black" fill="black" />
#> <circle cx="3.27055285439649" cy="2.75740673391357" r="1.95479822834646" stroke="black" fill="black" />
#> <circle cx="3.59119592161781" cy="3.34573437695307" r="1.95479822834646" stroke="black" fill="black" />
#> <circle cx="2.84302876476805" cy="3.25921560591785" r="1.95479822834646" stroke="black" fill="black" />
#> <circle cx="1.13293240625432" cy="0.715563737482354" r="1.95479822834646" stroke="black" fill="black" />
#> <circle cx="1.13293240625432" cy="0.414478414279785" r="1.95479822834646" stroke="black" fill="black" />
#> <circle cx="2.66489372742287" cy="0.551178072515433" r="1.95479822834646" stroke="black" fill="black" />
#> <circle cx="8.97087404944225" cy="5.99320877063083" r="1.95479822834646" stroke="black" fill="black" />
#> <circle cx="8.25833390006153" cy="7.00547839174292" r="1.95479822834646" stroke="black" fill="black" />
#> <circle cx="9.50527916147779" cy="6.62479579918794" r="1.95479822834646" stroke="black" fill="black" />
#> <circle cx="5.08753023531732" cy="5.53465928414416" r="1.95479822834646" stroke="black" fill="black" />
#> <circle cx="2.94990978717516" cy="3.709113215301" r="1.95479822834646" stroke="black" fill="black" />
#> <circle cx="2.84302876476805" cy="3.85619512606087" r="1.95479822834646" stroke="black" fill="black" />
#> <circle cx="2.16611562285637" cy="3.15539308067558" r="1.95479822834646" stroke="black" fill="black" />
#> <circle cx="4.2681090635295" cy="3.14674120357206" r="1.95479822834646" stroke="black" fill="black" />
#> <circle cx="7.15389666852142" cy="6.4517582571175" r="1.95479822834646" stroke="black" fill="black" />
#> <circle cx="6.69074557142395" cy="6.0970312958731" r="1.95479822834646" stroke="black" fill="black" />
#> <circle cx="8.25833390006153" cy="7.18197668465477" r="1.95479822834646" stroke="black" fill="black" />
#> <circle cx="3.05679080958227" cy="4.31474461254755" r="1.95479822834646" stroke="black" fill="black" />
#> <circle cx="4.44624410087468" cy="5.00689478082931" r="1.95479822834646" stroke="black" fill="black" />
#> <circle cx="2.77177474982998" cy="3.62259444426578" r="1.95479822834646" stroke="black" fill="black" />
#> <circle cx="5.05190322784829" cy="4.98959102662227" r="1.95479822834646" stroke="black" fill="black" />
#> <text x="0.279155251141552" y="6.40528385477171" transform="rotate(0, 0.279155251141552, 6.40528385477171)" fill="black">2</text>
#> <text x="0.279155251141552" y="4.6749084340673" transform="rotate(0, 0.279155251141552, 4.6749084340673)" fill="black">3</text>
#> <text x="0.279155251141552" y="2.94453301336288" transform="rotate(0, 0.279155251141552, 2.94453301336288)" fill="black">4</text>
#> <text x="0.279155251141552" y="1.21415759265846" transform="rotate(0, 0.279155251141552, 1.21415759265846)" fill="black">5</text>
#> <polyline points="0.676263318112633,6.33928385477171 0.714315068493151,6.33928385477171" stroke="black" fill = "none" />
#> <polyline points="0.676263318112633,4.6089084340673 0.714315068493151,4.6089084340673" stroke="black" fill = "none" />
#> <polyline points="0.676263318112633,2.87853301336288 0.714315068493151,2.87853301336288" stroke="black" fill = "none" />
#> <polyline points="0.676263318112633,1.14815759265846 0.714315068493151,1.14815759265846" stroke="black" fill = "none" />
#> <polyline points="0.99042437637818,7.55840334855403 0.99042437637818,7.52035159817351" stroke="black" fill = "none" />
#> <polyline points="2.77177474982998,7.55840334855403 2.77177474982998,7.52035159817351" stroke="black" fill = "none" />
#> <polyline points="4.55312512328178,7.55840334855403 4.55312512328178,7.52035159817351" stroke="black" fill = "none" />
#> <polyline points="6.33447549673359,7.55840334855403 6.33447549673359,7.52035159817351" stroke="black" fill = "none" />
#> <polyline points="8.11582587018539,7.55840334855403 8.11582587018539,7.52035159817351" stroke="black" fill = "none" />
#> <polyline points="9.89717624363719,7.55840334855403 9.89717624363719,7.52035159817351" stroke="black" fill = "none" />
#> <text x="0.745979931933736" y="7.72084474885845" transform="rotate(0, 0.745979931933736, 7.72084474885845)" fill="black">10</text>
#> <text x="2.52733030538554" y="7.72084474885845" transform="rotate(0, 2.52733030538554, 7.72084474885845)" fill="black">15</text>
#> <text x="4.30868067883734" y="7.72084474885845" transform="rotate(0, 4.30868067883734, 7.72084474885845)" fill="black">20</text>
#> <text x="6.09003105228914" y="7.72084474885845" transform="rotate(0, 6.09003105228914, 7.72084474885845)" fill="black">25</text>
#> <text x="7.87138142574094" y="7.72084474885845" transform="rotate(0, 7.87138142574094, 7.72084474885845)" fill="black">30</text>
#> <text x="9.65273179919274" y="7.72084474885845" transform="rotate(0, 9.65273179919274, 7.72084474885845)" fill="black">35</text>
#> <text x="4.93716133942161" y="7.92389649923896" transform="rotate(0, 4.93716133942161, 7.92389649923896)" fill="black">mpg</text>
#> <text x="0.241103500761035" y="4.10378310502283" transform="rotate(-90, 0.241103500761035, 4.10378310502283)" fill="black">wt</text>
#> </svg>
Open the output in an SVG viewer
This plot output doesn’t look that great because we cut a lot of corners to get here; this includes:
strWidth
and metricInfo
aren’t supported yet, the drawing system really doesn’t know how to position the fonts well, or how to leave space for them near other objects.