diff --git a/NEWS.md b/NEWS.md index f1082b8f..299ccde7 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,3 +1,7 @@ +# Changes in version 2025.10.31 (PR#272) + +- Download status table now displays three new columns: `total_MB` (total disk space used by all downloaded chunks), `mean_MB` (average chunk size), and `rows` (total number of data rows). Chunk sizes are calculated in R using `file.size()` and exported via plot.json for efficient display updates after each chunk download. + # 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. diff --git a/R/geom-.r b/R/geom-.r index 92e2b8a3..f7707b32 100644 --- a/R/geom-.r +++ b/R/geom-.r @@ -632,6 +632,14 @@ Geom <- gganimintproto("Geom", data.table::fwrite( data.or.null$common, file = tsv.path, row.names = FALSE, sep = "\t") + # Track common chunk size and rows + if(!exists("chunk_info", envir=meta)) { + meta$chunk_info <- list() + } + meta$chunk_info[[tsv.name]] <- list( + bytes = file.size(tsv.path), + rows = nrow(data.or.null$common) + ) data.or.null$varied } list(g=g, g.data.varied=g.data.varied, timeValues=AnimationInfo$timeValues) diff --git a/R/z_animint.R b/R/z_animint.R index 3beb7dd7..53958970 100644 --- a/R/z_animint.R +++ b/R/z_animint.R @@ -212,8 +212,21 @@ storeLayer <- function(meta, g, g.data.varied){ ## Save each variable chunk to a separate tsv file. meta$chunk.i <- 1L meta$g <- g + # Initialize chunk_info only if it doesn't exist (common chunk may have been saved) + if(!exists("chunk_info", envir=meta)) { + meta$chunk_info <- list() + } g$chunks <- saveChunks(g.data.varied, meta) g$total <- length(unlist(g$chunks)) + + ## Add chunk size information to geom - filter to only this geom's chunks + g$chunk_info <- list() + geom_prefix <- paste0(g$classed, "_chunk") + for(chunk_name in names(meta$chunk_info)) { + if(startsWith(chunk_name, geom_prefix)) { + g$chunk_info[[chunk_name]] <- meta$chunk_info[[chunk_name]] + } + } ## Finally save to the master geom list. meta$geoms[[g$classed]] <- g diff --git a/R/z_animintHelpers.R b/R/z_animintHelpers.R index f3686454..1de93360 100644 --- a/R/z_animintHelpers.R +++ b/R/z_animintHelpers.R @@ -917,9 +917,21 @@ saveChunks <- function(x, meta){ # fwrite defaults ensure fields are quoted so that embedded # newlines or tabs in string fields do not break the TSV format # when read by d3.tsv. + csv.path <- file.path(meta$out.dir, csv.name) data.table::fwrite( - na.omit(x), file.path(meta$out.dir, csv.name), + na.omit(x), csv.path, row.names=FALSE, sep="\t") + # Calculate chunk size and row count + chunk_bytes <- file.size(csv.path) + chunk_rows <- nrow(na.omit(x)) + # Store chunk info + if(!exists("chunk_info", envir=meta)) { + meta$chunk_info <- list() + } + meta$chunk_info[[csv.name]] <- list( + bytes = chunk_bytes, + rows = chunk_rows + ) meta$chunk.i <- meta$chunk.i + 1L this.i }else if(is.list(x)){ diff --git a/inst/htmljs/animint.js b/inst/htmljs/animint.js index 02c8d760..be204f36 100644 --- a/inst/htmljs/animint.js +++ b/inst/htmljs/animint.js @@ -225,10 +225,31 @@ var animint = function (to_select, json_file) { // Add a row to the loading table. g_info.tr = Widgets["loading"].append("tr"); g_info.tr.append("td").text(g_name); - g_info.tr.append("td").attr("class", "chunk"); - g_info.tr.append("td").attr("class", "downloaded").text(0); - g_info.tr.append("td").text(g_info.total); - g_info.tr.append("td").attr("class", "status").text("initialized"); + g_info.td_files = g_info.tr.append("td").attr("class", "files").style("text-align", "right"); + g_info.td_MB = g_info.tr.append("td").attr("class", "MB").style("text-align", "right"); + g_info.td_rows = g_info.tr.append("td").attr("class", "rows").style("text-align", "right"); + + // Initialize size tracking + g_info.total_bytes = 0; + g_info.total_rows = 0; + g_info.downloaded_chunks = 0; + + // Calculate total possible bytes and rows from chunk_info + g_info.possible_bytes = 0; + g_info.possible_rows = 0; + g_info.total_possible_chunks = g_info.total; + if(g_info.chunk_info){ + var tsv_count = 0; + for(var chunk_name in g_info.chunk_info){ + if(chunk_name.endsWith('.tsv')){ + g_info.possible_bytes += g_info.chunk_info[chunk_name].bytes; + g_info.possible_rows += g_info.chunk_info[chunk_name].rows; + tsv_count++; + } + } + // chunk_info includes the common chunk, so total_possible_chunks should include it + g_info.total_possible_chunks = tsv_count; + } // load chunk tsv g_info.data = {}; @@ -243,6 +264,21 @@ var animint = function (to_select, json_file) { d3.tsv(common_path, function (error, response) { var converted = convert_R_types(response, g_info.types); g_info.data[common_tsv] = nest_by_group.map(converted); + // Track common chunk download for size information + if(g_info.chunk_info && g_info.chunk_info[common_tsv]){ + var info = g_info.chunk_info[common_tsv]; + g_info.total_bytes += info.bytes; + g_info.total_rows += info.rows; + g_info.downloaded_chunks += 1; + // Update display + var downloaded_count = g_info.downloaded_chunks; + var total_count = g_info.total_possible_chunks; + var downloaded_MB = (g_info.total_bytes / 1048576).toFixed(2); + var possible_MB = (g_info.possible_bytes / 1048576).toFixed(2); + g_info.td_files.text(downloaded_count + " / " + total_count); + g_info.td_MB.text(downloaded_MB + " / " + possible_MB); + g_info.td_rows.text(g_info.total_rows + " / " + g_info.possible_rows); + } }); } else { g_info.common_tsv = null; @@ -997,8 +1033,27 @@ var animint = function (to_select, json_file) { }); var chunk = nest.map(response); g_info.data[tsv_name] = chunk; - g_info.tr.select("td.downloaded").text(d3.keys(g_info.data).length); g_info.download_status[tsv_name] = "saved"; + + // Update size information after download + if(g_info.chunk_info && g_info.chunk_info[tsv_name]){ + var info = g_info.chunk_info[tsv_name]; + g_info.total_bytes += info.bytes; + g_info.total_rows += info.rows; + g_info.downloaded_chunks += 1; + + // Update display with "downloaded / total" format + var downloaded_count = g_info.downloaded_chunks; + var total_count = g_info.total_possible_chunks; + g_info.td_files.text(downloaded_count + " / " + total_count); + + var downloaded_MB = (g_info.total_bytes / 1048576).toFixed(2); + var possible_MB = (g_info.possible_bytes / 1048576).toFixed(2); + g_info.td_MB.text(downloaded_MB + " / " + possible_MB); + + g_info.td_rows.text(g_info.total_rows + " / " + g_info.possible_rows); + } + funAfter(chunk); }); }); @@ -1007,7 +1062,6 @@ var animint = function (to_select, json_file) { // update_geom is responsible for obtaining a chunk of downloaded // data, and then calling draw_geom to actually draw it. var draw_geom = function(g_info, chunk, selector_name, PANEL){ - g_info.tr.select("td.status").text("displayed"); var svg = SVGs[g_info.classed]; // derive the plot name from the geometry name var g_names = g_info.classed.split("_"); @@ -2372,10 +2426,9 @@ var animint = function (to_select, json_file) { Widgets["loading"] = loading; var tr = loading.append("tr"); tr.append("th").text("geom"); - tr.append("th").attr("class", "chunk").text("selected chunk"); - tr.append("th").attr("class", "downloaded").text("downloaded"); - tr.append("th").attr("class", "total").text("total"); - tr.append("th").attr("class", "status").text("status"); + tr.append("th").attr("class", "files").style("text-align", "right").text("files"); + tr.append("th").attr("class", "MB").style("text-align", "right").text("MB"); + tr.append("th").attr("class", "rows").style("text-align", "right").text("rows"); // Add geoms and construct nest operators. for (var g_name in response.geoms) { diff --git a/tests/testthat/test-download-status-table.R b/tests/testthat/test-download-status-table.R new file mode 100644 index 00000000..27d1d448 --- /dev/null +++ b/tests/testthat/test-download-status-table.R @@ -0,0 +1,28 @@ +acontext("download status table") +viz <- animint( + ggplot()+ + geom_point(aes(Sepal.Length, Sepal.Width), data=iris) +) +info <- animint2HTML(viz) +test_that("table has correct column headers", { + table_headers <- getNodeSet(info$html, '//table[@id="download_status"]//th') + header_text <- sapply(table_headers, xmlValue) + expect_true("geom" %in% header_text) + expect_true("files" %in% header_text) + expect_true("MB" %in% header_text) + expect_true("rows" %in% header_text) + expect_false("status" %in% header_text) + expect_false("mean MB" %in% header_text) + expect_false("selected chunk" %in% header_text) +}) +test_that("numeric columns are right-justified", { + files_header <- getNodeSet(info$html, '//table[@id="download_status"]//th[@class="files"]') + mb_header <- getNodeSet(info$html, '//table[@id="download_status"]//th[@class="MB"]') + rows_header <- getNodeSet(info$html, '//table[@id="download_status"]//th[@class="rows"]') + expect_equal(length(files_header), 1) + expect_equal(length(mb_header), 1) + expect_equal(length(rows_header), 1) + expect_match(xmlGetAttr(files_header[[1]], "style"), "text-align.*right") + expect_match(xmlGetAttr(mb_header[[1]], "style"), "text-align.*right") + expect_match(xmlGetAttr(rows_header[[1]], "style"), "text-align.*right") +}) diff --git a/tests/testthat/test-renderer1-variable-value.R b/tests/testthat/test-renderer1-variable-value.R index 13aa8480..1da7fb01 100644 --- a/tests/testthat/test-renderer1-variable-value.R +++ b/tests/testthat/test-renderer1-variable-value.R @@ -269,8 +269,10 @@ test_that("Widgets for regular selectors", { chunk.counts <- function(html=getHTML()){ node.set <- - getNodeSet(html, '//td[@class="downloaded"]') - as.integer(sapply(node.set, xmlValue)) + getNodeSet(html, '//td[@class="files"]') + text.vec <- sapply(node.set, xmlValue) + downloaded.vec <- sapply(strsplit(text.vec, " / "), "[[", 1) + as.integer(downloaded.vec) } test_that("counts of chunks downloaded or not at first", {