Related
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)
))
}
)
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('⊖');
}
});"
))
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('⊖');
}
});
table.rows().every( function () {
this.child( format(this.data()) ).show();
} );"
)
The result:
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)
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)
I am trying to show a different table or plot in a different div when a bar on a a bar plot is clicked. I have come this far.
server.R
library(shiny)
shinyServer(function(input,output,session) {
custom_data = # a data frame in R
barclick_func <- "#! function() {
var cats = ['100','200','3000','4000'];
var datum = [20,40,30,10];
}
$('#container_subcat').highcharts({
chart: {
type: 'column',
},
xAxis: { categories:cats },
series: [{data :datum}]
});
} !#"
output$my_plot <- renderChart2({
a <- hPlot(x="id",y="variable",data=custom_data,type="column")
a$tooltip( animation = 'true', formatter = "#! function() {return '<b>' + 'Frequency of tag_id ' + this.x + '</b>' + ' is ' + this.y;} !#")
a$plotOptions(series = list(color = '#388E8E'),column = list(dataLabels = list(enabled = T, rotation =-90, align = 'right', color = '#FFFFFF', x = 4, y = 10),
cursor = 'pointer', point = list(events = list(click = barclick_func))))
return(a)
})
})
ui.R
library(shiny)
require(rCharts)
shinyUI(fluidPage(
tags$head(
tags$link(rel = "stylesheet", type = "text/css", href = "custom.css"),
tags$script(src="http://code.jquery.com/jquery-git2.min.js"),
),
titlePanel("Test"),
fluidRow(
showOutput("my_plot","highcharts")
),
div(id="container_subcat",style="min-width: 310-px; height: 400px; margin: 0 auto; margin-top:100px;")
)
))
In the above server.R script, in barclick_func() function, the data(variables cats and datum) is hardcoded. The above app works as expected when a bar is clicked, another plot pops up properly with the data.
But, if I want to use another data, that is if I have another data frame in R and want to use that data frame in barclick_func(), the console is throwing an error that the variable is not recognized and if I look at the type of that variable, it shows as 'undefined'. Can anyone suggest how to send data to the javascript function in this particular case. Ideally, my desired code is this.
server.R
library(shiny)
shinyServer(function(input,output,session) {
custom_data = # a data frame in R
custom_data2 = # another data frame in R for which I wanna shoe a plot when the bar is clicked.
barclick_func <- "#! function() {
var cats = #subsetting custom_data2 ;
var datum = #subsetting custom_data2 ;
}
$('#container_subcat').highcharts({
chart: {
type: 'column',
},
xAxis: { categories:cats },
series: [{data :datum}]
});
} !#"
output$my_plot <- renderChart2({
a <- hPlot(x="id",y="variable",data=custom_data,type="column")
a$tooltip( animation = 'true', formatter = "#! function() {return '<b>' + 'Frequency of tag_id ' + this.x + '</b>' + ' is ' + this.y;} !#")
a$plotOptions(series = list(color = '#388E8E'),column = list(dataLabels = list(enabled = T, rotation =-90, align = 'right', color = '#FFFFFF', x = 4, y = 10),
cursor = 'pointer', point = list(events = list(click = barclick_func))))
return(a)
})
})
You could try using paste to add the data to your barclick_func.
For example:
custom_data2 = data.frame(cats=c(100,200,3000,400),datum=c(20,40,30,10))
barclick_func <- paste("#! function() {
var cats = ",paste('[',paste(custom_data2$cats,collapse=','),']',sep=''),"
var datum = ",paste('[',paste(custom_data2$datum,collapse=','),']',sep=''),";
}
$('#container_subcat').highcharts({
chart: {
type: 'column',
},
xAxis: { categories:cats },
series: [{data :datum}]
});
} !#")
Should give the same barclick_func as in your hardcoded version.