object =, show_cutoffs() requires
show_labels =, and stat_cutoff() uses part =. The retired plot,
labels, and func arguments are no longer translated. Intentional gf_*()
aliases remain.linewidth for line width. A supplied size is translated
for compatibility, and an explicit linewidth takes precedence. Point,
text, and annotation-marker sizes are unchanged.geom_model() and stat_model() for fitted and inferred models. The
exported GeomModel and StatModel extensions now also power gf_model().geom_resid(), geom_square_resid(), geom_reduce(),
geom_square_reduce(), stat_resid(), and stat_reduce(). The ggplot2 and
ggformula interfaces share model planning, prediction, and drawing. Facets,
omitted rows, inherited mappings, weights, orientation, and jitter now follow
the same contract through either interface.geom_b() for annotating a ggplot2 model layer with its coefficients.
It uses the same coefficient planning as gf_b() and gf_coef().geom_squareplot() and stat_squareplot() for the countable squareplot
drawn by gf_squareplot(). Counts, mappings, delayed aesthetics, coordinates,
and layer data now pass through one ggplot2 lifecycle.stat_sd_ruler() beneath gf_sd_ruler(), and complete the distribution
annotation surface with stat_dist_mean(), stat_cutoff(), geom_cutoff(),
guide_cutoff(), and their exported ggplot2 extension classes. Native and
formula interfaces now share their calculations and na.rm = FALSE default.show_dgp() with position guides. Population and estimate frames stay
outside the data panel and remain aligned after scale, coordinate, zoom, and
facet changes.show_cutoffs() preserves the numeric axis and draws scale-aligned triangles,
leaders, and translucent callouts in the panel. Labels use compact lanes and
reflow in narrow or flipped panels; leaders fall back to elbow connections
when a direct route does not fit.position_jitter() is supported by the native
layers while fitted endpoints and grand means remain fixed.palette.colour.discrete and palette.fill.discrete in theme(), or add
scale_discrete_coursekata() when colour and fill should share one scale.
Loading and unloading the CourseKata theme restores the caller's settings..conflicts.OK marker used to suppress duplicate
attachment-conflict messages, resolving an R-devel check warning.gf_b(), with gf_coef() as the alias readers who know coef() will look for. It
annotates a fitted model's coefficients on the plot they describe: a continuous predictor
gets a rise-over-run triangle, with the rise labeled for the run it spans (b[1] when that
run is 1, 10 %*% b[1] when it is 10) and a hollow dot at (0, b0); a categorical
predictor gets a reference line at the b0 group's mean and one labeled arrow to each of the
other groups; the empty model gets the b0 line alone.
Where gf_model() draws the fit, gf_b() draws the numbers that describe it. Every mark
is placed from the model's coefficients rather than from any drawn point, so jitter never
moves an arrow, and every mark is a separately named layer, so a script can find one
without counting layers. show_b0 = TRUE, the default, expands the predictor's own axis to
include 0 on a continuous model, because a picture of b0 that does not show the predictor
at 0 is not a picture of b0. With no model, gf_b() reads the model the plot implies and
fits it at the call; on a faceted plot that is refused, because one set of arrows drawn
over a per-panel line would describe a fit no panel actually has.
Three shapes of model are refused rather than drawn, because every mark is placed from the
coefficients rather than from the points and so nothing about drawing one would look
wrong. A model whose predictor the plot does not draw: lm(Thumb ~ Sex) over a plot of
five race groups, or lm(Thumb ~ log(Height)) over a raw Height axis, where the
triangle lands at 4.23 on an axis running 59 to 76.5. A model with no intercept, which has
no b0 for the marks to be measured from. And a categorical predictor coded any way but
treatment -- contr.sum, contr.helmert, an ordered factor's contr.poly -- where
coefficient k is not group k's difference from a reference group, which is the only thing
an arrow can mean. Choosing a different reference level is still fine: the plot orders its
groups by the same factor the model coded.gf_reduce() and gf_square_reduce(), the latter also spelled
gf_squareduce(). They draw the distance from the grand mean to each fitted
value. Squaring and summing those distances across observations gives SS
Model; doing the same with residuals gives SS Error. The identity is between
the three sums of squares, not between the three squares for one observation.
Like a residual, a reduction runs along whichever axis holds the model's
outcome and starts at the other-axis position where its point is drawn,
including jitter. The grand mean comes from the rows used to fit the model.
On a faceted plot, every panel is measured against that same mean and shows a
piece of one whole-data decomposition. All square layers in the comparison
must use the same aspect so their areas share a scale. Fits without an
intercept and fits with weights are refused. The familiar unweighted identity
SS Total = SS Error + SS Model is guaranteed for an unweighted least-squares
fit with an intercept. Weighted least squares instead guarantees an identity
based on weighted sums and a weighted mean, which the plain areas do not
show. Without an intercept, the unweighted identity need not hold. For
lm(Thumb ~ Height - 1), SS Error plus SS Model is 11700.01 while SS Total is
11880.21. gf_resid() still measures either fit because it does not rely on
that identity.gf_model() called with no model draws the model the plot implies. A numeric predictor
draws the regression line, a categorical predictor draws one mark at each group's mean,
and a plot of an outcome alone draws the grand mean. ggformula::gf_lm() draws nothing at
all on a categorical x, and gf_model() itself would only ever draw a model you had
named. The inferred model is fit per panel, which is the distinction worth knowing: a
faceted gf_point(body_mass_kg ~ flipper_length_m | species) %>% gf_model() fits each
species its own line, where naming a model above would repeat that one whole-data fit in
every panel. On the regression shape, ... reaches the fitting vocabulary gf_lm() uses:
se = TRUE draws the confidence band, n = sets the prediction grid's length,
method.args = is gf_lm()'s lm.args =, and formula = y ~ poly(x, 2) fits a curve.
A model's line runs across the data it was fit on and stops there, however wide the axis
around it gets. Inside that range every point the line interpolates has observations on
both sides of it; outside there are none, and saying the pattern continues needs a theory
or a physical constraint behind it rather than a plot that happens to have room. So a
gf_lims(), or the b0 dot gf_b() places at zero, leaves the line where it was.shuffle(Thumb)
names a different permutation every time it is evaluated, and independently in every layer
that carries it, so there is no seed to declare and nothing for a freshly fit model to
agree with. A gf_model() call with no model therefore pins the drawn values onto the
plot it returns -- the same bargain gf_resid() already makes for jitter. The plot you
passed in is untouched, and the returned plot still reads in your own words: its axis
titles, its refusals and its messages name shuffle(Thumb), not the fixed column standing
in for it.shuffle(Thumb) ~ Height,
gf_model(Thumb ~ Height) computed the right predictions and then left the outcome
aesthetic unmapped, so the layer inherited the plot's own shuffle(Thumb) and ggplot2
re-evaluated it against the prediction grid at draw time -- a fresh permutation, in whose
order geom_line() then joined the predictions. The formula names what to fit and the
plot names what to show, so the plot's outcome expression is now evaluated once, at the
call, and travels as a plain column. A plain transformation such as sqrt() still lands
on the transformed axis. Neither the fix nor the pinning above moves your random stream:
drawing a model over a plot does not cost you a sample.show_cutoffs() takes the distribution part directly, as its second argument:
show_cutoffs(p, middle(Thumb, .95)). That is what every notebook wants to say, and until
now the only way to say it was to shade the plot with a matching fill first. An explicit
part overrides the fill rather than having to agree with it, because marking the 99%
cutoffs on a plot shaded for the 95% loses nothing and is a normal thing to want. The
argument is read by shape and never by value, so show_cutoffs(p, "red") and
col <- "red"; show_cutoffs(p, col) are refused alike -- a color in a position that does
not take one.show_cutoffs() refuses two plots it used to mark meaninglessly: one whose first layer
does not draw a distribution -- cutoffs describe where a distribution's mass sits, and a
scatterplot, boxplot or violin has no mass to cut into -- and, where the part is given
explicitly, a part naming a variable other than the one the plot puts on x. The variable
is compared by name and never evaluated, so a part naming a column that does not exist is
refused by name rather than failing inside eval_tidy().show_cutoffs() twice stacks a second, independent set of markers, and nothing
about the first call has to change for the second to land correctly. Labels are the one
part of that picture that does not stack: both sets are drawn at the same height, so a
second labels = TRUE call now warns that they will overlap, and draws them anyway -- the
plot is still the one you asked for, just harder to read.StatCutoff export: the cutoff rule as a ggplot2::Stat, emitting the intercepts to
pair with ggplot2::GeomVline in a plot you are assembling yourself. It calls the same
function show_cutoffs() calls, so the two cannot compute a different cutoff for the same
distribution part. It computes per panel, which is what a stat does with the rows ggplot2
hands it; show_cutoffs() keeps marking the whole distribution, because that is what a
middle() fill shades. Its func and prop are checked, which the show_cutoffs() route
got for free by reading a call: unchecked, func = "bogus" marked a lower cutoff and
prop = 2 marked the smallest observation as an upper one, both indistinguishable on the
page from a real mark. It plans in the data's own space and carries the result back, so a
cutoff means the same tail on scale_x_reverse() as anywhere else -- a quantile survives a
transformation that increases and turns over under one that decreases.gf_jitter() %>% gf_model() %>% gf_resid() drew every
point on its group's center. Adding a residual to an unseeded jitter now returns a plot
whose jitter is fixed, so a second overlay lands on the same dots; the plot you passed
in is left as it was.gf_ function behaves --
ggformula builds the layer with parameter checking off -- and gf_model(),
gf_sd_ruler(), gf_squareplot() and the five residual functions are ordinary
gf_ functions now, so they behave that way too. The exception is the six
arguments gf_squareplot() itself used to take -- show_mean, show_dgp,
xrange, xbreaks, mincount and auto_subdivide -- each of which is refused
by name and told what draws it now. Left to fall through, a notebook written
against the old signature would keep running while the mark it asked for was
simply absent, which is worth answering. The list is bounded to the arguments the
function actually had and does not grow; ggplot2 keeps size on the line geoms
for the same reason. A genuine misspelling is the ordinary story: gf_squareplot(~Thumb, data = Fingers, binwidht = 5) draws a plot that ignores it, while a correctly spelled
neighbor on the same call is applied. If a parameter appears to do nothing,
check its spelling first; ggplot2::ggplot_build(p)$data[[1]] shows what the
layer actually received.gf_squareplot() is now a real ggformula layer. It carries the data it was given
and maps x, so it facets with ~ x | group, accepts a data frame piped into it,
takes a mapped fill, can be added to a plot you already have, and can be read by
gf_model() and gf_sd_ruler() -- none of which worked before, because the
function built an empty plot and handed the layer a renamed one-column frame.
~log(Thumb) and any other expression now plots what it says.gf_squareplot() bins the values that are actually drawn. It used to choose the
binwidth and the origin from the raw data when the call was made, then hand them to
a stat that runs after the scales have transformed everything, so scale_x_log10()
drew four values spanning three orders of magnitude as a single column on an axis
running to 10^33. The stat decides now.gf_squareplot()'s scale and annotation arguments have moved to the scales and
annotations that own them: xrange is gf_lims(x = ), xbreaks is
scale_x_continuous(breaks = ), mincount is expand_limits(y = ), and show_mean
and show_dgp are %>% show_mean() and %>% show_dgp(). auto_subdivide is gone
because what it did is now what happens anyway: it was the opt-in that split a bin of
more than 75 observations into sub-columns so the squares stayed countable, and squares
now stay countable at any size without being asked. Passing one of the old names is
refused, with the replacement named.%>% gf_refine(coord_transform(y = "sqrt")) -- coord_trans() on ggplot2 3.5,
which is the same coord under its older name. Each square still spans exactly one
count, so the squares are drawn shorter the higher up the stack they sit --
which is the point: the distortion is what shows that the unit changes as the
scale climbs. The borders between squares are fitted per square rather than once
for the layer, so the compressed ones at the top are not swallowed by their own
outlines. A scale_y_*() transformation is still refused, because a scale
transforms the counts before the squares are built and the squares would be
drawn in one space and labeled in another; the refusal now names the coord
spelling your ggplot2 exports instead of saying a transformed axis is impossible.
A discrete y scale is still refused, and now says why: there is no count for a
square to be one of.bins,
binwidth, center, boundary, closed, breaks and pad all mean exactly
what they mean on gf_histogram(), because a squareplot now bins through the same
code a histogram does. The default grid moved as a result -- bins are centered on
round numbers rather than starting on them -- and a maximum sitting on a bin edge
no longer gets a column of its own past the end of the data.bins, center, boundary, closed and breaks are now named parameters of
gf_squareplot(), not merely accepted through ..., so they appear in its usage,
in autocomplete, and in the help sheet a bare gf_squareplot() call prints.gf_bar()
positions its bars, instead of a column of level numbers sitting half a step past
the labels they belong to. A factor with more than 51 levels no longer merges
neighboring levels into shared columns. A binning argument passed alongside a
discrete x -- binwidth, bins, center, boundary, closed, breaks, pad --
now warns that it has no effect, rather than being silently discarded.color colors the bar on a squareplot, which is the only thing it ever affected,
and an explicit "black" is now drawn black rather than silently redrawn as the
default grey.print.gf_squareplot() method is gone. It existed to swallow a
warning the old constructor caused by building a plot with no complete layer of its
own; a real ggformula layer never triggers that warning, so gf_squareplot() now
prints like any other plot.gf_squareplot().GeomSquareplot can draw a bin as the bar its squares add up to, through a bars
parameter taking "none", "outline" or "solid", with bar_color and
bar_linewidth to style it. All three are one layer on one set of bins, and the bar
is derived from the squares rather than binned again, so it cannot land anywhere the
squares did not. A solid bar shows its bin's group composition too: a mapped fill
stacks in the bar the way it stacks in the squares.show_mean() and show_dgp(): pipe a plot of one distribution through them to mark
its mean, or to frame it with the process that generated it -- the population model on a
top axis, the sample estimate below the plot, and a marker at the null hypothesis. They
describe a distribution rather than one particular way of drawing one, so the same frame
now goes on a gf_histogram() or a gf_dotplot() of the same data, and each layer they
add carries a name so it can be found and changed afterwards. On a faceted plot
show_mean() draws each panel's own mean. show_dgp() raises the count axis to make
room for the population band rather than hanging it in the margin: countable squares size
the separator between them from the fraction of the panel they fill, so a band drawn
outside the panel silently redraws every square 39% taller.gf_sd_ruler() is now a real ggformula layer. It takes a formula rather than
y and x arguments -- gf_sd_ruler(Thumb ~ Height) where you used to write
gf_sd_ruler(y = Thumb, x = Height) -- and with that comes everything the other
gf_ functions already had: y ~ x | group faceting, data-first piping,
title=/xlab=/ylab=, and a plot whose aesthetics live on a layer rather
than on the plot. y and x are names the layer no longer recognizes and are
ignored like any other; size still works and says to write linewidth.gf_sd_ruler() placing the ruler where no observations are drawn. On a
categorical x it derived positions in order of first appearance while the axis
orders them alphabetically, so on unbalanced data where = "median" put the
ruler over one group and the median over another.gf_sd_ruler() on a faceted plot now measures each panel's own data. It used
to compute one ruler from the pooled data and stamp the same segment into every
panel, which is the one thing a facet exists to avoid.gf_sd_ruler() measures the values the plot draws. A transformed axis or a
computed mapping such as ~log(Thumb) used to be refused outright; both are now
measured in the space they are drawn in.gf_sd_ruler() draws one ruler per panel, so an aesthetic mapped on the call --
gf_sd_ruler(color = ~Sex) -- is refused and points at y ~ x | Sex. It used to
be discarded without a word.StatSdRuler export. gf_sd_ruler() now draws through a real ggplot2 stat
instead of computing the ruler by hand, and is exported so you can put a
standard deviation ruler into a plot you are building yourself.gf_model() is now built the same way every other gf_ layer is, so it behaves like one. Calling it with no arguments prints its own help instead of reporting a missing argument, and calling it on a plot with no model says which model it needs and names both ways of giving one -- a fitted lm() or aov(), or the formula for one -- rather than surfacing R's own missing-argument error. show.help = TRUE now prints that same help on a plot that already has a model to check, rather than running the check anyway and reporting its result instead of the help you asked for. Everything it draws is unchanged.gf_model() draws a model whose predictor is transformed. gf_model(lm(Thumb ~ log(Height))) over a plot of Thumb ~ Height, and the same claim written in place as gf_model(Thumb ~ log(Height)), were both refused as using variables the plot does not have, because log(Height) was compared against the plot's columns as though it were the name of one. The prediction grid is now built over the columns a term is made of, which is what predict() needs, and drawn against the plot's own mapping. A transformed outcome is still refused, and now says so in those terms rather than failing while computing aesthetics.gf_model() refuses a one-sided formula by name. gf_model(~flipper_length_m) used to fail inside lm.fit() with incompatible dimensions; it now says the model has no outcome and shows where to write one.gf_model() says at the call, rather than while drawing, that a fit line or a group mark needs its outcome mapped by the plot rather than by a layer underneath it. Those two shapes leave the outcome's axis free and inherit it, which is what lets a flipped plot draw correctly without any orientation logic; when there is nothing to inherit, ggplot2 used to report a missing y from deep inside the build.gf_model() draws a group model as a plain mark at each group mean rather than as an errorbar. An errorbar glyph reads as uncertainty -- a standard error, a confidence interval -- to students who are weeks away from meeting interval estimates, while a group model claims a single value per group and says nothing about how sure of it you should be. The mark is drawn at the same place, the same width and the same color as before; what goes away is the eight-point path with two identical caps around a zero-length stem, and the ymin, ymax and flipped_aes columns that described an interval that was never there. A group mark drawn with alpha is now as translucent as you asked for, instead of nearly twice as dark where the two identical caps overlapped.gf_model() now says a model's outcome has to be numeric, instead of letting lm() coerce it and report NA/NaN/Inf in 'y'. The carefully worded refusal was already written; it just sat after the fit, where nothing with a categorical outcome could ever reach it. On the one path that did reach it -- a logical outcome, which lm() accepts -- it reported the type as "character", because it read the class of the variable's name rather than of the variable.gf_model(size = ) no longer trips ggplot2's size-is-now-linewidth deprecation warning, which told the reader that coursekata had done something wrong and asked them to file an issue. The value was correctly folded into linewidth and then also passed along under its old name, where the geom accepted and discarded it. A size mapped by the plot underneath also used to overwrite a linewidth given explicitly to gf_model(); the explicit one now wins.gf_model(), show_cutoffs() and the residual overlays now read a plot through one reader, which finds a variable whether it was mapped on the plot or on the plot's first layer, and finds the data there too. Plots built with ggformula always map at plot level, so this changes nothing about the documented pipelines. For show_cutoffs() and the residual overlays it changes how they read rather than what they can draw: a plot written as ggplot(data) + geom_point(aes(x, y)) already worked. What it buys is gf_model(), which used to refuse that plot outright and now draws an intercept model on it. A fit line or a group mark still needs its outcome mapped on the plot rather than on a layer beneath it -- those two shapes leave the outcome's axis free and inherit it, so there is nothing for them to inherit from -- and that is now refused by name, at the call. The residual overlays' refusal, for a plot that really has no x or y anywhere, names the axis that is absent.gf_resid(), gf_square_resid(), gf_squaresid(),
gf_resid_fun() and gf_square_resid_fun() -- are now built the same way every other
gf_ layer is, which closes the last place this package had two ways of making the
same kind of thing. Nothing they draw has changed. What arrives with it is the rest of
the family's behavior: a bare call prints its own help, title=/xlab=/ylab= reach
the plot, and a first argument that is not a plot says so rather than failing with
attempt to apply non-function. A call with no model, or no function, names what it
needs instead of reporting R's own missing-argument error.object, the name every other gf_ function
uses for it, and their remaining arguments have to be named. The released signatures
put linewidth third on gf_resid(), and aspect and alpha third and fourth on
gf_square_resid(); those positions belong to the arguments every generated layer
carries there, so write gf_resid(p, model, linewidth = 0.5) and
gf_square_resid(p, model, aspect = 1). A value left in the third position is refused
by ggformula, in the words it uses for any other gf_ function.gf_resid() and gf_square_resid() now measure the residual along whichever axis the plot puts the model's outcome on. A plot of Thumb ~ Height with lm(Height ~ Thumb) over it drew every segment vertically, to a predicted height read off as though it were a thumb length, with nothing to say so; the squares squared that same wrong distance. The residual now runs across x for a model of the x variable, its square turns with it, and the fitted end lands on the line gf_model() draws for the same model.show_cutoffs() now draws its markers on a plot whose count axis has been transformed.
The triangles hang below the axis, which means a negative count, and scale_y_sqrt() or
scale_y_log10() has no such value to offer: both markers came out at NA and both
dashed uprights lost their lower end, with eight warnings, and the labels, which did
survive, were placed by treating a square root as a count and landed a third of the way up
the panel where they belong two thirds of the way up. None of those heights was ever a
count -- they are fractions of the panel -- so they are now measured in the space the axis
is drawn in and handed over as positions no scale is asked to represent.show_cutoffs() on a plot that already had markers moved
them, because the second call measured the axis the first one had stretched. On a
coord_flip() histogram it was worse -- the heights were measured against the range of
the variable on the vertical axis rather than the counts, so the labels were placed off
the end of the count axis and the bars were redrawn at under two thirds of their length.
The reference figure for middle(Thumb, .95) changes accordingly: the markers are in the
same place relative to the bars, and the empty band beneath the axis is gone.middle(), tails(), and outer() dropping one value from each tail when the tail's exact size is a whole number. middle(x, .90) on 20 values highlighted all twenty instead of eighteen. The tail proportion is computed as (1 - prop) / 2, which cannot represent .05 exactly, so the count landed a fraction below the whole number and rounded down. prop = .95 was unaffected, which is why this survived.library(coursekata) no longer attaches fivethirtyeightdata, and the drat repository that existed only to serve it is gone. No CourseKata teaching content uses any of its 19 exclusive datasets, and it accounted for roughly 63 MiB of the browser-based Playground bundle -- 93% of the cost of shipping every suggested data package. fivethirtyeight itself is unaffected; if you use a dataset that lived only in fivethirtyeightdata, install it with install.packages("fivethirtyeightdata", repos = "https://fivethirtyeightdata.github.io/drat/").coursekata is actually used.mosaic 1.10.2 or later, which raises the minimum R version to 4.1. Earlier mosaic releases are broken by rlang 1.2.0: do(1000) * b1(shuffle(...)) returns the same value a thousand times instead of a sampling distribution, with no error to warn you. Since coursekata attaches mosaic for you, an old copy meant silently wrong sampling distributions in your own work.ggformula 0.12.0 or later. Two separate reasons, both measured rather than assumed. On R 4.4 and later, ggformula 0.10.1's own create_extras_and_dots() fails every gf_* call, coursekata's included, with unique() applies only to vectors -- a bug in ggformula itself, fixed in 0.10.2. Beyond that, coursekata now builds several layers with ggformula::layer_factory(), handing it this package's own Stat and Geom objects; only 0.12.0 and later capture those unevaluated, and earlier versions resolve them while coursekata's namespace is still being built, so the package cannot be installed at all.do() and shuffle(). They construct the same distributions with base R, so a reference page cannot break because an attached package changed the shape of what it returns. The full shuffle-and-estimate workflow is now taught in the sampling distributions guide, alongside a new model visualization guide.gf_resid(), gf_square_resid(), gf_resid_fun(), and gf_square_resid_fun() overlays misaligning with jittered points: jitter positions are now pinned to the plot itself, so they are stable across repeated builds and chained calls, and the user's random seed is no longer reset.gf_square_resid() drawing rectangles instead of squares on jittered plots. The vertical side was measured from the displayed (jittered) position, but the horizontal side was measured against the model's unjittered response, so the two sides disagreed by the size of the jitter. The whole point of the plot is to show squared error as area, so a shape that was not actually square undermined it.geom_jitter() or position = "jitter" shares one position object with every other such layer in the session, so pinning it in place fixed the jitter for unrelated plots too.gf_sd_ruler() now works on histograms, drawing a horizontal ruler from the mean to mean + SD along the baseline.gf_sd_ruler() now reports which variable it could not use, instead of failing inside sd() on a categorical outcome or silently drawing nothing when handed a name that is not in the data.gf_sd_ruler()'s y and x arguments. Bare (unquoted) names previously errored despite being documented, and a variable holding a column name stopped resolving when that was fixed; all three spellings now work.gf_squaresid() is no longer deprecated: it remains a fully supported alias of gf_square_resid(). With our appreciation to Tyler Haslam (@TH4SL4M), the Utah high school teacher whose efforts shaped the residual and squared-residual visualizations -- including working out how gf_resid() and gf_square_resid() handle jitter plots, and the insight to emphasize the area of the squares rather than their outline -- and who requested the function by this name.gf_resid() and gf_square_resid() no longer refuse a model that was fit on fewer rows than the data holds. lm() drops rows with missing values, and those are the same rows the plot cannot draw a point for, so the two always agreed about what is on screen -- but the check counted rows before the plot dropped them and reported 157 points against 128 fitted values for a plot that draws 128 of each. The residual is now drawn for every point that is drawn, and the error that remains is the one worth raising: the model uses a variable the plot's data does not have, named.gf_square_resid() and gf_square_resid_fun() drawing every square in every facet panel. The squares were built as a standalone table of coordinates that carried none of the data's other columns, so ggplot2 had nothing to split them by and repeated all of them in each panel; their side was also scaled against the first panel's ranges wherever they landed. Each square is now drawn where its observation is.StatResid, GeomResid and GeomSquareResid exports. The residual overlays are real ggplot2 layers now rather than a pre-rendered table of coordinates, so they follow the plot they are added to -- through facets, through a jitter, and through rows the model dropped -- and you can use them in a plot you are assembling yourself.gf_model() failing with 'from' must be a finite number when a predictor on an axis has any missing values. The grid of values the model is drawn over was spanned between the predictor's smallest and largest value without excluding the gaps, so a single missing observation made both ends undefined and nothing was drawn at all -- including for a model lm() had fit perfectly well on the rows that were complete.gf_model() now errors when an aesthetic is mapped to a variable that is not one of the model's predictors, instead of silently dropping the mapping. A mapping the model could not honor used to just vanish from the plot without a word, which is a hard thing to debug in a notebook.gf_squareplot() now names the variable it could not use. A misspelled column, a data-first pipe, a character column and a date column all reported the same `x` must be numeric., and a formula holding an expression such as ~log(Thumb) silently plotted the untransformed variable and labeled the axis with it. na.rm = FALSE now says it is unsupported instead of failing inside range() or shipping a rectangle at NA. A two-sided formula such as gf_squareplot(y ~ x) used to silently plot x and discard y with no error; it now says the formula must be one-sided.gf_squareplot() keeps drawing countable squares on large samples. Above 75 observations in a bin it used to replace every square with a solid bar, so the 2000-observation example in its own documentation drew 27 bars and not one countable square. The separator between squares is now fitted to the squares -- at 2000 observations a square is about 1.2 pt tall while the old separator was 1.4 pt wide, so each square erased itself. A linewidth set explicitly on the layer is still honored, now with a warning when the border is wide enough to hide the observations behind it.StatSquareplot and GeomSquareplot exports. gf_squareplot() now draws through a real ggplot2 stat and geom instead of assembling rectangles itself, and both are exported so you can put countable squares into a plot you are building yourself -- including with a mapped fill, which stacks its groups within each bin rather than drawing them on top of one another.show_cutoffs() now reads its fill aesthetic the way R reads any call. fill = ~middle(Thumb) was told it needed at least two arguments even though prop is documented to default to .95; named arguments in any other order, such as ~middle(prop = .9, x = Thumb) or ~middle(Thumb, greedy = FALSE, prop = .9), were still read by position and failed on whatever landed in the third slot; and ~coursekata::middle(Thumb, .95) crashed on a length-3 coercion rather than being recognized as middle(). The call is matched against the real function's formals now, so naming arguments, reordering them, leaving them at their documented defaults, and qualifying the call with coursekata:: all behave as they do everywhere else in R. greedy is honored too, having previously been ignored.show_cutoffs() now puts its markers on values the fill actually shades. The marker positions reimplemented the arithmetic that decides how many observations fall in a tail rather than asking for it, and the two disagreed whenever the tail's exact size was a whole number: with upper(x, .05) on 200 observations the triangle sat on the 190th value while the shading started at the 191st. They come from the same count now, so they cannot drift apart. The labels also compared floating-point values against literals and never matched, so the flagship example annotated its cutoffs 0.025 where a textbook writes .025.show_cutoffs() now refuses a plot it cannot mark instead of guessing at it. An x aesthetic holding an expression rather than a bare variable surfaced a raw rlang coercion error, and a plot without cartesian axes -- coord_polar(), say -- fell back on an invented y axis running to 30 and drew the markers at heights that meant nothing. Both are named errors now, raised before any position is computed.gf_model() now documents the model you can write in place. Alongside a model already fit by lm() or aov(), it takes the formula for one -- gf_model(body_mass_kg ~ species) -- and fits it against the data the plot was built from, with body_mass_kg ~ NULL for the empty model. That has always worked, and the help page promised lm() or aov() only, so the shortest way to draw a claim was also the least discoverable one. The outcome still has to be named: ~species would describe predictors and no claim, and the only way to draw it would be to guess the outcome off the axes, which is what gf_lm() already does.gf_resid_fun(), gf_square_resid_fun(), gf_sd_ruler(), gf_squareplot(), show_cutoffs(), and outer().gf_squaresid() to gf_square_resid() with deprecation warning for the old name.coursekata.check_missing option to control the missing-package install prompt, with automatic suppression on Emscripten/WASM environments.>= 3.5.2 to support Emscripten/WASM environments.supernova when using default arguments.gf_resid() and gf_squaresid() functions for residual and squared residual plots that layer onto ggformula::gf_point() plots.Fingers$Gender to Fingers dataset to prevent awkward naming in exercises.pak. pak was initially used to manage and parse dependencies, but itself depends on curl. curl is not available on all platforms (e.g. WASM), so we have removed the dependency on pak and opted for pure R where possible (or remotes which has a pure R fallback).FoodQuality to TipExperiment dataset.rhub platforms, don't run vdiffr tests on CI, allow tests to run in parallel.Remotes field from DESCRIPTIONvdiffr gave erroneous resultsFingers.messy to FingersMessypak::pkg_status() to improve startup timerequire(lib.loc = ...) was sometimes being passed NAgf_model_old() functioncoursekata.quiet option to suppress startup messagescoursekata.quickstart and coursekata.quiet options in READMEcoursekata.quickstart option, which can reduce load times significantly.gf_model tests for density plots now that upstream is fixed.test_fit() simple model stats to help teachers evaluate student models.sse(), ssm(), ssr(), SSE(), SSM(), SSR() functions: they conflict with Metrics package.palmerpenguins and WorldMetrics packagezarglefevdata$AGE values were truncated by first character. This has been fixed.pak for package management.game_data dataset.Patch release to resolve R CMD CHECK failures.
size for line widths to linewidth.