Interactive Scatter & Bubble Charts in R with echarts4r

Add hover tooltips, point sizing, and colour-by-group to a ggplot-style scatter plot

Data Visualization

Turn a static ggplot2 scatter plot into an interactive one with echarts4r — hover a point to read its values, size points by a third variable to make a bubble chart, and colour by group with one series per group, all from a familiar pipe workflow. Each chart loads on demand so the page stays fast.

Published

July 9, 2026

Modified

July 9, 2026

TipKey takeaways
  • echarts4r wraps Apache ECharts, a free, open-source JavaScript charting library, in a tidy R pipe — e_charts() starts a chart, e_scatter() adds points.
  • A scatter plot shows the relationship between two numeric variables; interactivity adds a hover tooltip that names each point, so a dense cloud becomes readable.
  • Sizing points by a third variable turns a scatter into a bubble chart; drawing one series per group (a separate e_scatter() per category) colours the points by group so you can read three or four variables from one figure.
  • On this page every interactive chart sits behind a static poster you click to load — the heavy chart library only downloads on demand, so the page stays within the mobile performance budget.
  • The static poster is a plain ggplot2 chart, produced by code you can copy and run — the interactive version is the same data, made explorable.

Introduction

You already make scatter plots with ggplot2: geom_point(), an x and a y, done. That static chart is perfect for a report. But a scatter plot with thirty overlapping points hides which car is which — and a static PNG can’t tell you. That is where echarts4r comes in: it brings Apache ECharts — a mature, free, open-source charting library — into R with a pipe workflow that feels familiar if you know the tidyverse. Hover any point and it names the car and reads off its exact values.

This lesson builds the same scatter plot twice: first as a static ggplot2 figure (the reproducible baseline, and the fast-loading poster you see on the page), then as an interactive echarts4r widget you load with a click. We start with a plain scatter, add point sizing to make a bubble chart, then colour the points by group.

NoteWhy each chart loads on click

An interactive ECharts widget ships about 1 MB of JavaScript and draws to an HTML canvas (a pixel drawing surface the browser paints on the fly, rather than a static image). Loading several at once would slow the page on a phone and hurt its Largest Contentful Paint — LCP, the time until the main content appears. So on this page each interactive chart is represented by a static image first; clicking ▶ Load interactive chart swaps in the real, fully interactive echarts4r widget. You get the speed of a static page and the full interactivity on demand. In your own report or Shiny app you would simply print the widget directly — the code is identical.

The static baseline: a ggplot2 scatter plot

We will chart car weight against fuel economy for the built-in mtcars dataset. First, prepare a small data frame with base R — no dplyr needed — then draw the scatter with ggplot2. This static figure is the reproducible baseline, and on this page the main image the page loads first.

library(ggplot2)

# Prepare a small data frame (base R — no dplyr needed)
cars <- data.frame(
  model = rownames(mtcars),
  wt    = mtcars$wt,      # weight, in 1000s of lbs
  mpg   = mtcars$mpg,     # miles per gallon
  hp    = mtcars$hp,      # horsepower
  cyl   = mtcars$cyl      # number of cylinders
)

ggplot(cars, aes(wt, mpg)) +
  geom_point(colour = "#3a86d4", size = 3) +
  labs(x = "Weight (1000 lbs)", y = "Miles per gallon",
       title = "Heavier cars use more fuel") +
  theme_minimal(base_size = 13)

A scatter plot of car weight against miles per gallon for 32 cars, azure points, showing a clear downward trend.

The points use the brand azure #3a86d4, and theme_minimal() keeps it clean. The downward trend is clear — heavier cars get fewer miles per gallon. This is a publication-ready chart. Now let’s make it explorable.

Your first interactive scatter plot

echarts4r reads a data frame: you name the x column in e_charts(), then add points with e_scatter(). The pipe (|>) chains the steps, exactly like adding layers in ggplot2. We assign the chart to scat rather than printing it inline, so the page stays light — then load it behind the poster below.

library(echarts4r)

scat <- cars |>
  e_charts(wt) |>                          # the x axis
  e_scatter(mpg, bind = model) |>          # points from mpg; bind the model name for the tooltip
  e_color("#3a86d4") |>                    # brand azure, matching the static chart
  e_tooltip() |>                           # show a tooltip on hover
  e_x_axis(name = "Weight (1000 lbs)") |>
  e_y_axis(name = "MPG") |>
  e_legend(show = FALSE)                    # one series — no legend needed

Read the pipe top to bottom: start a chart on wt, add an mpg point series, bind the model column so the tooltip can name each car, colour the points azure, enable hover tooltips, and label the axes. bind attaches a label to every point — the echarts4r way to carry an identifier that a static plot cannot show. Now save the widget and display it behind a poster.

Scatter plot of car weight against miles per gallon showing a downward trend. Hover a point to read its exact weight and MPG.

Hover a point and ECharts shows the car’s name, weight, and MPG in a floating tooltip — the interaction you cannot get from a static PNG. Everything else is the same data and the same azure palette.

A bubble chart: size points by a third variable

A scatter plot shows two variables. Encode a third one in the size of each point and you have a bubble chart. Here we let horsepower drive the point size: pass the hp column to e_scatter()’s size argument. Larger bubbles are more powerful cars.

bubble <- cars |>
  e_charts(wt) |>
  e_scatter(mpg, size = hp, bind = model) |>   # point size = horsepower
  e_color("#3a86d4") |>
  e_tooltip() |>
  e_x_axis(name = "Weight (1000 lbs)") |>
  e_y_axis(name = "MPG") |>
  e_legend(show = FALSE)

The only change from the basic chart is size = hp inside e_scatter() — echarts4r scales each point’s radius to its horsepower. Read three variables at once: weight on x, fuel economy on y, and power in the bubble size. The pattern that jumps out is that the heavy, thirsty cars in the lower-right are also the big, powerful ones.

Bubble chart of weight against MPG with point size showing horsepower. Larger bubbles are more powerful cars; hover for the exact values.

Hover any bubble and the tooltip reports the car, its weight, MPG, and horsepower together — the size encoding tells the story, and the tooltip gives the exact numbers.

Colour points by group

To colour a scatter by a category, give each group its own point series — one e_scatter() call per group, each in its own colour. The base-R way to do that without dplyr is to split the mpg values into one column per cylinder count: ifelse() keeps a car’s MPG in its own column and sets it to NA everywhere else, and echarts4r simply skips the NA points. This is the same “wide columns, multiple series” idiom the bar and line lessons use.

# Colour by group the base-R way: one mpg column per cylinder count (shared x = wt).
# ifelse() masks the other groups to NA; echarts4r simply skips the NA points.
cars$`4 cylinders` <- ifelse(cars$cyl == 4, cars$mpg, NA)
cars$`6 cylinders` <- ifelse(cars$cyl == 6, cars$mpg, NA)
cars$`8 cylinders` <- ifelse(cars$cyl == 8, cars$mpg, NA)

grp <- cars |>
  e_charts(wt) |>
  e_scatter(`4 cylinders`, bind = model, symbol_size = 11) |>
  e_scatter(`6 cylinders`, bind = model, symbol_size = 11) |>
  e_scatter(`8 cylinders`, bind = model, symbol_size = 11) |>
  e_color(c("#440154", "#21908c", "#fde725")) |>   # viridis, colourblind-safe
  e_tooltip() |>
  e_x_axis(name = "Weight (1000 lbs)") |>
  e_y_axis(name = "MPG")

Here is the idea. ifelse(cars$cyl == 4, cars$mpg, NA) builds a 4 cylinders column that holds the MPG of the four-cylinder cars and NA for every other car; the 6 cylinders and 8 cylinders columns do the same for their groups. Each column becomes its own e_scatter() series against the shared wt axis, so every group is drawn separately — e_color() then hands the three series a colourblind-safe viridis triple, and symbol_size fixes every point to the same radius (no accidental sizing). The payoff: colour cleanly marks the group, the legend lets a reader click a cylinder count to toggle it on or off, and the colours match the static poster below.

Scatter of weight against MPG with points coloured by cylinder count. Colour marks the number of cylinders; hover a point for its details.

Colour now carries the group: the four-cylinder cars cluster top-left (light, efficient), the eight-cylinder cars bottom-right (heavy, thirsty). Because each cylinder count is its own series, the legend is interactive in the loaded chart — click a legend entry to toggle that group on or off and isolate the pattern you want.

Every chart above plots the same small table derived from mtcars — one row per car, with weight, fuel economy, horsepower, and cylinder count:

First 8 cars: weight (1000 lbs), MPG, horsepower, and cylinders.
model wt mpg hp cyl 4 cylinders 6 cylinders 8 cylinders
Mazda RX4 2.620 21.0 110 6 NA 21.0 NA
Mazda RX4 Wag 2.875 21.0 110 6 NA 21.0 NA
Datsun 710 2.320 22.8 93 4 22.8 NA NA
Hornet 4 Drive 3.215 21.4 110 6 NA 21.4 NA
Hornet Sportabout 3.440 18.7 175 8 NA NA 18.7
Valiant 3.460 18.1 105 6 NA 18.1 NA
Duster 360 3.570 14.3 245 8 NA NA 14.3
Merc 240D 3.190 24.4 62 4 24.4 NA NA

The full table has 32 rows. wt is on the x axis, mpg on the y axis; hp drives bubble size and cyl drives the group colour.

Common issues

The chart doesn’t appear / the page just shows the poster. That’s by design on this site — click ▶ Load interactive chart and the real widget loads. In your own document, print(scat) (or letting scat be the last line of a chunk) renders the widget directly with no poster.

My groups all overlap into one colour. Each group needs its own e_scatter() series. Split the y column into one column per group with ifelse() (the group’s value where it belongs, NA elsewhere), add one e_scatter() call per column, then pass one colour per series to e_color(c(...)). echarts4r skips the NA points, so each series draws only its own group.

My points are huge blobs. The second positional argument of e_scatter() is size, not a colour column — so e_scatter(mpg, cyl) maps cyl to point size and you get giant bubbles (and symbol_size will not override a mapped size). To colour by group instead, don’t pass the group column there: mask the y values into separate NA columns and give each its own e_scatter() series with a fixed symbol_size.

My bubbles are all the same size. The size variable goes to e_scatter()’s size argument — e_scatter(mpg, size = hp). If you set symbol_size as well, it overrides the sizing with a fixed radius, which is what you want for a coloured group scatter but not for a bubble chart.

Frequently asked questions

Yes. echarts4r is released under the Apache 2.0 licence and wraps Apache ECharts, which is also Apache 2.0 — a permissive, free, open-source licence with no per-site or commercial fee. You can use it in reports, dashboards, and public sites without a paid licence.

Use the bind argument of e_scatter(): e_scatter(mpg, bind = model) attaches the model column to each point, and e_tooltip() then shows it. Without bind, the tooltip reports only the x and y values.

A scatter plot encodes two variables — one on each axis. A bubble chart adds a third by mapping it to point size. In echarts4r you switch from one to the other by passing a column to e_scatter()’s size argument.

Give each group its own point series. Split the y column into one column per group with ifelse() (the value where the row belongs, NA otherwise), add one e_scatter() call per column, and pass one colour per series to e_color(c(...)). This keeps your teaching code in base R — no dplyr pipe — while still producing a cleanly coloured scatter with an interactive legend.

ggiraph makes your existing ggplot2 charts interactive by rendering them as SVG, so the look is identical to ggplot2 and the widget is lightweight. echarts4r draws with a different engine (Apache ECharts on an HTML canvas) that offers richer built-in interactions — visual-map filtering, zoom sliders, animated transitions — at the cost of a larger JavaScript payload. Use ggiraph when you want your publication ggplot look with light interactivity; reach for echarts4r when you need those richer built-in controls.

Test your understanding

Using the built-in mtcars, prepare a base-R data frame with wt, mpg, and qsec (quarter-mile time), then draw an interactive bubble chart with echarts4r: weight on the x axis, MPG on the y axis, and point size driven by qsec. Colour the points azure, bind the model name for the tooltip, and label both axes. Assign it to a variable and print it to view.

Build the data frame with data.frame(model = rownames(mtcars), wt = mtcars$wt, mpg = mtcars$mpg, qsec = mtcars$qsec), then chain e_charts(wt) with e_scatter(mpg, size = qsec, bind = model). Set the colour with e_color("#3a86d4") and the tooltip with e_tooltip().

library(echarts4r)
cars <- data.frame(
  model = rownames(mtcars),
  wt    = mtcars$wt,
  mpg   = mtcars$mpg,
  qsec  = mtcars$qsec
)

cars |>
  e_charts(wt) |>
  e_scatter(mpg, size = qsec, bind = model) |>
  e_color("#3a86d4") |>
  e_tooltip() |>
  e_x_axis(name = "Weight (1000 lbs)") |>
  e_y_axis(name = "MPG")

Passing qsec to size scales every point’s radius to the quarter-mile time; bind = model lets the tooltip name each car.

Quick check. You want to colour a scatter by transmission (am, where 0 = automatic and 1 = manual). Using the base-R, no-dplyr approach from this lesson, how many e_scatter() calls do you write, and what feeds each one?

Two e_scatter() calls — one per group. Build two NA-masked columns, e.g. cars$Automatic <- ifelse(cars$am == 0, cars$mpg, NA) and cars$Manual <- ifelse(cars$am == 1, cars$mpg, NA), then add e_scatter(Automatic, symbol_size = 11) and e_scatter(Manual, symbol_size = 11). Pass two colours to e_color(c(...)) and each series draws only its own group.

Conclusion

echarts4r turns a familiar scatter plot into an explorable one: e_charts() sets the x axis, e_scatter() adds points, and a short pipe adds tooltips, point sizing for a bubble chart, and one series per group for colour-by-group. The static ggplot2 poster keeps the page fast; one click reveals the full interactive chart. Every result on this page was produced by the code shown — copy any block and run it to reproduce them.

Reuse

Citation

BibTeX citation:
@online{2026,
  author = {},
  title = {Interactive {Scatter} \& {Bubble} {Charts} in {R} with
    Echarts4r},
  date = {2026-07-09},
  url = {https://www.datanovia.com/learn/data-visualization/echarts4r/interactive-scatter-plots},
  langid = {en}
}
For attribution, please cite this work as:
“Interactive Scatter & Bubble Charts in R with Echarts4r.” 2026. July 9. https://www.datanovia.com/learn/data-visualization/echarts4r/interactive-scatter-plots.