halfmoon (development version)
plot_model_calibration()no longer appliesggplot2::theme_minimal()to ahalfmoon_calibrationobject. Both methods now leave the theme to the user, asplot_qq()and the other halfmoon plotting functions do.check_qq()andbal_qq()read the column a renaming selection points at and label the method with the new name, so.weights = c(ate = w_ate)works as it does incheck_balance(). It previously looked for a column named after the method and failed to find it.check_balance()raiseshalfmoon_empty_errornaming.datawhen the data has no rows or no columns, rather than failing inside the covariate selection with an error about a column that does not exist.geom_ecdf()supports a flipped orientation. Mapping the variable toy, or passingorientation = "y", computes the same weighted curve and draws it across the panel. It previously ignored the request and drew the curve as if the variable had been mapped tox.geom_ecdf()raiseshalfmoon_range_errorfor a negative weight, which made the cumulative distribution non-monotone, consistent with the weight validation everywhere else in the package.geom_roc()emits ahalfmoon_data_warningwhen the exposure has no observed levels, rather than drawing an empty layer without comment.geom_mirror_histogram()andgeom_mirror_density()name the geom and the observed group count when a panel holds three or more groups. This is still an error, deliberately: a mirrored plot draws one group above the axis and one below, so there is no partial plot to fall back on.weighted_quantile()gainsna.rm. With the defaultFALSEa missing value or a missing weight now makes every quantileNA_real_, matching the policy thebal_*()functions follow;na.rm = TRUEdrops the observation and computes the quantiles from the rest, which is what the function always did.check_qq(),bal_qq(),plot_qq(), andgeom_qq2()apply their own missing-value policy before they call it, so their results are unchanged.plot_qq()no longer appliesggplot2::theme_minimal()to ahalfmoon_qqobject. Both methods now leave the theme to the user, as the other halfmoon plotting functions do.plot_model_calibration()gains a documentedbinning_methodargument, which was previously reachable only through....plot_model_calibration()raiseshalfmoon_type_errorfor aglmorlmwhose response does not take exactly two observed values. A calibration curve reads the response as the event indicator, so a model of anything else produced a curve with no meaning.bal_prognostic_score()raiseshalfmoon_group_error, rather thanhalfmoon_reference_error, when no control observations are left to fit the prognostic model on, whichna.rm = TRUEcan cause by dropping them all.check_model_roc_curve()andcheck_model_auc()raisehalfmoon_type_errornaming a selected weight column that does not hold weights. They previously warned and left that method out of the result, which reported fewer methods than the call asked for.bal_energy(criterion = "dcor")lets the weights enter only the quadratic form that evaluates the dependence. The variances the distances are scaled by and the denominator that standardizes them are properties of the sample and are now computed unweighted. Weighted values change, and they now matchcobalt::bal.compute(cobalt::bal.init(x, treat, stat = "distance.cor"), weights = w)rather than thes.weightsinitialization, which reweighted the scale along with the sample. The unweighted value is unchanged.check_balance(),check_qq(),bal_qq(), andcheck_ess()refuse a weight method named"observed", whether the column carries that name or the selection renames one to it, withhalfmoon_arg_error. That label names the unweighted rows, so the selection previously produced a duplicate set of unweighted results instead of the weighted ones.check_ess()validates every selected weight column before it summarizes any of them, so a non-numeric column raiseshalfmoon_type_errornaming the column. It also reads the column a renaming selection points at, where it previously looked for a column named after the method.bal_energy()raiseshalfmoon_arg_errorwhen.focal_levelis supplied with anestimandother than"ATT"or"ATC". The focal level had no target to name there and was silently ignored.check_balance()raiseshalfmoon_arg_errorfor anexposure_typethat is not one of its documented values, matching every other option argument in the package.weighted_quantile()computes the weighted generalization of the definitionstats::quantile()uses by default,type = 7. A constant positive weight now reproducesstats::quantile(values, quantiles)exactly, the result is invariant to the order ofvaluesand to the scale of the weights, and it is monotone inquantiles. It previously inverted the weighted empirical cumulative distribution function, which returned lower values thanstats::quantile()did for the same data. Every weighted QQ value changes, and the weighted and observed quantiles incheck_qq(),bal_qq(),plot_qq(), andgeom_qq2()are now computed the same way.weighted_quantile()excludes observations with zero weight instead of averaging them into the neighboring quantiles. Matching weights are 0 or 1, so the quantiles of a matched sample are now the quantiles of the matched observations, where the unmatched observations previously pulled them toward the whole sample. Fewer than two observations with a positive weight leave the quantiles undefined and returnNA_real_, where two observations with one zero weight previously raised an error fromstats::approx().weighted_quantile()validates its arguments. A probability outside[0, 1]or a negative weight raiseshalfmoon_range_error, a non-numeric argument raiseshalfmoon_type_error, and weights whose length does not matchvaluesraisehalfmoon_length_error. Each previously produced a quiet wrong answer or an error about interpolation.check_qq(),bal_qq(),plot_qq(), andgeom_qq2()resolve.reference_levelthe way the rest of the package does: it names the reference group, which is the unexposed group, and defaults to the first observed level. A level can be given by value or by position. The default output is unchanged, since the exposed group is still the level that is not the reference, but an explicit.reference_levelnow names the opposite group from before.plot_qq()andgeom_qq2()put the reference (unexposed) group on the x axis and the exposed group on the y axis.plot_qq()on a data frame previously transposed the plot thatplot_qq()on acheck_qq()result drew. Both methods now usecoord_equal().check_qq(),bal_qq(), andplot_qq()count only the observed levels of the exposure. A factor that declares a level no observation takes is valid binary input, and an exposure with one observed level raiseshalfmoon_group_errorrather than returning a column of missing values.check_qq(),bal_qq(), andplot_qq()treat missing weights the way they treat a missing variable or exposure: withna.rm = FALSEthey raisehalfmoon_na_errornaming the weight column, and withna.rm = TRUEthey drop the rows. Missing weights were previously dropped without comment under either setting.plot_qq()also raiseshalfmoon_na_errorfor a missing exposure, which is new: it previously checked.varalone and dropped rows with a missing exposure without comment.geom_qq2()requires thetreatmentaesthetic to have exactly two observed levels and raiseshalfmoon_group_errorotherwise. A treatment with three or more levels previously pooled every level other than the reference into a single group and drew that comparison without comment.geom_qq2()drops a group that holds a single treatment level, with a warning naming the group’s observed level, and draws the rest of the panel. Such a group previously discarded every curve in the panel.geom_qq2()keeps the weighting when some weights are missing. A single missing weight previously discarded the weights of every observation and drew the unweighted QQ plot.geom_qq2()draws one curve per group whengroupis mapped explicitly. The groups were previously pooled into a single curve, which counted each observation once per group.check_balance()resolves.reference_levelonce for the whole call and uses the result for every metric and every label. It previously resolved the argument one way for the standardized mean difference and another way for the variance ratio and the Kolmogorov-Smirnov statistic, so a single table could compare against two different reference groups and label the rows with the wrong one. On a 0/1 exposure the default now references the level0for every metric, where the standardized mean difference previously referenced the level1: those rows change sign relative to the previous output, and thegroup_levelcolumn reports1rather than0. The default is nowNULL, documented as the first observed level, and a value that matches a level is taken as that level, so.reference_level = 0on a 0/1 exposure means the level0for every metric.check_balance()validates.reference_levelbefore computing anything. A value that names no group raiseshalfmoon_reference_errorand an index out of range raiseshalfmoon_range_error, where both previously produced rows of missing values without comment for the variance ratio and the Kolmogorov-Smirnov statistic.check_balance()warns once, naming the affected metrics and variables, when it reports a combination it could not compute asNA. Missing values thatna.rm = FALSEasks it to keep are not such a combination and stay silent.check_balance()honors a renamed selection..weights = c(myw = w_ate)now weights byw_ateand reports the method asmyw, where it previously reported unweighted estimates under the new name, and a renamed.varsselection now reports the covariate under its new name instead of failing or returningNA.check_balance()drops the grouping of a grouped data frame instead of adding the grouping variables to.varsand repairing the duplicated names.check_balance()reads a logical covariate as a 0/1 indicator, where every metric previously reportedNAfor it.check_balance()labels the comparison groups of a categorical exposure by removing the reference level from the names the balance functions return, rather than by splitting those names on_vs_. A reference level that itself contains_vs_no longer truncates the labels. A metric that fails for a categorical exposure now reports one missing row per comparison level instead of a single row labeled with one of them.check_balance()forms the distance matrix behind the energy metric once per call and evaluates every weight column against it, rather than rebuilding it for each column. The estimates are unchanged.bal_energy()compares the weighted groups against an unweighted target population forestimand = "ATE". It previously used the balancing weights for both sides of the comparison, which understated the energy distance when the weights improved balance and overstated it when they did not. Every weighted ATE energy distance changes, and the values now agree withcobalt::bal.compute(stat = "energy.dist", estimand = "ATE").bal_energy()compares against the unweighted focal group forestimand = "ATT"andestimand = "ATC". Weighting the focal group as well left the focal target with no effect, so"ATT"and"ATC"both returned the between-group energy distance whatever the weights were. They now differ from each other and from the between-group distance, and agree with cobalt. With uniform weights all three statistics still coincide, as the definitions require.bal_energy()defaults.focal_levelto the last observed level of the exposure forestimand = "ATT"and the first forestimand = "ATC", and validates a supplied value against the levels the exposure takes. It previously coerced the levels to numbers to find the focal group, so a text-valued exposure selected no group at all and returned a negative energy distance alongside a coercion warning, as did a.focal_levelthat named no group. A value that names no group now raiseshalfmoon_reference_error.bal_energy()reads the levels a factor exposure actually takes, so a declared level with no observations no longer divides by a group size of zero.bal_energy()returnsNA_real_whenna.rm = FALSEand the covariates, the exposure, or the weights contain missing values, where it previously raisedhalfmoon_na_error. This follows the convention that the atomic balance functions report missing values rather than refusing them, and it meanscheck_balance()reports an energy row ofNAwithout a warning instead of treating the metric as one it could not compute.bal_energy()gains anexposure_typeargument, mirroring the one incheck_balance(), so the continuous or the categorical statistic can be asked for directly. The default"auto"keeps the existing rule, which is now documented: a numeric exposure taking more than ten unique values is treated as continuous. The argument sits beforena.rmin the signature, so a fully positional call that passedna.rmin that slot now raiseshalfmoon_arg_errorrather than reading a logical as an exposure type.bal_energy()raiseshalfmoon_arg_errorfor an option argument that is not a single value.estimand = character(0),criterion = character(0), and a length-2na.rmpreviously produced a base R error, carrying no halfmoon class, about a condition of length zero.bal_smd()now reports the comparison group minus the reference group, so a positive value means the comparison group has the higher mean or proportion. This is the convention the documentation has always described and the onecobalt::col_w_smd()uses, but the estimate previously carried the opposite sign. Every standardized mean difference, frombal_smd(),check_balance(), andplot_balance(abs_smd = FALSE), changes sign. Categorical results follow the same convention:X_vs_refis levelXminus the reference level.bal_smd()resolves.reference_levelagainst the levels of the exposure rather than the order in which those levels first appear in the data. Estimates no longer depend on the row order, and a.reference_levelthat names no group now raises a halfmoon error rather than passing through to the smd package.Functions that require a binary exposure count the levels an exposure actually takes rather than the levels a factor declares. A factor with unused levels and two observed groups is now valid input for binary
bal_smd(),bal_vr(),bal_ks(),bal_qq(), andplot_mirror_distributions(), all of which previously rejected it for declaring too many levels. An exposure with a single observed group now raiseshalfmoon_group_errorinbal_smd(),bal_vr(), andbal_ks(), instead of failing inside the smd package or returningNAwithout comment, and inbal_qq(), which previously returned a table pairing the observed group’s quantiles with a column of missing values.bal_prognostic_score()reports that same error for an exposure with one observed group, where it previously reported a missing control level.bal_smd(),bal_vr(), andbal_ks()treat a missing exposure the way they treat a missing covariate or weight: withna.rm = FALSEthe result isNA, and withna.rm = TRUEthe affected rows are dropped.bal_vr()andbal_ks()previously dropped rows with a missing exposure without being asked, and the categorical versions of all three did the same. A categorical exposure with missing data now returns an all-NAnamed vector by default, so calls onnhefs_weights$alcoholfreq_catneedna.rm = TRUE.bal_smd(na.rm = TRUE)drops rows with missing weights instead of failing inside the smd package, which only removes missing covariate values.bal_smd(),bal_vr(), andbal_ks()returnNAwhen a group carries no weight. Zero weights are valid input, so a group they empty has no mean, variance, or distribution to report.bal_vr()andbal_ks()previously failed with a base R error about a missing value or an interpolation with no points, andbal_smd()was worse: the smd package treats a group with no weight as having a mean and variance of zero, so an undefined statistic came back as a plausible number.bal_corr()returnsNAwhen the weights sum to zero, rather than failing with a base R error about a missing value.check_balance()gains anexposure_typeargument, one of"binary","categorical", or"continuous". It defaults to"auto", which reads the type from.exposureand reports what it found.options(halfmoon.quiet = TRUE)silences that report..metricsincheck_balance()now defaults toNULL, which computes every metric that applies to the exposure type: the standardized mean difference, the variance ratio, the Kolmogorov-Smirnov statistic, and the energy distance for a binary or categorical exposure, and the weighted correlation and the energy distance for a continuous one. Results for binary and categorical exposures are unchanged. Asking for a metric that does not apply to the exposure type is now an error, so a continuous exposure no longer produces a standardized mean difference for every distinct value it takes, and a binary exposure no longer produces a correlation.check_balance()computes the energy distance for the exposure type it resolved rather than from the count of distinct exposure values. A numeric exposure with many repeated values, such as a change score on a bounded count, reads as categorical and now contributes a between-group energy distance instead of a continuous one. The change runs the other way too: a numeric exposure with few distinct values in a small sample, such as eight distinct values across twenty rows, reads as continuous and now contributes a continuous energy distance instead of a between-group one. Passexposure_typeexplicitly for the previous behavior. A direct call tobal_energy()is unchanged.plot_balance()marks the reference for the correlation metric at 0.ess()is now a re-export of the generic of the same name from causalgenerics. Attaching halfmoon alongside another package that re-exports that same generic no longer produces a masking conflict, because both packages export the one object. A package that defines its own unrelatedess()still masks, as before. The calculation is unchanged for numeric weights.Because the generic names its first argument
x,ess()no longer accepts the argument namewts. Pass the weights positionally, as iness(w).ess(), andbal_ess()through it, now error on non-numeric input instead of returning a meaningless number. Previouslyess(NULL)andbal_ess(NULL)returnedNaN, and factors, logicals, data frames, dates, time differences, and complex vectors each produced a value:bal_ess(factor("a"))returned1.8.ess(rep(0, 5))andess(numeric(0))still returnNaN.An argument that is neither a column name nor something that evaluates to one now reports the function the user called, such as
check_qq(), rather than the internal handler framevalue[[3L]](cond)..reference_levelmust name a single group. A value longer than one, orNA, is now ahalfmoon_arg_errorinstead of the base R errorthe condition has length > 1.A
.reference_levelused as a position must be a whole number. Previouslybal_vr(x, g, .reference_level = 1.5)silently truncated to the first level and returned that answer; it is now ahalfmoon_arg_error. A numeric that equals one of the exposure’s level values is still read as that value rather than as a position, so.reference_level = 0on a 0/1 exposure still means the level0.Errors raised while resolving the exposure levels or the reference level now report the function the user called, such as
bal_vr(), rather than the internal helpersplit_by_group()..weightsis now validated withcausalgenerics::is_causal_wt(), so any causal weight object is accepted rather than only thepswobjects from propensity. The error message names a causal weight object instead of apswobject.check_model_auc()andbal_model_auc()integrate the ROC curve in the order the curve is drawn. The points were previously re-sorted by false positive rate, which reversed each vertical run of the curve and paired the trapezoids with the wrong corners whenever two points shared a false positive rate. Every AUC can change slightly, and an AUC computed from a small sample or from scores with many ties can change materially. The values now equal the weighted Mann-Whitney concordance, sobal_model_auc()on truth(1, 0, 1, 0)with scores(4, 3, 2, 1)reports0.75rather than0.625.check_model_roc_curve()andcheck_model_auc()applyna.rmto the weight columns as well as to the exposure and the fitted values. A missing weight previously survived into the cumulative sums and produced a curve of missing values, from which the AUC was computed over the surviving fragment. Withna.rm = TRUEthe rows with a missing weight are dropped for that weight column, and withna.rm = FALSEa missing weight raiseshalfmoon_na_errornaming the column.bal_model_auc()andbal_model_roc_curve()require an exposure with exactly two observed levels, ascheck_model_auc()andcheck_model_roc_curve()already did. A three-level exposure previously compared one level against the rest without comment, and a factor whose second declared level had no observations picked that empty level as the event and returned an AUC near zero. Unused declared levels are dropped, so a factor with two observed levels is valid input and gives the same answer as the same factor with its levels dropped.bal_model_auc()andbal_model_roc_curve()drop observations with zero or negative weights and warn withhalfmoon_data_warning, matchingcheck_model_auc()andcheck_model_roc_curve(). Such weights previously passed through and made the cumulative totals non-monotone, which sent sensitivity and specificity outside[0, 1]and left the two layers reporting different AUCs for the same input..focal_leveldefaults to the last observed level of the exposure, or the maximum value of a numeric exposure, in the ROC and AUC functions. This is what the documentation has always described; the code used the second level, which is the same level for the binary input these functions accept.bal_model_auc(),bal_model_roc_curve(),check_model_auc(), andcheck_model_roc_curve()report a.exposure,.fitted, or.weightscolumn that does not exist ashalfmoon_column_errornaming the argument, rather than passing through thevctrssubscript error from tidyselect. A.focal_levelthat names no level of the exposure now reportscheck_model_roc_curve()rather than the internalcompute_roc_curve_imp(), and a condition raised fromcheck_model_auc()reportscheck_model_auc()rather than thecheck_model_roc_curve()it delegates to.The
na.rmdocumentation forbal_model_auc(),bal_model_roc_curve(), andcheck_model_auc()records the actual default ofTRUE. The inherited text described a default ofFALSE.check_model_calibration()now defaults tona.rm = TRUE, matchingcheck_model_roc_curve()andcheck_model_auc(), and raiseshalfmoon_na_errorwhenna.rm = FALSEand.fittedor.exposureholds a missing value. The"windowed"and"logistic"methods previously failed with a base R error on such input.check_model_calibration()requires an exposure with exactly two observed levels and raiseshalfmoon_group_errorotherwise. An exposure that took a single value previously became its own focal level and reported an observed rate of 1 in every bin, and an exposure with more than two levels reported one level against all the others without comment.check_model_calibration()andplot_model_calibration()resolve the default.focal_levelto the last OBSERVED level of the exposure, and validate a supplied value against the observed levels. A factor with an unused trailing declared level previously took that absent level as the event and reported an observed rate of 0 in every bin. A.focal_levelthat names no observed level now raiseshalfmoon_reference_errorwith the levels it could have named, where the message previously failed to format.check_model_calibration()reports a.fittedor.exposurecolumn that does not exist ashalfmoon_column_error. The check was previously skipped whenever.focal_levelwas supplied, which left a base R error from deeper in the summary.check_model_calibration()andgeom_calibration()return a single calibration bin, with ahalfmoon_data_warning, when.fittedis constant. Both binning methods previously failed with'breaks' are not unique.The
"breaks"method counts the observations whose outcome is known, so the bin count, the observed rate, and the confidence interval share a denominator. The interval was previously computed from a count inflated by rows whose outcome was missing.geom_calibration()passesbinning_methodto its line layer, so the line now follows the same bins as the points and the ribbon rather than always using equal-width bins.geom_calibration()renders an empty layer for a group with nothing left to summarize, rather than reportingComputation failed in stat_calibration(). Missing values follow the ggplot2 convention for a stat: they are dropped, andna.rm = FALSEreports how many rows went.geom_calibration()resolves the default.focal_levelonce from the whole layer, before it is split into panels, and validates a supplied value against the levels the exposure takes. A facet holding a single group previously took that group as the event and reported an observed rate of 1 throughout, so a numeric exposure and the same values as a factor disagreed on the same panel.check_model_calibration()classes an empty resulthalfmoon_calibration, soplot()andautoplot()dispatch on a result with no rows the way they do on every other result.The
"breaks"and"windowed"calibration methods report a real confidence interval for a bin or window in which every observation is an event, or none is. Such bins are routine for a well separated propensity model, and both methods previously fell back to a normal approximation whose standard error is exactly zero at a rate of 0 or 1, so they reported[0, 0]or[1, 1]and asserted certainty they had no basis for. A bin of 0 events out of 116 now reports[0, 0.038], asprop.test()gives it. Interval values change for those bins only.bal_prognostic_score()resolves.reference_levelagainst the exposure itself rather than against its levels in sorted order, so a factor keeps its declared order. On a factor whose declared control level was not the alphabetically first, the outcome model was previously fit on the treated group.bal_prognostic_score()reports an exposure whose declared control level is never observed as such, naming the absent level, rather than as a bare count of levels.bal_prognostic_score()now defaults tona.rm = TRUEand raiseshalfmoon_na_errorwhenna.rm = FALSEand a model variable, the exposure, or a weight is missing. It previously returned a score vector with missing values that later models would carry silently.check_ess()andplot_ess()work on a data frame that has a column namedmethodorweight. Reshaping the data previously collided with the names of the columnscheck_ess()builds, so any such data frame raisedNames must be unique. A weight or exposure column with either name may also be selected, and the returned tibble still names its columnsmethodandgroup.check_ess()andplot_ess()gainna.rm, which defaults toFALSEas it does iness()andbal_ess(). A missing weight now produces anNAeffective sample size for that weighting method rather than being dropped without notice. Passna.rm = TRUEfor the previous behavior.check_ess()reportsn, and thereforeess_pct, against the observations whose weight is not missing. Weights of 90 ones and 10 missing values previously reported an effective sample size of 90 as 90% of 100 observations. The 90 observations it was computed from are all weighted equally, so it is now reported as 100%.add_ess_header()gives the group columns of a table with an overall column fromgtsummary::add_overall()the denominator they have without one, the sum of the group effective sample sizes. ESS is not additive, so dividing a group ESS by the ESS of the whole sample could exceed 1: the header of theqsmk = 0column ofnhefs_weightsweighted byw_atereportedpas 1.11. The group percentages now sum to 100%, and the overall column reports apof 1.bal_ess()validates.weightsitself, so an invalid weight reportsbal_ess()and.weightswith a halfmoon condition class.bal_ess(NULL)previously raised acausalgenericserror about an argument namedxin a call toess.default(). A negative weight is now ahalfmoon_range_error, wherebal_ess(c(1, -1))previously returned 0.geom_roc()andstat_roc()draw one curve per group whengroupis mapped explicitly. Long data holding several weighting schemes previously collapsed into a single curve that counted each subject once per scheme, so mappinggroupgave a different answer from mappingcolour.geom_roc()andstat_roc()count only the observed levels of theexposureaesthetic. A factor that declares a level no observation takes is valid binary input, where it previously failed to compute, and the default.focal_levelis the last observed level rather than the last declared one. A group that holds a single observed level is dropped with a warning and the rest of the panel is still drawn.geom_roc()andstat_roc()validate.focal_levelagainst the observed exposure values and raisehalfmoon_reference_errorfor a value the data does not take. An absent focal level previously drew a curve spanning the full range of the panel, computed from no events at all.geom_roc()andstat_roc()drop rows with a missing weight the way they drop rows with a missing estimate or exposure: silently underna.rm = TRUE, and with a report of the number of rows removed underna.rm = FALSE. A missing weight previously spread through the curve underna.rm = FALSE.plot_model_roc_curve()labels the color aesthetic only when it maps one, so a plot of a single method no longer reports that it is ignoring an unknown label.geom_ecdf()honorspadandnwhen weights are mapped. The weighted curve previously ignored both, so it neither reached the edges of the panel nor could be interpolated onto a grid, and it stepped once per observation rather than once per distinct value, which placed the steps of tied values wrongly.geom_ecdf()drops rows with a missing weight, silently underna.rm = TRUEand with a report of the number of rows removed underna.rm = FALSE. A single missing weight previously turned the whole curve into missing values. A group whose weights sum to zero is dropped with a warning, where it previously returnedNaNfor every point.geom_mirror_histogram()andgeom_mirror_density()mirror every statistic their stat computes, soafter_stat(density),after_stat(ncount),after_stat(ndensity), andafter_stat(wdensity)are drawn below the axis for the mirrored group. Onlycountwas mirrored for histograms, andwdensitywas left upright for densities, so asking for any of the others drew both groups above the axis.plot_balance()draws the SMD threshold in the SMD facet whether or not other metrics are shown.vline_xintercept,vline_color, andvlinewidthpreviously did nothing unless SMD was the only metric plotted, which made the documented example a no-op.vline_xintercept = NULLnow leaves the line out instead of adding an empty layer to the plot.
