Skip to content

Resolve line type(s) inconsistencies - #683

Merged
grantmcdermott merged 9 commits into
mainfrom
issue679
Aug 20, 2026
Merged

Resolve line type(s) inconsistencies#683
grantmcdermott merged 9 commits into
mainfrom
issue679

Conversation

@grantmcdermott

@grantmcdermott grantmcdermott commented Aug 18, 2026

Copy link
Copy Markdown
Owner

Fixes #679

HU @zeileis

Problem 1

As we discussed in the issue thread, I opted to order non-numeric x data by (coerced) factor levels, thus ensuring better consistency with type_points(), etc. Using your running example from the original issue, where I first plot tyoe_points as a foil.

pkgload::load_all("~/Documents/Projects/tinyplot/")
#> ℹ Loading tinyplot

LOTR = data.frame(
  name = c("Fellowship", "Two Towers", "Return"),
  runtime = c(178, 179, 201)
)

tinyplot(runtime ~ name, data = LOTR, type = type_points())

tinyplot(runtime ~ name, data = LOTR, type = type_lines(type = "p"))

However, note that users can override via an explicit xlevels = "asis" argument, which orders the x values according to first appearance in the data. This works for both lines and points (and also other types like type_spineplot with a preexisting xlevels argument).

tinyplot(runtime ~ name, data = LOTR, type = type_points(xlevels = "asis"))

tinyplot(runtime ~ name, data = LOTR, type = type_lines(type = "p", xlevels = "asis"))

Created on 2026-08-17 with reprex v2.1.1

Problem 2

Character labels are now preserved correctly with flipped axes. As an aside: the solution turned out to be a nice example where I could cut a lot of conditional type checking code (thus fragile) and just put in a simpler general label check. Again, using your example from the original issue, first with type_points as a foil.

tinyplot(runtime ~ name, data = LOTR, type = type_points(), flip = TRUE)

tinyplot(runtime ~ name, data = LOTR, type = type_lines(type = "b"), flip = TRUE)

The free-facet branch selected y-axis tick labels by listing eligible
plot types by name. Any type outside that list lost its `at` values
while keeping the `labels` inherited from the shared axis arguments,
which axis() rejects outright:

    Error in axis(...) : 'labels' is supplied and not 'at'

Gate on `!is.null(ylabs)` instead, matching the fixed-scale branch
above (#677). Named `ylabs` means the type placed categories on the
y-axis, which is the property the code actually depends on. The old
`isTRUE(flip)` condition was a poor proxy for it, since flipping is
only one of the ways categories reach the y-axis: a plain `type = "p"`
with a factor y variable hit the same error without any flip.

Refs #679
align_layer() mapped each row of an added layer onto the base layer's
axis by indexing the layer's own x positions with the looked-up
original positions. That treats a per-row lookup as a permutation. It
only coincided with the correct result when the layer's rows arrived
in ascending order, which is the common case of one sorted row per
category.

Assign the lookup directly instead. Rows that arrived in any other
order were previously permuted, and repeated categories collapsed onto
a single x position.

Refs #679
Levels are set in data order so the snapshot is independent of how a
type orders its categories, and stays valid either way should that
ordering change.

Refs #679
Both layers use the same plot type, and the disagreement between them
comes from the levels each data frame declares. Keeping the two types
identical means the test does not depend on any type's category
ordering, which is still under discussion.

The added layer's rows deliberately arrive out of ascending order, so
that a position-based mapping cannot accidentally agree with a
category-based one.

Refs #679
Line types (type = "l", "b", "h", ... via type_lines()) now place
categorical data exactly like type_points() does: categories follow
the factor levels rather than their order of appearance in the data,
so an explicit factor(x, levels = ...) is honoured and layered point/
line types land on the same categories. Factor y variables are also
handled now, instead of falling back to a numeric axis for every line
type except "p".

To reorder on the fly, type_points(), type_lines(), type_errorbar(),
and type_pointrange() gain an `xlevels` argument, extending the
convention already established by type_barplot(), type_spineplot(),
and type_ridge() (ylevels). Accepted values are a character vector of
level names, numeric level indexes (e.g. 3:1), or the new keyword
"data" (first appearance in the data), which the three existing types
now also accept. The argument only affects categorical variables and
is forwarded automatically from the top-level tinyplot() call.

type_errorbar() and type_pointrange() default to xlevels = "data",
preserving their existing behaviour (typically coefficient plots,
where the row order of the data is intentional) while making it
overridable via xlevels = NULL.

The level-reordering logic that was previously copy-pasted across
barplot/spineplot/ridge is consolidated into a shared
sanitize_xlevels() helper, which all seven types now use. Ridge
additionally gains the unknown-level warning the other types had.

Closes #679
Line types (type = "l", "b", "h", ... via type_lines()) now place
categorical data exactly like type_points() does: categories follow
the factor levels rather than their order of appearance in the data,
so an explicit factor(x, levels = ...) is honoured and layered point/
line types land on the same categories. Factor y variables are also
handled now, instead of falling back to a numeric axis for every line
type except "p".

To reorder on the fly, type_points(), type_lines(), type_errorbar(),
and type_pointrange() gain an `xlevels` argument, extending the
convention already established by type_barplot(), type_spineplot(),
and type_ridge() (ylevels). Accepted values are a character vector of
level names, numeric level indexes (e.g. 3:1), or the new keyword
"asis", which takes the categories in the order that they appear in
the data (cf. the `as.is` argument of read.table()). The three
existing types accept the keyword too. The argument only affects
categorical variables and is forwarded automatically from the
top-level tinyplot() call.

type_errorbar() and type_pointrange() default to xlevels = "asis",
preserving their existing behaviour (typically coefficient plots,
where the row order of the data is intentional) while making it
overridable via xlevels = NULL.

The level-reordering logic that was previously copy-pasted across
barplot/spineplot/ridge is consolidated into a shared
sanitize_xlevels() helper, which all seven types now use. Ridge
additionally gains the unknown-level warning the other types had.

Closes #679

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Resolves categorical-axis inconsistencies across plot types and layers.

Changes:

  • Adds shared xlevels/ylevels ordering, including "asis".
  • Preserves categorical labels for line plots, flipped axes, and free facets.
  • Corrects categorical layer alignment and adds snapshot coverage.

Reviewed changes

Copilot reviewed 19 out of 33 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
R/type_spineplot.R Uses shared level ordering.
R/type_ridge.R Adds "asis" level ordering.
R/type_points.R Adds xlevels.
R/type_pointrange.R Adds configurable ordering.
R/type_lines.R Aligns categorical axes and labels.
R/type_errorbar.R Exposes xlevels.
R/type_barplot.R Uses shared level ordering.
R/sanitize_xlevels.R Implements shared ordering logic.
R/facet.R Fixes free-facet categorical labels.
R/align_layer.R Corrects per-row category alignment.
NEWS.md Documents changes.
man/type_spineplot.Rd Updates spineplot documentation.
man/type_ridge.Rd Updates ridge documentation.
man/type_points.Rd Documents xlevels.
man/type_lines.Rd Documents categorical axes.
man/type_errorbar.Rd Documents interval-type ordering.
man/type_barplot.Rd Documents "asis".
inst/tinytest/test-type_pointrange.R Tests pointrange ordering.
inst/tinytest/test-type_lines.R Tests line-axis behavior.
inst/tinytest/test-tinyplot_add.R Tests layer alignment.
inst/tinytest/test-facet.R Tests free-facet labels.
inst/tinytest/_tinysnapshot/type_points_xlevels_idx.svg Point-order snapshot.
inst/tinytest/_tinysnapshot/type_lines_xlevels_asis.svg Appearance-order snapshot.
inst/tinytest/_tinysnapshot/type_lines_layer_h_p.svg Layer alignment snapshot.
inst/tinytest/_tinysnapshot/type_lines_flip_labels.svg Flipped-label snapshot.
inst/tinytest/_tinysnapshot/type_lines_explicit_levels.svg Explicit-level snapshot.
inst/tinytest/_tinysnapshot/type_lines_categorical_y.svg Categorical-y snapshot.
inst/tinytest/_tinysnapshot/type_lines_categorical_points.svg Point baseline snapshot.
inst/tinytest/_tinysnapshot/type_lines_categorical_lines.svg Line consistency snapshot.
inst/tinytest/_tinysnapshot/tinyplot_add_layer_category_alignment.svg Added-layer snapshot.
inst/tinytest/_tinysnapshot/pointrange_xlevels_null.svg Factor-order snapshot.
inst/tinytest/_tinysnapshot/facet_free_categorical_yaxis.svg Free-facet snapshot.
altdoc/pkgdown.yml Updates documentation-build metadata.
Files not reviewed (3)
  • man/type_barplot.Rd: Generated file
  • man/type_errorbar.Rd: Generated file
  • man/type_lines.Rd: Generated file

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread NEWS.md Outdated
@grantmcdermott
grantmcdermott merged commit f942c2b into main Aug 20, 2026
3 checks passed
@grantmcdermott
grantmcdermott deleted the issue679 branch August 20, 2026 00:30
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.

Inconsistencies between type_points and type_lines

2 participants