Skip to content

implementing geom_label_aligned #203

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

Open
wants to merge 64 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
9c13d0d
initial commit for geom_aligned_box
suhaani-agarwal Jun 6, 2025
6c6fc81
shifted quadprog to js side & resolved some errors
suhaani-agarwal Jun 9, 2025
950fc8e
added quadprog computing to quadprog.js as a function
suhaani-agarwal Jun 9, 2025
d4b8dbd
optimised positions for colliding boxes
suhaani-agarwal Jun 12, 2025
df30379
added example code for geom_aligned_boxes
suhaani-agarwal Jun 12, 2025
9c13736
improved example
suhaani-agarwal Jun 12, 2025
b2a9395
updated optimizeAlignedBoxes function and removed Bumpup and Bumpleft
suhaani-agarwal Jun 13, 2025
674919d
added example
suhaani-agarwal Jun 13, 2025
35b8304
removed label.padding
suhaani-agarwal Jun 13, 2025
751b269
added renderer test for geom_aligned_boxes
suhaani-agarwal Jun 14, 2025
e3e1b50
added interactivity test for geom_aligned_boxes
suhaani-agarwal Jun 15, 2025
fe0fe6e
replaced gapminder with worldbank
suhaani-agarwal Jun 15, 2025
122bf15
updated styling for geom_aligned_boxes to avoid double styling in bot…
suhaani-agarwal Jun 17, 2025
cb4e597
updated test
suhaani-agarwal Jun 17, 2025
3084116
removed unnescessary comments
suhaani-agarwal Jun 17, 2025
870de32
Merge branch 'master' into quadprog
suhaani-agarwal Jun 17, 2025
127d2ae
geom aligned boxes
tdhock Jun 20, 2025
d9b0f0d
changed default fill from black to white
suhaani-agarwal Jun 22, 2025
56826b7
Merge branch 'quadprog' of https://github.com/animint/animint2 into q…
suhaani-agarwal Jun 22, 2025
8321aeb
combined both test files and made seperate collision testing function
suhaani-agarwal Jun 22, 2025
e29df4e
deleted previous test-renderer1-geom-aligned-boxes.R file
suhaani-agarwal Jun 22, 2025
f46f00e
renamed geom to geom_label_aligned
suhaani-agarwal Jun 22, 2025
1c80c6c
updated test
suhaani-agarwal Jun 22, 2025
10278f4
corrected horizontal width calculation of labeled boxes
suhaani-agarwal Jun 22, 2025
16c6d39
added support for hjust alignment
suhaani-agarwal Jun 22, 2025
c6e1b42
added comments on when and how QP is used
suhaani-agarwal Jun 23, 2025
c6616a3
updated example
suhaani-agarwal Jun 23, 2025
ac3c2d3
added grouping in optimisation of positions (running qp several times)
suhaani-agarwal Jun 26, 2025
730ae0a
added stricter boundary constraints and shrinking mechanism for limit…
suhaani-agarwal Jun 27, 2025
e979ae7
corrected fontsize shrinking function
suhaani-agarwal Jun 27, 2025
f6b9463
made calcLabelBox function to avoid redundancy
suhaani-agarwal Jun 27, 2025
cc9282f
shifted position of calcLabelBox function
suhaani-agarwal Jun 28, 2025
9c8a8d8
improved grouping logic
suhaani-agarwal Jun 28, 2025
61e7c8c
added vjust support for horizontal alignment
suhaani-agarwal Jun 28, 2025
738842a
added test cases for shrinking mechanism and boundary constraints
suhaani-agarwal Jun 30, 2025
5b2ffd5
parallelPeaks data and code
tdhock Jul 4, 2025
ec263bb
added option to hide background_rect
suhaani-agarwal Jul 4, 2025
e012df3
Merge branch 'quadprog' of https://github.com/animint/animint2 into q…
suhaani-agarwal Jul 4, 2025
ca72e37
Merge remote-tracking branch 'origin/master' into quadprog
suhaani-agarwal Jul 11, 2025
7b81433
updated documentation and added dplyr to Suggests
suhaani-agarwal Jul 12, 2025
8cb6a3f
removed syntax error in DESCRIPTION
suhaani-agarwal Jul 12, 2025
38e4977
smooth transitions for geom_label_aligned and updated docs
suhaani-agarwal Jul 12, 2025
e7b4d0c
updated documentation
suhaani-agarwal Jul 12, 2025
b619254
updated grouping mechanism to exact position grouping, added test cas…
suhaani-agarwal Jul 14, 2025
7d87eb4
corrected smooth transitions test
suhaani-agarwal Jul 15, 2025
ef4262c
updated build.sh for correcting CRAN check output
suhaani-agarwal Jul 15, 2025
3f0bcd5
updated docs
suhaani-agarwal Jul 15, 2025
0183340
fixed mesureText function to fix label 0 rect calculation
suhaani-agarwal Jul 15, 2025
e606c34
added test for checking label 0 rect exists
suhaani-agarwal Jul 15, 2025
49fe01a
fix example and white space
tdhock Jul 15, 2025
833ac81
added tests for label.r, min.distance
suhaani-agarwal Jul 16, 2025
aa56753
changed argument seperators to _ and removed label.size
suhaani-agarwal Jul 16, 2025
f6c6d8d
replaced dplyr with data.table, made label_r accept numeric value
suhaani-agarwal Jul 16, 2025
83b4b8d
label size test fails
Jul 16, 2025
feb8d59
added failing test for min_distance default, added getTextValue usage
suhaani-agarwal Jul 16, 2025
636cf39
removed failing test, removed empty spaces
suhaani-agarwal Jul 17, 2025
79f41ac
fix: respect user-specified font size in geom_label_aligned
suhaani-agarwal Jul 18, 2025
5cd84c6
removed +d.size/3 in vertical text alignment, added dominant-baseline…
suhaani-agarwal Jul 18, 2025
548628f
Merge branch 'quadprog' of https://github.com/animint/animint2 into q…
Jul 18, 2025
551f258
update doc
Jul 18, 2025
ce4252f
wb.viz
Jul 18, 2025
0b9641e
updated tests (label_r , missing data param, size test, group)
suhaani-agarwal Jul 19, 2025
2c61ab2
updated tests to use getPropertyValue()
suhaani-agarwal Jul 19, 2025
fd860da
updated min_distance units in doc, added test for vjust
suhaani-agarwal Jul 20, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,5 @@
*ANIMINT_TEST_FOO
*pids.txt
*~
.vscode/settings.jsonnode_modules/
node_modules/
.vscode/settings.json
/node_modules
1 change: 1 addition & 0 deletions DESCRIPTION
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@ Collate:
'geom-histogram.r'
'geom-hline.r'
'geom-jitter.r'
'geom-label-aligned.R'
'geom-label.R'
'geom-linerange.r'
'geom-point.r'
Expand Down
2 changes: 2 additions & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ export(GeomFreqpoly)
export(GeomHex)
export(GeomHline)
export(GeomLabel)
export(GeomLabelAligned)
export(GeomLine)
export(GeomLinerange)
export(GeomLogticks)
Expand Down Expand Up @@ -290,6 +291,7 @@ export(geom_histogram)
export(geom_hline)
export(geom_jitter)
export(geom_label)
export(geom_label_aligned)
export(geom_line)
export(geom_linerange)
export(geom_map)
Expand Down
155 changes: 155 additions & 0 deletions R/geom-label-aligned.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
#' Non-overlapping label boxes
#'
#' This geom creates boxes with labels that are aligned either vertically or horizontally,
#' using quadratic programming to optimize their positions and avoid overlaps. The QP solver
#' is applied after all showSelected filtering occurs, and operates as follows:
#'
#' For vertical alignment (default):
#' - QP optimizes Y positions while keeping X positions fixed
#' - Constraints ensure boxes don't overlap vertically
#' - Boxes are aligned along the vertical axis at their original X positions
#'
#' For horizontal alignment:
#' - QP optimizes X positions while keeping Y positions fixed
#' - Constraints ensure boxes don't overlap horizontally
#' - Boxes are aligned along the horizontal axis at their original Y positions
#'
#' The QP solver minimizes the total squared distance from original positions while
#' enforcing minimum spacing constraints between boxes.
#'
#' @inheritParams layer
#' @inheritParams geom_point
#' @param label_r Radius of rounded corners. Defaults to 0.15 lines.
#' @param alignment One of "vertical" (QP on Y axis) or "horizontal" (QP on X axis)
#' @param min_distance Minimum distance between boxes in pixels.
#' @param background_rect Disables text background rect if set to FALSE.
#' @export
#' @examples
#' library(nlme)
#' data(BodyWeight, package = "nlme")
#' # Extracting the last point of each rat's trajectory
#' library(data.table)
#' label_data <- data.table(BodyWeight)[Time == max(Time)]
#' library(animint2)
#' viz <- animint(
#' bodyPlot = ggplot() +
#' theme_bw() +
#' theme_animint(width=800)+
#' geom_line(aes(
#' x = Time, y = weight, group = Rat),
#' clickSelects="Rat",
#' size=3,
#' data = BodyWeight) +
#' geom_line(aes(
#' x = Time, y = weight, group = Rat, colour = Rat),
#' clickSelects="Rat",
#' data = BodyWeight) +
#' geom_label_aligned(aes(
#' x = Time, y = weight, label = Rat, fill = Rat),
#' clickSelects="Rat",
#' hjust = 0,
#' data = label_data) +
#' facet_grid(~Diet) +
#' ggtitle("Rat body weight over time by diet") +
#' xlab("Time (days)") +
#' ylab("Body Weight (grams)")
#' )
#' viz
geom_label_aligned <- function
(mapping = NULL, data = NULL,
stat = "identity", position = "identity",
...,
label_r = 0.15,
alignment = "vertical",
min_distance = 0.1,
background_rect = TRUE,
na.rm = FALSE,
show.legend = NA,
inherit.aes = TRUE) {
layer(
data = data,
mapping = mapping,
stat = stat,
geom = GeomLabelAligned,
position = position,
show.legend = show.legend,
inherit.aes = inherit.aes,
params = list(
label_r = label_r,
alignment = alignment,
min_distance = min_distance,
background_rect = background_rect,
na.rm = na.rm,
...
)
)
}

#' @rdname animint2-gganimintproto
#' @format NULL
#' @usage NULL
#' @export
GeomLabelAligned <- gganimintproto(
"GeomLabelAligned",
Geom,
required_aes = c("x", "y", "label"),
default_aes = aes(
colour = "black", fill = "white", size = 12,
angle = 0, hjust = 0.5, vjust = 0.5, alpha = 1,
family = "", fontface = 1, lineheight = 1.2
),
draw_panel = function
(self, data, panel_scales, coord,
label_r = 0.15,
alignment = "vertical",
min_distance = 0.1,
background_rect = TRUE,
na.rm = FALSE) {
if (empty(data)) return(zeroGrob())
coords <- coord$transform(data, panel_scales)
coords$label_r <- label_r
coords$alignment <- alignment
coords$min_distance <- min_distance
coords$background_rect <- background_rect
rect_grobs <- lapply(1:nrow(coords), function(i) {
grid::roundrectGrob(
x = unit(coords$x[i], "native"),
y = unit(coords$y[i], "native"),
width = unit(0.1, "npc"),
height = unit(0.1, "npc"),
just = "center",
r = unit(coords$label_r[i], "native"),
gp = grid::gpar(
col = coords$colour[i],
fill = scales::alpha(coords$fill[i], coords$alpha[i])
)
)
})
text_grobs <- lapply(1:nrow(coords), function(i) {
grid::textGrob(
coords$label[i],
x = unit(coords$x[i], "native"),
y = unit(coords$y[i], "native"),
just = "center",
gp = grid::gpar(
col = coords$colour[i],
fontsize = coords$size[i],
fontfamily = coords$family[i],
fontface = coords$fontface[i],
lineheight = coords$lineheight[i]
)
)
})
grobs <- mapply(
function(r, t) grid::gTree(children = grid::gList(r, t)),
rect_grobs, text_grobs)
class(grobs) <- "gList"
ggname("geom_label_aligned", grid::grobTree(children = grobs))
},
pre_process = function(g, g.data, ...) {
# This ensures our geom is identified as "label_aligned" in JS
g$geom <- "label_aligned"
return(list(g = g, g.data = g.data))
},
draw_key = draw_key_label
)
24 changes: 15 additions & 9 deletions build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -32,17 +32,23 @@ PKG_TGZ=$(R CMD build animint2-release|grep building|sed "s/.*\(animint2.*.tar.g
echo built $PKG_TGZ so now we INSTALL
R CMD INSTALL $PKG_TGZ
echo "Running R CMD check --as-cran $PKG_TGZ"
check_output=$(R CMD check --as-cran $PKG_TGZ 2>&1)
check_status=$?
echo "$check_output"
# Check for WARNINGs or NOTEs in the output
if echo "$check_output" | grep -q -E "WARNING|NOTE"; then
echo "CRAN check generated WARNINGs or NOTEs:"
# temporary log file
LOG_FILE=$(mktemp)
trap 'rm -f "$LOG_FILE"' EXIT
# Run check and capture output
R CMD check --as-cran $PKG_TGZ 2>&1 | tee "$LOG_FILE"
CHECK_STATUS=${PIPESTATUS[0]}
# Check for WARNINGs or NOTEs
if grep -q -E "WARNING|NOTE" "$LOG_FILE"; then
echo "::error:: CRAN check generated WARNINGs or NOTEs:"
grep -E "WARNING|NOTE" "$LOG_FILE"
exit 1
fi
# Exit with original status if no WARNINGs/NOTEs but check failed
if [ $check_status -ne 0 ]; then
echo "R CMD check failed with status $check_status"
exit $check_status
if [ $CHECK_STATUS -ne 0 ]; then
echo "::error:: R CMD check failed with status $CHECK_STATUS"
echo "Full output:"
cat "$LOG_FILE"
exit $CHECK_STATUS
fi
echo "CRAN check completed successfully"
Binary file added data/parallelPeaks.RData
Binary file not shown.
5 changes: 3 additions & 2 deletions inst/examples/WorldBank-facets-map.R
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ wb.facets <- animint(
size=4,
alpha=1,
alpha_off=0.1)+
geom_text(aes(
geom_label_aligned(aes(
year, life.expectancy, colour=region, label=country),
showSelected="country",
clickSelects="country",
Expand Down Expand Up @@ -118,11 +118,12 @@ wb.facets <- animint(
alpha_off=0.3,
chunk_vars=character(),
data=SCATTER(not.na))+
geom_text(aes(
geom_label_aligned(aes(
fertility.rate, life.expectancy, label=country,
key=country), #also use key here!
showSelected=c("country", "year", "region"),
clickSelects="country",
alpha=0.7,
help="Names of selected countries",
chunk_vars=character(),
data=SCATTER(not.na))+
Expand Down
137 changes: 137 additions & 0 deletions inst/examples/geom_label_aligned_examples.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
library(animint2)
set.seed(42)
# Create synthetic labels
label_names <- paste("Label", 1:5)
n_timepoints <- 10

line_data <- do.call(rbind, lapply(label_names, function(label) {
data.frame(
Time = 1:n_timepoints,
Value = cumsum(rnorm(n_timepoints, mean = 0.5, sd = 2)) + runif(1, 20, 30),
Label = label
)
}))

# Manually override the final y-values of some labels to create overlaps
line_data$Value[line_data$Label == "Label 1" & line_data$Time == n_timepoints] <- 40
line_data$Value[line_data$Label == "Label 2" & line_data$Time == n_timepoints] <- 40
line_data$Value[line_data$Label == "Label 3" & line_data$Time == n_timepoints] <- 40

# Create label data for the aligned labels at final time point
label_data <- line_data[line_data$Time == n_timepoints, ]
label_data$label <- label_data$Label

p <- ggplot() +
geom_line(
data = line_data,
aes(x = Time, y = Value, color = Label, group = Label),
size = 1.2
) +
geom_label_aligned(
data = label_data,
aes(x = Time, y = Value, label = label, fill = Label),
alignment = "vertical"
) +
ggtitle("Synthetic Trends with Smart Aligned Labels") +
xlab("Time") +
ylab("Value")

viz <- list(syntheticTrend = p)
animint2dir(viz, "smart_aligned_labels")

# Plot 2 : Collisions with axes and other boxes at the same time
library(nlme)
library(dplyr)
data(BodyWeight, package = "nlme")
# Extracting the last point of each rat's trajectory
label_data <- BodyWeight %>%
group_by(Rat) %>%
filter(Time == max(Time)) %>%
ungroup() %>%
mutate(label = as.character(Rat))

viz2 <- list(
bodyPlot = ggplot() +
geom_line(aes(x = Time, y = weight, group = Rat, colour = Rat),
data = BodyWeight) +
geom_label_aligned(aes(x = Time, y = weight, label = label, fill = Rat),
data = label_data) +
facet_wrap(~Diet, nrow = 1) +
ggtitle("Rat body weight over time by diet") +
xlab("Time (days)") +
ylab("Body Weight (grams)")
)

# Render to directory
animint2dir(viz2, "bodyweight-label-aligned")

# Example 3: World Bank Data with Interactive Aligned Labels
library(data.table)
data(WorldBank, package = "animint2")

WorldBank <- as.data.table(WorldBank)
# subset of countries
tracked_countries <- c(
"United States", "Vietnam", "India", "China", "Brazil",
"Nigeria", "Mali", "South Africa", "Canada")

# Filter WorldBank data
wb <- WorldBank[
country %in% tracked_countries &
!is.na(life.expectancy) & !is.na(fertility.rate),
.(country, year = as.integer(year), life.expectancy, fertility.rate)]
# Label data for the time series
label_data_line <- wb[, .SD[year == max(year)], by = country]
# Text data for year display
year_text_data <- data.table(year = unique(wb$year))
wb.viz <- list(
lifeExpectancyPlot = ggplot() +
geom_line(
data = wb,
aes(x = year, y = life.expectancy, group = country, color = country, key=country),
size = 1.2,
clickSelects = "country",
showSelected = "country"
) +
geom_label_aligned(
data = label_data_line,
aes(
x = year, y = life.expectancy, label = country,
fill = country, key = country),
alignment = "vertical",
hjust = 1,
min_distance = 3,
size=10,
color = "white",
showSelected = "country",
clickSelects = "country"
) +
ggtitle("Life Expectancy Over Time") +
xlab("Year") +
ylab("Life Expectancy (years)"),
worldbankAnim = ggplot() +
geom_point(
data = wb,
aes(x = fertility.rate, y = life.expectancy, color = country, key = country),
size = 8,
showSelected = "year",
clickSelects = "country"
) +
geom_label_aligned(
data = wb,
aes(x = fertility.rate, y = life.expectancy, label = country, fill = country, key = country),
size=5,
alignment = "vertical", color = "#ffffd1", label_r = 9,
showSelected = "year",
clickSelects = "country"
) +
make_text(year_text_data, x = 4, y = 82, label = "year") +
ggtitle("Life Expectancy vs Fertility Rate") +
xlab("Fertility Rate") +
ylab("Life Expectancy"),
time = list(variable = "year", ms = 3000),
duration = list(year = 2000, country=2000),
first = list(year = min(wb$year)),
selector.types = list(country = "multiple")
)
animint2dir(wb.viz, "worldbank-label-aligned")
Loading