Comments (8)
I had a similar thought - I wonder if we should just convert vjust to an offset at an early stage and then use the calculation for the offset line that you asked about on SO
from geomtextpath.
Yeah I have tried that and while it's great for simplifying the calculation, that approach gave me minor imprecisions. E.g. in the unit tests, where angles should be -45
, 0
and 45
, I got -45.3...
, 0
and 45.4...
. See illustration below. The curvature approach has some rounding error imprecisions as well, but they are on a much smaller scale (somewhere around the 13th decimal place). The aim of this endeavour is to avoid calling .add_path_data()
and .get_path_points
in a loop for the same data, where only the offset changes.
from geomtextpath.
I think the offset path is just calculated as
xout <- .data$x + cos(rads + pi / 2) * c(offset[1], offset)
yout <- .data$y + sin(rads + pi / 2) * c(offset[1], offset)
and if you need the starting points it would be
xstart <- xout[1]
ystart <- yout[1]
The only problem is that you will need to convert the offset from text space to plotting space (i.e. the offset path ends up too far away from the original path)
from geomtextpath.
Alright, let me put the problem in terms of a reprex.
We can use this function based on the SO question (using a lag of 2 in diff()
seems to work better than a lag of 1):
calc_offset_direct <- function(x, y, offset = 0) {
n <- length(x)
dx <- diff(x, 2)
dy <- diff(y, 2)
ang <- atan(dy / dx)
if (length(ang) > 1) {
dang <- diff(ang)
dang <- ifelse(dang < - pi / 2, dang + pi, dang)
dang <- ifelse(dang > + pi / 2, dang - pi, dang)
ang <- cumsum(c(ang[1], 0, dang))
} else {
ang <- rep(ang, 2)
}
ang <- c(ang, ang[length(ang)])
xout <- x + cos(ang + pi / 2) * offset
yout <- y + sin(ang + pi / 2) * offset
list(x = xout, y = yout)
}
Whereas this attempts to do the same thing using curvature:
calc_offset_curvature <- function(x, y, offset = 0) {
n <- length(x)
dx <- diff(x)
dy <- diff(y)
ang <- atan(dy / dx)
if (length(ang) > 1) {
dang <- diff(ang)
dang <- ifelse(dang < - pi / 2, dang + pi, dang)
dang <- ifelse(dang > + pi / 2, dang - pi, dang)
ang <- cumsum(c(ang[1], 0, dang))
} else {
dang <- c(0, 0)
ang <- rep(ang, 2)
}
xstart <- x[1] + cos(ang[1] + pi / 2) * offset
ystart <- y[1] + sin(ang[1] + pi / 2) * offset
lens <- sqrt(dx^2 + dy^2)
dang <- approx(seq_along(dang), dang,
seq(1, length(dang), length.out = n - 1))$y
curv <- (dang / lens)
curv <- 1 - curv * offset
eff_len <- c(0, curv * lens)
xout <- cumsum(cos(ang) * eff_len) + rep(xstart, each = n)
yout <- cumsum(sin(ang) * eff_len) + rep(ystart, each = n)
list(
x = xout,
y = yout
)
}
Now we can test this with a simple path:
x <- 1:5
y <- c(1:3,2,1)
off_direct <- calc_offset_direct(x, y, offset = 1)
off_curve <- calc_offset_curvature(x, y, offset = 1)
xlim <- range(c(x, off_direct$x, off_curve$x))
ylim <- range(c(y, off_direct$y, off_curve$y))
plot(x, y, type = 'b', xlim = xlim, ylim = ylim)
lines(off_direct$x, off_direct$y, type = 'b', col = 2)
lines(off_curve$x, off_curve$y, type = 'b', col = 3)
You can see that the 'direct' method in red places the middle point at the offset distance from the original data, but that it affects the angles, because the distance to the middle point should be sqrt(2) == 1.414214
. On the other hand, the curvature method in green keeps the angles nice and consistent, but there is a small inaccuracy in there that I can't seem to wrap my head around.
# The curve method starts great but ends inaccurately
sqrt((off_curve$x - x)^2 + (off_curve$y - y)^2)
#> [1] 1.000000 1.000000 1.447972 1.048261 1.048261
# The direct method doesn't place the middle point correctly in terms of angles,
# but does respect the distance
sqrt((off_direct$x - x)^2 + (off_direct$y - y)^2)
#> [1] 1 1 1 1 1
Created on 2021-11-23 by the reprex package (v2.0.1)
from geomtextpath.
The only reason to calculate curvature should be to get a correct adj_length
. I think there is probably a better way to do this. In an attempt to simplify, I have added another little utility file of trig helpers, which we might find useful. These include simple and safe calculations for calculating gradients, angles, arc lengths and offset paths. There is even a function to calculate the adjusted length ratios of a path at a given offset distance, that should remove the need for curvature calculations. I have not used these in the rest of the codebase at the moment in case that interferes with your current work, but it might be worth having a look to see if any of these could be useful.
from geomtextpath.
Please go ahead with anything you've planned, I've isolated my experiment in a separate branch and I'll have a look at how to incorporate this later once I've got it up and running.
I found another method using angle bisector that seems to do a good job in terms of angles and distances, and isn't extremely complicated:
offset_bisect <- function(x, y, offset = 0) {
n <- length(x)
dx <- diff(x)
dy <- diff(y)
ang <- atan(dy / dx)
dang <- diff(ang)
dang <- ifelse(dang < - pi / 2, dang + pi, dang)
dang <- ifelse(dang > + pi / 2, dang - pi, dang)
# Get orthogonal angles
ang <- cumsum(c(ang[1], dang)) + pi / 2
# Left / right aligned indices
before <- c(1L, seq_along(ang))
after <- c(seq_along(ang), length(ang))
# Calculate angle bisector
bis <- (ang[before] + ang[after])/2
# Calculate x position at angle bisector
xx <- cos(ang)
xx <- xx[before] * xx[after]
# Calculate y position at angle bisector
yy <- sin(ang)
yy <- yy[before] * yy[after]
# Find appropriate length along bisector
len <- offset * sqrt(2) / sqrt(1 + xx + yy)
# Project new points at the bisector
xout <- len * cos(bis) + x
yout <- len * sin(bis) + y
return(list(x = xout, y = yout))
}
I'll be trying to incorporate this, but the issue is resolved :)
from geomtextpath.
I'm glad you've found a way round. To be honest, I'm a bit stumped by your example, since it is not obvious to me that the green offset is necessarily the "correct" one. It really depends on how we define it, and what visual effect would look best going round a shape with sharp angles. I would have thought that a curve with a radius of the offset would be best in this scenario - something like the blue line here, which I guess you would define as the set of points which can be connected to the path with a line segment of length one:
from geomtextpath.
Well a rounded corner also make sense for the round
join type of lines, whereas I was looking for a miter
style. I think both types are correct for offsetting a path, but it depends which is preferable. For this positive offset where the offset path goes around the angle, round make sense, whereas it doesn't if the offset was negative.
from geomtextpath.
Related Issues (20)
- Long labels with arrow throw cryptic "Error: Cannot create zero-length unit vector ("unit" subsetting)" error
- Strange behaviour of halign HOT 4
- Feature Request: Set angle for geom_labelvline HOT 6
- Feature request - test for text self-overlap on sharp contours HOT 1
- ggplot coord_polar() issue with multiple classes: 'from' must be a finite number error HOT 1
- session aborted when library in Rstudio HOT 2
- Add compatibility with `ggbump` HOT 2
- geom_textsf randomly throws erros when specification of font family is missing (OpenSUSE Leap 15.5, R 4.3.2) HOT 2
- Release geomtextpath 0.1.2 HOT 1
- library(geomtextpath) abort the R session HOT 2
- Prevent rounding of numeric labels HOT 2
- geomtextpath example code does not seem to work HOT 6
- Warning messages: "All aesthetics have length 1, but the data has XX rows" HOT 2
- vjust and hjust as aesthetics in geom_textsf HOT 3
- Different behavior for line breaks when there's no data
- geomtextline not accommodating RGBA colors
- show.legend not working in geom_textabline
- Support for `geom_step()`? HOT 2
- Partial match of 'xoff' to 'xoffset'
- geom_textsf seems to ignore remove_long=F
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
D3
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
-
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from geomtextpath.