R Shiny Datatable Child Row Selection and Info Issue - r

I am having an issue selecting the child rows in a R Shiny DT Table with JS callback.
When expanding the parent row, I try to select the child rows, and all rows are selected in that child (including child's background).
If I select 2nd child row, the background is deselcted and it shows my 2 childs selected (every other click selects all child rows, then shows ones selected repeatedly)
Also, how to get the information on which child rows are selected?
Thank you very much!
Alex B
I am trying to play with the datatable settings in the JS callback.
'''
library(data.table)
library(DT)
library(shiny)
library(jsonlite)
ui <- fluidPage(DT::dataTableOutput(width = "100%", "table"))
server <- function(input, output) {
output$table = DT::renderDataTable({
mtcars_dt = data.table(mtcars)
setkey(mtcars_dt,mpg,cyl)
mpg_dt = unique(mtcars_dt[, list(mpg, cyl)])
setkey(mpg_dt, mpg, cyl)
cyl_dt = unique(mtcars_dt[, list(cyl)])
setkey(cyl_dt, cyl)
mtcars_dt = mtcars_dt[, toJSON(.SD), by = list(mpg,cyl)]
setnames(mtcars_dt,'V1','mtcars')
mtcars_dt[, ' ' := '►']
df1 = mtcars_dt
df1 = df1[c(1,6),]
setcolorder(df1, c(length(df1),c(1:(length(df1) - 1))))
DT::datatable(
data = df1,
rownames = FALSE,
escape = FALSE,
selection="multiple",
options = list(
# dom = 'Bfrti',
stripeClasses = list(),
deferRender = TRUE,
# scrollX = TRUE,
pageLength = 25,
scrollY = "1000",
scroller = TRUE,
scollCollapse = TRUE,
lengthMenu = c(20, 50, 100, 500),
searchHighlight = TRUE,
tabIndex = 1,
columnDefs = list(
list(orderable = FALSE, className = 'details-control', targets = 0),
list(visible = FALSE, targets = -1 )
)
),
callback = JS("
//table.header().to$().css({'background-color': '#000', 'color': '#fff'})
table.column(01).nodes().to$().css({cursor: 'pointer'})
var table_id = 1000
// Format child object into another table
var format = function(table_id, columns) {
if(columns != null){
var result = ('<table id=\"' + table_id + '\"><thead><tr>')
for (var i in columns){
result += '<th>' + columns[i] + '</th>'
}
result += '</tr></thead></table>'
return result
}else{
return ''
}
}
var format_datatable = function( table_id, newtable, columns) {
if(newtable != null){
var column_defs = []
for (var i in columns)
{
if (i == 0)
{
column_defs[i] = {'data': columns[i], 'targets': parseInt(i), 'orderable': false, 'className': 'details-control'}
}
else
{
column_defs[i] = {'data': columns[i], 'targets': parseInt(i)}
}
}
/* alert(JSON.stringify(column_defs)) */
//var printTable = document.getElementById(newtable)
//document.write(newtable)
//document.write(columns)
var subtable = $(('table#' + table_id)).DataTable({
'data': newtable,
'autoWidth': false,
'deferRender': true,
'stripeClasses': [],
'info': false,
'select': { style: 'os',
},
'lengthChange': false,
'ordering': false,
'paging': false,
'scrollX': false,
'scrollY': false,
'searching': false,
'columnDefs': column_defs
}).draw()
}
}
table.on('click', 'td.details-control', function() {
var td = $(this)
var table = $(td).closest('table')
var row = $(table).DataTable().row(td.closest('tr'))
if (row.child.isShown()) {
row.child.hide()
td.html('►')
}
else
{
var row_data = row.data()
if (!Array.isArray(row_data))
{
row_data = Object.keys(row_data).map(function (key) {
return row_data[key]
});
}
var newtable = JSON.parse(row_data[row_data.length-1])
var columns = Object.keys(newtable[0])
table_id++
row.child(format(table_id, columns)).show()
format_datatable(table_id, newtable, columns)
console.log(table_id)
td.html('▼')
}
})
")
)
})
observe({
print(input$table_rows_selected)
print(input$newtable_rows_selected)
})
}
shinyApp(ui = ui, server = server)
'''
I would like to highlight individual child rows and know which child rows are selected. Currently it highlights all child rows each time it clicks.

Here is an attempt. This works, but the selection on the main table is disabled.
library(data.table)
library(DT)
library(shiny)
library(jsonlite)
initComplete <- paste(
"function(settings){",
" var table = settings.oInstance.api();",
" var tbl = table.table().node();",
" var id = $(tbl).closest('.dataTable').attr('id');",
" table.on('click', 'tbody tr', function(){",
" // send selected columns to Shiny",
" setTimeout(function(){",
" var indexes = table.rows({selected:true}).indexes();",
" var indices = Array(indexes.length);",
" for(var i = 0; i < indices.length; ++i){",
" indices[i] = indexes[i];",
" }",
" Shiny.setInputValue('childrow_rows_selected', {child: id, rows: indices});",
" },0);",
" });",
"}",
sep = "\n"
)
ui <- fluidPage(DT::dataTableOutput(width = "100%", "table"))
server <- function(input, output) {
output$table = DT::renderDataTable({
mtcars_dt = data.table(mtcars)
setkey(mtcars_dt,mpg,cyl)
mpg_dt = unique(mtcars_dt[, list(mpg, cyl)])
setkey(mpg_dt, mpg, cyl)
cyl_dt = unique(mtcars_dt[, list(cyl)])
setkey(cyl_dt, cyl)
mtcars_dt = mtcars_dt[, toJSON(.SD), by = list(mpg,cyl)]
setnames(mtcars_dt,'V1','mtcars')
mtcars_dt[, ' ' := '►']
df1 = mtcars_dt
df1 = df1[c(1,6),]
setcolorder(df1, c(length(df1),c(1:(length(df1) - 1))))
DT::datatable(
data = df1,
rownames = FALSE,
escape = FALSE,
selection = "none",
extensions = "Select",
options = list(
# dom = 'Bfrti',
stripeClasses = list(),
deferRender = TRUE,
# scrollX = TRUE,
pageLength = 25,
scrollY = "1000",
scroller = TRUE,
scollCollapse = TRUE,
lengthMenu = c(20, 50, 100, 500),
searchHighlight = TRUE,
tabIndex = 1,
columnDefs = list(
list(orderable = FALSE, className = 'details-control', targets = 0),
list(visible = FALSE, targets = -1 )
)
),
callback = JS("
table.column(0).nodes().to$().css({cursor: 'pointer'});
// var table_id = 1000
// Format child object into another table
var format = function(table_id, columns) {
if(columns != null){
var result = ('<table id=\"' + table_id + '\"><thead><tr>')
for (var i in columns){
result += '<th>' + columns[i] + '</th>'
}
result += '</tr></thead></table>'
return result
}else{
return ''
}
}
var format_datatable = function( table_id, newtable, columns) {
if(newtable != null){
var column_defs = []
for (var i in columns)
{
if (i == 0)
{
column_defs[i] = {'data': columns[i], 'targets': parseInt(i), 'orderable': false, 'className': 'details-control'}
}
else
{
column_defs[i] = {'data': columns[i], 'targets': parseInt(i)}
}
}
var subtable = $(('table#' + table_id)).DataTable({
'data': newtable,",
sprintf("initComplete: %s,", initComplete),
" 'autoWidth': false,
'deferRender': true,
'stripeClasses': [],
'info': false,
'select': {style: 'multi'},
'lengthChange': false,
'ordering': false,
'paging': false,
'scrollX': false,
'scrollY': false,
'searching': false,
'columnDefs': column_defs
}).draw()
}
}
table.on('click', 'td.details-control', function() {
var td = $(this);
var table = $(td).closest('table');
var row = $(table).DataTable().row(td.closest('tr'));
var table_id = 'child' + row.index();
if (row.child.isShown()) {
row.child.hide();
td.html('►');
}
else
{
var row_data = row.data();
if (!Array.isArray(row_data))
{
row_data = Object.keys(row_data).map(function (key) {
return row_data[key];
});
}
var newtable = JSON.parse(row_data[row_data.length-1])
var columns = Object.keys(newtable[0])
//table_id++
row.child(format(table_id, columns)).show()
format_datatable(table_id, newtable, columns)
console.log(table_id)
td.html('▼')
}
})
")
)
})
observe({
# print(input$table_rows_selected)
# print(input$newtable_rows_selected)
print(input$childrow_rows_selected)
})
}
shinyApp(ui = ui, server = server)

Related

How to add multiple scrollbar in R Shiny data table

I am trying to build a shiny app that has the following feature:
It has a parent-child feature that can expand and collapse as per
user interaction (Requirement 1 - Done)
When the rows are expanded, several child rows are displayed in the
data table. I want to introduce multiple scroll bars in this table.
1st scrollbar will be for 1st 4 columns and another scrollbar for the
rest of the columns. (Requirement 2 - Not Done)
The below code is able to produce results for the 1st requirement (using JQuery) however, I am unable to find a way out for Requirement 2.
Can anyone assist here?
packages = c(
'shiny',
'shinydashboard',
'tidyverse',
'dplyr',
'magrittr',
'plotly',
'ggplot2',
'scales',
'DT',
"shinyWidgets",
"fontawesome"
)
for (p in packages) {
if (!require(p, character.only = T)) {
install.packages(p)
}
library(p, character.only = T)
}
DataIn <- mtcars
DataIn <- DataIn %>% tidyr::nest(-cyl)
DataIn <- DataIn %>%
{
bind_cols(data_frame(
' ' = rep(
'<img src=\"https://raw.githubusercontent.com/DataTables/DataTables/master/examples/resources/details_open.png\"/>',
nrow(.)
)
), .)
}
# get dynamic info and strings
nested_columns <-
which(sapply(DataIn, class) == "list") %>% setNames(NULL)
not_nested_columns <-
which(!(seq_along(DataIn) %in% c(1, nested_columns)))
not_nested_columns_str <-
not_nested_columns %>% paste(collapse = "] + '_' + d[") %>% paste0("d[", ., "]")
CallBack <- paste0(
"
table.column(1).nodes().to$().css({cursor: 'pointer'});
// Format data object (the nested table) into another table
var format = function(d) {
if(d != null){
var result = ('<table id=\"child_' + ",
not_nested_columns_str,
" + '\">') + '<thead><tr>'
for (var col in d[",
nested_columns,
"][0]){
result += '<th>' + col + '</th>'
}
result += '</tr></thead></table>'
return result
}else{
return '';
}
}
var format_datatable = function(d) {
var dataset = [];
for (i = 0; i <= d[",
nested_columns,
"].length-1; i++) {
var datarow = $.map(d[",
nested_columns,
"][i], function(value, index) {
return [value];
});
dataset.push(datarow);
}
var subtable = $(('table#child_' + ",
not_nested_columns_str,
")).DataTable({
'data': dataset,
'autoWidth': true,
'deferRender': true,
'info': false,
'lengthChange': false,
'ordering': true,
'paging': false,
'scrollX': false,
'scrollY': false,
'searching': false
// 'fnRowCallback': function (nRow, aData, iDisplayIndex, iDisplayIndexFull) {
// $('td', nRow).css('background-color', 'Red')}
});
};
table.on('click', 'td.details-control', function() {
var td = $(this), row = table.row(td.closest('tr'));
if (row.child.isShown()) {
row.child.hide();
td.html('<img src=\"https://raw.githubusercontent.com/DataTables/DataTables/master/examples/resources/details_open.png\"/>');
} else {
row.child(format(row.data())).show();
td.html('<img src=\"https://raw.githubusercontent.com/DataTables/DataTables/master/examples/resources/details_close.png\"/>');
format_datatable(row.data())
}
});"
)
shinyApp(
ui = fluidPage(DT::dataTableOutput('tbl')),
server = function(input, output) {
output$tbl = DT::renderDataTable(datatable(
DataIn,
escape = -2,
# raw HTML in column 2
options = list(columnDefs = list(
list(visible = FALSE, targets = c(0, nested_columns)),
# Hide row numbers and nested columns
list(
orderable = FALSE,
className = 'details-control',
targets = 1
) # turn first column into control column
)),
callback = JS(CallBack)
))
}
)

How to start DT datatable with all child rows expanded?

In this DT example with child rows, how to start the table with all the child rows expanded?
library(DT)
datatable(
cbind(' ' = '⊕', mtcars), escape = -2,
options = list(
columnDefs = list(
list(visible = FALSE, targets = c(0, 2, 3)),
list(orderable = FALSE, className = 'details-control', targets = 1)
)
),
callback = JS("
table.column(1).nodes().to$().css({cursor: 'pointer'});
var format = function(d) {
return '<div style=\"background-color:#eee; padding: .5em;\"> Model: ' +
d[0] + ', mpg: ' + d[2] + ', cyl: ' + d[3] + '</div>';
};
table.on('click', 'td.details-control', function() {
var td = $(this), row = table.row(td.closest('tr'));
if (row.child.isShown()) {
row.child.hide();
td.html('⊕');
} else {
row.child(format(row.data())).show();
td.html('&CircleMinus;');
}
});"
))
PS: stackoverflow forced me to include more details to the question but there is nothing else to add...
You can use your existing callback to also iterate over each row in the table. In that iteration you can create and open each child record:
table.rows().every( function () {
this.child( format(this.data()) ).show();
} );
This snippet needs to be appended to the end of your callback = JS(...) option as shown below:
callback = JS(
"
table.column(1).nodes().to$().css({cursor: 'pointer'});
var format = function(d) {
return '<div style=\"background-color:#eee; padding: .5em;\"> Model: ' +
d[0] + ', mpg: ' + d[2] + ', cyl: ' + d[3] + '</div>';
};
table.on('click', 'td.details-control', function() {
var td = $(this), row = table.row(td.closest('tr'));
if (row.child.isShown()) {
row.child.hide();
td.html('⊕');
} else {
row.child(format(row.data())).show();
td.html('&CircleMinus;');
}
});
table.rows().every( function () {
this.child( format(this.data()) ).show();
} );"
)
The result:

How to correclty use MutationObserve in Shiny app

I'm trying to observe changes to css using javascript mutationObserver in Shiny. I'm using rhandsontable because we can change the width of a table element in the app, and I'm trying to pick up this change iwth the mutationObserver.
The javascript doesn't seem to be working. I'm unsure why. Nothing is logged to the console, no alert message, and shiny doesn't register the variable being set by javascript.
MutationObserver code
jsCode <- "
const observer = new MutationObserver(
# this function runs when something is observed.
function(mutations){
console.log('activated')
var i;
var text;
var widthArray = [];
text = ''
for (i = 0; i < document.getElementsByClassName('htCore')[0].getElementsByTagName('col').length; i++) {
text += document.getElementsByClassName('htCore')[0].getElementsByTagName('col')[i].style.width + '<br>';
widthArray.push(document.getElementsByClassName('htCore')[0].getElementsByTagName('col')[i].style.width);
}
alert(text)
Shiny.setInputValue('colWidth', widthArray);
}
)
const cols = document.getElementsByClassName('htCore')[0].getElementsByTagName('col')
observer.observe(cols, {
attributes: true # observe when attributes of ul.bears change (width, height)
})
"
Shiny code:
library(shiny)
library(rhandsontable)
ui <- fluidPage(
tags$head(tags$script(HTML(jsCode))),
rhandsontable::rHandsontableOutput("dataTable")
)
server <- function(input, output, session) {
df = data.frame(
company = c('a', 'b', 'c', 'd'),
bond = c(0.2, 1, 0.3, 0),
equity = c(0.7, 0, 0.5, 1),
cash = c(0.1, 0, 0.2, 0),
stringsAsFactors = FALSE
)
output$dataTable <- renderRHandsontable({
rhandsontable(df, manualColumnResize = TRUE, manualRowResize = TRUE)
})
observeEvent(input$colWidth, {
print(input$colWidth)
})
}
shinyApp(ui, server)
This works:
jsCode <- "
$(document).on('shiny:connected', function(){
setTimeout(function(){
const observer = new MutationObserver(
function(mutations){
console.log('activated')
var i;
var text;
var widthArray = [];
text = ''
for (i = 0; i < document.getElementsByClassName('htCore')[0].getElementsByTagName('col').length; i++) {
text += document.getElementsByClassName('htCore')[0].getElementsByTagName('col')[i].style.width + '<br>';
widthArray.push(document.getElementsByClassName('htCore')[0].getElementsByTagName('col')[i].style.width);
}
alert(text)
Shiny.setInputValue('colWidth', widthArray);
}
)
const cols = document.getElementsByClassName('htCore')[0].getElementsByTagName('colgroup')[0]
observer.observe(cols, {
attributes: true, subtree: true
});
}, 500);
});
"

Draggable interactive bar chart Rshiny

I would love to know if building something like this is possible is RShiny. I have experience with interactive plots/charts using plotly, ggplot and ggplotly but I can't see how to do something like this. I love how the graph engages the user to make a guess and then shows the real data.
If anyone could please point me in the direction of any documentation I will be forever grateful!
https://www.mathematica-mpr.com/dataviz/race-to-the-top
Here is a Shiny implementation of this jsfiddle.
library(shiny)
library(jsonlite)
barChartInput <- function(inputId, width = "100%", height = "400px",
data, category, value, minValue, maxValue,
color = "rgb(208,32,144)"){
tags$div(id = inputId, class = "amchart",
style = sprintf("width: %s; height: %s;", width, height),
`data-data` = as.character(toJSON(data)),
`data-category` = category,
`data-value` = value,
`data-min` = minValue,
`data-max` = maxValue,
`data-color` = color)
}
dat <- data.frame(
country = c("USA", "China", "Japan", "Germany", "UK", "France"),
visits = c(3025, 1882, 1809, 1322, 1122, 1114)
)
ui <- fluidPage(
tags$head(
tags$script(src = "http://www.amcharts.com/lib/4/core.js"),
tags$script(src = "http://www.amcharts.com/lib/4/charts.js"),
tags$script(src = "http://www.amcharts.com/lib/4/themes/animated.js"),
tags$script(src = "barchartBinding.js")
),
fluidRow(
column(8,
barChartInput("mybarchart", data = dat,
category = "country", value = "visits",
minValue = 0, maxValue = 3500)),
column(4,
tags$label("Data:"),
verbatimTextOutput("data"),
br(),
tags$label("Change:"),
verbatimTextOutput("change"))
)
)
server <- function(input, output){
output[["data"]] <- renderPrint({
if(is.null(input[["mybarchart"]])){
dat
}else{
fromJSON(input[["mybarchart"]])
}
})
output[["change"]] <- renderPrint({ input[["mybarchart_change"]] })
}
shinyApp(ui, server)
The file barchartBinding.js, to put in the www subfolder of the app file:
var barchartBinding = new Shiny.InputBinding();
$.extend(barchartBinding, {
find: function (scope) {
return $(scope).find(".amchart");
},
getValue: function (el) {
return null;
},
subscribe: function (el, callback) {
$(el).on("change.barchartBinding", function (e) {
callback();
});
},
unsubscribe: function (el) {
$(el).off(".barchartBinding");
},
initialize: function (el) {
var id = el.getAttribute("id");
var $el = $(el);
var data = $el.data("data");
var dataCopy = $el.data("data");
var categoryName = $el.data("category");
var valueName = $el.data("value");
var minValue = $el.data("min");
var maxValue = $el.data("max");
var barColor = $el.data("color");
am4core.useTheme(am4themes_animated);
var chart = am4core.create(id, am4charts.XYChart);
chart.hiddenState.properties.opacity = 0; // this makes initial fade in effect
chart.data = data;
chart.padding(40, 40, 0, 0);
chart.maskBullets = false; // allow bullets to go out of plot area
var text = chart.plotContainer.createChild(am4core.Label);
text.text = "Drag column bullet to change its value";
text.y = 92;
text.x = am4core.percent(100);
text.horizontalCenter = "right";
text.zIndex = 100;
text.fillOpacity = 0.7;
// category axis
var categoryAxis = chart.xAxes.push(new am4charts.CategoryAxis());
categoryAxis.title.text = categoryName;
categoryAxis.title.fontWeight = "bold";
categoryAxis.dataFields.category = categoryName;
categoryAxis.renderer.grid.template.disabled = true;
categoryAxis.renderer.minGridDistance = 50;
// value axis
var valueAxis = chart.yAxes.push(new am4charts.ValueAxis());
valueAxis.title.text = valueName;
valueAxis.title.fontWeight = "bold";
// we set fixed min/max and strictMinMax to true, as otherwise value axis will adjust min/max while dragging and it won't look smooth
valueAxis.strictMinMax = true;
valueAxis.min = minValue;
valueAxis.max = maxValue;
valueAxis.renderer.minWidth = 60;
// series
var series = chart.series.push(new am4charts.ColumnSeries());
series.dataFields.categoryX = categoryName;
series.dataFields.valueY = valueName;
series.tooltip.pointerOrientation = "vertical";
series.tooltip.dy = -8;
series.sequencedInterpolation = true;
series.defaultState.interpolationDuration = 1500;
series.columns.template.strokeOpacity = 0;
// label bullet
var labelBullet = new am4charts.LabelBullet();
series.bullets.push(labelBullet);
labelBullet.label.text = "{valueY.value.formatNumber('#.')}";
labelBullet.strokeOpacity = 0;
labelBullet.stroke = am4core.color("#dadada");
labelBullet.dy = -20;
// series bullet
var bullet = series.bullets.create();
bullet.stroke = am4core.color("#ffffff");
bullet.strokeWidth = 3;
bullet.opacity = 0; // initially invisible
bullet.defaultState.properties.opacity = 0;
// resize cursor when over
bullet.cursorOverStyle = am4core.MouseCursorStyle.verticalResize;
bullet.draggable = true;
// create hover state
var hoverState = bullet.states.create("hover");
hoverState.properties.opacity = 1; // visible when hovered
// add circle sprite to bullet
var circle = bullet.createChild(am4core.Circle);
circle.radius = 8;
// while dragging
bullet.events.on("drag", event => {
handleDrag(event);
});
bullet.events.on("dragstop", event => {
handleDrag(event);
var dataItem = event.target.dataItem;
dataItem.column.isHover = false;
event.target.isHover = false;
dataCopy[dataItem.index][valueName] = dataItem.values.valueY.value;
Shiny.setInputValue(id, JSON.stringify(dataCopy));
Shiny.setInputValue(id + "_change", {
index: dataItem.index,
category: dataItem.categoryX,
value: dataItem.values.valueY.value
});
});
function handleDrag(event) {
var dataItem = event.target.dataItem;
// convert coordinate to value
var value = valueAxis.yToValue(event.target.pixelY);
// set new value
dataItem.valueY = value;
// make column hover
dataItem.column.isHover = true;
// hide tooltip not to interrupt
dataItem.column.hideTooltip(0);
// make bullet hovered (as it might hide if mouse moves away)
event.target.isHover = true;
}
// column template
var columnTemplate = series.columns.template;
columnTemplate.column.cornerRadiusTopLeft = 8;
columnTemplate.column.cornerRadiusTopRight = 8;
columnTemplate.fillOpacity = 0.8;
columnTemplate.tooltipText = "drag me";
columnTemplate.tooltipY = 0; // otherwise will point to middle of the column
// hover state
var columnHoverState = columnTemplate.column.states.create("hover");
columnHoverState.properties.fillOpacity = 1;
// you can change any property on hover state and it will be animated
columnHoverState.properties.cornerRadiusTopLeft = 35;
columnHoverState.properties.cornerRadiusTopRight = 35;
// show bullet when hovered
columnTemplate.events.on("over", event => {
var dataItem = event.target.dataItem;
var itemBullet = dataItem.bullets.getKey(bullet.uid);
itemBullet.isHover = true;
});
// hide bullet when mouse is out
columnTemplate.events.on("out", event => {
var dataItem = event.target.dataItem;
var itemBullet = dataItem.bullets.getKey(bullet.uid);
itemBullet.isHover = false;
});
// start dragging bullet even if we hit on column not just a bullet, this will make it more friendly for touch devices
columnTemplate.events.on("down", event => {
var dataItem = event.target.dataItem;
var itemBullet = dataItem.bullets.getKey(bullet.uid);
itemBullet.dragStart(event.pointer);
});
// when columns position changes, adjust minX/maxX of bullets so that we could only dragg vertically
columnTemplate.events.on("positionchanged", event => {
var dataItem = event.target.dataItem;
var itemBullet = dataItem.bullets.getKey(bullet.uid);
var column = dataItem.column;
itemBullet.minX = column.pixelX + column.pixelWidth / 2;
itemBullet.maxX = itemBullet.minX;
itemBullet.minY = 0;
itemBullet.maxY = chart.seriesContainer.pixelHeight;
});
// as by default columns of the same series are of the same color, we add adapter which takes colors from chart.colors color set
columnTemplate.adapter.add("fill", (fill, target) => {
return barColor; //chart.colors.getIndex(target.dataItem.index).saturate(0.3);
});
bullet.adapter.add("fill", (fill, target) => {
return chart.colors.getIndex(target.dataItem.index).saturate(0.3);
});
}
});
Shiny.inputBindings.register(barchartBinding);
Update
And below is a Shiny implementation of the amcharts4 grouped bar chart.
library(shiny)
library(jsonlite)
registerInputHandler("dataframe", function(data, ...) {
fromJSON(toJSON(data, auto_unbox = TRUE))
}, force = TRUE)
groupedBarChartInput <- function(inputId, width = "100%", height = "400px",
data, categoryField, valueFields,
minValue, maxValue,
ndecimals = 0,
colors = NULL,
categoryLabel = categoryField,
valueLabels = valueFields,
categoryAxisTitle = categoryLabel,
valueAxisTitle = NULL,
categoryAxisTitleFontSize = 22,
valueAxisTitleFontSize = 22,
categoryAxisTitleColor = "indigo",
valueAxisTitleColor = "indigo",
draggable = rep(FALSE, length(valueFields))){
tags$div(id = inputId, class = "amGroupedBarChart",
style = sprintf("width: %s; height: %s;", width, height),
`data-data` = as.character(toJSON(data)),
`data-categoryfield` = categoryField,
`data-valuefields` = as.character(toJSON(valueFields)),
`data-min` = minValue,
`data-max` = maxValue,
`data-ndecimals` = ndecimals,
`data-colors` = ifelse(is.null(colors), "auto", as.character(toJSON(colors))),
`data-valuenames` = as.character(toJSON(valueLabels)),
`data-categoryname` = categoryLabel,
`data-categoryaxistitle` = categoryAxisTitle,
`data-valueaxistitle` = valueAxisTitle,
`data-draggable` = as.character(toJSON(draggable)),
`data-categoryaxistitlefontsize` = categoryAxisTitleFontSize,
`data-valueaxistitlefontsize` = valueAxisTitleFontSize,
`data-categoryaxistitlecolor` = categoryAxisTitleColor,
`data-valueaxistitlecolor` = valueAxisTitleColor)
}
set.seed(666)
dat <- data.frame(
year = rpois(5, 2010),
income = rpois(5, 25),
expenses = rpois(5, 20)
)
ui <- fluidPage(
tags$head(
tags$script(src = "http://www.amcharts.com/lib/4/core.js"),
tags$script(src = "http://www.amcharts.com/lib/4/charts.js"),
tags$script(src = "http://www.amcharts.com/lib/4/themes/animated.js"),
tags$script(src = "groupedBarChartBinding.js")
),
fluidRow(
column(8,
groupedBarChartInput("mybarchart", data = dat[order(dat$year),],
categoryField = "year",
valueFields = c("income", "expenses"),
categoryLabel = "Year",
valueLabels = c("Income", "Expenses"),
valueAxisTitle = "Income and expenses",
minValue = 0, maxValue = 35,
draggable = c(FALSE, TRUE),
colors = c("darkmagenta","darkred"))),
column(4,
tags$label("Data:"),
verbatimTextOutput("data"),
br(),
tags$label("Change:"),
verbatimTextOutput("change"))
)
)
server <- function(input, output){
output[["data"]] <- renderPrint({
input[["mybarchart"]]
})
output[["change"]] <- renderPrint({ input[["mybarchart_change"]] })
}
shinyApp(ui, server)
The file groupedBarChartBinding.js, to put in the www subfolder:
var groupedBarChartBinding = new Shiny.InputBinding();
$.extend(groupedBarChartBinding, {
find: function(scope) {
return $(scope).find(".amGroupedBarChart");
},
getValue: function(el) {
return $(el).data("data");
},
getType: function(el) {
return "dataframe";
},
subscribe: function(el, callback) {
$(el).on("change.groupedBarChartBinding", function(e) {
callback();
});
},
unsubscribe: function(el) {
$(el).off(".groupedBarChartBinding");
},
initialize: function(el) {
var id = el.getAttribute("id");
var $el = $(el);
var data = $el.data("data");
var dataCopy = $el.data("data");
var categoryField = $el.data("categoryfield");
var valueFields = $el.data("valuefields");
var minValue = $el.data("min");
var maxValue = $el.data("max");
var colors = $el.data("colors");
var valueNames = $el.data("valuenames");
var categoryName = $el.data("categoryname");
var categoryAxisTitle = $el.data("categoryaxistitle");
var valueAxisTitle = $el.data("valueaxistitle");
var draggable = $el.data("draggable");
var ndecimals = $el.data("ndecimals");
var numberFormat = "#.";
for (var i = 0; i < ndecimals; i++) {
numberFormat = numberFormat + "#";
}
var categoryAxisTitleFontSize = $el.data("categoryaxistitlefontsize") + "px";
var valueAxisTitleFontSize = $el.data("valueaxistitlefontsize") + "px";
var categoryAxisTitleColor = $el.data("categoryaxistitlecolor");
var valueAxisTitleColor = $el.data("valueaxistitlecolor");
am4core.useTheme(am4themes_animated);
var chart = am4core.create(id, am4charts.XYChart);
chart.hiddenState.properties.opacity = 0; // this makes initial fade in effect
chart.data = data;
chart.padding(40, 40, 40, 40);
chart.maskBullets = false; // allow bullets to go out of plot area
// Create axes
var categoryAxis = chart.yAxes.push(new am4charts.CategoryAxis());
categoryAxis.dataFields.category = categoryField;
categoryAxis.numberFormatter.numberFormat = numberFormat;
categoryAxis.renderer.inversed = true;
categoryAxis.renderer.grid.template.location = 0;
categoryAxis.renderer.cellStartLocation = 0.1;
categoryAxis.renderer.cellEndLocation = 0.9;
categoryAxis.title.text = categoryAxisTitle;
categoryAxis.title.fontWeight = "bold";
categoryAxis.title.fontSize = categoryAxisTitleFontSize;
categoryAxis.title.setFill(categoryAxisTitleColor);
var valueAxis = chart.xAxes.push(new am4charts.ValueAxis());
valueAxis.renderer.opposite = true;
valueAxis.strictMinMax = true;
valueAxis.min = minValue;
valueAxis.max = maxValue;
if (valueAxisTitle !== null) {
valueAxis.title.text = valueAxisTitle;
valueAxis.title.fontWeight = "bold";
valueAxis.title.fontSize = valueAxisTitleFontSize;
valueAxis.title.setFill(valueAxisTitleColor);
}
function handleDrag(event) {
var dataItem = event.target.dataItem;
// convert coordinate to value
var value = valueAxis.xToValue(event.target.pixelX);
// set new value
dataItem.valueX = value;
// make column hover
dataItem.column.isHover = true;
// hide tooltip not to interrupt
dataItem.column.hideTooltip(0);
// make bullet hovered (as it might hide if mouse moves away)
event.target.isHover = true;
}
// Create series
function createSeries(field, name, barColor, drag) {
var series = chart.series.push(new am4charts.ColumnSeries());
series.dataFields.valueX = field;
series.dataFields.categoryY = categoryField;
series.name = name;
series.sequencedInterpolation = true;
var valueLabel = series.bullets.push(new am4charts.LabelBullet());
valueLabel.label.text = "{valueX}";
valueLabel.label.horizontalCenter = "left";
valueLabel.label.dx = 10;
valueLabel.label.hideOversized = false;
valueLabel.label.truncate = false;
var categoryLabel = series.bullets.push(new am4charts.LabelBullet());
categoryLabel.label.text = "{name}";
categoryLabel.label.horizontalCenter = "right";
categoryLabel.label.dx = -10;
categoryLabel.label.fill = am4core.color("#fff");
categoryLabel.label.hideOversized = false;
categoryLabel.label.truncate = false;
// column template
var columnTemplate = series.columns.template;
console.log(columnTemplate);
// columnTemplate.tooltipText = "{name}: [bold]{valueX}[/]";
columnTemplate.tooltipHTML =
"<div style='font-size:9px'>" + "{name}" + ": " + "<b>{valueX}</b>" + "</div>";
columnTemplate.height = am4core.percent(100);
columnTemplate.column.cornerRadiusBottomRight = 8;
columnTemplate.column.cornerRadiusTopRight = 8;
columnTemplate.fillOpacity = 1;
columnTemplate.tooltipX = 0; // otherwise will point to middle of the column
// hover state
var columnHoverState = columnTemplate.column.states.create("hover");
columnHoverState.properties.fillOpacity = 1;
// you can change any property on hover state and it will be animated
columnHoverState.properties.cornerRadiusBottomRight = 35;
columnHoverState.properties.cornerRadiusTopRight = 35;
// color
if (barColor !== false) {
columnTemplate.adapter.add("fill", (fill, target) => {
return barColor;
});
}
if (drag) {
// series bullet
var bullet = series.bullets.create();
bullet.stroke = am4core.color("#ffffff");
bullet.strokeWidth = 1;
bullet.opacity = 0; // initially invisible
bullet.defaultState.properties.opacity = 0;
// resize cursor when over
bullet.cursorOverStyle = am4core.MouseCursorStyle.horizontalResize;
bullet.draggable = true;
// create hover state
var hoverState = bullet.states.create("hover");
hoverState.properties.opacity = 1; // visible when hovered
// add circle sprite to bullet
var circle = bullet.createChild(am4core.Circle);
circle.radius = 5;
// dragging
// while dragging
bullet.events.on("drag", event => {
handleDrag(event);
});
bullet.events.on("dragstop", event => {
handleDrag(event);
var dataItem = event.target.dataItem;
dataItem.column.isHover = false;
event.target.isHover = false;
dataCopy[dataItem.index][field] = dataItem.values.valueX.value;
Shiny.setInputValue(id + ":dataframe", dataCopy);
Shiny.setInputValue(id + "_change", {
index: dataItem.index,
field: field,
category: dataItem.categoryY,
value: dataItem.values.valueX.value
});
});
// bullet color
if (barColor !== false) {
bullet.adapter.add("fill", (fill, target) => {
return barColor;
});
}
// show bullet when hovered
columnTemplate.events.on("over", event => {
var dataItem = event.target.dataItem;
var itemBullet = dataItem.bullets.getKey(bullet.uid);
itemBullet.isHover = true;
});
// hide bullet when mouse is out
columnTemplate.events.on("out", event => {
var dataItem = event.target.dataItem;
var itemBullet = dataItem.bullets.getKey(bullet.uid);
itemBullet.isHover = false;
});
// start dragging bullet even if we hit on column not just a bullet, this will make it more friendly for touch devices
columnTemplate.events.on("down", event => {
var dataItem = event.target.dataItem;
var itemBullet = dataItem.bullets.getKey(bullet.uid);
itemBullet.dragStart(event.pointer);
});
// when columns position changes, adjust minY/maxY of bullets so that we could only dragg horizontally
columnTemplate.events.on("positionchanged", event => {
var dataItem = event.target.dataItem;
var itemBullet = dataItem.bullets.getKey(bullet.uid);
var column = dataItem.column;
itemBullet.minY = column.pixelY + column.pixelHeight / 2;
itemBullet.maxY = itemBullet.minY;
itemBullet.minX = 0;
itemBullet.maxX = chart.seriesContainer.pixelWidth;
});
}
}
for (var i = 0; i < valueFields.length; i++) {
var color = colors === "auto" ? null : colors[i];
createSeries(valueFields[i], valueNames[i], color, draggable[i]);
}
}
});
Shiny.inputBindings.register(groupedBarChartBinding);
Update 2
I have done a package now : shinyAmBarCharts. I have added a button (optional) allowing to update the data to another dataset. This fulfills the OP's desideratum:
the graph engages the user to make a guess and then shows the real
data
library(shiny)
library(shinyAmBarCharts)
# create a dataset
set.seed(666)
df0 <- data.frame(
species = rep(c("sorgho","poacee","banana"), each = 3),
condition = rep(c("normal", "stress", "Nitrogen"), 3),
value = rpois(9, 10)
)
df1 <- df0; df1[["value"]] <- 10
dat <- tidyr::spread(df0, condition, value) # true data
dat2 <- tidyr::spread(df1, condition, value) # data template
# grouped bar chart
ui <- fluidPage(
br(),
fluidRow(
column(9,
amBarChart(
"mygroupedbarchart", data = dat2, data2 = dat, height = "400px",
category = "species", value = c("normal", "stress", "Nitrogen"),
valueNames = c("Normal", "Stress", "Nitrogen"),
minValue = 0, maxValue = 20,
draggable = c(TRUE, TRUE, TRUE),
theme = "dark", backgroundColor = "#30303d",
columnStyle = list(fill = c("darkmagenta", "darkred", "gold"),
stroke = "#cccccc",
cornerRadius = 4),
chartTitle = list(text = "Grouped bar chart",
fontSize = 23,
color = "firebrick"),
xAxis = list(title = list(text = "Species",
fontSize = 21,
color = "silver"),
labels = list(color = "whitesmoke",
fontSize = 17)),
yAxis = list(title = list(text = "Value",
fontSize = 21,
color = "silver"),
labels = list(color = "whitesmoke",
fontSize = 14)),
columnWidth = 90,
button = list(text = "Show true data"),
caption = list(text = "[font-style:italic]shinyAmBarCharts[/]",
color = "yellow"),
gridLines = list(color = "whitesmoke",
opacity = 0.4,
width = 1),
tooltip = list(text = "[bold;font-style:italic]{name}: {valueY}[/]",
labelColor = "#101010",
backgroundColor = "cyan",
backgroundOpacity = 0.7)
)
),
column(3,
tags$label("Data:"),
verbatimTextOutput("data"),
br(),
tags$label("Change:"),
verbatimTextOutput("change"))
)
)
server <- function(input, output){
output[["data"]] <- renderPrint({
input[["mygroupedbarchart"]]
})
output[["change"]] <- renderPrint({ input[["mygroupedbarchart_change"]] })
}
shinyApp(ui, server)

Child tables with datatables (expand/collapse)

I like to build a datatable with a Childtable for example with this data:
test = data.table(c(375, 789, 72, 663, 100), c(1237, 1237, 1237, 663, 100), c("abc", "abc", "abc", "d", "e"), c("a","b","c","d","e"))
First i like to have a table:
datatable(test[, .(V2,V3)][3:5])
on click on abc i want to be able to expand that datatable so that the following is shown below:
datatable(test[, .(V1, V4)][1:3])
Output would be a html file written in rmarkdown.
Appreciate any help and thanks in advance.
Here something you can start with.
Code based on #Stéphane's answer here
library(DT)
datatable(
cbind(' ' = '<img src=\"https://raw.githubusercontent.com/DataTables/DataTables/master/examples/resources/details_open.png\"/>',
mtcars), escape = -2,
options = list(
columnDefs = list(
list(visible = FALSE, targets = c(0, 2, 3)),
list(orderable = FALSE, className = 'details-control', targets = 1)
)
),
callback = JS("
table.column(1).nodes().to$().css({cursor: 'pointer'});
var format = function(d) {
return '<table cellpadding=\"5\" cellspacing=\"0\" border=\"0\" style=\"padding-left:50px;\"> ' +
'<thead>'+
'<tr>'+
'<th>1st column</th>'+
'<th>2nd column</th>'+
'</tr>'+
'</thead>'+
'<tbody>'+
'<tr>'+
'<td>'+d[2]+'</td>'+
'<td>'+d[3]+'</td>'+
'</tr>' +
'</tbody>'
'</table>';
};
table.on('click', 'td.details-control', function() {
var td = $(this), row = table.row(td.closest('tr'));
if (row.child.isShown()) {
row.child.hide();
td.html('<img src=\"https://raw.githubusercontent.com/DataTables/DataTables/master/examples/resources/details_open.png\"/>');
} else {
row.child(format(row.data())).show();
td.html('<img src=\"https://raw.githubusercontent.com/DataTables/DataTables/master/examples/resources/details_close.png\"/>');
}
});"
))
See datatable website here for more details.

Resources