Android Apache POI Excel开发:散点图Y轴添加字符串分类值及自定义数据标签实现咨询
Android Apache POI Excel开发:散点图Y轴添加字符串分类值及自定义数据标签实现咨询
最近在做Android端用Apache POI生成Excel散点图的需求,需要把Y轴的字符串分类转换成对应数值lane,同时给每个系列的第一个点自定义从指定单元格读取的标签,还得隐藏掉不需要的图表元素。整理了实现代码和关键细节,跟大家分享下:
一、核心功能说明
- 将Y轴的字符串分类(如事件类型)映射为连续数值lane,实现时间线式散点图效果
- 给每个系列的第一个点添加自定义标签,标签内容从指定辅助单元格读取
- 隐藏系列连线,仅保留圆形标记点
- 关闭所有默认数据标签,仅显示自定义单个点标签
- 清除图表级数据标签,避免干扰自定义设置
二、完整实现代码
fun eventsTimelineScatter( drawSheet: XSSFSheet, // ΠΟΥ ζωγραφίζει (π.χ. "Charts") dataSheet: XSSFSheet, // ΑΠΟ ΠΟΥ διαβάζει (π.χ. "PlotData") lastRow: Int, title: String, anchorCols: IntRange, anchorRows: IntRange, xCol: Int, // στήλη χρόνου (Excel serial, numeric) typeCol: Int // στήλη κατηγορίας (String) ) { data class Cat(val key: String, val display: String) fun norm(raw: String?): Cat { val d = (raw ?: "Unknown").trim().replace(Regex("\\s+"), " ") return Cat(d.lowercase().ifEmpty { "unknown" }, d.ifEmpty { "Unknown" }) } val byType = mutableMapOf<String, MutableList<Pair<Double,Double>>>() val keyToDisplay = mutableMapOf<String,String>() val laneOf = mutableMapOf<String,Int>() fun lane(k: String) = laneOf.getOrPut(k) { laneOf.size + 1 }.toDouble() var xMin = Double.POSITIVE_INFINITY var xMax = Double.NEGATIVE_INFINITY for (r in 1..lastRow) { val row = dataSheet.getRow(r) ?: continue val cat = norm(row.getCell(typeCol)?.toString()) val x = row.getCell(xCol)?.numericCellValue ?: continue byType.getOrPut(cat.key) { mutableListOf() }.add(x to lane(cat.key)) keyToDisplay.putIfAbsent(cat.key, cat.display) if (x < xMin) xMin = x if (x > xMax) xMax = x } if (byType.isEmpty()) return // 创建辅助单元格存储系列名称(供标签引用) val legendCol = (anchorCols.last + 2).coerceAtLeast(24) val legendRow0 = anchorRows.first val keysInOrder = keyToDisplay.keys.sortedBy { keyToDisplay[it]!!.lowercase() } keysInOrder.forEachIndexed { i, k -> val row = drawSheet.getRow(legendRow0 + i) ?: drawSheet.createRow(legendRow0 + i) row.createCell(legendCol).setCellValue(keyToDisplay[k]) } // 初始化图表容器与坐标轴 val drawing = drawSheet.createDrawingPatriarch() as XSSFDrawing val anchor = drawing.createAnchor(0,0,0,0, anchorCols.first, anchorRows.first, anchorCols.last+1, anchorRows.last+1) val chart = drawing.createChart(anchor) as XSSFChart val xAxis = chart.createValueAxis(AxisPosition.BOTTOM).apply { crosses = AxisCrosses.MIN majorTickMark = AxisTickMark.OUT minorTickMark = AxisTickMark.NONE tickLabelPosition = AxisTickLabelPosition.LOW } val yAxis = chart.createValueAxis(AxisPosition.LEFT).apply { majorTickMark = AxisTickMark.NONE minorTickMark = AxisTickMark.NONE tickLabelPosition = AxisTickLabelPosition.NONE } // 创建散点图数据系列 val data = chart.createData(ChartTypes.SCATTER, xAxis, yAxis) as XDDFScatterChartData data.setVaryColors(false) try { data.style = ScatterStyle.MARKER } catch (_: Throwable) {} keysInOrder.forEachIndexed { i, k -> val pts = byType[k]!!.sortedBy { it.first } val xs = XDDFDataSourcesFactory.fromArray(pts.map { it.first }.toTypedArray()) val ys = XDDFDataSourcesFactory.fromArray(pts.map { it.second }.toTypedArray()) val s = data.addSeries(xs, ys) as XDDFScatterChartData.Series val ref = CellReference(drawSheet.sheetName, legendRow0 + i, legendCol, true, true) s.setTitle(null, ref) s.setMarkerStyle(MarkerStyle.CIRCLE) s.setMarkerSize(7) s.isSmooth = false } chart.plot(data) chart.setTitleText(title) chart.setTitleOverlay(false) // --- CT层级深度自定义(XDDF API未覆盖的细节)--- chart.ctChart.plotArea.scatterChartList.lastOrNull()?.let { sc -> // 全局设置:仅显示标记点,隐藏连线 val st = sc.scatterStyle ?: sc.addNewScatterStyle() st.`val` = STScatterStyle.MARKER sc.serList.forEachIndexed { i, ser -> // 隐藏当前系列的连线 val sp = if (ser.isSetSpPr) ser.spPr else ser.addNewSpPr() val ln = if (sp.isSetLn) sp.ln else sp.addNewLn() if (!ln.isSetNoFill) ln.addNewNoFill() // 重置系列默认数据标签,创建自定义标签组 if (ser.isSetDLbls) ser.unsetDLbls() val dLbls = ser.addNewDLbls() // 仅给第0个点添加标签,内容引用辅助单元格 val dl = dLbls.addNewDLbl() dl.addNewIdx().`val` = 0L val ref = CellReference(drawSheet.sheetName, legendRow0 + i, legendCol, true, true) dl.addNewTx().addNewStrRef().f = ref.formatAsString() // 关闭所有默认标签显示项 dl.addNewShowSerName().`val` = false dl.addNewShowVal().`val` = false dl.addNewShowCatName().`val` = false dl.addNewShowLegendKey().`val` = false dl.addNewShowPercent().`val` = false dl.addNewShowBubbleSize().`val` = false // 设置标签位置 dl.addNewDLblPos().`val` = STDLblPos.L } // 清除图表级数据标签,避免干扰自定义设置 if (sc.isSetDLbls) sc.unsetDLbls() } }
三、关键细节解释
1. Y轴字符串分类转数值Lane
用laneOf Map给每个唯一分类分配递增整数(从1开始),转成Double作为Y轴数值,把离散字符串分类映射为连续Y轴刻度,实现垂直lane的时间线效果。
2. 自定义数据标签的核心逻辑
XDDF API未完全覆盖所有数据标签自定义选项,直接操作POI的CT层级对象是解决方案:
- 给每个系列创建
DLbls,仅给idx=0的点添加标签 - 通过
StrRef引用辅助单元格内容作为标签文本 - 强制关闭所有默认标签开关,避免冗余内容
- 清除图表级
DLbls,防止默认设置覆盖自定义配置
3. 隐藏系列连线
通过设置系列SpPr下的Ln为NoFill,配合全局scatterStyle设为MARKER,实现仅显示圆形标记点、隐藏连线的效果。
4. 辅助单元格的作用
在绘图工作表中创建辅助单元格存储系列名称,数据标签和系列标题统一引用这些单元格,确保内容一致性,也方便后续修改维护。
如果大家做类似POI图表自定义需求时,遇到XDDF API不够用的情况,直接操作CT层级对象是个很有效的办法,记得注意不同POI版本的兼容性哦~




