Skip to content

Centralize type-specific logic via self-declared flags (re: #543)#637

Draft
grantmcdermott wants to merge 2 commits into
mainfrom
consolidate-type-flags
Draft

Centralize type-specific logic via self-declared flags (re: #543)#637
grantmcdermott wants to merge 2 commits into
mainfrom
consolidate-type-flags

Conversation

@grantmcdermott

@grantmcdermott grantmcdermott commented Jun 19, 2026

Copy link
Copy Markdown
Owner

Draft / proof-of-concept — opening move toward #543.

This is a first, deliberately small step toward the consolidation discussed in #543: moving hard-coded type == "..." logic branches out of the main pipeline (tinyplot.R, facet.R, legend.R, lim.R) and into flags that each plot type declares for itself via its data_<type>() constructor. It directly implements the design floated in that thread:

  • @grantmcdermott (comment, point 1): "keep all type-specific behaviour in the dedicated constructors (data_<type> and draw_<type>), which is the principled design choice. But we couldn't figure out a way to do that at the time."
  • @grantmcdermott (point 2): a par_overrides-style escape hatch on the settings container that types plug into.
  • @katrinabrock: the missing_xi_means_empty_plot proposal (≈ our draw_empty_facet), and the idea that settings could be a nested named list of semantic type attributes (e.g. x_optional) rather than 1:1 flags.

Mechanism

A new settings$type_axes_hints slot (NULL by default): a named list of behaviour flags that a type's data_*() function writes via the existing env2env() write-back (same pattern as group_offsets/offsets_axis). The pipeline reads isTRUE(type_axes_hints[["<flag>"]]) instead of matching the type string, so the generic layout/legend code no longer needs to know specific types exist.

What this PR does (spineplot only, as the pilot)

Converts the six scattered type == "spineplot" checks to five self-declared flags:

Flag Replaces (was hard-coded in) Behaviour
self_axes tinyplot.R dynmar block (#635) reserve tick-row margin despite xaxt/yaxt = "n"
rhs_axis facet.R ×2, legend.R (outer-legend bump) reserve right margin for a secondary axis
proportional_lim lim.R ×2 [0,1] limits; don't expand to axis breaks
legend_fill_from_col legend.R (pt.bg) legend swatch fill from col
draw_empty_facet tinyplot.R empty-facet guard draw with zero per-group rows

type_axes_hints is threaded through the exported draw_legend() (new NULL-default param + @param), build_legend_env(), and the multi-legend path.

Safety

Pure refactor — behaviour is byte-identical. All five flags are TRUE for spineplot and NULL for every other type, so each site evaluates exactly as before. Verified cmp-identical PNG renders across: simple, breaks/flip, facet, by-group, outer-legend (the RHS bump path), and the xlab/ylab = NA cases from #635.

Still to do (scope for discussion before expanding)

This PR intentionally migrates one type to prove the pattern and invite design feedback before a broader sweep. Open questions / follow-ups:

  • Naming + shape. Is type_axes_hints the right umbrella, or should hints be grouped differently (e.g. a broader type_hints/settings$hints nest covering non-axis concerns too)? Per @katrinabrock, nesting keeps settings readable as this grows.
  • Migrate neighbours. draw_empty_facet still co-exists with a hard-coded c("histogram","hist","rect","segments") list — those types should self-declare it too (this is exactly @katrinabrock's missing_xi_means_empty_plot). Likewise the else if (type == "ridge") legend-fill branch sits right next to the converted legend_fill_from_col check and is a natural next migration.
  • Other scattered type == / type %in% checks. Inventory beyond spineplot: the boxplot-flip and ridge whtsbp checks (duplicated across tinyplot.R + facet.R), the categorical-axis type lists in facet.R, density/hist/barplot flags. Decide which are genuine "type properties" worth a flag vs. which are fine as-is (cf. @grantmcdermott point 3: branching at the primitive level — points/lines/rect — may be acceptable).
  • Custom-type story (Custom types: Clarify how name impacts behavior, consider alternatives to hardcoded types in tinyplot::tinyplot #543 point 4). Once flags live on the type, document them in the types vignette so custom-type authors get the same toolbox, and revisit registering convenience strings for custom types.
  • Dual conventions interim. Until the above land, the codebase has two idioms (string checks + hints). Worth a tracking note so the inconsistency is intentional.

Replace six scattered hardcoded `type == "spineplot"` checks with semantic
flags the type declares for itself. data_spineplot() now writes a
type_axes_hints list (self_axes, rhs_axis, proportional_lim,
legend_fill_from_col, draw_empty_facet) to settings via the same env2env
write-back pattern used by group_offsets. The main pipeline reads these
flags instead of matching the type string.

Sites converted:
- R/lim.R: proportional_lim (suppress axis-break range expansion)
- R/tinyplot.R: self_axes (dynmar tick-row margin, #635) + draw_empty_facet
- R/facet.R: rhs_axis (omar[4] RHS-axis margin, x2; drops the FIXMEs)
- R/legend.R: rhs_axis (outer-legend bump) + legend_fill_from_col (pt.bg)
- R/legend_multi.R: thread type_axes_hints through the multi-legend path

type_axes_hints is threaded through the exported draw_legend() (new NULL
default + @param) -> build_legend_env() -> stored on legend_env. Pure
refactor: all five flags are TRUE for spineplot, NULL elsewhere, so every
site evaluates identically. Verified byte-identical renders across simple,
flip, facet, by-group, outer-legend, and xlab/ylab=NA (#635) cases. Adds a
spineplot_legend_outer_right snapshot to cover the RHS legend-margin path.
The default legend position is already "right!" (outer), so the existing
spineplot_facet_by test exercises the outer_side + rhs_axis legend-margin
branch. A dedicated outer-right test added no new coverage.
@vincentarelbundock

Copy link
Copy Markdown
Collaborator

Overall, I think this is a very good path to go on. I agree we should remove the type specific logic from the main functions and put as much as we can in the type-specific functions.

The one thing that worries me a bit is not specific to the new flags you just adopted, but relates to our naming conventions. I feel that we haven't been super strict in naming variables internally, and that can make things harder to read and maintain than they should be. For example, group_offsets vs offsets_axis, where we don't follow a clear pattern, like verb_noun_adjective.

This is of course a different issue, but I wanted to raise it here because this PR will likely lead to the creation of several new internal variables.

@katrinabrock

Copy link
Copy Markdown

@grantmcdermott Let me know if you are looking for any kind of feedback from me.


Also, not related exclusively to this issue, I've recently made public the project I (and @rhirk) have been working on that's making use of tinyplot: https://github.com/livingingroups/trackframe

The custom types are here:

They don't suffer too badly from this issue, but @rhirk had previously built the type_arrows functionality based on l rather than segments (here) and I was a bit surprised and confused by how that name = argument impacted the behavior which prompted me to raise #543 in the first place

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.

3 participants