ARTICLE · 1357723
坑死我了!PDF 气泡图 X 轴传数字,图表直接凭空消失|信用报告完整源码
坑死我了!PDF 气泡图 X 轴传数字,图表直接凭空消失|信用报告完整源码标签:jquick-pdf,Java PDF,报表开发,气泡图,信用报告 摘要:气泡图是散点图的加料版——x、y 定位置,size 定敞口,颜色定等级,一条数据同时编码四个维度。本文以 jquick-pdf 气泡图为例,跟着"客户C:账龄 9 个月、逾期率 6.8%、敞口 410、高风险"这条数据走完全程:从 Map 组装、轴与图例挂载,到 renderChart 出 SVG、setFlexLayout 开并排、模板落版成 PDF,每一站都有代码和渲染结果对照。附完整 Java 源码与模板。项目地址:https://www.jquick.org/jquick-pdf/ 旅程起点:一条待画进报告的风险数据 评级报告的风险章节里躺着一条记录:客户C,账龄 9 个月,逾期率 6.8%,风险敞口 410 万元,风险等级"高风险"。业务方的要求是把六家授信客户的风险画在一张图上:横轴看账龄、纵轴看逾期率、气泡大小看敞口、颜色看等级。四个维度挤一张图,柱状图装不下、折线图更不合适,气泡图是散点图的加料版——x、y 定位置,size 定气泡大小,颜色定等级,一条数据同时编码四个维度,是这类需求的标准答案。这篇按官方 Demo JQuickCreditReport14BubbleTest 的真实代码跟着客户C 这条数据走完全程:Map 组装、挂轴挂图例、入列渲染、进模板落版,每一站都给代码和渲染结果对照,输出 D:\test\creditreport\14_bubble.pdf。 第一站:Map 组装——一条数据的六件行李 private staticMap<String, Object> bubblePoint(String name, String x, double y, double size, String category) { Map<String, Object> point = newHashMap<>(); point.put("x", x); point.put("y", y); point.put("size", size); point.put("category", category); point.put("name", name); point.put("color", bubbleColor(category)); return point; } 客户C 的数据在第一站被装进一个 Map,六件行李各管一件事:x 是账龄"9",y 是逾期率 6.8,size 是敞口 410——它决定气泡画多大;category 是"高风险",name 是"客户C",color 由等级换算出来。这里最值得记的是类型约定:x 传字符串"9"、y 传 double,因为散点系数据里 x 走类目轴所以是类目标签,y 走数值轴所以是数值。这个类型搞反渲染不会报错,但点位直接从图上消失,排查起来非常费时间。 color 这一趟在 Java 侧就定死了: private staticColor bubbleColor(String category) { if ("低风险".equals(category)) return newColor(2, 136, 209, 180); if ("中风险".equals(category)) return newColor(255, 167, 38, 180); return newColor(229, 57, 53, 180); // ★ 高风险红,alpha 180 半透明 } 低风险蓝 (2,136,209)、中风险橙 (255,167,38)、高风险红 (229,57,53),三档 alpha 全是 180——半透明的气泡叠在网格上不挡坐标线,多个气泡挨着时也能透出边界。客户C 拿到的是那颗红色。这里有个容易忽略的细节:alpha 写满 255 全不透明,气泡一重叠就把彼此的边界盖死,高风险客户扎堆时整张图糊成一团,180 的半透明是留给重叠场景的余量,别随手改掉。 第二站:挂轴挂图例——坐标语义在这里定下 JOption option = newJOption(); option.title().text("风险气泡分布").subtext("气泡大小代表风险敞口,颜色代表风险等级"); option.legend("低风险", "中风险", "高风险"); option.xAxis(newCategoryAxis().name("账龄(月)")); option.yAxis(newValueAxis().name("逾期率(%)")); 这一站决定数据"站在哪里、归哪一类"。x 轴用 CategoryAxis 挂名"账龄(月)",y 轴用 ValueAxis 挂名"逾期率(%)"——轴名会渲染在坐标轴旁,报告评审不读正文也知道两条轴是什么。副标题这行写法值得记:title().text().subtext() 链式两连,副标题"气泡大小代表风险敞口,颜色代表风险等级"直接承担了图例说明的职责,四维编码里的 size 和 color 不用在正文再解释一遍。 legend() 传的三个词和 Map 里 category 的取值一一对应——图例不是摆设,它是渲染器给同 category 气泡归类标注的依据,词对不上,图例就真的只剩摆设功能,客户C 的"高风险"在图例里找不到自己的位置,气泡顶上也就挂不上名字。 第三站:入列渲染——六条数据变成一颗 SVG ScatterSeries series = newScatterSeries("风险敞口"); seriesData.add(bubblePoint("客户C", "9", 6.8, 410, "高风险")); // ★ 其余五条同理:客户A 3/1.2/120/低风险,客户E 12/9.4/560/高风险 ... series.data(seriesData.toArray()); option.series(series); String svg = JChartRendererFactory.renderChart(JChartType.Bubble, option); 六条数据按账龄排进 ScatterSeries——系列名"风险敞口"和气泡语义对齐。data() 收 Object 数组,每个元素就是一个 Map;渲染类型 JChartType.Bubble,注意各图的枚举命名风格不统一(有全大写也有驼峰),照 Demo 抄最稳。这一站结束,客户C 变成了 SVG 里的一颗半透明红圆:圆心落在横轴"9"、纵轴 6.8 的交叉点附近,半径由敞口 410 换算,名字"客户C"标在气泡上。整幅图的信息量在渲染这一步全部定型。 六条数据的排布也值得在入列前检查一遍。Demo 里账龄从 3 到 12、逾期率从 1.2 到 9.4,等级两低、两中、两高三档均衡——三档颜色在图上都有代表,图例三个词没有一个落空。真实授信组合不会这么巧:某一批客户全是低风险时,图例里"高风险"三项还挂着却图上无泡,评审第一句就会问"红泡呢"。遇到这种数据,要么把空档等级从 legend() 里摘掉,要么在报告正文里补一句"本期无高风险敞口",别让图例说图外的话。 两条轴的选型也有讲究。y 轴选 ValueAxis 的实际好处是刻度由数值范围自动算:六家客户逾期率落在 1.2 到 9.4 之间,纵轴刻度自动铺开,不用手动定 0 到 10 的步长;x 轴选 CategoryAxis 则是因为账龄在授信场景里天然是档位(3 个月、6 个月、9 个月、12 个月),按类目原样摆标签比按连续数值算刻度更贴合业务读法。两条轴一连续一离散,坐标语义在选型时就想清楚,渲染出来才不会出现刻度怪相。 第四站:进模板落版——SVG 与说明面板并肩 <div style="display:flex; justifyContent:space-between"> <svg style="width:390px;height:400px">${svg}</svg> <div style="width:300px; backgroundColor:#e1f5fe; borderRadius:6px; padding:12px"> <h3 style="color:#01579B; fontSize:12; bold:true">'评级指标说明'</h3> <!-- ★ 四条评分:财务90 / 经营95 / 偿债85 / 潜力92 --> </div> </div> SVG 以 390×400 的尺寸注入 ${svg},右侧 300px 宽的浅蓝 #e1f5fe 圆角卡装四条评分。这是旅程的最后一道工序,也是最容易翻车的一站:display:flex 要生效,Java 侧的 config.getLayoutConfig().setFlexLayout(true) 必须开着,否则图和面板上下堆叠,整个第二屏直接多出一截。出 PDF 的固定流程就五行: JPdfConfig config = newJPdfConfig(); // ★ 开启 flex 行布局:忘开就是图与说明面板上下堆叠 config.getLayoutConfig().setFlexLayout(true); JQuickPdfFactory factory = newJQuickPdfFactory(config); factory.bind("svg", svg); byte[] bytes = factory.executeResource("report/credit_report_14_bubble.txt"); 整页天蓝主题从抬头贯穿到页脚:渐变蓝抬头(#01579B → #03A9F4)白字居中,企业信息表 #0288D1 蓝底表头加 #e1f5fe 浅蓝标签列,综合评价卡浅蓝底加 4px 蓝色左肩线,页脚小字用 #4fc3f7 亮蓝。抬头这块渐变带的配色梯度值得留意一句:起点 #01579B 是很深的海蓝,终点 #03A9F4 是明亮的天蓝,135 度对角渐变从左上深到右下浅;标题、编号、机构三行文字全压白字,只用 #e1f5fe 极浅蓝做小字与主标题的层次差。渐变跨度大时,纯白小字在浅蓝天底上会糊,灰色小字又压不住深蓝端,#e1f5fe 在两头都站得住。这套抬头在三档蓝之间只动了一个变量(明度),改模板配色时照这个思路调,比东改一个色值西改一个色值稳。 终点站:PDF 里它长什么样 客户C 这条数据最终在 PDF 第二屏出现:左半边一颗 410 量级的红色气泡压在坐标 (9, 6.8) 上,名字可读;右边面板四条评分分数齐全;再往下综合评价卡的 AAA 结论收尾。评审从气泡图上直接得到"高敞口客户集中在高账龄高逾期区间"的判断,面板分数和评价卡给出结论闭环——四个维度的编码在这一页全部兑现。 整页的阅读动线也顺:抬头定基调,信息表核对主体,财务表看经营数字,气泡图把风险维度补上,说明面板给分数,评价卡下结论。气泡图放在财务表之后不是随便排的——先看过营收净利润的规模,再看敞口 120 到 560 万元的量级才有参照系,敞口数字和营收数字在同一个量级世界里,图读起来不悬空。若把气泡图提到信息表前面,评审带着"这公司是谁"的疑问读图,注意力反而散了。 复现清单与项目地址 1. 依赖 io.github.paohaijiao:jquick-pdfx:4.7.0+(Apache-2.0 协议); 2. bubblePoint 组装 Map:x(类目字符串)、y(数值)、size(敞口)、category、name、color 六键,类型别搞反; 3. 气泡颜色用带 alpha 的 Color(三档 180),ScatterSeries 收 Object 数组,JOption.series() 挂载; 4. CategoryAxis/ValueAxis 挂轴名,legend() 三项对齐 category 取值,title().text().subtext() 链式写副标题; 5. JChartType.Bubble 渲染 SVG,图上没有的等级从 legend() 里摘掉; 6. setFlexLayout(true) 后 bind("svg", svg),executeResource 出 D:\test\creditreport\14_bubble.pdf。 
jquick-pdf 的完整示例源码与文档:项目官网 https://www.jquick.org/jquick-pdf/ ,GitHub 仓库 https://github.com/paohaijiao/jquick-pdf 。官网示例页:https://www.jquick.org/jquick-pdf/demo/14_bubble/ 。下一篇拆时间线图,同一份信用报告模板继续换图。 — 完 —
















































































