-
Notifications
You must be signed in to change notification settings - Fork 0
Feature 60 Use DT's Render for HTML Formatting in Plot Table #62
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
Conversation
…on for plot table
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR implements client-side HTML rendering for the Plot table using DT's JavaScript render functions instead of server-side HTML generation. It also introduces the Inter font family with tabular numbers for improved readability and numerical comparison.
- Replaces server-side HTML string generation with data list structures processed by DT's JavaScript render functions
- Adds Inter font from RSME CDN with tabular numbers and stylistic sets for better typography
- Updates test suite to reflect new data structures and function naming changes
Reviewed Changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/testthat/test_plot_table.R | Updates tests to verify new list-based data structures instead of HTML strings |
| R/ui.R | Adds Inter font loading and extensive CSS customization with tabular numbers |
| R/table_plot.R | Converts HTML generation to data list creation with JavaScript rendering |
| R/table_community.R | Adjusts column widths and adds right alignment for numeric data |
| R/table.R | Minor adjustment to scroll height calculation |
| R/server.R | Updates event handlers to use new input IDs and removes redundant validation |
| R/detail_view.R | Removes excessive bold formatting and fixes table header structure |
Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.
Co-authored-by: Copilot <[email protected]>
Co-authored-by: Copilot <[email protected]>
Co-authored-by: Copilot <[email protected]>
Co-authored-by: Copilot <[email protected]>
…m/NCEAS/vegbank-web into feature-60-use-dt-html-formatting
|
Merging after reviewing over Zoom with @regetz |
regetz
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@DarianGill Thanks for doing this! I think shifting to client-side rendering of the table is a great enhancement. Couple small comments/questions, plus a suggested optimization that I hope will help reduce the plot table load time.
| accession_code <- input$see_obs_details | ||
| if (is.null(accession_code) || is.na(accession_code) || accession_code == "") { | ||
| shiny::showNotification( | ||
| paste0("No accession code found for that plot observation: ", accession_code), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Given that the if condition limits accession_code to one of NULL, NA, or an empty string, then will this be a useful error message?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oop, no I suppose not. I'll update it to match the ones for other entities below.
| #' @noRd | ||
| create_taxa_vectors <- function(plot_data, taxa_data) { | ||
| merged <- dplyr::left_join(plot_data, taxa_data, by = "observation_id") | ||
| taxa_lists <- merged |> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Based on some ad hoc timings done on my local machine, the new code to create taxa_lists takes quite a bit longer than the earlier code, such that the net change (including the speedup due to client-side table rendering) is slower to first load of the plot table in the UI.
However, I believe this code produces the same result much faster:
taxa_lists <- merged |>
dplyr::mutate(
str_max_cover = format(round(.data$max_cover, 2),
nsmall = 2, trim = TRUE),
taxon_details = mapply(
function(x, y, z) list(code=x, name=y, cover=z),
.data$taxon_observation_accession_code,
.data$int_curr_plant_sci_name_no_auth,
str_max_cover,
SIMPLIFY = FALSE,
USE.NAMES = FALSE)) |>
dplyr::group_by(.data$observation_id) |>
dplyr::summarize(
taxa = list(
if (all(is.na(.data$int_curr_plant_sci_name_no_auth)) &
all(is.na(.data$max_cover))) {
list()
} else {
taxon_details
}
),
.groups = "drop"
)Sample timings
Original code that creates a column of HTML fragments:
user system elapsed
7.558 1.381 9.404
PR code that creates a column of lists for client-side rendering:
user system elapsed
14.597 3.945 20.347
Proposed new code to create column of lists:
user system elapsed
2.038 0.441 2.590
At least when run on our ~110K-row cached dataset, the output of my proposed code is identical to that of the current PR code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@regetz Whoa that is way faster (and much more readable)! Is it because we're avoiding the lapply within the summarize and just using a simple conditional there? I'll implement this style with a mutate first for communities as well.
| .groups = "drop" | ||
| ) | ||
| result <- rep("No Taxa Data", nrow(plot_data)) | ||
| # Match index to plot_data to avoid duplicates / NAs where no taxa exist |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is this code section doing that wasn't already handled in the summarize above? I.e., are there conditions under which result would differ from taxa_lists[["taxa"]] from above?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@regetz I thought this matching was necessary to maintain row alignment between the plot_data dataframe in the ultimate result.
I think just returning taxa_lists$taxa assumes that:
- plot_data$observation_id and taxa_lists$observation_id are in the same order
- The order of the data isn't changed by the group_by (which preserves the order of rows within each group, but seems to internally order the groups themselves in ascending order based on the grouping variables)
Those may be fine assumptions to make for our data, since the taxa endpoint is pulled according to our own query, but I wasn't sure. The match was a defensive approach against something like the following:
Input Data (Different Order)
plot_data$observation_id = c("E", "A", "C", "B", "D") # Plot5, Plot1, Plot3, Plot2, Plot4
taxa_data$observation_id = c("A", "A", "C", "C", "E") # Only some plots have taxa
After left_join (Preserves plot_data order)
Row 1: E, Plot5 → gets T5/Elm/10
Row 2: A, Plot1 → gets T1/Oak/25
Row 3: A, Plot1 → gets T2/Pine/15
Row 4: C, Plot3 → gets T3/Maple/30
Row 5: C, Plot3 → gets T4/Birch/20
Row 6: B, Plot2 → gets NAs
Row 7: D, Plot4 → gets NAs
After group_by + summarize (Re-orders alphabetically by observation_id)
taxa_lists:
observation_id taxa
1 A [Oak, Pine] # Position 1
2 B [] # Position 2
3 C [Maple, Birch] # Position 3
4 D [] # Position 4
5 E [Elm] # Position 5
So taxa_lists$taxa gives us:
Row 1: Taxa for "A"
Row 2: Taxa for "B"
Row 3: Taxa for "C"
Row 4: Taxa for "D"
Row 5: Taxa for "E"
But plot_data (and the table built from it expects):
Row 1: Taxa for "E" (should be [Elm])
Row 2: Taxa for "A" (should be [Oak, Pine])
Row 3: Taxa for "C" (should be [Maple, Birch])
Row 4: Taxa for "B" (should be [])
Row 5: Taxa for "D" (should be [])
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, the missing piece for me was that the output needs to be in the same order as plot_data (because it is used in parallel with that data later). Then yes, I agree this is necessary in the event that plot_data is not in increasing order by "observation_id" -- the API makes no guarantees about this, and as you mentioned, dplyr::group_by(observation_id) will cause the taxa_lists to be reordered by observation_id.
Can you update the code comment to better explain this? It currently mentions duplicates (which should never exist in plot_data because "observation_id" is unique across that table, and never exist in taxa_lists because of the group_by) and missing taxa (which we've already handled and turned into empty lists with the combo of left_join and what we do in summarize, right?).
By the way, my instinct would've been to accomplish the reordering by left-joining the summarized table back on the original plot_data, which works because the dplyr::left_join is order-preserving for the left table:
result <- dplyr::left_join(plot_data["observation_id"], taxa_lists, by="observation_id")$taxaBut based on some quick tests, they're both super fast (sub-second) for the 100K records, and yours doesn't depend on dplyr behavior, so feel free to keep as is :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like that more! I'll update the comment. And I think they're really only used in parallel with the show on map button right now. From what I was reading at the time, I thought that was a more "Shiny" approach, but I think it'd probably be fine to refactor the event it to just pass the lat and lng and any other information to the observer instead of looking it up by matching indices between the table and the plot_data dataframe.
|
Sorry for jumping the gun here! I'll make another PR after this one with my updates to the same branch since you can't re-open a merged PR. Would love to know your thoughts on whether the matching is necessary first though. |
What:
This PR marks the first step in #60 by using DT's columnRender function and JS to create the action buttons, top taxa, and community list in the Plot table. It also gives the app a new font, Inter, and enables disambiguation and tabular numbers for better numerical comparison across rows. If this is the approach we'd like for optimization moving forward it can be reproduced for all the DT tables throughout the app.
Why:
As I understand it, in Shiny's DT package, the columnDefs render function is executed on the client-side (in the web browser) when the table is initially rendered or whenever the data displayed in the table changes. As such, using the render function sets us up to better take advantage of server-side pagination and saves us from formatting html for all 111k plots for the underlying dataframe in the shiny server. The Inter font update also allows for clearer and more compact text and numerical comparison.
How:
Convert string manipulation for HTML vectors to list constructors that compile the necessary data for the action buttons, top taxa, and community lists
Use DT::JS() in the column render options for table_plot.R to convert that data into formatted HTML
Source the Inter font from the RSME CDN (google doesn't serve the version with stylistic sets)
Apply font using general css selector in the ui.R bslib theme customizer
Adjust boldness and font size to better fit the primacy of information and Inter's style
Update plot table tests accordingly
Documentation & Testing:
Updated comments for functions where necessary and adjusted testing to reflect new field names/structure.
devtools::check() passes with only notes and all testthat::test() tests pass