Build Minesweeper with Reactable and Shiny

R and Interactive tables

Jinhwan Kim
5 min read4 days ago

R. is a programming language with strengths in statistical analysis, but it also has strengths in tables and visualisation.

In recent years, R has been used to solve more and more problems, as techniques such as Shiny, webR, and wasm are being explored to make it possible to perform analyses online.

In this article, I’ll show you how to use some R packages to create interactive tables in R.

The main purpose of a table is to present data in a way that is easy to understand, but it also requires some context for the data.

For example, when presenting “drug data” in a table, a reader with an understanding of drugs will get different things from the table than a reader without an “understanding of drugs”.

For this reason, rather than using specific data, this table showcase uses a “minesweeper game” that is more accessible to a broader audience.

Minesweeper

Image from minesweeper.online

Most of you probably understand this game, but here’s a quick explanation.

Minesweeper is a puzzle game where you find safe spaces on a grid-like board while avoiding hidden mines. When you click on a square, a number is displayed showing how many of the eight neighbouring squares contain mines.

In other words, Minesweeper is a pretty good candidate for the aforementioned table interactions because it has a UI that uses a grid (table) and an interaction called “clicking”.

This Minesweeper project additionally includes “game” logic and sharing the results to the web using Shiny, but I won’t go into that as there are many better articles on the subject.

Results

The resulting Shiny application is shown in the following image, and can be accessed and viewed via this URL.

The main points of using tables here are twofold.

  1. rendering the table results using HTML (you can express more than just text in a table !)
  2. Interact with the user using Javascript on top of the HTML content.

In other words, if you do these two things well, you can use R’s table packages as standalone web pages (utilising HTML, CSS, and JS).

Additionally. The reason for using three different tables (DT, reactable, and gt) is to show that although there is not much difference in functionality.

But when using tables in R, you may need to use a specific table package depending on your needs, so you can implement the interaction as shown in the example not only with one table but also with various table packages.

I personally prefer reactable, sometimes use DT for compatibility with existing work, and have recently been using gt for the Clinical part of my work.

Reactable

The reactable is responsible for the grid in minesweeper.

In other words, it takes in the user’s click, calculates which cell was clicked, how many mines are in the cell and its neighbors, and returns the result to populate the clicked cell with a value. Here, the resulting number is represented using color (HTML).

Excluding the game logic part, which is the part that counts the number of mines in neighboring cells, the simplified code for the logic looks like this. (I intentionally removed code that is not essential part)

reactable(
board_df,
onClick = JS("function(rowInfo, column) {
Shiny.setInputValue('cell_click', {
row: rowInfo.index + 1,
column: column.id }); }"
)
)

The main point is to use the onClick parameter of the reactable and a JS function to pass the position of the clicked cell to the (rowInfo.index, column) shinyEvent.

You should use rowInfo.index + 1 because Javascript starts indexing from 0 and R starts from 1.

Although not reactable, Shiny’s cell_click logic that handles the above values can be simplified as follows

observeEvent(input$cell_click, {
click <- input$cell_click
row <- click$row
col <- click$col
....
})

Then we can use defaultColDef(html = TRUE) to render the HTML result to all columns of the reactable.

This can later be used to render the result in HTML like the code below.

color_text <- function(value, revealed) {
if (!revealed) {
return("")
}
if (value == -1) {
return('<span style="color: red;">💣</span>')
}
colors <- c("blue", "green", "red", "purple", "orange", "brown", "cyan", "magenta")
color <- if (value == 0) "lightgrey" else colors[value]
sprintf('<span style="color: %s; font-size: 2em;">%s</span>', color, value)
}

DT

Next is DT.

Like reactable, DT can render HTML results and interact with the user using JavaScript. However, since the table for minesweeper already uses reactable, we used DT to implement a “button that opens a new window so you can see the code for this project”.

The main code is shown below.

output$about.dt <- renderDataTable({
datatable(
data.frame(c('<button
type="button"
class="btn btn-primary"
style="width:100%"
onclick="window.open(`https://github.com/jhk0530/mine.tables`)"
>See code</button>')),
escape = FALSE,
caption = htmltools::tags$caption(
"Table 3: Code Button with DT"
)
)
})

I’ve implemented one button (HTML) using a data.frame, with onclick inside it, and modified it to render the HTML directly using escape = FALSE.

The caption has nothing to do with what the button does, but without it, it doesn’t look much different from a regular shiny actionbutton, so I deliberately included it to show that it’s made with DT (even though it’s made with DT!).

gt

output$document.gt <- render_gt({
data.frame(intToUtf8(160)) |>
gt() |>
tab_header(
title = gt::html(
'<button type="button"
class="btn btn-primary"
style="width:100%"
onclick="Shiny.setInputValue(`foo`, {priority: `event`});">
Documents
</button>')
) |>
tab_options(
column_labels.hidden = TRUE
) |>
tab_style(
style = cell_borders(sides = "all", weight = px(0)),
locations = cells_body()
) |>
tab_footnote("Table 4: Document button with gt")
})

observeEvent(input$foo, {
shinyjs::runjs('window.open("https://jhk0530.github.io/mine.tables/")')
})

Finally, the gt part.

I’ve implemented a simple button for gt as well, but gt is a little different from DT and reactable. *Please refer to the official manual for details.

Similarly, I rendered the code that creates an event in tab_header and shiny using html functions, and additionally marked the logic of the shiny code that interacts with the button.

To do this, we added the runjs function of shinyjs package to execute the javascript code.

Extra

Of course, in addition to the table, the project also implements other features that I briefly mentioned earlier, such as how to use Shiny, how to use cicerone to put guides in Shiny applications, and even how to use Google’s LLM, gemini, to solve minesweeper with artificial intelligence, but I won’t go into those in this article.

Additionally, this project is for Posit’s table contest 2024, and you can find the code and documentation used in the project, as well as another explanatory article, at the respective links.

If you have any questions regarding the project, please feel free to contact me.

Thanks !

--

--