Skip to content

Preserve order of categories in the plot#448

Closed
elinw wants to merge 12 commits into
easystats:mainfrom
elinw:plot_groupmeans_plot
Closed

Preserve order of categories in the plot#448
elinw wants to merge 12 commits into
easystats:mainfrom
elinw:plot_groupmeans_plot

Conversation

@elinw

@elinw elinw commented Jul 6, 2026

Copy link
Copy Markdown

I noticed that when the category names are not in alphabetical order, ggplot2 alphabetizes when defining the x axis. This matters because sometimes order is important (in the case I noticed they defined a Likert scale). From what I learned doing some reading the way to address this is to turn the Category column into a factor. THat's what this PR does. That may have other consequences, I'm not sure yet.

@gemini-code-assist gemini-code-assist Bot 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.

Code Review

This pull request introduces new S3 plot methods for grouped means (plot.dw_groupmeans and plot.dw_groupmeans_list) and crosstabs (plot.datawizard_crosstab), along with corresponding documentation and test suites. The code review highlights several critical and high-severity issues, including an undefined variable t_base in the crosstab plot function, a syntax error in the documentation examples, and compatibility issues with the native pipe operator |> on older R versions. Additionally, the feedback suggests improvements to confidence interval limit calculations (handling NA and negative values), ensuring category ordering is preserved in list plots, avoiding explicit print() calls in S3 plot methods, and correcting documentation tags.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread R/plot_dw_crosstabs.R
x_long <- datawizard::data_to_long(
x,
rows_to = "row_var",
select = names(t_base)[-1]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

critical

The variable t_base is not defined anywhere in this function or file. This will cause an object 't_base' not found error at runtime. It should be x instead.

      select = names(x)[-1]

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

This all was included by accident see new pR

Comment thread R/plot.dw_groupmeans.R
Comment on lines +29 to +31
#' group_means_object <- datawizard::means_by_group(
#' iris$Sepal.Width, iris$Species)
#' )

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

There is a syntax error in the examples section due to an extra closing parenthesis on line 31. This will cause an error when running the examples or during package checks.

#' group_means_object <- datawizard::means_by_group(\n#'   iris$Sepal.Width, iris$Species\n#' )

Comment thread R/plot.dw_groupmeans.R
Comment on lines +54 to +57
if ("CI_low" %in% names(trimmed)) {
lower_lim <- .9 * min(trimmed$CI_low)
upper_lim <- 1.1 * max(trimmed$CI_high)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

The current calculation of lower_lim and upper_lim has several issues:\n1. It does not handle NA values, which will cause min() and max() to return NA unless na.rm = TRUE is specified.\n2. It breaks for negative values (e.g., multiplying a negative minimum by 0.9 makes it larger/closer to zero, which truncates the lower part of the confidence interval).\n\nA more robust approach is to calculate the range of the confidence intervals and expand the limits relative to that range.

  if ("CI_low" %in% names(trimmed)) {\n    ci_range <- max(trimmed$CI_high, na.rm = TRUE) - min(trimmed$CI_low, na.rm = TRUE)\n    lower_lim <- min(trimmed$CI_low, na.rm = TRUE) - 0.1 * ci_range\n    upper_lim <- max(trimmed$CI_high, na.rm = TRUE) + 0.1 * ci_range\n  }

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

This is wrong since these are never NA.

Comment thread R/plot.dw_groupmeans.R

x_long <- do.call(rbind, x)

trimmed <- datawizard::data_filter(x_long, Category != "Total")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

In plot.dw_groupmeans_list, trimmed$Category is not converted to a factor. This means the main objective of this PR (preserving the order of categories in the plot) will not be applied when plotting a list of group means. We should convert trimmed$Category to a factor here as well, using unique(trimmed$Category) to handle the duplicate categories across different variables.

  trimmed <- datawizard::data_filter(x_long, Category != "Total")\n  trimmed$Category <- factor(trimmed$Category, levels = unique(trimmed$Category))

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

See new pr

Comment thread R/plot.dw_groupmeans.R
Comment on lines +124 to +127
if ("CI_low" %in% names(trimmed)) {
lower_lim <- .9 * min(trimmed$CI_low)
upper_lim <- 1.1 * max(trimmed$CI_high)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

Just like in plot.dw_groupmeans, the calculation of lower_lim and upper_lim here does not handle NA values and breaks for negative values. Let's use the same robust range-based expansion.

  if ("CI_low" %in% names(trimmed)) {\n    ci_range <- max(trimmed$CI_high, na.rm = TRUE) - min(trimmed$CI_low, na.rm = TRUE)\n    lower_lim <- min(trimmed$CI_low, na.rm = TRUE) - 0.1 * ci_range\n    upper_lim <- max(trimmed$CI_high, na.rm = TRUE) + 0.1 * ci_range\n  }

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

This is wrong.

Comment thread R/plot.dw_groupmeans.R
#' (if present in the object should be graphed), and `caption` for if
#' a caption summarizing ANOVA results should be included.
#'
#' @value

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

In roxygen2, the standard tag to document the return value of a function is @return, not @value. Using @value may prevent the return value from being correctly parsed and documented.

#' @return

Comment thread R/plot.dw_groupmeans.R
caption_text = ""
}
trimmed <- datawizard::data_filter(x, Category != "Total")
trimmed$Category <- factor(trimmed$Category, levels = trimmed$Category)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

When converting Category to a factor, it is safer to use unique(trimmed$Category) for the levels argument. While duplicates might not be expected in a single group means table, using unique() is a more robust and defensive programming practice.

  trimmed$Category <- factor(trimmed$Category, levels = unique(trimmed$Category))

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

These can never be duplicates because of how the object is created.

Comment thread R/plot.dw_groupmeans.R

x_attributes <- lapply(x, attributes)
x_var_names <- lapply(x_attributes, `[[`, "var_mean_label")
x_captions <- mapply(.build_caption, x, x_var_names)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Using mapply with list arguments that can contain NULL elements (such as x_var_names if the "var_mean_label" attribute is missing) can lead to unexpected recycling behavior or errors in R. Using vapply with an index is much safer and guarantees a character vector output.

  x_captions <- vapply(seq_along(x), function(i) {\n    .build_caption(x[[i]], x_var_names[[i]])\n  }, character(1))

Comment thread R/plot.dw_groupmeans.R
Comment on lines +151 to +152
p <- p + plotlist
print(p)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

S3 plot methods should return the plot object and let the auto-printing mechanism or the user handle printing. Explicitly calling print(p) inside the function causes the plot to be drawn twice in interactive sessions when assigned to a variable (e.g., p <- plot(x)).

  p + plotlist

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

This is incorrect when the plot is produced inside a loop.

Comment thread R/plot_dw_crosstabs.R
Comment on lines +15 to +17
if (is.null(proportion_type)) {
x_long
plotlist[[1]] <- aes(x = row_var, y = name, fill = value)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The standalone expression x_long on line 16 is redundant and does nothing. It should be removed.

  if (is.null(proportion_type)) {\n    plotlist[[1]] <- aes(x = row_var, y = name, fill = value)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Not supposed to be part of PR

@bwiernik

bwiernik commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

There are a variety of places where this code won't work as expected. Could you give a reproducible example of a case where the levels of a factor aren't being preserved as expected with the current version of the package?

Note--if you haven't converted the variable to factor before modeling, that is the best place to do so

@strengejacke

strengejacke commented Jul 7, 2026

Copy link
Copy Markdown
Member

We already have methods for plotting objects returned by means_by_group(), you should build upon that code instead of re-implementing a second plot() method, especially, since plot() for that class (.dw_groupmeans) is already defined in the datawizard package.

The relevant code is here:
https://github.com/easystats/see/blob/main/R/plot.means_by_group.R

The current implementation is also more elegant as it handles arguments directly, which makes the code in parts shorter and easier to maintain. We don't need to capture all the arguments via ....

@elinw

elinw commented Jul 7, 2026

Copy link
Copy Markdown
Author

Sorry sent the wrong branch

@elinw

elinw commented Jul 7, 2026

Copy link
Copy Markdown
Author

Hi

We already have methods for plotting objects returned by means_by_group(), you should build upon that code instead of re-implementing a second plot() method, especially, since plot() for that class (.dw_groupmeans) is already defined in the datawizard package.

The relevant code is here: https://github.com/easystats/see/blob/main/R/plot.means_by_group.R

The current implementation is also more elegant as it handles arguments directly, which makes the code in parts shorter and easier to maintain. We don't need to capture all the arguments via ....

Lol that was contributed by me, though in fact ... methods are easier to maintain because you don't have to add everything users suggest and they don't violate the OOP principle of matching the signature of the parent method.

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