diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml
index f5ca6bb34..832f201bf 100644
--- a/.github/workflows/tests.yaml
+++ b/.github/workflows/tests.yaml
@@ -20,6 +20,7 @@ jobs:
GITHUB_PAT: ${{ secrets.PAT_GITHUB }}
GH_ACTION: "ENABLED"
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
+ ANIMINT2_TEST_GHPAGES: ${{ github.event_name == 'push' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/main') }}
steps:
- uses: actions/checkout@v3
- name: install and update texlive
@@ -48,6 +49,10 @@ jobs:
run: |
npm install v8-to-istanbul
echo "Node modules installed"
+
+ - name: Install xvfb (JS_coverage)
+ if: matrix.test-suite == 'JS_coverage'
+ run: /usr/bin/sudo DEBIAN_FRONTEND=noninteractive apt-get install -y xvfb
- name: run tests
run: |
@@ -55,7 +60,7 @@ jobs:
bash build.sh
elif [ "$TEST_SUITE" == "JS_coverage" ]; then
echo "Running testthat with JS coverage collection..."
- Rscript -e "source('tests/testthat.R', chdir = TRUE)"
+ xvfb-run -a Rscript -e "source('tests/testthat.R', chdir = TRUE)"
else
echo "Running testthat with R coverage collection..."
Rscript -e 'covr::codecov(quiet = FALSE, type = "tests", test_files = "tests/testthat.R", flags = "r")'
diff --git a/DESCRIPTION b/DESCRIPTION
index 43ca2e728..fe548504c 100644
--- a/DESCRIPTION
+++ b/DESCRIPTION
@@ -281,6 +281,7 @@ Collate:
'uu_zzz.r'
'z_animint.R'
'z_animintHelpers.R'
+ 'z_multiline.R'
'z_facets.R'
'z_geoms.R'
'z_helperFunctions.R'
diff --git a/NEWS.md b/NEWS.md
index 6184d117b..4a7e1a57f 100644
--- a/NEWS.md
+++ b/NEWS.md
@@ -14,6 +14,11 @@
- `update_axes`: Fixed issue #273 where axis tick text font-size was inconsistent between plots with and without `update_axes`. Previously, plots using `theme_animint(update_axes="x")` would lose `theme(axis.text = element_text(size=...))` styling after axis updates.
+# Changes in version 2025.11.7 (PR#261)
+
+- Fixed multiline text spacing: plot titles no longer overlap with plot area, and X/Y axis label spacing is now consistent.
+- Fixed axis titles to scale correctly with `theme(text=element_text(size=X))` (issue #64).
+
# Changes in version 2025.10.31 (PR#271)
- `geom_point()` now warns when shape parameter is set to a value other than 21, since animint2 web rendering only supports shape=21 for proper display of both color and fill aesthetics.
@@ -30,6 +35,10 @@
- `geom_text(vjust!=0)` warning mentions vjust support in `geom_label_aligned()`.
+# Changes in version 2025.10.21 (PR#221)
+
+- Multi-line text support: `\n` now works in plot titles, axis titles, legend titles, and `geom_text()` labels. Created `R/z_multiline.R` with helper to convert `\n` to `
` during R compilation. JavaScript renderer converts `
` to SVG `` elements for proper multi-line display.
+
# Changes in version 2025.10.17 (PR#255)
- `getCommonChunk()` uses default group=1 (previously 1:N which was slower).
@@ -259,7 +268,7 @@
# Changes in 2022.5.25
-- Add ability to rotate geom_text labels, following ggplot2's semantics of rotation direction.
+- Add ability to rotate geom_text labels, following ggplot2\'s semantics of rotation direction.
# Changes in 2022.5.24
@@ -324,4 +333,4 @@
# Changes in 2017.08.24
-- DSL: clickSelects/showSelected are now specified as parameters rather than aesthetics.
+- DSL: clickSelects/showSelected are now specified as parameters rather than aesthetics.
\ No newline at end of file
diff --git a/R/z_animint.R b/R/z_animint.R
index c50bfee02..8d6b51971 100644
--- a/R/z_animint.R
+++ b/R/z_animint.R
@@ -130,7 +130,7 @@ parsePlot <- function(meta, plot, plot.name){
for (xy in c("x", "y")) {
s <- function(tmp) sprintf(tmp, xy)
# one axis name per plot (ie, a xtitle/ytitle is shared across panels)
- plot.info[[s("%stitle")]] <- if(is.blank(s("axis.title.%s"))){
+ axis_title_raw <- if(is.blank(s("axis.title.%s"))){
""
} else {
scale.i <- which(plot$scales$find(xy))
@@ -143,6 +143,10 @@ parsePlot <- function(meta, plot, plot.name){
lab.or.null
}
}
+ # Convert newlines to
for multi-line axis titles (Issue #221)
+ plot.info[[s("%stitle")]] <- convertNewlinesToBreaks(axis_title_raw)
+ ## axis title size.
+ plot.info[[s("%stitle_size")]] <- getTextSize(s("axis.title.%s"), theme.pars)
## panel text size.
plot.info[[s("strip_text_%ssize")]] <- getTextSize(
s("strip.text.%s"), theme.pars)
@@ -187,12 +191,13 @@ parsePlot <- function(meta, plot, plot.name){
# grab the unique axis labels (makes rendering simpler)
plot.info <- getUniqueAxisLabels(plot.info)
- # grab plot title if present
- plot.info$title <- if(is(theme.pars$plot.title, "blank")){
+ # grab plot title if present and convert newlines (Issue #221)
+ plot_title_raw <- if(is(theme.pars$plot.title, "blank")){
""
}else{
plot$labels$title
}
+ plot.info$title <- convertNewlinesToBreaks(plot_title_raw)
plot.info$title_size <- getTextSize("plot.title", theme.pars)
## Set plot width and height from animint.* options if they are
@@ -747,7 +752,6 @@ getLegendList <- function(plistextra){
gdefs <- guides_merge(gdefs)
gdefs <- guides_geom(gdefs, layers, default_mapping)
} else (zeroGrob())
- names(gdefs) <- sapply(gdefs, function(i) i$title)
## adding the variable used to each LegendList
for(leg in seq_along(gdefs)) {
@@ -827,6 +831,9 @@ getLegendList <- function(plistextra){
}
}
legend.list <- lapply(gdefs, getLegend)
+ # Use the 'class' field from getLegend output for legend key names (Issue #221)
+ # This ensures JSON keys don't contain newlines or other special characters
+ names(legend.list) <- sapply(legend.list, function(i) i$class)
## Add a flag to specify whether or not there is both a color and a
## fill legend to display. If so, we need to draw the interior of
## the points in the color legend as the same color.
diff --git a/R/z_animintHelpers.R b/R/z_animintHelpers.R
index 97cce5a7f..f3b4b2cf1 100644
--- a/R/z_animintHelpers.R
+++ b/R/z_animintHelpers.R
@@ -702,6 +702,8 @@ getLegend <- function(mb){
names(data) <- paste0(geom, names(data))# aesthetics by geom
names(data) <- gsub(paste0(geom, "."), "", names(data), fixed=TRUE) # label isn't geom-specific
data$label <- paste(data$label) # otherwise it is AsIs.
+ # Convert newlines to
for multi-line legend labels (Issue #221)
+ data$label <- convertNewlinesToBreaks(data$label)
data
}
dataframes <- mapply(function(i, j) cleanData(i$data, mb$key, j, i$params),
@@ -726,10 +728,16 @@ getLegend <- function(mb){
if(guidetype=="none"){
NULL
}else{
+ # Convert newlines to
for multi-line legend title (Issue #221)
+ legend_title <- convertNewlinesToBreaks(mb$title)
+ # For the 'class' field (used as JSON key), sanitize newlines to spaces
+ # to avoid JSON parsing issues with control characters (Issue #221)
+ safe_title <- gsub("\n", " ", mb$title, fixed = TRUE)
+ legend_class <- if(mb$is.discrete) mb$selector else safe_title
list(guide = guidetype,
geoms = unlist(mb$geom.legend.list),
- title = mb$title,
- class = if(mb$is.discrete)mb$selector else mb$title,
+ title = legend_title,
+ class = legend_class,
selector = mb$selector,
is_discrete= mb$is.discrete,
legend_type = mb$legend_type,
@@ -913,6 +921,10 @@ split_recursive <- function(x, vars){
##' @author Toby Dylan Hocking
saveChunks <- function(x, meta){
if(is.data.frame(x)){
+ # Convert newlines to
in label column for multi-line text (Issue #221)
+ if("label" %in% names(x)){
+ x$label <- convertNewlinesToBreaks(x$label)
+ }
this.i <- meta$chunk.i
csv.name <- sprintf("%s_chunk%d.tsv", meta$g$classed, this.i)
## Some geoms should be split into separate groups if there are NAs.
diff --git a/R/z_multiline.R b/R/z_multiline.R
new file mode 100644
index 000000000..544f4503c
--- /dev/null
+++ b/R/z_multiline.R
@@ -0,0 +1,4 @@
+convertNewlinesToBreaks <- function(text) {
+ gsub("\n", "
", text, fixed = TRUE)
+}
+
diff --git a/inst/htmljs/animint.js b/inst/htmljs/animint.js
index bfcc4b07f..4c80394ea 100644
--- a/inst/htmljs/animint.js
+++ b/inst/htmljs/animint.js
@@ -125,24 +125,61 @@ var animint = function (to_select, json_file) {
var measureText = function(pText, pFontSize, pAngle, pStyle) {
if (pText === undefined || pText === null || pText.length === 0) return {height: 0, width: 0};
if (pAngle === null || isNaN(pAngle)) pAngle = 0;
-
+
+ // Create temporary container to measure text
var container = element.append('svg');
- // do we need to set the class so that styling is applied?
- //.attr('class', classname);
-
- container.append('text')
- .attr({x: -1000, y: -1000})
+ var textElement = container.append('text')
.attr("transform", "rotate(" + pAngle + ")")
.attr("style", pStyle)
- .attr("font-size", pFontSize)
- .text(pText);
-
+ .attr("font-size", pFontSize);
+
+ // Check if text contains
tags (multi-line)
+ var textStr = String(pText || '');
+ var lines = textStr.split('
');
+
+ // Always use setMultilineText for consistent rendering
+ setMultilineText(textElement, pText);
+
+ // Get bounding box after rendering
var bbox = container.node().getBBox();
+
+ // Clean up temporary element
container.remove();
-
+
+ // Return measured dimensions
return {height: bbox.height, width: bbox.width};
};
+// Set multi-line text on SVG text elements.
+// Converts
tags to elements for proper SVG rendering.
+var setMultilineText = function(textElement, text) {
+ textElement.each(function(d) {
+ var textStr = typeof text === 'function' ? text(d) : text;
+ // Check for null/undefined, but allow falsy values like 0 or ""
+ if (textStr === null || textStr === undefined) return;
+ var lines = String(textStr).split('
');
+ var el = d3.select(this);
+ el.text('');
+ // Line height: 1.2em is standard SVG spacing between text lines
+ var lineHeight = 1.2;
+ var y = el.attr('y') || 0;
+ var x = el.attr('x') || 0;
+ // Get dominant-baseline from parent, if any
+ var dominantBaseline = el.attr('dominant-baseline');
+ lines.forEach(function(line, i) {
+ var tspan = el.append('tspan')
+ .attr('x', x)
+ .attr('dy', i === 0 ? 0 : lineHeight + 'em')
+ .text(line);
+ // Inherit dominant-baseline from parent text element if set
+ if (dominantBaseline) {
+ tspan.attr('dominant-baseline', dominantBaseline);
+ }
+ });
+ });
+};
+
+
var nest_by_group = d3.nest().key(function(d){ return d.group; });
var dirs = json_file.split("/");
dirs.pop(); //if a directory path exists, remove the JSON file from dirs
@@ -292,9 +329,10 @@ var animint = function (to_select, json_file) {
var npanels = Math.max.apply(null, panel_names);
// Note axis names are "shared" across panels (just like the title)
- var xtitlepadding = 5 + measureText(p_info["xtitle"], default_axis_px).height;
- var ytitlepadding = 5 + measureText(p_info["ytitle"], default_axis_px).height;
-
+ var xtitle_size = p_info["xtitle_size"] || (default_axis_px + "pt");
+ var ytitle_size = p_info["ytitle_size"] || (default_axis_px + "pt");
+ var xtitlepadding = 5 + measureText(p_info["xtitle"], xtitle_size).height;
+ var ytitlepadding = 5 + measureText(p_info["ytitle"], ytitle_size).height;
// 'margins' are fixed across panels and do not
// include title/axis/label padding (since these are not
// fixed across panels). They do, however, account for
@@ -336,23 +374,34 @@ var animint = function (to_select, json_file) {
var titlepadding = measureText(p_info.title, p_info.title_size).height;
// why are we giving the title padding if it is undefined?
if (p_info.title === undefined) titlepadding = 0;
+
+ // Add extra margin below title for multiline text to prevent overlap
+ // with plot area. The measureText already accounts for multiline height,
+ // but we need additional bottom margin.
+ var titleBottomMargin = 5; // pixels of space below title
+
plotdim.title.x = p_info.options.width / 2;
- plotdim.title.y = titlepadding;
- svg.append("text")
- .text(p_info.title)
+ // Position title at top margin, let it extend downward
+ plotdim.title.y = margin.top;
+ var titleText = svg.append("text")
.attr("class", "plottitle")
.attr("font-family", "sans-serif")
.attr("font-size", p_info.title_size)
.attr("transform", "translate(" + plotdim.title.x + "," +
plotdim.title.y + ")")
- .style("text-anchor", "middle");
+ .style("text-anchor", "middle")
+ .attr("dominant-baseline", "hanging");
+ // Use multi-line text helper for plot titles (Issue #221)
+ setMultilineText(titleText, p_info.title);
// grab max text size over axis labels and facet strip labels
+ // Base spacing between tick labels and axis titles.
+ // Y-axis uses 5px base (tick labels extend horizontally).
+ // X-axis uses 30px base to account for rotated tick labels.
var axispaddingy = 5;
if(p_info.hasOwnProperty("ylabs") && p_info.ylabs.length){
axispaddingy += Math.max.apply(null, p_info.ylabs.map(function(entry){
- // + 5 to give a little extra space to avoid bad axis labels
- // in shiny.
+ // + 5 to give a little extra space to avoid bad axis labels in shiny.
return measureText(entry, p_info.ysize).width + 5;
}));
}
@@ -360,7 +409,7 @@ var animint = function (to_select, json_file) {
if(p_info.hasOwnProperty("xlabs") && p_info.xlabs.length){
// TODO: throw warning if text height is large portion of plot height?
axispaddingx += Math.max.apply(null, p_info.xlabs.map(function(entry){
- return measureText(entry, p_info.xsize, p_info.xangle).height;
+ return measureText(entry, p_info.xsize, p_info.xangle).height;
}));
// TODO: carefully calculating this gets complicated with rotating xlabs
//margin.right += 5;
@@ -421,7 +470,7 @@ var animint = function (to_select, json_file) {
var graph_height = p_info.options.height -
nrows * (margin.top + margin.bottom) -
strip_height -
- titlepadding - n_xaxes * axispaddingx - xtitlepadding;
+ titlepadding - titleBottomMargin - n_xaxes * axispaddingx - xtitlepadding;
// Impose the pixelated aspect ratio of the graph upon the width/height
// proportions calculated by the compiler. This has to be done on the
@@ -559,7 +608,7 @@ var animint = function (to_select, json_file) {
var strip_h = cum_height_per_row[current_row-1];
plotdim.ystart = current_row * plotdim.margin.top +
(current_row - 1) * plotdim.margin.bottom +
- graph_height_cum + titlepadding + strip_h;
+ graph_height_cum + titlepadding + titleBottomMargin + strip_h;
// room for xaxis title should be distributed evenly across
// panels to preserve aspect ratio
plotdim.yend = plotdim.ystart + plotdim.graph.height;
@@ -761,30 +810,30 @@ var animint = function (to_select, json_file) {
} //end of for(layout_i
// After drawing all backgrounds, we can draw the axis labels.
if(p_info["ytitle"]){
- svg.append("text")
- .text(p_info["ytitle"])
+ var ytitleText = svg.append("text")
.attr("class", "ytitle")
.style("text-anchor", "middle")
- .style("font-size", default_axis_px + "px")
+ .style("font-size", ytitle_size)
.attr("transform", "translate(" +
ytitle_x +
"," +
(ytitle_top + ytitle_bottom)/2 +
- ")rotate(270)")
- ;
+ ")rotate(270)");
+ // Use multi-line text helper for y-axis title (Issue #221)
+ setMultilineText(ytitleText, p_info["ytitle"]);
}
if(p_info["xtitle"]){
- svg.append("text")
- .text(p_info["xtitle"])
+ var xtitleText = svg.append("text")
.attr("class", "xtitle")
.style("text-anchor", "middle")
- .style("font-size", default_axis_px + "px")
+ .style("font-size", xtitle_size)
.attr("transform", "translate(" +
(xtitle_left + xtitle_right)/2 +
"," +
xtitle_y +
- ")")
- ;
+ ")");
+ // Use multi-line text helper for x-axis title (Issue #221)
+ setMultilineText(xtitleText, p_info["xtitle"]);
}
Plots[p_name].scales = scales;
}; //end of add_plot()
@@ -1493,10 +1542,11 @@ var animint = function (to_select, json_file) {
.attr("y", toXY("y", "y"))
.attr("font-size", get_size)
.style("text-anchor", get_text_anchor)
- .attr("transform", get_rotate)
- .text(function (d) {
- return d.label;
- })
+ .attr("transform", get_rotate);
+ // Use multi-line text helper for geom_text labels (Issue #221)
+ setMultilineText(e, function (d) {
+ return d.label;
+ })
;
};
eAppend = "text";
@@ -1742,7 +1792,7 @@ var animint = function (to_select, json_file) {
"stroke": get_colour_off,
"fill": get_fill_off
};
- // TODO cleanup.
+ // TODO cleanup.
var select_style_default = ["opacity","stroke","fill"];
g_info.select_style = select_style_default.filter(
X => g_info.style_list.includes(X));
@@ -2227,10 +2277,15 @@ var animint = function (to_select, json_file) {
var first_th = first_tr.append("th")
.attr("align", "left")
.attr("colspan", 2)
- .text(l_info.title)
.attr("class", legend_class)
- .style("font-size", l_info.title_size)
- ;
+ .style("font-size", l_info.title_size);
+ // Use multi-line text helper for legend title (Issue #221)
+ if (l_info.title && l_info.title.indexOf('
') > -1) {
+ // Multi-line title: replace
with actual line breaks in HTML
+ first_th.html(l_info.title.replace(/
/g, '
'));
+ } else {
+ first_th.text(l_info.title);
+ }
var legend_svgs = legend_rows.append("td")
.append("svg")
.attr("id", function(d){return d["id"]+"_svg";})
diff --git a/tests/testthat/test-compiler-ghpages.R b/tests/testthat/test-compiler-ghpages.R
index 202643c24..4dd1e32f7 100644
--- a/tests/testthat/test-compiler-ghpages.R
+++ b/tests/testthat/test-compiler-ghpages.R
@@ -29,12 +29,23 @@ expect_Capture <- function(L){
expect_no_Capture <- function(L){
expect_false(file.exists(file.path(L$local_clone,"Capture.PNG")))
}
+skip_if_no_org_pat <- function(){
+ if(!identical(Sys.getenv("ANIMINT2_TEST_GHPAGES"), "true")){
+ skip("Skipping GitHub Pages integration tests (set ANIMINT2_TEST_GHPAGES=true to enable).")
+ }
+ token <- Sys.getenv("GITHUB_PAT")
+ token2 <- Sys.getenv("GITHUB_PAT_GITHUB_COM")
+ if(identical(token, "") && identical(token2, "")){
+ skip("Skipping GitHub Pages integration tests (no GitHub token available).")
+ }
+}
## The test below requires a github token with repo delete
## permission. Read
## https://github.com/animint/animint2/wiki/Testing#installation to
## see how to set that up on your local computer, or on github
## actions.
reset_test_repo <- function(){
+ skip_if_no_org_pat()
## https://docs.github.com/en/rest/repos/repos?apiVersion=2022-11-28#delete-a-repository says The fine-grained token must have the following permission set: "Administration" repository permissions (write) gh api --method DELETE -H "Accept: application/vnd.github+json" -H "X-GitHub-Api-Version: 2022-11-28" /repos/OWNER/REPO
tryCatch({
gh::gh("DELETE /repos/animint-test/animint2pages_test_repo")
@@ -124,6 +135,9 @@ test_that("animint2pages raises an error if no GitHub token is present", {
## be called to set the env vars/token.
repo.root <- system("git rev-parse --show-toplevel", intern=TRUE)
config.file <- file.path(repo.root, ".git", "config")
+ if(file.access(config.file, 2) != 0){
+ skip(sprintf("Skip: can not write %s", config.file))
+ }
config.old <- file.path(repo.root, ".git", "config.old")
file.copy(config.file, config.old, overwrite = TRUE)
cat("[credential]\n\tusername = FOO", file=config.file, append=TRUE)
diff --git a/tests/testthat/test-compiler-multiline-text.R b/tests/testthat/test-compiler-multiline-text.R
new file mode 100644
index 000000000..761ddab07
--- /dev/null
+++ b/tests/testthat/test-compiler-multiline-text.R
@@ -0,0 +1,92 @@
+context("Multi-line text rendering (Issue #221)")
+
+test_that("plot title supports multi-line text", {
+ data <- data.frame(x = 1:5, y = 1:5)
+ viz <- list(
+ plot1 = ggplot(data, aes(x, y)) +
+ geom_point() +
+ ggtitle("Title Line 1\nTitle Line 2")
+ )
+ info <- animint2dir(viz, "test-title-multiline", open.browser = FALSE)
+ json <- RJSONIO::fromJSON(file.path(info$out.dir, "plot.json"))
+ expect_true(grepl("
", json$plots$plot1$title, fixed = TRUE))
+ expect_equal(json$plots$plot1$title, "Title Line 1
Title Line 2")
+})
+
+test_that("x-axis title supports multi-line text", {
+ data <- data.frame(x = 1:5, y = 1:5)
+ viz <- list(
+ plot1 = ggplot(data, aes(x, y)) +
+ geom_point() +
+ xlab("X Axis\nLine 2")
+ )
+ info <- animint2dir(viz, "test-xaxis-multiline", open.browser = FALSE)
+ json <- RJSONIO::fromJSON(file.path(info$out.dir, "plot.json"))
+ expect_true(grepl("
", json$plots$plot1$xtitle, fixed = TRUE))
+ expect_equal(json$plots$plot1$xtitle, "X Axis
Line 2")
+})
+
+test_that("y-axis title supports multi-line text", {
+ data <- data.frame(x = 1:5, y = 1:5)
+ viz <- list(
+ plot1 = ggplot(data, aes(x, y)) +
+ geom_point() +
+ ylab("Y Axis\nLine 2")
+ )
+ info <- animint2dir(viz, "test-yaxis-multiline", open.browser = FALSE)
+ json <- RJSONIO::fromJSON(file.path(info$out.dir, "plot.json"))
+ expect_true(grepl("
", json$plots$plot1$ytitle, fixed = TRUE))
+ expect_equal(json$plots$plot1$ytitle, "Y Axis
Line 2")
+})
+
+test_that("geom_text labels support multi-line text", {
+ data <- data.frame(
+ x = 1:3, y = 1:3,
+ label = c("One", "Two\nLines", "Three\nLines\nHere")
+ )
+ viz <- list(
+ plot1 = ggplot(data, aes(x, y, label = label)) + geom_text()
+ )
+ info <- animint2dir(viz, "test-geomtext-multiline", open.browser = FALSE)
+ tsv_files <- list.files(info$out.dir, pattern = "text.*\\.tsv$", full.names = TRUE)
+ expect_true(length(tsv_files) > 0)
+ text_data <- read.table(tsv_files[1], header = TRUE, sep = "\t", quote = "\"")
+ multiline_labels <- text_data$label[grepl("
", text_data$label, fixed = TRUE)]
+ expect_true(length(multiline_labels) >= 2)
+ expect_true(any(grepl("Two
Lines", multiline_labels, fixed = TRUE)))
+ expect_true(any(grepl("Three
Lines
Here", multiline_labels, fixed = TRUE)))
+})
+
+test_that("legend title supports multi-line text", {
+ data <- data.frame(x = 1:6, y = 1:6, category = rep(c("A", "B", "C"), 2))
+ viz <- list(
+ plot1 = ggplot(data, aes(x, y, color = category)) +
+ geom_point() +
+ scale_color_discrete(name = "Category\nName")
+ )
+ info <- animint2dir(viz, "test-legend-multiline", open.browser = FALSE)
+ json <- RJSONIO::fromJSON(file.path(info$out.dir, "plot.json"))
+ expect_true("legend" %in% names(json$plots$plot1))
+ legend_keys <- names(json$plots$plot1$legend)
+ expect_true(length(legend_keys) > 0)
+ has_multiline_title <- FALSE
+ for (key in legend_keys) {
+ legend_title <- json$plots$plot1$legend[[key]]$title
+ if (!is.null(legend_title) && grepl("
", legend_title, fixed = TRUE)) {
+ has_multiline_title <- TRUE
+ expect_equal(legend_title, "Category
Name")
+ break
+ }
+ }
+ expect_true(has_multiline_title)
+})
+
+
+test_that("convertNewlinesToBreaks works correctly", {
+ expect_equal(animint2:::convertNewlinesToBreaks("Line1\nLine2"), "Line1
Line2")
+ expect_equal(animint2:::convertNewlinesToBreaks("A\nB\nC\nD"), "A
B
C
D")
+ expect_equal(animint2:::convertNewlinesToBreaks("No newlines here"), "No newlines here")
+ expect_equal(animint2:::convertNewlinesToBreaks(""), "")
+ result <- animint2:::convertNewlinesToBreaks(c("A\nB", "C", "D\nE\nF"))
+ expect_equal(result, c("A
B", "C", "D
E
F"))
+})
diff --git a/tests/testthat/test-renderer1-multiline-spacing.R b/tests/testthat/test-renderer1-multiline-spacing.R
new file mode 100644
index 000000000..4d5f09e72
--- /dev/null
+++ b/tests/testthat/test-renderer1-multiline-spacing.R
@@ -0,0 +1,21 @@
+acontext("multiline text spacing")
+data <- data.frame(x = 1:10, y = 1:10)
+viz <- list(
+ plot1 = ggplot(data, aes(x, y)) +
+ geom_point() +
+ ggtitle("Multiline Title\nLine Two\nLine Three") +
+ ylab("Y Axis\nLabel Two") +
+ theme(text = element_text(size = 20))
+)
+info <- animint2HTML(viz)
+test_that("multiline plot title with large font does not overlap plot area", {
+ title_bbox <- get_element_bbox("text.plottitle")
+ plot_rect <- get_element_bbox("#plot_plot1 rect.background_rect")
+ expect_lt(title_bbox$top + title_bbox$height, plot_rect$top)
+})
+test_that("multiline y-axis title with large font does not overlap plot area", {
+ ytitle_bbox <- get_element_bbox("text.ytitle")
+ plot_rect <- get_element_bbox("#plot_plot1 rect.background_rect")
+ expect_lt(ytitle_bbox$left + ytitle_bbox$width, plot_rect$left)
+})
+