Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@DarianGill
Copy link
Collaborator

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

@DarianGill DarianGill requested a review from Copilot August 28, 2025 23:30
Copy link
Contributor

Copilot AI left a 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.

@DarianGill DarianGill requested a review from regetz August 28, 2025 23:53
@DarianGill
Copy link
Collaborator Author

Merging after reviewing over Zoom with @regetz

@DarianGill DarianGill merged commit a436bcb into develop Sep 10, 2025
Copy link
Collaborator

@regetz regetz left a 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),
Copy link
Collaborator

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?

Copy link
Collaborator Author

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 |>
Copy link
Collaborator

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.

Copy link
Collaborator Author

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
Copy link
Collaborator

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?

Copy link
Collaborator Author

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:

  1. plot_data$observation_id and taxa_lists$observation_id are in the same order
  2. 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 [])

Copy link
Collaborator

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")$taxa

But 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 :)

Copy link
Collaborator Author

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.

@DarianGill
Copy link
Collaborator Author

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants