Last active
May 28, 2019 14:12
-
-
Save stla/33d5339f4ac1c72aa9a5745611334808 to your computer and use it in GitHub Desktop.
amcharts 4 grouped bar chart for Shiny
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 theme = $el.data("theme"); | |
var valueNames = $el.data("valuenames"); | |
var categoryName = $el.data("categoryname"); | |
var legend = $el.data("legend"); | |
var showValueLabels = $el.data("showvaluelabels"); | |
console.log(showValueLabels); | |
var categoryAxisTitle = $el.data("categoryaxistitle"); | |
var valueAxisTitle = $el.data("valueaxistitle"); | |
var draggable = $el.data("draggable"); | |
var ndecimals = $el.data("ndecimals"); | |
var numberFormat = "#."; | |
for (var d = 0; d < ndecimals; d++) { | |
numberFormat = numberFormat + "#"; | |
} | |
var categoryAxisTitleFontSize = $el.data("categoryaxistitlefontsize") + "px"; | |
var valueAxisTitleFontSize = $el.data("valueaxistitlefontsize") + "px"; | |
var categoryAxisTitleColor = $el.data("categoryaxistitlecolor"); | |
var valueAxisTitleColor = $el.data("valueaxistitlecolor"); | |
// set theme | |
if (theme !== null) { | |
if(theme === "dataviz") { | |
am4core.useTheme(am4themes_dataviz); | |
}else if(theme === "material"){ | |
am4core.useTheme(am4themes_material); | |
}else if(theme === "kelly"){ | |
am4core.useTheme(am4themes_kelly); | |
}else if(theme === "dark"){ | |
am4core.useTheme(am4themes_dark); | |
}else if(theme === "frozen"){ | |
am4core.useTheme(am4themes_frozen); | |
}else if(theme === "moonrisekingdom"){ | |
am4core.useTheme(am4themes_moonrisekingdom); | |
}else if(theme === "spiritedaway"){ | |
am4core.useTheme(am4themes_spiritedaway); | |
} | |
} | |
am4core.useTheme(am4themes_animated); | |
// initialize chart | |
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; | |
// tooltip text color and background color, and scaling | |
var tooltip = series.tooltip; | |
tooltip.getFillFromObject = false; | |
tooltip.background.fill = am4core.color("#101010"); | |
tooltip.autoTextColor = false; | |
tooltip.label.fill = am4core.color("#FFFFFF"); | |
tooltip.properties.scale = 0.6; | |
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; | |
if (showValueLabels) { | |
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; | |
columnTemplate.tooltipText = "[font-style:italic]{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.column.fillOpacity = 0.9; | |
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; | |
// }); | |
columnTemplate.fill = barColor; | |
columnTemplate.stroke = "#cccccc"; | |
columnTemplate.strokeWidth = 1; | |
} | |
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]); | |
} | |
// Add legend | |
if (legend) { | |
chart.legend = new am4charts.Legend(); | |
chart.legend.useDefaultMarker = false; | |
console.log(chart.legend.markers); | |
var markerTemplate = chart.legend.markers.template; | |
console.log(markerTemplate); | |
markerTemplate.height = 10; | |
// marker.cornerRadius(12, 12, 12, 12); | |
markerTemplate.strokeWidth = 1; | |
markerTemplate.strokeOpacity = 1; | |
// markerTemplate.stroke = am4core.color("#000000"); no effect | |
} | |
} | |
}); | |
Shiny.inputBindings.register(groupedBarChartBinding); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | |
theme = NULL, | |
backgroundColor = ifelse(identical(theme,"dark"), | |
"#30303d", "#ffffff"), | |
legend = TRUE, | |
categoryLabel = categoryField, | |
valueLabels = valueFields, | |
showValueLabels = !legend, | |
categoryAxisTitle = categoryLabel, | |
valueAxisTitle = NULL, | |
categoryAxisTitleFontSize = 22, | |
valueAxisTitleFontSize = 22, | |
categoryAxisTitleColor = "indigo", | |
valueAxisTitleColor = "indigo", | |
draggable = rep(FALSE, length(valueFields))){ | |
if(!is.null(theme)){ | |
theme <- match.arg(theme, c("dataviz","material","kelly","dark", | |
"frozen","moonrisekingdom","spiritedaway")) | |
} | |
tags$div( | |
id = inputId, class = "amGroupedBarChart", | |
style = | |
sprintf("width: %s; height: %s; background-color: %s;", | |
width, height, backgroundColor), | |
`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-theme` = theme, | |
`data-valuenames` = as.character(toJSON(valueLabels)), | |
`data-showvaluelabels` = ifelse(showValueLabels, "true", "false"), | |
`data-categoryname` = categoryLabel, | |
`data-legend` = ifelse(legend, "true", "false"), | |
`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/dataviz.js"), | |
tags$script(src = "http://www.amcharts.com/lib/4/themes/material.js"), | |
tags$script(src = "http://www.amcharts.com/lib/4/themes/kelly.js"), | |
tags$script(src = "http://www.amcharts.com/lib/4/themes/dark.js"), | |
tags$script(src = "http://www.amcharts.com/lib/4/themes/frozen.js"), | |
tags$script(src = "http://www.amcharts.com/lib/4/themes/moonrisekingdom.js"), | |
tags$script(src = "http://www.amcharts.com/lib/4/themes/spiritedaway.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"), | |
theme = "dark")), | |
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) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment