Skip to content

Commit

Permalink
feat: Creates table from rust
Browse files Browse the repository at this point in the history
  • Loading branch information
andyquinterom committed Jul 18, 2024
1 parent 66b2809 commit 8f40758
Show file tree
Hide file tree
Showing 8 changed files with 223 additions and 30 deletions.
2 changes: 1 addition & 1 deletion NAMESPACE
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Generated by roxygen2: do not edit by hand

export(build_spyctable_html)
export(get_spyc_table_selection)
export(renderSpyCTable)
export(spyCTableOutput)
export(spyc_header_create)
useDynLib(spyctable, .registration = TRUE)
2 changes: 1 addition & 1 deletion R/extendr-wrappers.R
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ NULL
filter_from_values_vec <- function(values_vec, data) .Call(wrap__filter_from_values_vec, values_vec, data)

#' @export
spyc_header_create <- function(names) .Call(wrap__spyc_header_create, names)
build_spyctable_html <- function(data, names, nrow, format, na, id) .Call(wrap__build_spyctable_html, data, names, nrow, format, na, id)


# nolint end
16 changes: 13 additions & 3 deletions R/table.R
Original file line number Diff line number Diff line change
@@ -1,11 +1,21 @@
#' @export
renderSpyCTable <- function(expr, env = parent.frame(), quoted = FALSE) {
renderSpyCTable <- function(expr, env = parent.frame(), quoted = FALSE, id) {
func <- shiny::exprToFunction(expr, env, quoted)
session <- shiny::getDefaultReactiveDomain();
shiny::reactive({
to_render <- func()
list(
data = to_render,
thead = jsonlite::unbox(spyc_header_create(colnames(to_render)))
# data = to_render,
html = jsonlite::unbox(
build_spyctable_html(
to_render,
colnames(to_render),
nrow(to_render),
"K",
"dash",
id = shiny::getCurrentOutputInfo()$name
)
)
)
})
}
Expand Down
9 changes: 3 additions & 6 deletions inst/example/app.R
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,17 @@ ui <- fluidPage(
spyCTableOutput("tabla")
)

char_iris <- 1:100 |>
purrr::map_df(~ iris) |>
dplyr::mutate(dplyr::across(tidyselect::everything(), as.character))

server <- function(input, output, session) {

output$tabla <- renderSpyCTable({
char_iris
iris |>
dplyr::mutate(Species = as.character(Species))
}) |>
bindEvent(input$rerender)

observe({
print(
get_spyc_table_selection(input$tabla_cells_selected, char_iris)
get_spyc_table_selection(input$tabla_cells_selected, iris)
)
})
}
Expand Down
26 changes: 8 additions & 18 deletions src/rust/src/header.rs
Original file line number Diff line number Diff line change
Expand Up @@ -188,39 +188,29 @@ impl<'a> ContainerBuilder<'a> {
}
Some((rowspan, colspan))
}
fn build(mut self) -> String {
let mut buffer = Vec::<u8>::new();
let _ = write!(&mut buffer, "<thead>");
fn build(mut self, buffer: &mut Vec<u8>) {
let _ = write!(buffer, "<thead>");

for row in 0..self.data.nrow {
let _ = write!(&mut buffer, "<tr>");
let _ = write!(buffer, "<tr>");
for col in 0..self.data.ncol {
if let Some((rowspan, colspan)) = self.render_th(row, col) {
let _ = write!(
&mut buffer,
buffer,
r#"
<th colspan={colspan} rowspan={rowspan} class="border text-center align-middle">{}</th>
"#,
self.data.get(row, col).unwrap()
);
}
}
let _ = write!(&mut buffer, "</tr>");
let _ = write!(buffer, "</tr>");
}

let _ = write!(&mut buffer, "</thead>");

unsafe { String::from_utf8_unchecked(buffer) }
let _ = write!(buffer, "</thead>");
}
}

/// @export
#[extendr]
fn spyc_header_create(names: Strings) -> String {
ContainerBuilder::new(&names).build()
}

extendr_module! {
mod header;
fn spyc_header_create;
pub fn spyc_header_create(names: Strings, buffer: &mut Vec<u8>) {
ContainerBuilder::new(&names).build(buffer);
}
26 changes: 25 additions & 1 deletion src/rust/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
use extendr_api::prelude::*;

use crate::tbody::{Formatting, NAFormatting};
use std::io::Write;
mod header;
mod tbody;
mod tfoot;

fn list_from_vec_vec(values: Vec<Vec<&str>>, names: StrIter) -> List {
let mut result_list = List::from_iter(values.into_iter().map(|mut values| {
Expand Down Expand Up @@ -35,11 +40,30 @@ fn filter_from_values_vec(values_vec: Integers, data: List) -> List {
list_from_vec_vec(result_list, data.names().expect("Must have names"))
}

/// @export
#[extendr]
fn build_spyctable_html(
data: List,
names: Strings,
nrow: i32,
format: Formatting,
na: NAFormatting,
id: &str,
) -> String {
let mut buffer = Vec::new();
let _ = write!(&mut buffer, r#"<table id="{id}_inner_table">"#);
header::spyc_header_create(names, &mut buffer);
tbody::build_tbody_and_foot(nrow, data, format, na, &mut buffer);
let _ = write!(&mut buffer, "</table>");

unsafe { String::from_utf8_unchecked(buffer) }
}

// Macro to generate exports.
// This ensures exported functions are registered with R.
// See corresponding C code in `entrypoint.c`.
extendr_module! {
mod spyctable;
use header;
fn filter_from_values_vec;
fn build_spyctable_html;
}
172 changes: 172 additions & 0 deletions src/rust/src/tbody.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
use extendr_api::prelude::*;
use extendr_api::AsTypedSlice;
use std::io::Write;

pub enum Formatting {
Millions,
Thousands,
Default,
}

impl<'a> FromRobj<'a> for Formatting {
fn from_robj(robj: &'a Robj) -> std::result::Result<Self, &'static str> {
if let Some(val) = robj.as_str() {
return Ok(match val {
"MM" => Formatting::Millions,
"K" => Formatting::Thousands,
"default" => Formatting::Default,
_ => panic!("Invalid formatting"),
});
}
Ok(Formatting::Default)
}
}

pub enum NAFormatting {
Dash,
Zero,
}

impl<'a> FromRobj<'a> for NAFormatting {
fn from_robj(robj: &'a Robj) -> std::result::Result<Self, &'static str> {
if let Some(val) = robj.as_str() {
return Ok(match val {
"dash" => NAFormatting::Dash,
"zero" => NAFormatting::Zero,
_ => panic!("Invalid NA formatting"),
});
}
Ok(NAFormatting::Zero)
}
}

pub fn build_tbody_and_foot(
nrow: i32,
data: List,
format: Formatting,
na: NAFormatting,
buffer: &mut Vec<u8>,
) {
let nrow = nrow as usize;
let is_real_col_lookup: Vec<bool> = data.iter().map(|(_, col)| col.is_real()).collect();
let mut total_sum_lookup: Vec<f64> = is_real_col_lookup
.iter()
.map(|is_real| if *is_real { 0.0 } else { -1.0 })
.collect();
let ncol = is_real_col_lookup.len();

let _ = write!(
buffer,
r#"<tbody onmousedown="enable_dragging()" onmouseleave="disable_dragging()" onmouseenter="disable_dragging()">"#
);

(0..nrow).for_each(|i| {
let _ = write!(buffer, "<tr>");
for c in 0..ncol {
if is_real_col_lookup[c] {
if let Some(real_slice) = data[c].as_real_slice() {
let val = real_slice[i];
if val.is_na() {
let _ = match na {
NAFormatting::Dash => write!(
buffer,
r#"<td class="border text-center align-middle">-</td>"#
),
NAFormatting::Zero => write!(
buffer,
r#"<td class="border text-center align-middle">0</td>"#
),
};
} else {
total_sum_lookup[c] += val;
let _ = match format {
Formatting::Default => write!(
buffer,
r#"<td class="border text-center align-middle">{val:.2}</td>"#
),
Formatting::Millions => write!(
buffer,
r#"<td class="border text-center align-middle">{val:.0}MM</td>"#,
val = val / 1_000_000.0
),
Formatting::Thousands => write!(
buffer,
r#"<td class="border text-center align-middle">{val:.0}K</td>"#,
val = val / 1_000.0
),
};
}
}
} else {
let _ = match AsTypedSlice::<'_, Rstr>::as_typed_slice(&data[c]) {
Some(str_slice) => {
let val = &str_slice[i];
write!(
buffer,
r#"
<td class="border text-center align-middle" onmouseover="mouse_over_event()" onmousedown="mouse_down_event()">{val}</td>
"#
)
}
None => write!(
buffer,
r#"<td class="border text-center align-middle" onmouseover="mouse_over_event()" onmousedown="mouse_down_event()"></td>"#
),
};
}
}
let _ = write!(buffer, "</tr>");
});

let _ = write!(buffer, "</tbody>");
let _ = write!(buffer, "<tfoot>");

let _ = write!(buffer, "<tr>");
let _ = write!(
buffer,
r#"<td class="border text-center align-middle">Total</td>"#
);
(1..ncol).for_each(|c| {
let val = total_sum_lookup[c];
if val == -1.0 {
let _ = write!(
buffer,
r#"<td class="border text-center align-middle"></td>"#
);
} else {
let _ = write!(
buffer,
r#"<td class="border text-center align-middle">{val}</td>"#
);
}
});
let _ = write!(buffer, "</tr>");

let _ = write!(buffer, "<tr>");
let _ = write!(
buffer,
r#"<td class="border text-center align-middle">Average</td>"#
);
(1..ncol).for_each(|c| {
let val = total_sum_lookup[c];
if val == -1.0 {
let _ = write!(
buffer,
r#"<td class="border text-center align-middle"></td>"#
);
} else {
let _ = write!(
buffer,
r#"<td class="border text-center align-middle">{val}</td>"#,
val = val / nrow as f64
);
}
});
let _ = write!(buffer, "</tr>");

let _ = write!(buffer, "</tfoot>");
}

extendr_module! {
mod tbody;
}
Empty file added src/rust/src/tfoot.rs
Empty file.

0 comments on commit 8f40758

Please sign in to comment.