Echarts4r:基于图例选择动态更新累计求和结果
动态更新堆叠柱状图的累计求和折线(Echarts4r + Shiny)
核心问题分析
当前代码中cum_sum_total是基于所有program的总和提前计算的,当图例隐藏部分系列时,累计值无法自动适配剩余系列的总和。解决方案是通过ECharts的事件监听机制,在图例选中状态变化时实时计算累计值。
实现思路
- 移除提前计算的合计、百分比及累计值,改用JS逻辑动态计算
- 绑定
legendselectchanged事件,监听图例选中状态变化 - 在事件回调中:
- 提取当前选中的series
- 计算选中series的每个zipcode合计值
- 基于选中系列的总合计,计算百分比和累计百分比
- 更新折线图数据
修改后的完整代码
# 数据准备:仅保留排序逻辑,移除提前计算的静态统计值 data2 <- data.frame( zipcode= as.factor(round(rnorm(10, 87965, 50),0)), program_1 = rnorm(10, 200, 20), program_2 = rnorm(10, 700, 100), program_3 = rnorm(10, 500, 50) ) data <- data2 %>% mutate(zipcode = fct_reorder(zipcode, desc(rowSums(across(where(is.numeric)))))) %>% arrange(desc(rowSums(across(where(is.numeric))))) # 图表渲染+动态累计逻辑 data %>% e_charts(zipcode) %>% e_bar(program_1, stack = "g", bind=round(100*program_1/rowSums(across(program_1:program_3)),1), name="A", y_index = 0, emphasis = list(focus = "series")) %>% e_bar(program_2, stack = "g", bind=round(100*program_2/rowSums(across(program_1:program_3)),1), name="B", y_index = 0, emphasis = list(focus = "series")) %>% e_bar(program_3, stack = "g", bind=round(100*program_3/rowSums(across(program_1:program_3)),1), name="C", y_index = 0, emphasis = list(focus = "series")) %>% # 初始化折线图,后续通过JS动态更新数据 e_line( legend=T, data = rep(0, nrow(data)), y_index = 1, name = "Cumulative %", emphasis = list(focus = "series"), tooltip=list(show=F), label = list( show = TRUE, formatter = htmlwidgets::JS("function(params) { return params.value + '%' }"), rotate = 0, textStyle = list(fontFamily = "Arial", fontSize = 10) ) ) %>% e_x_axis(name = "Zip Code", show=TRUE, nameLocation = "middle", nameGap=50, nameTextStyle = list(color = "#87786B", fontSize = 15), axisLabel = list(fontFamily="Arial", interval = 0, rotate = 30, fontSize=12, color="#87786B")) %>% e_y_axis(name="n° of visits", index = 0, show = TRUE, nameLocation ="middle", nameGap=70, align="right", nameTextStyle = list(color = "#87786B", fontSize = 12), axisLabel = list(fontSize=12, color="#87786B")) %>% # 新增右侧Y轴用于显示百分比 e_y_axis(index = 1, show = TRUE, name="Cumulative %", nameLocation ="middle", nameGap=70, nameTextStyle = list(color = "#87786B", fontSize = 12), axisLabel = list(formatter = "{value}%", fontSize=12, color="#87786B")) %>% e_legend(itemWidth=30, top = 30, right=10, type="scroll", padding=0, selected = list('A' = TRUE, 'B' = TRUE, 'C' = TRUE, 'Cumulative %' = FALSE), textStyle=list(fontFamily="arial", fontSize=12, color="#87786B")) %>% e_tooltip( formatter = htmlwidgets::JS("function(params){ if(params.seriesName === 'Cumulative %') return ''; return(params.marker + params.seriesName + '<br>' + 'Total: ' + echarts.format.addCommas(params.value[1]) + ' (' + params.data + '%)'); }"), textStyle=list(fontFamily="Arial", fontSize=12) ) %>% e_animation(duration=1500) %>% e_toolbox(show =T, top=0, right= 10, itemSize=10, emphasis=list(iconStyle=list(textFill="#6C2C4E"))) %>% e_toolbox_feature(feature = c("saveAsImage", "restore")) %>% e_theme_custom('{"color":["#F8AD3B", "#D15425", "#9CAC3B", "#6C2C4E"]}') %>% # 监听图例选择事件,动态更新累计折线 e_on( event = "legendselectchanged", handler = htmlwidgets::JS("function(params) { var chart = this; var selectedSeries = []; // 筛选当前选中的业务系列(排除累计折线) for(var key in params.selected) { if(key !== 'Cumulative %' && params.selected[key]) { selectedSeries.push(key); } } // 映射系列名称到数据列 var seriesMap = {'A': 'program_1', 'B': 'program_2', 'C': 'program_3'}; var selectedCols = selectedSeries.map(s => seriesMap[s]); // 收集选中系列的原始数据 var seriesData = {}; chart.getOption().series.forEach(s => { if(selectedSeries.includes(s.name)) { seriesData[seriesMap[s.name]] = s.data.map(d => d[1]); } }); // 计算每个zipcode的合计值 var totals = []; for(let i=0; i<chart.getOption().xAxis[0].data.length; i++) { let sum = 0; selectedCols.forEach(col => sum += seriesData[col][i]); totals.push(sum); } // 计算累计百分比 var grandTotal = totals.reduce((a,b) => a+b, 0); var perTotals = totals.map(t => (t/grandTotal)*100); var cumSum = []; let currentSum = 0; perTotals.forEach(p => { currentSum += p; cumSum.push(currentSum.toFixed(1)); }); // 更新折线图数据 chart.setOption({ series: [{ name: 'Cumulative %', data: cumSum }] }); }") ) %>% // 页面加载完成后初始化累计折线数据 e_on( event = "finished", handler = htmlwidgets::JS("function() { this.dispatchAction({ type: 'legendselectchanged', selected: this.getOption().legend[0].selected }); }") )
关键修改说明
- 数据层简化:仅保留zipcode排序逻辑,所有统计值改为动态计算
- 双Y轴配置:新增右侧Y轴专门展示百分比,避免数值范围冲突
- 事件监听:通过
legendselectchanged实时响应图例变化,finished事件初始化折线数据 - JS计算逻辑:在回调中完成选中系列的合计、百分比、累计值计算,并更新折线图
内容的提问来源于stack exchange,提问作者ddl
相关产品推荐
相关产品推荐

