Turning {ggplot2} Into a PoS (Point-of-Sale) System

At the end of March, I caught a fleeting tweet that showcased an Epson thermal receipt printer generating a new “ticket” whenever a new GitHub issue was filed on a repository. @aschmelyun documents it well in this blog post. It’s a pretty cool hack, self-contained on a Pi Zero.

Andrew’s project birthed an idea: could I write an R package that will let me plot {ggplot2}/{grid} objects to it? The form factor of the receipt printer is tiny (~280 “pixels” wide), but the near infinite length of the paper means one can play with some data visualizations that cannot be done in other formats (and it would be cool to be able to play with other content to print to it in and outside of R).

One of the features that makes Andrew’s hack extra cool is that he used an Epson receipt printer model that was USB connected. I don’t see the need to dedicate and extra piece of plastic, metal, and silicon to manage the printing experience, especially since I already have a big linux server where I run personal, large scale data science jobs. I ended up getting a used (lots of restaurants close down each week) Epson TM-T88V off of eBay since it has Ethernet and is supports ESC/POS commands.

After unpacking it, I needed to get it on the local network. There are many guides out there for this, but this one sums up the process pretty well:

  • Plug the printer in and reset it
  • Hook up a system directly to it (Ethernet to Ethernet)
  • Configure your system to use the Epson default IP addressing scheme
  • Access the web setup page
  • Configure it to work on your network
  • Disconnect and restart the printer

To make sure everything worked, I grabbed one of the (weirdly) many projects on GitHub that provided a means for formatting graphics files to an ESC/POS compatible raster bitmap and processed a simple R-generated png to it, then used netcat to shunt the binary blob over to the printer on the default port of 9100.

I did some initial experiments with {magick}, pulling the graphics bits out of generated plots and then wrapping some R code around doing the conversion. It was clunky and tedious, and I knew there had to be a better way, so I hunted for some C/C++, Rust, or Go code that already did the conversion and found png2escpos by The Working Group. However, I’ve switched to png2pos by Petr Kutalek as the dithering it does won’t require the R user to produce only black-and-white plots for them to look good.

I thought about implementing a graphics device to support any R graphics output, but there are enough methods to convert a base R plot to a grid/grob object that I decided to mimic the functionality of ggsave() and make a ggpos() function. The comment annotations in the code snippet below walk you through the extremely basic process:

ggpos <- function(plot = ggplot2::last_plot(),
                  port = 9100L,
                  scale = 2,
                  width = 280,
                  height = 280,
                  units = "px",
                  dpi = 144,
                  bg = "white",
                  ...) {

  # we generate a png file using ggsave()

  png_file <- tempfile(fileext = ".png")

    filename = png_file,
    plot = plot,
    scale = scale,
    width = width,
    height = height,
    units = units,
    dpi = dpi,
    bg = bg,

  # we call an internal C function to convert the generated png file to an ESC/POS raster bitmap file

  res <- png_to_raster(png_file)

  if (res != "") { # if the conversion ended up generating a new file

    # read in the raw bytes

    escpos_raster <- stringi::stri_read_raw(res)

    # open up a binary socket to the printer 

      host = host_pos,
      port = port,
      open = "a+b"
    ) -> con


    # shunt all the bytes over to it

      object = escpos_raster,
      con = con,
      useBytes = TRUE




The only work I needed to do on the original C code was to have it output directly to a file instead of stdout.

Now, plotting to the printer is as straightforward as:


ggplot(mtcars) +
    aes(wt, mpg)
  ) +
    title = "Test of {ggpos}"
  ) +
  theme_ipsum_es(grid="XY") +
    panel.grid.major.x = element_line(color = "black"),
    panel.grid.major.y = element_line(color = "black")
  ) -> gg


That code produces this output (I’m still getting the hang of ripping the spooled paper off this thing):

ggplot receipt

This is the whole thing in action:

One of the 2022 #30DayChartChallenge topics was “part-to-whole”, so I rejiggered my treemap entry into a very long plot that would make CVS cashiers feel quite inferior.

You can find {escpos} over on GitHub.


One big caveat for this is that these printers have a tiny memory buffer, so very long, complex plots aren’t going to work out of the box. I had to break up my faceted heatmaps info individual ones and shunt them over one-by-one.

I’ll be switching over the the new C library soon, and adding a small DSL to handle text formatting and printing from R (the device has 2 fonts and almost no styles). I’ve even threatened to make a ShinyPOS application, but we’ll see how the motivation for that goes.

Kick the tyres and let me know if you end up using the package (+ share your creation to the 🌎).

Cover image from Data-Driven Security
Amazon Author Page