Centralize type-specific logic via self-declared flags (re: #543)#637
Centralize type-specific logic via self-declared flags (re: #543)#637grantmcdermott wants to merge 2 commits into
Conversation
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.
|
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, 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. |
|
@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 |
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 itsdata_<type>()constructor. It directly implements the design floated in that thread:data_<type>anddraw_<type>), which is the principled design choice. But we couldn't figure out a way to do that at the time."par_overrides-style escape hatch on thesettingscontainer that types plug into.missing_xi_means_empty_plotproposal (≈ ourdraw_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_hintsslot (NULL by default): a named list of behaviour flags that a type'sdata_*()function writes via the existingenv2env()write-back (same pattern asgroup_offsets/offsets_axis). The pipeline readsisTRUE(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:self_axestinyplot.Rdynmar block (#635)xaxt/yaxt = "n"rhs_axisfacet.R×2,legend.R(outer-legend bump)proportional_limlim.R×2[0,1]limits; don't expand to axis breakslegend_fill_from_collegend.R(pt.bg)coldraw_empty_facettinyplot.Rempty-facet guardtype_axes_hintsis threaded through the exporteddraw_legend()(newNULL-default param +@param),build_legend_env(), and the multi-legend path.Safety
Pure refactor — behaviour is byte-identical. All five flags are
TRUEfor spineplot andNULLfor every other type, so each site evaluates exactly as before. Verifiedcmp-identical PNG renders across: simple, breaks/flip, facet, by-group, outer-legend (the RHS bump path), and thexlab/ylab = NAcases 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:
type_axes_hintsthe right umbrella, or should hints be grouped differently (e.g. a broadertype_hints/settings$hintsnest covering non-axis concerns too)? Per @katrinabrock, nesting keepssettingsreadable as this grows.draw_empty_facetstill co-exists with a hard-codedc("histogram","hist","rect","segments")list — those types should self-declare it too (this is exactly @katrinabrock'smissing_xi_means_empty_plot). Likewise theelse if (type == "ridge")legend-fill branch sits right next to the convertedlegend_fill_from_colcheck and is a natural next migration.type ==/type %in%checks. Inventory beyond spineplot: the boxplot-flip and ridgewhtsbpchecks (duplicated acrosstinyplot.R+facet.R), the categorical-axis type lists infacet.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).nameimpacts behavior, consider alternatives to hardcoded types intinyplot::tinyplot#543 point 4). Once flags live on the type, document them in thetypesvignette so custom-type authors get the same toolbox, and revisit registering convenience strings for custom types.