Three things changed in 2025 that make plotly a risky dependency for new work:
If your charts need statistical overlays, the cost of working around plotly’s gaps exceeds the cost of switching.
# myIO
myIO(data = mtcars) |>
addIoLayer(type = "point", label = "Cars",
mapping = list(x_var = "wt", y_var = "mpg"))myIO uses a layered pipe API instead of a single function with mode flags.
# plotly
plot_ly(economics_long, x = ~date, y = ~value, color = ~variable,
type = "scatter", mode = "lines")# myIO
myIO(data = economics_long) |>
addIoLayer(type = "line", label = "Trends",
mapping = list(x_var = "date", y_var = "value", group = "variable"))Groups are declared in the mapping, not as a top-level aesthetic.
# myIO
myIO(data = data.frame(x = c("A","B","C"), y = c(10,20,15))) |>
addIoLayer(type = "bar", label = "Values",
mapping = list(x_var = "x", y_var = "y")) |>
defineCategoricalAxis(xAxis = TRUE)myIO requires an explicit defineCategoricalAxis() call
for discrete x-axes.
# myIO
myIO(data = mtcars) |>
addIoLayer(type = "histogram", label = "MPG Distribution",
mapping = list(x_var = "mpg"),
options = list(bins = 15))Bin count is set via options$bins rather than a layout
parameter.
# myIO
myIO(data = iris) |>
addIoLayer(type = "boxplot", label = "Sepal Length",
mapping = list(x_var = "Species", y_var = "Sepal.Length"),
options = list(showOutliers = TRUE)) |>
defineCategoricalAxis(xAxis = TRUE)myIO boxplots decompose into sub-layers (IQR box, whiskers, median, outliers), each independently styled and interactive.
plotly #1472 – CI ribbons on regression lines do not render correctly.
# plotly (broken — CI band misaligns or disappears)
model <- lm(mpg ~ wt, data = mtcars)
preds <- data.frame(wt = seq(min(mtcars$wt), max(mtcars$wt), length.out = 50))
preds <- cbind(preds, predict(model, preds, interval = "confidence"))
plot_ly() |>
add_markers(data = mtcars, x = ~wt, y = ~mpg) |>
add_ribbons(data = preds, x = ~wt, ymin = ~lwr, ymax = ~upr) |>
add_lines(data = preds, x = ~wt, y = ~fit)# myIO (one call, CI computed internally)
myIO(data = mtcars) |>
addIoLayer(type = "regression", label = "MPG vs Weight",
mapping = list(x_var = "wt", y_var = "mpg"),
options = list(method = "lm", showCI = TRUE, showStats = TRUE))myIO computes the CI via stats::predict() and renders it
as a first-class area layer – no manual pre-computation.
plotly #1687 –
ggplotly() drops stat_compare_means()
annotations.
# plotly (annotations lost in ggplotly conversion)
library(ggpubr)
p <- ggboxplot(iris, x = "Species", y = "Sepal.Length") +
stat_compare_means(method = "t.test", comparisons = list(
c("setosa", "versicolor"), c("versicolor", "virginica")))
ggplotly(p) # brackets and p-values vanish# myIO (pairwise tests rendered natively)
myIO(data = iris) |>
addIoLayer(type = "comparison", label = "Sepal Length",
mapping = list(x_var = "Species", y_var = "Sepal.Length"),
options = list(method = "t.test"))The comparison composite expands into boxplots plus
significance brackets with p-values, computed in R and rendered in
D3.js.
# plotly
plot_ly(mtcars, x = ~wt, y = ~mpg, type = "scatter", mode = "markers") |>
layout(template = "plotly_dark")# myIO
myIO(data = mtcars) |>
addIoLayer(type = "point", label = "Cars",
mapping = list(x_var = "wt", y_var = "mpg")) |>
setTheme(background = "#1a1a2e", text = "#e0e0e0",
grid = "#2a2a4a", font = "Inter")myIO theming uses CSS custom properties, so colors apply consistently across all layers including CI bands, annotations, and export buttons.
regression,
comparison, qq, violin, and
ridgeline auto-expand into coordinated sub-layers.lm,
loess, ci, mean_ci,
residuals, pairwise_test, and qq
mix freely across layers.setBrush() returns
selected rows; setAnnotation() enables click-to-label with
CSV export.scatter3d,
surface, mesh3d – myIO is 2D only.scattergeo,
choropleth, Mapbox – myIO has no map types.If you need 3D, maps, or broad chart-type coverage, plotly remains the better choice. If you need statistical overlays that actually work, myIO is worth the switch.