Skip to content

Support facet formulas y~1 and 1~x for 1-column or 1-row layouts#562

Merged
grantmcdermott merged 6 commits into
mainfrom
facet-formula
Jun 5, 2026
Merged

Support facet formulas y~1 and 1~x for 1-column or 1-row layouts#562
grantmcdermott merged 6 commits into
mainfrom
facet-formula

Conversation

@zeileis
Copy link
Copy Markdown
Collaborator

@zeileis zeileis commented Mar 24, 2026

In #558 we discussed the idea that facet = y ~ 1 could be used as a shortcut for facet = ~ y, facet.args = list(nrow = length(unique(y))). Analogously, facet = 1 ~ x could be used as a shortcut for facet = ~ x, facet.args = list(nrow = 1).

I have implemented this now by tweaking tinyframe() slightly. This now distinguishes ~ x from 1 ~ x by returning NULL for the yfacet in the former case but a zero-column data frame in the latter case. In other words, a zero-column data frame signals: This part of the formula had a specification but no variables in it. In tinyplot.formula() I've extended the processing of xfacet and yfacet correspondingly.

In principle, this works but there are two caveats:

  1. tinyplot.default() handles the case with facet.args slightly differently from facet attributes.
  2. The processing has only been extended in the formula method. I just noticed now that the is an additional function get_facet_fml() (https://github.com/grantmcdermott/tinyplot/blob/facet-formula/R/facet.R#L613-L658) that does something similar to my tinyformula()/tinyframe() processing but is only used in the default method. We should probably try to use the same code in both cases.

To illustrate the first point, consider

library("tinyplot")
tinytheme("clean2")
p = transform(penguins, group = factor(paste(species, island, sep = "-")))
tinyplot(bill_dep ~ bill_len, data = p, facet = 1 ~ group)                            ## top
tinyplot(bill_dep ~ bill_len, data = p, facet = ~ group, facet.args = list(nrow = 1)) ## bottom
facet2
tinyplot(bill_dep ~ bill_len, data = p, facet = group ~ 1)                            ## left
tinyplot(bill_dep ~ bill_len, data = p, facet = ~ group, facet.args = list(nrow = 5)) ## right
facet1

Note that in the formula-based specification we get the right facet strips as well. Probably this should only be done in case both the number of rows and the number of columns is greater than 1? I wasn't sure though where to best add this information.

@grantmcdermott
Copy link
Copy Markdown
Owner

Thanks @zeileis.

In #558 we discussed the idea that facet = y ~ 1 could be used as a shortcut for facet = ~ y, facet.args = list(nrow = length(unique(y))).

Minor comment: but why not just use facet = ~ y, facet.args = list(ncol = 1) instead of the latter?

Regarding your plots... Hmmm, I'm not sure that I like the "formula" output. Taking the very top plot as an example, why do we have a duplicate "Gentoo-Biscoe" facet label on the right (but none of the other groups)? Ideally, these should give the exact same output as the facet.args = list(nrow = 1) case, right? Apologies if I'm just re-stating your point 1, but I want to make sure that we're aligned on the desired plot/outcome.

Regarding 2, yes I agree. Ideally, we would have some singular (modular) code that both "methods" can call. It's been a long time since I wrote that up, and can't remember my exact reason. It might just have been the simplest solution at the time, even though it incurred a bit of tech debt.

@zeileis
Copy link
Copy Markdown
Collaborator Author

zeileis commented Mar 26, 2026

Regarding your three comments/questions:

When using facet.args you could indeed set ncol rather than nrow. But when adding passing the information through attributes of the facet variable, then only the facet_nrow attribute is used but not facet_ncol. Hence, in the formula method we only compute facet_nrow.

Correct, the formula-based output is currently not ideal. Apologies for not bringing this out clearly. I also think that we should get the same output as via facet.args! Note that the current behavior is not related to the formula processing in tinyplot.formula. It is caused by the difference in handling a plain facet variable plus facet.args in tinyplot.default vs. a facet variable with attributes. The following example causes the same problem:

p <- na.omit(p) ## omit missing values from transformed penguin data above
g <- p$group ## set up group variable with facet attributes
attr(g, "facet_grid") <- TRUE
attr(g, "facet_nrow") <- 1
tinyplot(p$bill_len, p$bill_dep, facet = g) ## unnecessary right facet strip
tinyplot(p$bill_len, p$bill_dep, facet = p$group, facet.args = list(nrow = 1)) ## ok

Should I try to replace get_facet_fml()? If so, anything I should try to pay attention to? Or better leave it as it is for now?

@grantmcdermott
Copy link
Copy Markdown
Owner

Thanks @zeileis. Appreciate the clarifications.

Stepping back for a sec, I'm planning to submit a patch 0.6.1 release to CRAN imminently---in part, b/c I'm trying to update my maintainer email across all of my packages. I was hoping to get this PR (and #558) in as part of the submission, but it seems that we still have a bit of work to do. So I think it's best to leave these out for the moment and we can address afterwards. Hope that's okay?

@zeileis
Copy link
Copy Markdown
Collaborator Author

zeileis commented Mar 26, 2026

Sure, perfectly fine! 🚀

@zeileis
Copy link
Copy Markdown
Collaborator Author

zeileis commented Jun 4, 2026

I have addressed the problems above as follows now:

  • The route via facet_grid/facet_nrow attributes is only used for facet = y ~ x as before!
  • The case of facet = 1 ~ x and y ~ 1 is handled without attributes and using facet.args = list(nrow = ...).

Now these new specifications work as desired:

library("tinyplot")
tinytheme("clean2")
p = transform(penguins, group = factor(paste(species, island, sep = "-")))
tinyplot(bill_dep ~ bill_len, data = p, facet = 1 ~ group)
tinyplot-facet1
tinyplot(bill_dep ~ bill_len, data = p, facet = group ~ 1)
tinyplot-facet2

And these old specifications work as before:

tinyplot(bill_dep ~ bill_len, data = p, facet = ~ group)
tinyplot-facet3
tinyplot(bill_dep ~ bill_len, data = p, facet = island ~ species)
tinyplot-facet4

Notes:

  • If the user specifies something like facet = 1 ~ x, facet.args = list(nrow = n), then it is handled like facet = ~ x, facet.args = list(nrow = n). Thus, the user-specified nrow is used (without warning) and not the nrow implied by the 1 ~ x formula.
  • Test cases with snapshots have been added (expanding the ozone examples in test-facet.R).

@grantmcdermott grantmcdermott merged commit 5c01e97 into main Jun 5, 2026
3 checks passed
@grantmcdermott
Copy link
Copy Markdown
Owner

Thanks @zeileis!

@grantmcdermott grantmcdermott deleted the facet-formula branch June 5, 2026 01:31
@zeileis
Copy link
Copy Markdown
Collaborator Author

zeileis commented Jun 5, 2026

Great, thanks, Grant! I hadn't added a NEWS item, yet. Do you want to do so or should I? If so where/how?

Re: get_facet_fml() (https://github.com/grantmcdermott/tinyplot/blob/facet-formula/R/facet.R#L613-L658). Should I create an issue for this so that we remember to have a look at replacing it with tinyformula()?

@grantmcdermott
Copy link
Copy Markdown
Owner

Yes to both, thanks Achim. (For NEWS, I'm fine if you push direct to main once you've synced the latest changes.)

@grantmcdermott
Copy link
Copy Markdown
Owner

Had a sec this morning, so I just pushed through the NEWS entry in 7d37682.

But please still raise an issue for the below when you get a chance:

Re: get_facet_fml() (https://github.com/grantmcdermott/tinyplot/blob/facet-formula/R/facet.R#L613-L658). Should I create an issue for this so that we remember to have a look at replacing it with tinyformula()?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants