addIoLayer("radar") and addIoLayer("parallel") with a group mapping now
draw one polygon or line set per group instead of "No data to display".
addIoLayer() split group into one layer per value, and since both are
standalone types the engine refused to compose them. They now stay a single
layer with group in the mapping, which their renderers already handle.
A dplyr::group_by() data frame passed to these types is grouped the same
way, by its grouping variable (#146).
The regression and histogram-fit stats text (R², equation) and the
"comparison" significance brackets and p-value labels now follow the theme.
They read --text-color and --font-family, which no theme defines, so they
always drew #333 in sans-serif: nearly invisible under
setTheme(mode = "dark") and the dark presets, and deaf to
setTheme(textColor = , font = ). They now read --chart-text-color and
--chart-font, the same tokens as the axis labels. In the default light theme
this moves them from #333 to the axis-label gray and the chart font (#160).
Parallel-coordinates charts now show their dimension names. Each label sat
inside the axis group, where d3.axisLeft() sets fill="none", so the names
were in the DOM but never painted. Labels now use fill="currentColor", the
same ink as the tick labels (#159).
Boxplot whiskers and caps, error bars, candlestick wicks and waterfall
connectors are now visible under setTheme(mode = "dark") and the midnight,
ocean, forest and neon presets. Their strokes were hardcoded to black,
#374151 or #666, which sits at 1.1-2.9:1 against those backgrounds, so a
dark-mode boxplot showed boxes and outliers but no spread. They now read a new
--chart-mark-stroke token, which those themes set to their text color.
Light mode and the light presets don't define it and render exactly as before,
and setTheme(values = list("--chart-mark-stroke" = ...)) overrides it like
any other token (#149).
The "server" engine now works in Shiny (#123). A setBigData() chart
rendered with renderMyIO() under engine = "server" (what "auto"
resolves to in a Shiny session) never got data: the browser sent its query
under an input name no R handler was registered for, renderMyIO() never
registered the source with the session, and the browser sent SQL where R
only accepts a server-side template id. The browser now sends a template id
and column bindings, R composes the query against the source registered at
render time, and selections arrive as column/value filters bound as query
parameters, so no SQL text from the browser reaches the database. Inline
(data frame and Arrow) and DBIConnection sources are supported; a DBI
connection stays open when the session ends, because the app owns it. File
path and URL sources still need the "wasm" engine.
setLinkedCursor(axis = ), linkCharts(cursorAxis = ) and
setLinked(cursorAxis = ) now render. The argument was validated, stored and
published to the MCP schema since 1.2.0, but the engine only ever drew a
vertical rule, so "y" and "xy" were accepted and then silently ignored.
"y" draws a horizontal rule at the hovered y value and "xy" draws both,
which is what makes cursor sync useful for horizontally-oriented charts and
for scatter matrices. Each rule is drawn only when the receiving chart can map
the incoming value through its own scale, so a value outside a sibling's
domain draws nothing on that axis rather than clamping to an edge. The default
is still "x" and its output is unchanged.
transform_ci() accepts method = "polynomial". It previously stopped with
"Unknown ci method", so composite_regression() with a polynomial fit and a
confidence band never worked. The fit uses options$degree (default 2) and
loess now honors that same option instead of always fitting locally
quadratic. Polynomial fits require degree + 2 points rather than 3, since a
higher-degree fit through too few points has no residual degrees of freedom to
build an interval from.
setTransition(easing = ) now reaches every chart type. It was documented as
universal but only the enter and update joins of about half the renderers
actually applied it, so calling it on a radar, funnel, waffle, dumbbell,
bracket, beeswarm, bump, calendar heatmap, lollipop, parallel-coordinates or
quantile-dots chart changed nothing and produced no warning. All 144
transitions across the renderer set now resolve their easing through the
public setting, falling back to each renderer's previous curve when no easing
is set, so charts that never called setTransition() animate exactly as
before. setTransition(stagger = ) reaches the eleven renderers that had no
transition delays at all; transitions that use a delay to sequence one
animation behind another keep that sequencing.
Bump charts now draw rank 1 at the top of the plot with whole-number axis ticks, instead of rank 1 at the bottom with half-rank ticks such as 1.5. This flips the vertical orientation of every existing bump chart.
Confidence and prediction intervals are no longer the same width. transform_ci()
read interval but computed both from the standard error of the fit alone, so a
prediction interval was drawn as a confidence interval. Prediction intervals now
add the residual scale, and the t critical value uses the degrees of freedom the
prediction reports rather than a value derived from the effective parameter count.
Every prediction band drawn before this was too narrow.
Grouped charts no longer emit phantom rows for missing group values. Boxplot,
violin, ridgeline, quantile-dots and the other grouped transforms matched groups
with ==, which returns NA rather than FALSE for an NA group value, so
subsetting introduced all-NA rows into the result. Groups are matched by
position now, and a genuinely missing group is labeled "NA (missing)" — chosen
to avoid colliding with an existing label after the engine strips punctuation
from tag names.
transform_lm() returns a typed empty result instead of failing when fewer than
two finite points remain after dropping NA and infinite values. It also drops
those rows before fitting rather than passing them to lm().
Survival curves extend through the end of follow-up rather than stopping at the
last event time, and lines and bands honor curveType = "stepAfter" instead of
drawing a smooth monotone curve through step data.
myio_validate_spec() rejects mappings that are non-character, empty, NA, or
whitespace only. It previously accepted them and failed later in the engine. For
parallel charts it validates every column in the dimensions list rather than
only single-string mappings.
Layer opacity set through setLayerOpacity() is no longer overwritten by enter
transitions, and layer matching uses exact mark tokens — a label that was a
prefix of a neighboring label could hide the wrong marks.
setLayerOpacity() reaches donut, gauge and grouped-bar charts, which ignored
it without a warning. Together with the matching fix above, opacity now applies
on every chart type, including layers whose labels contain punctuation (#122).
Beeswarm charts center points on their category bands instead of starting at the
band edge, which placed points above their axis labels, and category tooltips no
longer read NaN.
Calendar heatmaps size cells from the chart's actual clip dimensions and account for the gaps between cells, so the last weekday row is no longer clipped in narrow containers.
Axis ticks render correctly for R Date values before 1970.
Charts clean up on destruction: delayed callbacks no longer fire against a removed chart, and binding subscriptions are released. Registration is retained while a chart is merely hidden, so charts in inactive tabs still render when their tab is shown.
setSlider() documentation describes what the function does. It does not
re-render on its own — the value has to be read from
input$`myIO-{outputId}-slider-{param}` in the render expression and passed
to the transform option. The previous example passed a fixed option value and a
param name that was documented as a transform option, so following it produced
a slider that changed nothing.
NEWS entries use the US spellings the package declares in Language: en-US.
The large-data vignette no longer advertises server-engine timings or
recommends the "server" engine for hosted Shiny. It now says that the
server engine does not run brush queries in this release (#123).
Radar charts follow the theme. Their grid rings, spokes and labels read
--chart-grid and --chart-fg, two variables nothing defines, so
setTheme(textColor = , gridColor = ), mode = "dark" and every preset were
ignored and the light fallbacks always rendered. Axis labels were close to
invisible in dark themes, at about 1.1:1 contrast. They now read
--chart-grid-color and --chart-text-color like every other renderer, so
the default light radar grid and labels also change to the theme's gray (#140).
setToolTipOptions(suppressY = TRUE) works again. The option was validated,
stored and documented, but nothing in the v1.0 engine read it, so the y value
appeared in every tooltip. Tooltips on point, bar, line, area, grouped-bar and
donut charts now show only the x title when it is set. Charts that never call
it are unchanged (#121).
Reference lines from setReferenceLines() follow the theme. They were drawn
with an inline gray stroke 3px wide, which overrode the
--chart-ref-line-color and --chart-ref-line-width values that every preset
and mode = "dark" set, so they could not be themed at all. They now read
those tokens, which also changes the default line to #9ca3af at 2px, matching
the tooltip crosshair (#141).
Hiding every layer from the legend no longer strands the chart. The empty state used to hide the legend-and-actions button and close the panel, so once the last visible series was switched off there was no way to bring any of them back without reloading. The button and panel now stay, with the entries shown as off and a "Show All" button.
The legend-and-actions button opens and closes the panel, and it keeps its menu icon. It used to turn into an X while the panel was open, and clicking that X did nothing.
Charts with a single series no longer show a one-entry legend in the panel. The entry repeated the layer name, sat above a divider with nothing to divide, and could only be used to hide the chart's only data. Legends with two or more entries and gradient legends are unchanged.
Funnel charts no longer show a legend. Their stages are already labeled on the bars, and clicking a legend entry did nothing. Waffle, radar, parallel-coordinates and treemap legends show a plain color key instead of entries that looked clickable but did nothing. Donut legend entries still hide and show segments.
Keyframe storytelling adds addKeyframe() for named, transformed data
snapshots and accessible previous/play-pause/next controls. Single-layer
charts accept a data frame; multi-layer charts accept a named list keyed by
layer label, with omitted layers retaining their prior state. Playback runs
once and stops at the final frame, while reduced-motion and zero-duration
transitions remain fully step- and play-capable. Shiny applications can use
setKeyframe() and stepKeyframe() through the existing instance registry.
WebR 0.6.0 compatibility is now a blocking CI contract: the package and its dependencies are compiled with the official r-wasm action, loaded in WebR, used to create and serialize a real widget, and rendered with the production bundle in Chromium. The verified path does not claim DuckDB-WASM support or universal compatibility across browser hosts.
Legend/button UI streamlining (#84): charts now show exactly one legend
surface at a time. When a discrete chart's compact in-plot legend is showing,
the chart-controls panel no longer repeats the same legend and becomes
actions-only (this supersedes the 1.2.0 note that kept the action-sheet
legend alongside the new inline one). The in-plot legend is now interactive —
click or keyboard (Enter/Space) toggles a series on/off, with role="switch"
semantics matching the panel legend. Charts with more than 10 series, or
containers too narrow to fit the strip legibly, move the full legend to the
panel instead of truncating (previously the strip silently cut off at 10
items). Inline legend rows now wrap by measured width rather than item count.
No R API changes; suppressLegend() behaves exactly as before.
Responsive chart-controls behavior now keys off the widget's own container width everywhere, instead of a mix of container width (panel docking) and browser viewport width (button position, sheet drag handle). A narrow widget embedded in a wide page — dashboard grids, side-by-side layouts — gets a coherent narrow-tier UI. The panel legend's two-column grid is now driven by item count on all sizes, not just narrow containers.
New setTransition(duration, easing, stagger) configures chart animations:
duration in milliseconds, easing (one of "linear", "quad", "cubic",
"sin", "exp", "circle", "back", "bounce", "elastic", mapped to the
corresponding d3 easing), and stagger (per-element cascade delay in ms).
All arguments are optional and additive; unset values keep each renderer's
existing defaults, so the change is fully backward compatible.
setTransitionSpeed() is now a thin wrapper over setTransition(duration = ).
Animation stays fully opt-out-able: duration = 0 disables it, and easing and
stagger automatically no-op when the effective duration is 0, including under
the viewer's prefers-reduced-motion: reduce system setting. A Playwright e2e
spec verifies animate-when-on, still-when-off, and still-under-reduced-motion.
New opt-in "lttb" transform for line layers downsamples a large series
with Largest-Triangle-Three-Buckets, shipping at most options$threshold
points (default 2000) while preserving the visual shape:
addIoLayer(type = "line", transform = "lttb", options = list(threshold = 1000)).
Off by default (identity); runs on the in-memory/SVG path and is independent
of the DuckDB-WASM engine's own SQL-side LTTB, so it never double-downsamples.
New myIOProxy() + updateMyIOData() update a rendered chart's layer data in
place from the Shiny server without re-running renderMyIO(). Layers are
matched by label and swapped through the existing data-join path, so only the
changed marks transition and brush/zoom/toggle state is preserved (the full
re-render destroyed and recreated the chart, flickering and dropping state):
myIOProxy("chart") |> updateMyIOData(series = new_df).
setLegendTitle() puts a title on the legend naming the variable its entries
come from, so a grouped chart whose series are labeled 5 6 7 8 9 can say
those are months. The title renders on whichever legend surface is active --
the compact in-plot strip or the panel behind the legend button -- and on
exported SVG, PNG and PDF. Pass a string, or TRUE to derive the title from
the grouping column supplied to addIoLayer(mapping = list(group = ...)).
Charts render exactly as before until the function is called.
A chart title too long for its container is now trimmed with an ellipsis instead of running underneath the legend button in the top-right corner. The untrimmed title is kept as the element's accessible name.
Toggling a grouped bar chart to its stacked layout no longer leaves the rotated y-axis title behind when the wider stacked tick labels grow the left margin. The title moved with the plot on a normal redraw but not on the layout toggle, so the labels still ran through it.
The legend button no longer sits on top of plotted data. Charts already
reserved the button's corner band on narrow layouts; wide layouts now do
the same, so a mark that lands in the top-right corner is never hidden
underneath it. An explicit setMargin() still wins, and sparklines and
non-axis charts are unaffected.
Waterfall charts no longer log a browser console warning about null values,
and the total bar's tooltip no longer reads Delta: null. A total row is
declared by putting NA in the value column, and that NA was traveling all
the way to the browser even though the transform had already worked out what
the bar's height should be. The total row now carries its own magnitude -- the
running total the bar spans -- so the chart reports a real number everywhere.
Blast radius: the value column in exported CSV changes for waterfall charts.
A total row's cell now holds the total instead of being blank, and a
non-total row written as NA now reads 0, which is the height that row has
always been drawn at. Nothing about how the chart renders changes.
Faceted charts now size each panel's left margin to fit its y tick labels.
Panels never ran the margin fit that ordinary charts run, so a facet grid with
a wide y format -- currency, or any large number -- drew its tick labels
through the rotated axis title and off the left edge of the panel, where the
same chart unfaceted laid out correctly. Blast radius: a faceted, axes-based
chart whose y labels are wider than its left margin now gains left margin and
loses the same amount of plot width; with facet(scales = "fixed") all
panels share one domain and so shift together, and panels that call
setMargin() are unchanged.
Grouped bar charts no longer let their y tick labels run through the y-axis
title after switching to the stacked layout. Stacking sums the series, so the
tick labels gain digits the chart's left margin was never sized for -- a
"$,.0f" format on data in the hundreds of thousands pushed the labels
several pixels past the rotated title. The left margin is now re-measured
after the bars are drawn, both when the layout is toggled from the chart's
action button and on every redraw of a chart already in stacked mode. Charts
that call setMargin() keep the margin they were given, and every other
chart type is unchanged.
Linked charts no longer disagree when a brush covers empty space. Dragging a
brush over a region containing no points left the brushed chart with all its
marks dimmed while every linked chart snapped back to full opacity, so the
two showed opposite states for the same selection. An empty brush is now
treated as what it is -- a selection of nothing -- and dims the marks on the
linked charts too. Removing the brush entirely still restores everything
everywhere, unchanged. Blast radius: any existing setLinked() or
linkCharts() page will now dim its linked charts while a brush sits over
empty space, where before they stayed bright. The myIO-{id}-brushed Shiny
input gains an active field carrying the distinction (TRUE while a brush
rectangle is on screen, FALSE once it is removed); existing code reading
keys, extent or data is unaffected.
setLinked() charts now cross-select against non-myIO widgets. A myIO chart
in a Crosstalk group used to broadcast and match on its own internal row
numbers rather than the keys the SharedData object was built with, so
brushing it selected nothing in a linked DT table, plotly figure or
leaflet map, and a selection made in any of those lit nothing in the myIO
chart. Selections now travel on the Crosstalk keys, so all four coordinate.
Two myIO charts linked to each other behaved consistently before and still
do, and widgets saved by earlier versions of myIO keep working. Blast radius:
the keys that reach the Crosstalk group change from myIO's private row_1,
row_2, ... to the real keys, so any custom JavaScript listening on the
group now sees the same keys every other widget uses. One limit is
unchanged: keys are paired with rows by position, so a chart built from a
frame that was re-sorted or re-filtered after shared$data() cannot be
matched. Where the row count no longer matches, the chart now falls back to
matching within itself rather than pairing keys with the wrong rows. See
?setLinked.
type = "boxplot" with options = list(whiskerType = "minmax") no longer
fails with $ operator is invalid for atomic vectors. The documented
"minmax" whisker style errored on every call; whiskers now render at each
group's own minimum and maximum as documented. The default "tukey" whiskers
are unaffected.
The compact in-plot legend no longer runs underneath the x-axis title on narrow charts. Its first row shares a baseline band with the centered axis title, so that row now stops short of it and wraps instead; the second row sits below the title and keeps the full width.
Treemap cells no longer render underneath the floating action button in the chart's top-right corner. The tiling now reserves the same button-width band that funnel and Sankey charts already reserve, so the corner cell and its label stay fully visible and clickable.
linkCharts() now actually links charts. Brushing one chart in a
linkCharts() group dims the non-matching marks on every other chart in the
group; previously nothing happened at all, in any environment. Three things
were wrong: the link mode written by linkCharts() was a value the
browser-side code did not recognize, so neither the outgoing nor the incoming
handler was ever connected; the cross-selection machinery required Crosstalk
to be loaded even though linkCharts() is documented as not needing it, so it
bailed out on a plain R Markdown or Quarto page; and rows were matched between
charts by position rather than by the on column, which is meaningless for
charts built from different data frames. Charts are now matched on the on
column's values as documented, and coordination works with or without
Crosstalk on the page. Charts linked with setLinked() are unaffected --
they continue to use Crosstalk and to match on the shared row key. Widgets
saved by earlier versions of myIO keep working against the new code.
Clearing a brush -- by clicking outside it, pressing Escape, or using the status bar's "Clear" button -- no longer overflows the call stack. The overflow left the chart's stored selection in place and skipped the cleared event, so linked target charts stayed dimmed until the page was reloaded.
Charts no longer pad the y axis down into negative territory for data that
never goes negative. Y-axis padding is now 5% of the data range per side,
matching the x axis, and the padded lower bound stops at zero when the data
minimum is at or above zero -- a yield series bottoming out near 0 no longer
renders a -5 tick. Limits set explicitly with setAxisLimits() are
unaffected.
type = "regression" now draws the raw scatter, the fitted line and the
confidence band in visually separable treatments. Previously all three shared
a single hue, so the fit and its uncertainty band were hard to distinguish
from the data. The scatter keeps the layer color, the fit line and band take
a second contrasting hue from the Okabe-Ito palette, and the band renders at a
lighter fill opacity. Pass a two-element color vector to set both
explicitly (color = c("#333", "#333") restores the previous single-hue
look), or set options$areaOpacity to control the band opacity.
Linked brushing set up with setLinked() now actually propagates: brushing a
chart configured as a Crosstalk source dims the non-selected points on every
target chart in the same group, and clearing the brush restores them. Target
charts previously stayed inert because the inbound Crosstalk subscription was
registered under an event name Crosstalk never emits. The filter = TRUE
variant, which hides rather than dims non-selected marks, is fixed likewise.
The demo screenshot script (scripts/screenshot-all.js) now enumerates the
navbar and any nested tabsets from the live DOM instead of a hard-coded tab
list, so it covers all 41 demo charts and no longer fails on tabs that are
plain top-level entries rather than dropdown menus. It also records console
errors and warnings per chart in report.json.
Waffle charts now show a legend. Previously no legend appeared at all -- neither the in-plot strip nor the bottom-sheet panel listed a single category, so there was no way to tell which color meant which category. The legend now lists one entry per category, in data order, with swatches that match the cell colors.
Bump charts now draw each series in its own color, matching the legend swatches. Every series previously rendered in the same blue, making the legend useless and the ranking lines impossible to tell apart.
Bump chart points and lines now line up with their x-axis ticks. On a categorical x axis the marks were drawn half a category width to the left of the tick they belonged to.
Small multiples created with setFacet() now show the chart title once, above
the grid, instead of repeating it inside every panel, and every panel draws
its own x and y axis with identical plot geometry so the panels are readable
and directly comparable. Previously only the first panel had a y axis, only
the bottom row had an x axis, and the panels that lost an axis were drawn
wider than the rest. The facet grid is also styled as a real CSS grid, so
ncol and minWidth take effect and panels sit side by side rather than
stacked at full width. An explicit suppressAxis() setting is now honored
inside facet panels.
Radar charts now draw concentric grid rings with radial value labels, so the
magnitude of each polygon vertex can actually be read. Previously the chart
showed only the radial spokes and their category labels, leaving the plot
without any value reference. The rings can be turned off or re-leveled per
layer with options = list(grid = FALSE) and options = list(gridLevels = 6).
Bar, grouped bar and lollipop charts now start their value axis at zero by
default, so bar lengths are proportional to the values they encode. The axis
previously began at a buffered data minimum (a grouped bar demo spanning
56-97 started at 50) while the bars themselves were still drawn from zero and
clipped, so the axis and the geometry disagreed. An explicit setAxisLimits()
still wins on either bound. Relatedly, a limit of exactly 0 passed to
setAxisLimits() is no longer silently discarded -- it was treated as "not
set" on every chart type, so ylim = list(min = 0) had no effect at all.
Violin charts now draw the median marker inside the interquartile box. The median sub-layer rendered nothing at all, so the box showed the IQR span with no indication of where the median sat within it. It now draws the same white rule that boxplot charts use, matched to the box width.
Ridgeline charts now label the y axis with the group each ridge belongs to
instead of printing the raw stacking offsets (0.8, 1.0, 1.2, ...), and default
the y axis title to the grouping variable. The axis previously read as a
density scale even though the tick positions were group baselines, so the
numbers were meaningless. An explicit setAxisFormat(yLabel = ) still wins.
Funnel and Sankey charts now show their numbers in the plot. Funnel stages
print the stage value and its conversion rate against the first stage; Sankey
nodes print their total and each flow prints its magnitude. Previously the
only text was the stage or node name and the values were reachable only by
hovering. Labels honor setAxisFormat(yAxis = ), are placed so they stay
readable against the mark they sit on, and are suppressed rather than
overlapped where the shape is too small. Pass showValues = FALSE to
addIoLayer() to restore the previous names-only labeling.
Sankey charts no longer push their terminal nodes against the right edge of the plot. The layout now reserves room for the right-hand node labels, so the last column and its labels sit inside the plot area instead of being clipped and drawn back on top of the flow ribbons. Node labels are also drawn in a high-contrast ink with a background-colored halo, so they stay legible where they cross a ribbon; on a dark chart background the ink flips to white.
Rotated y-axis titles set with setAxisLabels(yAxis = ) are no longer shaved
off at the left edge of the chart. The title was anchored 6px from the SVG
border, but a -90 degree rotation grows the glyphs leftward from that
anchor, so the tops of every letter were clipped on every chart with a y-axis
title. The anchor now sits far enough inside the border for the full glyph
height to fit.
Funnel and Sankey charts no longer draw under the floating action button in
the corner of the widget. Both filled their whole plot rectangle, so the
widest funnel stage and the right-hand Sankey column and its labels were
partly hidden behind the button. Both layouts now reserve that band. Charts
that already set setMargin(right = 56) or larger are unchanged.
An open legend/actions panel now closes when its chart's tab is switched away from. Tab frameworks hide the pane rather than destroying the widget, so the panel stayed open -- it was still there on return, its button still reported itself expanded, and two charts could hold competing keyboard focus traps at once.
Sankey flow values no longer stack on top of each other. Two links that cross inside the same column gap can compute the same label position, and each label's visibility was decided without reference to any other, so both were painted on the same pixel. A label that would land on one already drawn is now dropped, deterministically and in the links' own data order, so the same label wins on every re-render. The suppressed value is still on the link's tooltip and in the chart's data table.
Short funnels no longer drop their values with no trace. Value placement now
degrades in three steps instead of two: a stage band at least 34px tall keeps
the value on a second line under the stage name, a band between 18px and 34px
moves it onto the name's own line just outside the trapezoid, and only below
18px is the name drawn alone with the value left to the tooltip. Previously
anything under 34px hid every value outright, including values that would have
fitted perfectly well beside the stage. options = list(showValues = FALSE)
behaves exactly as before.
A funnel value label placed outside its trapezoid no longer runs under the legend/actions button. The funnel's stages were already kept clear of the button, but a label that no longer fits inside a stage is placed against the full plot width, so on a funnel with a small top margin the first stage's value could be painted beneath it. Only a label whose text actually falls in the button's band is affected -- labels lower down the funnel still use the full width, so nothing that was legible before is suppressed now.
The legend/actions button now sits in the top-right corner at every container
width. On containers narrower than 600px it dropped to the bottom-right, where
it covered the right-most x-axis tick label -- the bottom band there is fully
occupied by rotated tick labels, the x-axis title and the inline legend, so
there was no free space for it. Narrow axes charts that have not called
setMargin() now reserve a 48px top margin for the button so it never lands
on the plot. Blast radius: an axes chart in a container 600px wide or less
that has not called setMargin() gains 18px of top margin (30 becomes 48) and
loses the same amount of plot height. Charts wider than 600px, charts that
call setMargin(), sparklines, and non-axes charts such as funnel, sankey and
treemap are unchanged.
Sparklines no longer paint a floating action button. The button covered about 8% of a 60px inline chart and sat on top of its last data points, and it intercepted the pointer over that corner so those points had no tooltip. Sparkline mode already strips axes, legend, reference lines and interactions, and now strips the button too. Blast radius: the export menu (CSV, PNG, SVG, PDF, clipboard) is no longer reachable from a sparkline; export the same data from a full-size chart. The panel's legend was already suppressed in sparkline mode, so no legend is lost.
Legend entries for grouped layers now show the group value on its own instead
of concatenating the layer label with the group value -- "Core" rather than
"Rankings — Core". The chart title and axis labels already carry that
context. This affects addIoLayer(mapping = list(group = ...)), grouped data
frames from dplyr::group_by(), and the regression, qq and survfit
composites. Labels set explicitly on ungrouped layers are unchanged. When a
bare group value would collide with a layer already on the chart, the previous
"<label> — <group>" form is used automatically, so multi-layer charts keep
unique labels. Code that targets grouped layers by label -- updateMyIOData(),
addKeyframe() -- and any CSS or test selector pinned to a grouped layer's
generated class name must use the new labels.
type = "ridgeline" now stacks its groups in a deterministic order instead of
the order the group values happen to appear in the data. If the group column
is a factor, its level order is used; otherwise groups sort ascending
(character columns sort in the C locale so the result is identical on every
machine). Ascending order reads bottom-to-top, matching a discrete y axis in
ggplot2. This changes the rendering of any existing ridgeline whose group
column was not already sorted: the ridges move, the y tick labels move with
them, and because the color vector is applied by group position, the colors
re-map too. For example, a ridgeline over mtcars with cyl as a character
column previously stacked 6, 4, 8 from the bottom and now stacks 4, 6, 8. To
keep a specific non-alphabetical order, make the group column a factor with
the levels in the order you want.
type = "boxplot", type = "violin" and type = "comparison" now place
their groups along the x axis by the same deterministic rule ridgeline
already uses, instead of by the order the group values happen to appear in
the data: a factor's level order wins, anything else sorts ascending, and
character columns sort in the C locale so the result is identical on every
machine. This changes the rendering of any existing boxplot, violin or
comparison chart whose group column was not already sorted -- the boxes
move, the x tick labels move with them, and on a violin passed a vector of
colors the hues re-map, because color is applied by group position. Every
statistic stays with its own group: each box, whisker, median and outlier is
now looked up by group name rather than by position, and the significance
brackets on a comparison chart are positioned by the same rule, so they keep
spanning the pairs they are labeled with. Charts whose groups
already appear in sorted order -- including every factor whose rows follow
its levels -- render exactly as before. To keep a specific non-alphabetical
order, make the group column a factor with the levels in the order you want.
Charts left on the default left margin now widen it automatically when the y
tick labels would run into the rotated y-axis title. Previously a currency or
large-number y format -- setAxisFormat(yAxis = "$,.0f") on three-digit data,
for example -- produced labels wide enough to overlap the title. The engine
now measures the rendered tick labels and grows the left margin just enough to
clear the title band; it only ever grows, never shrinks below the configured
value, and it settles on the first render. A chart that calls setMargin()
keeps exactly the margins it was given and is never adjusted. Charts whose
labels already fit are unchanged. Note that a widget saved as HTML by an
earlier version of myIO has no record of whether setMargin() was called, so
re-rendering it against this version may widen a too-narrow left margin.
@hono/node-server 2.0.12 and declares Node.js
20 or newer as its runtime floor; its conformance, stdio smoke, and audit
gates pass with zero known npm vulnerabilities.inst/htmlwidgets/myIO/myIOapi.js drops from 2.32 MB to 1.20 MB raw
(398,650 to 298,757 bytes gzipped, -25%) with no behavior change; the
development watch build stays unminified for debugging.playwright.config.ts and a new
e2e CI workflow. The suite builds and loads the minified myIOapi.js,
guarding the production bundle that source-importing unit tests cannot catch.touchstart on a bar surfaces
the tooltip with the datum's content and touchend dismisses it, guarding the
mobile hover path against the production bundle.file:// deployment e2e (self-contained widget opened directly from disk)
is now exercised for real: its fixture loads the IIFE bundle via a classic
<script src> rather than an ES module, so the file-protocol → SVG-engine
fallback is verified under file:// instead of skipped.setBrush(onSelect),
setFacet(minWidth, labelPosition), setTheme(textColor, gridColor), and
setBigData(rowkeyCol) are the canonical forms (matching colorScheme, xAxis,
etc.). The previous snake_case names (on_select, min_width, label_position,
text_color, grid_color, rowkey_col) keep working as deprecated aliases that
emit a one-line warning; existing code is unaffected aside from the warning. When
both forms are supplied the camelCase value wins.setFacet(), setLayerOpacity(), and setTheme(mode = ) now report invalid
arguments with consistent, actionable messages (e.g.
setFacet(): \scales` must be "fixed", "free_x", "free_y", "free", not "x".) instead of bare stopifnot()failures.setColorScheme()` errors are likewise
function-prefixed. No change to which inputs are accepted.setTheme() now warns when passed an unknown argument that lacks the required
-- prefix (e.g. a misspelled text_colour) and suggests the intended
argument, instead of silently dropping it. Valid ---prefixed CSS overrides
are unaffected.setTheme() documents the named preset values ("midnight", "ocean",
"forest", "sunset", "monochrome", "neon", "corporate", "academic",
"nature", "minimal", "retro", "warm", plus "light"/"dark"); the
preset argument was already functional.setLinked() and linkCharts() now cross-reference each other in their
documentation to clarify when to use the Crosstalk path versus the
group-identifier path.midnight, ocean, forest, sunset, monochrome, neon, corporate,
academic, nature, minimal, retro, warm, plus light/dark) as
live, side-by-side previews, and shows how to layer custom CSS overrides on
top of a preset.Uint8Array.fromBase64
when the browser provides it (falling back to the previous atob loop),
avoiding a per-character JavaScript callback over large payloads in the
in-memory and DuckDB-WASM engines.addIoLayer()) is faster for large data: the
row-rectangling step now extracts columns once and indexes per row instead of
subsetting the data frame on every row, roughly 5x faster at 100k rows. The
emitted JSON is byte-identical to before (pinned by tests across numeric,
integer, character, logical, factor, Date, and POSIXct columns), so every
chart type renders exactly as it did.inst/myio-schema.json)
describing all chart types, required mappings, valid transforms, and function
signatures, generated from the package's own contracts.myio_list_chart_types(), myio_chart_schema(),
myio_validate_spec(), myio_list_functions(), myio_function_signature(),
and myio_validate_call(). A Model Context Protocol server in the package's
source repository exposes the same six tools to MCP-compatible clients.llm-tool-calling demonstrates a generate-validate-repair loop.quantile_dots chart type and quantile_dots transform render a
Wilkinson dot plot of predictive quantiles for communicating uncertainty.fan composite renders a fan chart of nested prediction intervals around
a central estimate.myIO(title = ...) and setTitle() add a backward-compatible chart title
surface rendered inside the SVG. Existing charts that omit a title are
unchanged.setAxisFormat(xLabel = ..., yLabel = ...) now render in
the chart SVG, and multi-series charts get a compact visible legend while
preserving the existing action-sheet legend.setLinked() now links waffle, beeswarm, lollipop, and dumbbell
layers in addition to the previously supported point, bar, groupedBar,
histogram, and hexbin. Selection dims non-matching elements; filter
hides them. Aggregate chart types (boxplot, violin, qq, regression,
density, ridgeline, survfit, comparison) remain outside crosstalk
in v1.2.crosstalk-linking demonstrates linking myIO with
DT::datatable() and reactable::reactable(), with code listings for
plotly and leaflet.myIO() widgets now declare a sizing policy with browser.fill = TRUE
and are tagged as html-fill-item via htmltools::bindFillRole(), so
they fill container height inside bslib::card(), Quarto Dashboard
{.fill} cards, and flexdashboard layouts. Rendering in plain RMarkdown
and Shiny at the default 400px height is unchanged.calendarHeatmap chart type — GitHub-contributions-style grid of daily
cells over a single calendar year. Usage:
addIoLayer(type = "calendarHeatmap", data, mapping = list(date = "day", value = "x")).
Supports Sunday or Monday week starts via options$weekStart, continuous
color legend, and linked-cursor sync across two linked calendars. Multi-year
data is rejected in v1.2; multi-year layouts are planned for v1.3. New CSS
variables: --chart-calendar-cell-gap, --chart-calendar-cell-stroke,
--chart-calendar-empty-fill.setTheme(preset = "midnight"). Available presets:
midnight, ocean, forest, sunset, monochrome, neon, corporate, academic,
nature, minimal, retro, warm. Plus light/dark via mode parameter.linkCharts(chart1, chart2, on = "column") enables cross-chart selection.
Brush in one chart highlights matching rows in linked charts. Works in Shiny
and static HTML. Aggregation-to-source-row key resolution for summary views.linkCharts() and setLinked() gain a cursor = TRUE argument that draws a
synchronized vertical crosshair on every linked chart when the user hovers
any chart in the group. Supports point, bar, line, area, groupedBar, hexbin,
and histogram layers (donut and treemap are excluded — no x-axis to sync on).
Off by default; enable per-link or retrofit with setLinkedCursor() on a
pre-linked widget. New --chart-cursor-rule-color/-width/-dasharray CSS
variables theme the crosshair.dplyr::group_by() tibble to addIoLayer() and it auto-creates one
layer per group with colors from the Okabe-Ito palette. Soft dplyr dependency.addIoLayer(type = "survfit", mapping = list(time = "time", status = "status"))
computes Kaplan-Meier estimator with Greenwood CI using base R only (no
survival package dependency). Composite expands to step-curve line, CI band,
and censored-observation markers. Group stratification supported.addIoLayer(type = "histogram_fit", mapping = list(value = "x"), options = list(family = "normal"))
fits normal, lognormal, or exponential distributions via MLE. Composite
renders histogram + fitted density curve + optional parameter annotation.setLayerOpacity(label, opacity) sets per-layer opacity (0-1). Critical for
CI bands overlaying scatter data.graphics-document, graphics-object, graphics-symbol) applied
to SVG chart structure for screen reader navigation.lollipop — vertical stem with circle head, supports mean and summary
transforms. Compatible with categorical x-axis charts.dumbbell — connected dots showing a range between low_y and high_y.waffle — 10x10 grid of colored squares representing proportions. Standalone.beeswarm — dodge-positioned points to avoid overlap. Inline dodge algorithm.bump — smooth S-curves showing rank/value changes over time with grouped lines.radar — spider/radar chart with radial axes and polygon data fill.funnel — narrowing horizontal bars for conversion pipeline data.parallel — parallel coordinates for multivariate exploration.
All new types support themed colors and standard tooltip formatting.setFacet(var, ncol, scales) splits charts into a CSS grid of panels, one per
unique value of the faceting variable. Supports fixed and free scale modes,
auto-layout with configurable minimum panel width, and responsive breakpoints.setExportOptions() controls which export buttons appear in the toolbar
(PNG, SVG, clipboard, CSV). New SVG download button and clipboard copy
(SVG + PNG) via the Clipboard API.setTheme(mode = "dark") applies a WCAG AA-verified dark palette across all
chart elements. Also supports "light" and "auto" (detects OS preference
and Quarto/Bootstrap data-bs-theme).setTheme(bg = "#fff") calls still work.overrides parameter for fine-grained CSS custom property control.myIO(sparkline = TRUE) renders a compact, axes-free chart suitable for
embedding in table cells (reactable, DT, gt). Strips legend, axes, reference
lines, and all interactions. Default height 20px. Supports line, bar, and
area layer types.myio_validate_spec() and the chart specification schema no longer reject
valid specs for chart types with a single required mapping (for example
histogram, gauge, and qq). The schema now always represents list-typed
fields as arrays.myIO now supports bidirectional data flow — user interactions return structured data, not just visual feedback.
setBrush(): rectangle brush selection on point, bar, histogram, hexbin,
and grouped bar charts. Selected points available as Shiny reactive input
or scoped CSV export in static HTML.setAnnotation(): click any data point to attach a label. Annotations
stored as structured data (source key, coordinates, label, category color,
timestamp) and exportable as CSV. Supports preset label dropdowns and
color category pickers.setLinked(): Crosstalk SharedData integration for cross-widget linked
brushing. Selection dims non-matching points; optional filter mode hides
them. Supports source, target, or bidirectional modes.setSlider(): parameter sliders below the chart that trigger Shiny
re-rendering with configurable debounce. Renders disabled with tooltip
in static HTML.dragPoints() now correctly emits the dragEnd event to Shiny
(input$myIO-{id}-dragEnd), which was previously registered but never
fired.type + transform = layer. Transforms
compute statistics in R; renderers display them in D3.js.lm, loess, polynomial, ci, smooth, mean, mean_ci,
residuals, summary, pairwise_test, qq.line, point, bar, area, groupedBar.histogram, hexbin, regression, qq.boxplot, violin, ridgeline, comparison.candlestick, waterfall, heatmap.donut, gauge, treemap, sankey.boxplot: expands into IQR box + whisker caps + median + outliers.violin: expands into density area + IQR box + median point.ridgeline: density curves stacked vertically by group.regression: scatter + trend line + CI band + R-squared annotation.qq: Q-Q scatter + reference line + confidence envelope.comparison: boxplots + pairwise significance brackets.setToolTipOptions().dragPoints().setReferenceLines().setToggle().myIOOutput() and renderMyIO().|>.setTheme().