Reputation: 57
I have a Shiny app example using echarts4r to visualize sales data by category and year.
I'm trying to add two features to this plot:
A dynamic total displayed at the top of the chart that updates based on the legend selection. I know how to create one but I don't know how to make it change when something in the legend gets deselected. I think I need some sort of reactive function that listens to selection and compute totals again?
A number on the right side of the bars showing the sum of categories, which also updates with the legend selection.
Here's my current code:
library(shiny)
library(echarts4r)
library(dplyr)
# Sample data
data <- data.frame(
Year = rep(c("2020", "2021", "2022"), each = 4),
Category = rep(c("Electronics", "Clothing", "Books", "Home"), 3),
Sales = c(300, 200, 150, 250,
350, 220, 180, 280,
400, 250, 200, 320)
)
ui <- fluidPage(
titlePanel("Sales by Category and Year"),
mainPanel(
echarts4rOutput("plot", height = "500px")
)
)
server <- function(input, output, session) {
output$plot <- renderEcharts4r({
data %>%
group_by(Year) %>%
e_charts(Category) %>%
e_bar(Sales, stack = "stack") %>%
e_x_axis(axisLabel = list(interval = 0, rotate = 0)) %>%
e_y_axis(splitLine = list(show = FALSE)) %>%
e_labels(position = "inside") %>%
e_tooltip(trigger = "axis") %>%
e_legend(orient = "horizontal", bottom = "bottom", type = "scroll") %>%
e_flip_coords()
})
}
shinyApp(ui, server)
Edit: What I need:
Upvotes: 1
Views: 98
Reputation: 18754
I want to make it clear that this method works in Shiny, but it has to modified to work outside of Shiny. I've added both in and out of Shiny in my answer because I figured even if you can't use, maybe someone else can.
I couldn't get the label formatter
to cooperate, so I used the ui
to add script
tags to the Shiny app.
In the script, there's comments so that you can understand what's happening. In the UI, you named the plot renderer 'plot'. That becomes a key word in the background of your app, and it is used two times in the Javascript script element. If you change the name of this renderer, you must change it in the script, as well.
In that code there are 3 primary functions, one to calculate the totals by bar, one to calculate the plot total, and one to identify which elements are visible after a legend event.
In your plot code in server
, I've added a few elements to create homes for the plot total and bar totals.
The plot total is a graphic text element. The bar totals are entered as a scatter plot. I placed the bar totals at 1200 on the x-axis, based on the data you're plotting. You can, of course, adjust this as you see fit.
Let me know if you have any questions.
The Shiny code:
library(tidyverse)
library(echarts4r)
library(shiny)
ui <- fluidPage(
tags$head(
tags$script(HTML(
"setTimeout(function() { /* give page time to load */
ms = []; /* array to collect data to cycle through */
/* 'plot' matches the name of the call in UI... */
document.querySelector('#plot').htmlwidget_data_init_result.getOpts().series.forEach((it, ix) => {
ms.push({name: it.name, type: it.type, data: it.data})
});
/* function for handling filtering by legend */
legendHandle = (event, series) => { /* update series when legend clicked */
/* collect names visible from legend */
nms = Object.keys(event.selected).filter(key => event.selected[key] === true);
nms.push('b'); /* add text trace (not in legend) */
visSeries = series.filter(serie => nms.includes(serie.name));
return seriesHandler(visSeries); /* calculate total of visible elements */
}
/* function for handling total by column */
seriesHandler = series => { /* find total by category of visible series */
return series.map((serie, index) => {
if (index === series.length - 1) { /* only show labels in right-most group */
return {...serie, label: { normal: {
show: true, position: 'right',
formatter: params => { /* calculate the total for the bar */
let total = 0
series.forEach((s, i) => {
if(i !== series.length - 1) { /* don't add scatter value */
total += Number(s.data[params.dataIndex].value[0])
}
})
return total
}
}}}
} else {
return serie
}
})
}
/* function for handling plot total */
function totalHandler(event, series) {
series2 = legendHandle(event, series);
let tTotal = 0;
series2.forEach((s, i) => { /* cycle through visible series - other than scatter */
if(i !== series2.length - 1)
s.data.forEach(s2 => { /* cycle through array of arrays of data */
tTotal += Number(s2.value[0]) /* calculate running total */
});
});
/* get graphics text element as originally coded */
eg = document.querySelector('#plot').htmlwidget_data_init_result.getOpts().graphic;
eg.style.text = 'Total Sales: ' + tTotal; /* modify graphic text element */
return eg
}
/* capture current echart */
e = echarts.getInstanceById( plot.getAttribute('_echarts_instance_') ); /* 'plot' matches the name of the call in UI... */
/* deploy label change and legend event */
e.setOption({series: seriesHandler(ms) });
e.on('legendselectchanged', /* update both the series' totals and plot total when legend clicked */
event => { e.setOption({series: legendHandle(event, ms),
graphic: totalHandler(event, ms) }) }
)
}, 300) "))
),
titlePanel("Sales by Category and Year"),
mainPanel(
echarts4rOutput("plot", height = "500px") # <---- this name 'plot' is used in the JS script; if it changes it has to change in the JS, as well
)
)
server <- function(input, output, session) {
output$plot <- renderEcharts4r({
data %>%
group_by(Year) %>%
e_charts(Category) %>%
e_bar(Sales, stack = "stack") %>%
e_x_axis(index = 0, # <--------------------------- I'm new!
axisLabel = list(interval = 0, rotate = 0)) %>%
e_y_axis(splitLine = list(show = FALSE)) %>%
e_tooltip(trigger = "axis") %>%
e_labels(show = T, position = 'inside') %>%
e_legend(orient = "horizontal", bottom = "bottom", type = "scroll") %>%
# --------- new from here until e_flip_coords() ----------
e_data(data.frame(a = unique(data$Category), b = 1200), x = a) %>% # 1200 chosen arbitrarily based on data in plot
e_scatter(b, legend = F, tooltip = list(show = F)) %>% # home for the totals by bar
e_x_axis(index = 1, show = F) %>% # don't show another x axis
e_text_g(top = 40, right = 10, style = list(
text = paste0('Total Sales: ', sum(data$Sales)) # home for plot total
)) %>%
e_flip_coords()
})
}
shinyApp(ui, server)
As I mentioned initially, this code is how you could accomplish the same task without Shiny.
Instead of a script
tag, this method uses htmlwidgets::onRender
for the JS. Because there is no plot renderer, the argument elementId
is called in the plot. In this code that argument is set to "myChart"
. In the JS, instead of "plot"
(as it is in the Shiny version), you'll find it replaced with "myChart"
.
data %>% group_by(Year) %>%
e_charts(Category, elementId = "myChart") %>% # <------- I'm new (elementId)
e_bar(Sales, stack = "stack") %>%
e_x_axis(index = 0, # <--------------------------- I'm new!
axisLabel = list(interval = 0, rotate = 0)) %>%
e_y_axis(splitLine = list(show = FALSE)) %>%
e_tooltip(trigger = "axis") %>%
e_labels(show = T, position = 'inside') %>%
e_legend(orient = "horizontal", bottom = "bottom", type = "scroll") %>%
# --------- new from here until e_flip_coords ---------------
e_data(data.frame(a = unique(data$Category), b = 1200), x = a) %>% # 1200 chosen arbitrarily based on data in plot
e_scatter(b, legend = F, tooltip = list(show = F)) %>% # home for the totals by bar
e_x_axis(index = 1, show = F) %>% # don't show another x axis
e_text_g(top = 40, right = 10, style = list(
text = paste0('Total Sales: ', sum(data$Sales)) # home for plot total
)) %>%
e_flip_coords() %>%
htmlwidgets::onRender(
"function(el, x) {
ms = []; /* data to cycle through whenever there's a change */
document.querySelector('#myChart').htmlwidget_data_init_result.getOpts().series.forEach((it, ix) => {
ms.push({name: it.name, type: it.type, data: it.data})
});
legendHandle = (event, series) => { /* update series when legend clicked */
/* collect names visible from legend */
nms = Object.keys(event.selected).filter(key => event.selected[key] === true);
nms.push('b'); /* add text trace (not in legend) */
visSeries = series.filter(serie => nms.includes(serie.name));
return seriesHandler(visSeries); /* calculate total of visible elements */
}
seriesHandler = series => { /* find total by category of visible series */
return series.map((serie, index) => {
if (index === series.length - 1) { /* only show labels in right-most group */
return {...serie, label: { normal: {
show: true, position: 'right',
formatter: params => { /* calculate the total for the bar */
let total = 0
series.forEach((s, i) => {
if(i !== series.length - 1) { /* don't add scatter value */
total += Number(s.data[params.dataIndex].value[0])
}
})
return total
}
}}}
} else {
return serie
}
})
}
function totalHandler(event, series) {
series2 = legendHandle(event, series);
let tTotal = 0;
series2.forEach((s, i) => { /* cycle through visible series - other than scatter */
if(i !== series2.length - 1)
s.data.forEach(s2 => { /* cycle through array of arrays of data */
tTotal += Number(s2.value[0]) /* calculate running total */
});
});
/* get graphics text element as originally coded */
eg = document.querySelector('#myChart').htmlwidget_data_init_result.getOpts().graphic;
eg.style.text = 'Total Sales: ' + tTotal; /* modify graphic text element */
return eg
}
/* get chart */
e = echarts.getInstanceById( myChart.getAttribute('_echarts_instance_') );
/* update chart */
e.setOption({series: seriesHandler(ms) });
/* e.on('legendselectchanged',
event => { totalHandler(event, ms)}); */
e.on('legendselectchanged', /* update both the series' totals and plot total when legend clicked */
event => { e.setOption({series: legendHandle(event, ms),
graphic: totalHandler(event, ms) }) }
)
}"
)
Upvotes: 2