Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Suggestion to change the default line end of axis lines in theme_classic #5978

Closed
psoldath opened this issue Jul 5, 2024 · 10 comments · Fixed by #5981
Closed

Suggestion to change the default line end of axis lines in theme_classic #5978

psoldath opened this issue Jul 5, 2024 · 10 comments · Fixed by #5981
Labels
visual change 👩‍🎨 Rendering change that will affect look of output

Comments

@psoldath
Copy link

psoldath commented Jul 5, 2024

Inspired by the newly released option to cap the axis lines (a great addition btw), I would like to suggest a cosmetic improvement to theme_classic as I believe the current default of the line end of the axis lines is not the best fit.

Theme_classic is the only original complete theme that uses axis lines. The line end of the axis lines in this theme is actually set to NULL but practically it works like the "butt" line end. The "butt" line end cuts off the very end of the lines. This is an appropriate setting for things like axis ticks etc in all other themes (as axis ticks with the "butt" line-end will align perfectly with either the panel border or panel background of all the other themes), but I don't think it is the best choice for theme_classic. In theme_classic, when the axis lines are not capped, they join together at the origin with a very ugly notch in the left lower corner as the lines are simply too short to come together correctly (unlike for instance the lines of the panel border in the themes that use that). The notch is pretty noticeable and becomes very apparent when zooming in. It gets even worse when the axis lines are capped as the notch will then be present at the upper-outer corner of both the first and last breaks of both axes.

A solution would be to change the default of the line end for the axis lines to the "square" or "round" lineend. Hereby, the two axes would join nicely at the origin when not capped and when they are capped, they would line up perfectly with the axis ticks of the first and last breaks eliminating the ugly notches. It would probably be most appropriate to change the default line end to the "square" line end as this would keep the straight lines, thereby being consistent with the form of the axis ticks and the panel border of some of the other themes etc. If there is any way to make it a default of all axis lines in ggplot2, it would probably be beneficiary to do that, so customized themes built on, say, theme_gray with added axis lines will also use the "square" line end. I can't see any downsides to make this change, but I am not an expert in either R or ggplot2, so I don't know if there is a good explanation to the current settings. I just think it would improve the aesthetics of plots made with theme_classic.

@teunbrand
Copy link
Collaborator

Just as an illustration. Ugly corner between x/y axis lines:

library(ggplot2)
p <- ggplot(mpg, aes(displ, hwy)) +
  geom_point() +
  theme_classic(base_line_size = 5) +
  theme(axis.ticks.length = unit(5, "mm"))

p

Ugly corners between axis line and ticks:

p + guides(x = guide_axis(cap = "both"))

Created on 2024-07-05 with reprex v2.1.0

It also doesn't quite make sense to me why there are two different shades of black here.

@psoldath
Copy link
Author

psoldath commented Jul 5, 2024

Thank you for the illustrations @teunbrand. I absolutely agree. I too never got the point of the grey axis ticks in theme_classic. It would in my opinion be a lot more aesthetically pleasing with all black axis lines and tick marks.

@clauswilke
Copy link
Member

The two shades of black are definitely a bug in my opinion. theme_classic() inherits from theme_gray() (via theme_bw()) which has dark gray axis ticks and no axis lines and then it sets black axis lines but doesn't adjust the colors of the axis ticks. Axis ticks should be black.

I believe I have commented on this before but don't remember in what context.

@teunbrand teunbrand added the visual change 👩‍🎨 Rendering change that will affect look of output label Jul 5, 2024
@teunbrand
Copy link
Collaborator

teunbrand commented Jul 5, 2024

I'm also in favour of unifying the shade of black.

With regards to theme hierarchy, almost all themes are built on top of theme_gray() which has axis.line = element_blank().
We could set axis.line = element_line(lineend = "round"/"square", colour = "transparent") to keep hiding the axis line but have the lineend propagate to subsequent themes.

EDIT: nevermind the suggestion would break other stuff. I can't at the moment see a clear mechanism for this.

@Ax3man
Copy link
Contributor

Ax3man commented Jul 5, 2024

If it is not fixable in general, can it at least be fixed for theme_classic() specifically by including lineend = 'square' here?

axis.line = element_line(colour = "black", linewidth = rel(1)),

I use theme_classic() quite a lot, and it is a slight annoyance!

@teunbrand
Copy link
Collaborator

Yes fixing theme_classic()'s axis lineend and tick colour is a good idea. It would just be nice to have a solution to propagate the lineend in some way, but I don't see a clear way to do this yet.

@psoldath
Copy link
Author

psoldath commented Jul 6, 2024

We all agree that the axis ticks should be changed to black to match the axis lines, but I would also suggest that the axis text should be changed to black as well since all the other text elements in theme_classic() (title, subtitle, axis titles, caption) are black. The axis text is the only text element that is specified with a color (grey30), which it inherits from theme_grey(), just like the axis ticks. It may make sense in that theme that the axis text is grey given the grey background panel, but for theme_classic(), which is otherwise completely black and white (at least after we change the color of the axis ticks!), I think it would definitely look better with black axis text instead of grey. Keeping only the axis text in grey doesn't make sense to me, but changing both the axis ticks and the axis text to black would truly mimic the classic base R plot, which arguably always must have been the real point of a theme named theme_classic().

I totally agree that we should strive towards finding a solution, where the axis lines will look right in all complete themes and theme customizations. However, it may not be as easy as we first thought; I have come across yet another problem. In the example given for capping of the axes on the tidyverse blog, the y-axis is only capped in the upper end in a plot that uses theme_grey(). We see that the capped end has an ugly corner, but we also see that the uncapped end aligns perfectly with the grey panel. So if we change the line end to "square", we get a nice looking corner of the upper line end and tick mark but an ugly lower one that extends further down than the grey panel! Right now, the line end argument only takes the three possible values of "butt", "round", or "square". I think the perfect solution would be something like the arrow argument, which takes the arrow() function, which allows for specification of which end to turn into an arrow? In this way it may be possible to write a conditional statement on which line end to be "butt" or "square"/"round"? I know this may be quite a laborious job, but I can't see any other solution that would assure that the axis lines will always match the other conditions.

The upper line end of the y-axis is not aligned with the tick mark of the last break, but the lower line end is perfectly aligned with the bottom of the panel background

library(ggplot2)
p <- ggplot(mpg, aes(displ, hwy)) +
  geom_point() +
  guides(
    x = guide_axis(cap = "both"),
    y = guide_axis(cap = "upper")
  ) +
  theme(axis.line = element_line(linewidth = 5),
        axis.ticks = element_line(linewidth = 5),
        axis.ticks.length = unit(5, "mm"))

p

p

The upper line end of the y-axis is now perfectly aligned with the tick mark of the last break, but in return the lower line end now extends beyond the bottom of the panel background

p +
  theme(axis.line = element_line(lineend = "square"))

p

@teunbrand
Copy link
Collaborator

write a conditional statement on which line end to be "butt" or "square"/"round"

The {grid} system on which ggplot2 is build doesn't accommodate different lineend settings for the two ends of a path and neither does the .svg specification, I think. This is just to indicate that there is no 'native' way of accommodating this request.

With some fiddling it might be possible to offset the first/last point of a path by half a linewidth, however, there is no device consensus on how linewidth is interpreted.

For example the documentation on lwd in ?par reads:

The line width, a positive number, defaulting to 1. The interpretation is device-specific, and some devices do not implement line widths less than one.

So there is no robust way of getting the size of half a linewidth correct all the time. Besides that, it would also make the position of the lineend graphically correct, but numerically wrong in that we couldn't do a smooth linejoin in a vector graphics editing suite with an orthogonal line, such as a tick mark or the opposite axis.

I think it would probably be wisest to just accept the limitations of the graphics systems instead of pursuing perfection in this case.

@psoldath
Copy link
Author

psoldath commented Jul 6, 2024

If that is the case, I completely agree with you @teunbrand.
The case I just brought up is also a very hypothetical case. I mean probably no one will ever have the need to cap one axis at both ends and the other only at one end? Either you would want to cap both axes at both ends or cap both axes only at one end and then everything works out fine with the “square” line end.

@teunbrand
Copy link
Collaborator

I've been giving the automatic inheritance some thoughts and here are some options.

Option 1: Use invisible line with square lineend

As mentioned in #5978 (comment). Essentially set axis.line = element_line(colour = "transparent", lineend = "square")

+ Minimally invasive
- Theme overrides, such as theme_gray() + theme(axis.line = element_line()) will be broken, as line will stay invisible.

Option 2: Axis guide internally set square lineend

As mentioned in #5982. Essentially give guide_axis() the power to break inheritance of the lineend parameter.

+ Overriding theme works as expected
- Pretty invasive

Option 3: Axis children get square lineend

As mentioned in #5983. Give lineend = "square" to axis.line.x and axis.line.y instead of the parent axis.line, along with inherit.blank = TRUE.

+ Minimally invasive
- No 'easy' swapping line ends

Option 4: New parent element

The idea here is to create an intermediate abstract theme element in between axis.line and line so that we can give axis.parent = element_line(lineend = "square").

+ Not too invasive
- It is not very intuitive, e.g. what should this even be named?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
visual change 👩‍🎨 Rendering change that will affect look of output
Projects
None yet
4 participants