How to Annotate a Reference Line at the Same Angle as the Reference Line Itself

ggplot2: Why are my text annotations not aligned right?

I believe this should give you what you want. See this answer for reference Get width of plot area in ggplot2.

#Plot so we can reference the viewport
ggplot(data.frame(x = seq(0, 14, 0.1)), aes(x = x)) +
stat_function(fun = function(x) {
14 - x
}, geom = "line")

#Get the currentVPtree and then convert the VP's height/width to inches
current.vpTree()
a <- convertWidth(unit(1,'npc'), 'inch', TRUE)
b <- convertHeight(unit(1,'npc'), 'inch', TRUE)

#Replot using the ratio of a/b to get the appropriate angle
ggplot(data.frame(x = seq(0, 14, 0.1)), aes(x = x)) +
stat_function(fun = function(x) {
14 - x
}, geom = "line") +
theme_bw()+
annotate(
geom = "text",
x = 7.5, y = 7.5,
label = "x + y = 14",
angle = (atan(a/b) * 180/pi) + 270)

We basically get the viewport width/height and then use simple geometry (inverse tangent since we have both sides of the triangle) to calculate what the actual angle of the line is.

Result:

Sample Image

r - How to annotate curves

The trickiest part was figuring out how to use the aspect ratio, which I found from this email. Since we've got different plot devices, you'll want to change things like text size and how much above the line you want the text to be.

Basically, we're just calculating the derivative of the curve at each point you want to make the annotation and adjusting for the aspect ratio of the figure window. From there, you calculate the angle in degrees. If you have to do this many times, you might want to consider making a function for each curve and its derivative.

upshift = 0.025
I0 <- log(1)
b <- .1
curve(exp(I0 - b * x), 0, 50, xlab = "No. of Species (R)", ylab = "Rate (I or E)", col = "blue", lwd = 2)

# Get aspect ratio
w <- par("pin")[1]/diff(par("usr")[1:2])
h <- par("pin")[2]/diff(par("usr")[3:4])
asp <- w/h

angle = atan(-b * exp(I0) * exp(-b * 10) / asp) * 180 / pi
text(10, exp(I0 - b * 10) + upshift, "Near", srt = angle)

d <- .01
curve(exp(d * x) - 1, 0, 50, add = TRUE, col = "orange", lwd = 2)
angle = atan(d * exp(d * 30) / asp) * 180 / pi
text(30, exp(d * 30)-1 + upshift, "Large", srt = angle)

I0 <- log(1/2)
curve(exp(I0 - b * x), 0, 50, add = TRUE, lty = 2, col = "green", lwd = 2)
angle = atan(-b * exp(I0) * exp(-b * 10) / asp) * 180 / pi
text(5, exp(I0 - b * 5) + upshift, "Far", srt = angle)

d <- .014
curve(exp(d * x) - 1, 0, 50, add = TRUE, lty = 2, col = "red", lwd = 2)
angle = atan(d * exp(d * 30) / asp) * 180 / pi
text(30, exp(d * 30)-1 + upshift, "Small", srt = angle)

title(main = "The equilibrium model of island biogeography")

Sample Image

How to rotate a custom annotation in ggplot?

You could use the magick package to rotate the png file:

library(magick)

bullet <- magick::image_read("bullet.png")

## To remove white borders from the example png
bullet <- magick::image_background(bullet, "#FF000000")

## Create angle column
gundf$angle <- seq(0,360, length.out = nrow(gundf))

## Plot
gundf %>%
ggplot(aes(x=year, y=deaths)) +
geom_line(size=1.2) +
mapply(function(x, y, angle) {
annotation_custom(rasterGrob(magick::image_rotate(bullet, angle)),
xmin = x-0.5,
xmax = x+0.5,
ymin = y-500,
ymax = y+500)
},
gundf$year, gundf$deaths, gundf$angle) +
theme_minimal()

Sample Image

As for your question about making the bullet to follow the line, see the comments to this answer. Making objects to have the same slope than a line in ggplot2 is tricky because you need to know the aspect ratio of the plotting region (information that is not printed anywhere at the moment, as far as I know). You can solve this by making your plot to a file (pdf or png) using a defined aspect ratio. You can then use the equation from @Andrie (180/pi * atan(slope * aspect ratio)) instead of the one I used in the example. There might be a slight mismatch, which you can try to adjust away using a constant. Also, it might be a good idea to linearly interpolate one point between each point in your dataset because now you are plotting the bullet where the slope changes. Doing that in animation would work poorly. It would probably be easier to plot the bullet where the slope is constant instead.

How to rotate matplotlib annotation to match a line?

Even though this question is old, I keep coming across it and get frustrated, that it does not quite work. I reworked it into a class LineAnnotation and helper line_annotate such that it

  1. uses the slope at a specific point x,
  2. works with re-layouting and resizing, and
  3. accepts a relative offset perpendicular to the slope.
x = np.linspace(np.pi, 2*np.pi)
line, = plt.plot(x, np.sin(x))

for x in [3.5, 4.0, 4.5, 5.0, 5.5, 6.0]:
line_annotate(str(x), line, x)

Annotated sinus

I originally put it into a public gist, but @Adam asked me to include it here.

import numpy as np
from matplotlib.text import Annotation
from matplotlib.transforms import Affine2D

class LineAnnotation(Annotation):
"""A sloped annotation to *line* at position *x* with *text*
Optionally an arrow pointing from the text to the graph at *x* can be drawn.
Usage
-----
fig, ax = subplots()
x = linspace(0, 2*pi)
line, = ax.plot(x, sin(x))
ax.add_artist(LineAnnotation("text", line, 1.5))
"""

def __init__(
self, text, line, x, xytext=(0, 5), textcoords="offset points", **kwargs
):
"""Annotate the point at *x* of the graph *line* with text *text*.

By default, the text is displayed with the same rotation as the slope of the
graph at a relative position *xytext* above it (perpendicularly above).

An arrow pointing from the text to the annotated point *xy* can
be added by defining *arrowprops*.

Parameters
----------
text : str
The text of the annotation.
line : Line2D
Matplotlib line object to annotate
x : float
The point *x* to annotate. y is calculated from the points on the line.
xytext : (float, float), default: (0, 5)
The position *(x, y)* relative to the point *x* on the *line* to place the
text at. The coordinate system is determined by *textcoords*.
**kwargs
Additional keyword arguments are passed on to `Annotation`.

See also
--------
`Annotation`
`line_annotate`
"""
assert textcoords.startswith(
"offset "
), "*textcoords* must be 'offset points' or 'offset pixels'"

self.line = line
self.xytext = xytext

# Determine points of line immediately to the left and right of x
xs, ys = line.get_data()

def neighbours(x, xs, ys, try_invert=True):
inds, = np.where((xs <= x)[:-1] & (xs > x)[1:])
if len(inds) == 0:
assert try_invert, "line must cross x"
return neighbours(x, xs[::-1], ys[::-1], try_invert=False)

i = inds[0]
return np.asarray([(xs[i], ys[i]), (xs[i+1], ys[i+1])])

self.neighbours = n1, n2 = neighbours(x, xs, ys)

# Calculate y by interpolating neighbouring points
y = n1[1] + ((x - n1[0]) * (n2[1] - n1[1]) / (n2[0] - n1[0]))

kwargs = {
"horizontalalignment": "center",
"rotation_mode": "anchor",
**kwargs,
}
super().__init__(text, (x, y), xytext=xytext, textcoords=textcoords, **kwargs)

def get_rotation(self):
"""Determines angle of the slope of the neighbours in display coordinate system
"""
transData = self.line.get_transform()
dx, dy = np.diff(transData.transform(self.neighbours), axis=0).squeeze()
return np.rad2deg(np.arctan2(dy, dx))

def update_positions(self, renderer):
"""Updates relative position of annotation text
Note
----
Called during annotation `draw` call
"""
xytext = Affine2D().rotate_deg(self.get_rotation()).transform(self.xytext)
self.set_position(xytext)
super().update_positions(renderer)

def line_annotate(text, line, x, *args, **kwargs):
"""Add a sloped annotation to *line* at position *x* with *text*

Optionally an arrow pointing from the text to the graph at *x* can be drawn.

Usage
-----
x = linspace(0, 2*pi)
line, = ax.plot(x, sin(x))
line_annotate("sin(x)", line, 1.5)

See also
--------
`LineAnnotation`
`plt.annotate`
"""
ax = line.axes
a = LineAnnotation(text, line, x, *args, **kwargs)
if "clip_on" in kwargs:
a.set_clip_path(ax.patch)
ax.add_artist(a)
return a

Rotate Axis for matplotlib Annotate text : python

Additional arguments to the .annotate()-method are passed to Text, so you can do e.g.:

for i,txt in enumerate(date_match_df['service_name_'].tolist()):
print(txt)
axess.annotate(txt,(mdates.date2num(idx2[i]),stock2[i]), ha='left', rotation=60)

Where the change from your code is the addition of ha='left', rotation=60 in the end.



Related Topics



Leave a reply



Submit