This short vignette assumes an S3 class that encapsulates a vector. It demonstrates how to extend this class so that it renders nicely in a tibble.
We define a class "latlon" that encodes geographic coordinates in a complex number. For simplicity, the values are printed as hours and minutes only.
latlon <- function(lat, lon) {
as_latlon(complex(real = lon, imaginary = lat))
}
as_latlon <- function(x) {
structure(x, class = "latlon")
}
`[.latlon` <- function(x, i) {
as_latlon(NextMethod())
}
format.latlon <- function(x, ..., formatter = deg_rad) {
lat <- Im(x)
lon <- Re(x)
paste(formatter(Im(x), c("N", "S")), formatter(Re(x), c("E", "W")))
}
deg_rad <- function(x, pm) {
sign <- sign(x)
x <- abs(x)
deg <- trunc(x)
x <- x - deg
rad <- round(x * 60)
ret <- sprintf("%d°%.2d'%s", deg, rad, pm[ifelse(sign >= 0, 1, 2)])
ret[is.na(x)] <- ""
format(ret, justify = "right")
}
print.latlon <- function(x, ...) {
cat(format(x), sep = "\n")
invisible(x)
}
latlon(32.7102978, -117.1704058)## 32°43'N 117°10'W
More methods are needed to make this class fully compatible with data frames, see e.g. the hms package for a more complete example.
Columns on this class can be used in a tibble right away, but the output will be less than ideal:
library(tibble)
data <- tibble(
venue = "rstudio::conf",
year = 2018:2019,
loc = latlon(c(32.7102978, NA), c(-117.1704058, NA))
)
data## # A tibble: 2 x 3
## venue year loc
## <chr> <int> <S3: latlon>
## 1 rstudio::conf 2018 -117.1704058+32.7102978i
## 2 rstudio::conf 2019 <NA>
The output has two main problems:
<S3: latlon>. While this is technically correct, we might like a shorter representation.format() method we have defined. This is by design.In the remainder I’ll show how to fix both problems, and also how to implement rendering that adapts to the available width.
To display <geo> as data type, we need to override the type_sum() method:
type_sum.latlon <- function(x) {
"geo"
}Because the value shown there doesn’t depend on the data, we just return a constant. (For date-times, the column info will eventually contain information about the timezone, see #53.)
data## # A tibble: 2 x 3
## venue year loc
## <chr> <int> <geo>
## 1 rstudio::conf 2018 -117.1704058+32.7102978i
## 2 rstudio::conf 2019 <NA>
To use our format method for rendering, we implement the pillar_shaft() method for our class. (A pillar is mainly a shaft (decorated with an ornament), with a capital above and a base below. Multiple pillars form a colonnade, which can be stacked in multiple tiers. This is the motivation behind the names in our API.)
library(pillar)
pillar_shaft.latlon <- function(x, ...) {
out <- format(x)
out[is.na(x)] <- NA
new_pillar_shaft(out, align = "right")
}The simplest variant calls our format() method, everything else is handled by pillar, in particular by the new_pillar_shaft() helper. Note how the align argument affects the alignment of NA values and of the column name and type.
data## # A tibble: 2 x 3
## venue year loc
## <chr> <int> <geo>
## 1 rstudio::conf 2018 32°43'N 117°10'W
## 2 rstudio::conf 2019 NA
We could also use left alignment and indent only the NA values:
pillar_shaft.latlon <- function(x, ...) {
out <- format(x)
out[is.na(x)] <- NA
new_pillar_shaft(out, align = "left", na_indent = 5)
}
data## # A tibble: 2 x 3
## venue year loc
## <chr> <int> <geo>
## 1 rstudio::conf 2018 32°43'N 117°10'W
## 2 rstudio::conf 2019 NA
If there is not enough space to render the values, the formatted values are truncated with an ellipsis. This doesn’t currently apply to our class, because we haven’t specified a minimum width for our values:
print(data, width = 35)## # A tibble: 2 x 3
## venue year loc
## <chr> <int> <geo>
## 1 rstudio:… 2018 32°43'N 117°10'W
## 2 rstudio:… 2019 NA
If we specify a minimum width when constructing the shaft, the loc column will be truncated:
pillar_shaft.latlon <- function(x, ...) {
out <- format(x)
out[is.na(x)] <- NA
new_pillar_shaft(out, align = "right", min_width = 10)
}
print(data, width = 35)## # A tibble: 2 x 3
## venue year loc
## <chr> <int> <geo>
## 1 rstudio::conf 2018 32°43'N 117…
## 2 rstudio::conf 2019 NA
This may be useful for character data, but for lat-lon data we may prefer to show full degrees and remove the minutes if the available space is not enough to show accurate values. A more sophisticated implementation of the pillar_shaft() method is required to achieve this:
pillar_shaft.latlon <- function(x, ...) {
deg <- format(x, formatter = deg)
deg[is.na(x)] <- style_na("NA")
deg_rad <- format(x)
deg_rad[is.na(x)] <- style_na("NA")
ret <- structure(
list(deg = deg, deg_rad = deg_rad),
class = c("pillar_shaft_latlon", "pillar_shaft")
)
ret <- set_width(ret, max(crayon::col_nchar(deg_rad), 0))
ret <- set_min_width(ret, max(crayon::col_nchar(deg), 0))
ret
}Here, pillar_shaft() returns an object of the "pillar_shaft_latlon" class (which is also a "pillar_shaft") that contains the necessary information to render the values, and also minimum and maximum width values. For simplicity, both formattings are pre-rendered, and the minimum and maximum widths are computed from there. Note that we also need to take care of NA values explicitly. (crayon::col_nchar() is like nchar() but strips the formatting added by style_na().)
For completeness, the code that implements the degree-only formatting looks like this:
deg <- function(x, pm) {
sign <- sign(x)
x <- abs(x)
deg <- round(x)
ret <- sprintf("%d°%s", deg, pm[ifelse(sign >= 0, 1, 2)])
ret[is.na(x)] <- ""
format(ret, justify = "right")
}All that’s left to do is to implement a format() method for our new "pillar_shaft_latlon" class. This method will be called with a width argument, which then determines which of the formattings to choose:
format.pillar_shaft_latlon <- function(x, width, ...) {
if (all(crayon::col_nchar(x$deg_rad) <= width)) {
ornament <- x$deg_rad
} else {
ornament <- x$deg
}
new_ornament(ornament)
}
data## # A tibble: 2 x 3
## venue year loc
## <chr> <int> <geo>
## 1 rstudio::conf 2018 32°43'N 117°10'W
## 2 rstudio::conf 2019 NA
print(data, width = 35)## # A tibble: 2 x 3
## venue year loc
## <chr> <int> <geo>
## 1 rstudio::conf 2018 33°N 117°W
## 2 rstudio::conf 2019 NA
Both new_pillar_shaft() and new_ornament() accept escape codes for coloring, emphasis, or other ways of highlighting text on terminals that support it. Some formattings are predefined, e.g. style_subtle() displays text in a light gray. For default data types, this style is used for insignificant digits. We’ll be formatting the degree and minute signs in a subtle style, because they serve only as separators. You can also use the crayon package to add custom formattings to your text.
pillar_shaft.latlon <- function(x, ...) {
out <- format(x, formatter = deg_rad_color)
out[is.na(x)] <- NA
new_pillar_shaft(out, align = "left", na_indent = 5)
}
deg_rad_color <- function(x, pm) {
sign <- sign(x)
x <- abs(x)
deg <- trunc(x)
x <- x - deg
rad <- round(x * 60)
ret <- sprintf(
"%d%s%.2d%s%s",
deg,
style_subtle("°"),
rad,
style_subtle("'"),
pm[ifelse(sign >= 0, 1, 2)]
)
ret[is.na(x)] <- ""
format(ret, justify = "right")
}
data## # A tibble: 2 x 3
## venue year loc
## <chr> <int> <geo>
## 1 rstudio::conf 2018 32°43'N 117°10'W
## 2 rstudio::conf 2019 NA
Currently, ANSI escapes are not rendered in vignettes, so the display here isn’t much different from earlier examples. This may change in the future.
Remember to use namespace imports instead of library() calls in package code!