Introduction
drawit() is designed to mimic the New York Times “You
Draw It” tool, where viewers are shown part of a graph and asked to
predict the trend before the full data is revealed. Earlier versions of
the youdrawitR package required users to work with
JavaScript and D3. This version only requires familiarity with
ggplot2.
In this vignette, we will demonstrate the drawit() tool
under a variety of circumstances, using both real and simulated data.
Although this package was originally created for graphical testing, it
can be used more broadly in any setting where users want to sketch or
draw on a graph.
How to Use drawit()
- First, create a
ggplot2object using any combination of up to two of the following geoms:- geom_point
- geom_smooth
- geom_line
- Then, pass that plot into
drawit(), either with a pipeline or by saving it as an object first - The output will display a graph with a yellow box. Click inside the box and move from left to right to draw your prediction. As you move, a line will appear. The yellow box indicates the section of the graph that still needs to be completed, so make sure to draw across the full highlighted region.
Examples
Data Connection and Smoothing
drawit() tries to match each x-value in the original
data. This is especially helpful if you want to collect the user-drawn
values and merge them back with the plotted data, as it could be
potentially useful to measure the distance from each original x-point to
the user-drawn y-point. However, in some situations it may be useful to
loosen this x-y connection.
Let’s see it used with mtcars.
(ggplot(data = mtcars, aes(x = wt, y = mpg)) +
geom_point()) |>
drawit()The smoothness or chunkiness of the drawn path depends on how densely
packed the data are. Compare the mtcars example to the
palmerpenguins dataset.
penguins_adelie <- dplyr::filter(penguins, species == "Adelie")
(ggplot(data = penguins_adelie, aes(x = bill_length_mm, y = bill_depth_mm)) +
geom_point()) |>
drawit()In this example, the drawn path is much more jittery and requires more precision in the mouse movement to connect smoothly. Moving your mouse slowly across the plot often helps the function capture the movement more accurately. If jitteriness becomes a problem, consider reducing the density of the data or increasing the rendered height and width of the plot.
Another option is to use the smoother parameter, which
often makes the interaction easier to control.
(ggplot(data = penguins_adelie, aes(x = bill_length_mm, y = bill_depth_mm)) +
geom_point()) |>
drawit(smoother = 0.5)Notice that the drawn line becomes smoother when the
smoother value increases from 0. This happens because
nearby x-values are more likely to be grouped together, which reduces
small, jittery movements in the drawing. While this can make the
interaction feel easier and more controlled, it may also reduce
precision when recording user data, as the resulting x-values will no
longer align as closely with the original data.
Choosing an appropriate smoothing value depends on your goal. If you need point-by-point comparisons, a lower value is more appropriate. If you are more interested in the overall trend, or want to make the drawing experience less sensitive to small mouse movements, a higher value can be helpful.
If you need more flexibility, check out sketchit(). That
function is more freeform and can be especially useful when the data are
dense.
ggplot2 Connection
While drawit() is not a perfect replica of
ggplot2, it does preserve much of the styling information
from the ggplot object you pass into it.
(ggplot(data = mtcars, aes(x = wt, y = mpg)) +
geom_line(linewidth = 2, color = "red") +
labs(title = "Drawit Cars Example",
subtitle = "Hope You Enjoy!",
x = "Weight",
y = "Miles Per Gallon") +
scale_y_continuous(limits = c(0, 32))+
scale_x_continuous(limits = c(1.5, 7))
) |>
drawit(smoother = 0.2)In the example above, the line color, line width, labels, and axis
limits are all reflected in the interactive version. However, when you
render your ggplot, some features may not appear exactly as expected.
youdrawitR is still a work in progress, so certain plot
elements may not yet be fully supported or may render differently from
the original ggplot2 object.
Drawing Boundaries and Reveals
drawit() is especially useful with simulated data and
forecasting-style tasks, since it was originally designed for graphical
testing. The function includes several arguments that let you control
which parts of a plot are shown to the user, where drawing begins, and
what is revealed after the interaction is complete.
# Here is the simulated data we will be using
data = tibble(x = seq(1, 25, .5),
y = exp((x-15)/30),
ypoints = exp(((x-15)/30) + rnorm(30, 0, 0.1)))
#> Warning in ((x - 15)/30) + rnorm(30, 0, 0.1): longer object length is not a
#> multiple of shorter object lengthOften, it is helpful to ask users to continue the trend beyond the
observed data. You can control where the drawing region begins with the
draw_start argument in drawit().
(ggplot(data, aes(x, ypoints)) +
geom_point(data = data |> mutate(ypoints2 = ifelse(x < 10, ypoints, NA)),
aes(y = ypoints2),
size = 2,
colour = "magenta"
) +
scale_y_continuous(limits = c(0.5, 1.5)) +
labs(x = "X-Label", y = "Y-Label")) |>
drawit(draw_start = 10)drawit() supports up to two geoms at a time. For
example, you can include the true line up to the starting point so the
user has a guide before continuing the pattern.
(ggplot(data, aes(x, ypoints)) +
geom_point(data = data |> mutate(ypoints2 = ifelse(x < 10, ypoints, NA)),
aes(y = ypoints2),
size = 2,
colour = "magenta"
) +
geom_line(data = data |> mutate(y = ifelse(x < 10, y, NA)),
aes(y = y)) +
scale_y_continuous(limits = c(0.5, 1.5)) +
labs(x = "X-Label", y = "Y-Label")) |>
drawit(draw_start = 10)Alternatively, you can reveal the full line after the user finishes
drawing by using the show_on_finish argument. If
show_on_finish is set to TRUE,
drawit() will use the secondary plot as the “revealed”
plot.
(ggplot(data, aes(x, ypoints)) +
geom_point(data = data |> mutate(ypoints2 = ifelse(x < 10, ypoints, NA)),
aes(y = ypoints2),
size = 2,
colour = "magenta"
) +
geom_line(data = data,
aes(y = y)) +
scale_y_continuous(limits = c(0.5, 1.5)) +
labs(x = "X-Label", y = "Y-Label")) |>
drawit(draw_start = 10, show_on_finish = TRUE)Shiny Integration
The drawit() function integrates directly with
Shiny, enabling interactive drawing inputs to be captured
and reused within reactive workflows. When used inside a
Shiny application, drawit() behaves
differently than in static contexts. Instead of returning only an
r2d3 widget, it returns a list, which contains:
-
youdrawit_plot: the interactive drawing widget -
points: a reactive tibble containing the user’s drawn data
The points object is a reactive()
expression that resolves to a tibble with:
-
x: x-values corresponding to the draw space -
y: corresponding user-drawn values
To enable communication between the browser and the
Shiny server, you must supply the
shiny_message_loc argument when using drawit()
in Shiny. This argument defines the input name used to send
drawn data from the browser back to Shiny. The drawing
interaction itself is handled in JavaScript, while
Shiny runs in R on the server, so
shiny_message_loc acts as the bridge between the two.
Example:
The following example demonstrates a minimal Shiny app
that captures user-drawn points and returns them as a tibble
library(shiny)
# Define the User Interface (ui)
ui <- fluidPage(
# Placeholder for the interactive drawit widget
uiOutput("widget_ui")
)
# Define the Server Logic
server <- function(input, output, session) {
# Create a ggplot object as the base plot for drawing
p <- ggplot(data = penguins, aes(x = bill_length_mm, y = bill_depth_mm)) +
geom_point(size = 2)
# This is the "bridge" between JavaScript (browser) and Shiny (R server)
shiny_message_loc <- "scatter_points"
# Initialize drawit
res <- drawit(
p, # the ggplot object
smoother = 2,
shiny_message_loc = shiny_message_loc
)
# res contains:
# - youdrawit_plot: the interactive plot
# - points(): a reactive object containing the user-drawn points as a tibble
# Render the widget in the UI placeholder
output$widget_ui <- renderUI({
res$youdrawit_plot
})
# observeEvent() watches res$points(), the reactive object
# Once the user completes their drawing, res$points() is populated
observeEvent(res$points(), {
# Because points is reactive, call res$points() inside reactive code
stopApp(res$points()) # stops the app and outputs the tibble of points
})
}
# runApp() launches the app and returns the user-drawn points once complete
points <- runApp(shinyApp(ui, server))After the user finishes drawing (when the yellow box has been
completely filled in), res$points() returns a tibble with
one row for each x-value in the drawable region. It might look something
like this:
The exact number of rows will depend on the x-values in the original plot and the user’s drawing. The resulting x and y values will be on a similar scale to the original dataset, allowing for easy merges and comparisons.
