jquick-pdf 逾期贷款饼图实战:三个选型问题先回答,再谈五级分类怎么画
官网示例:
- FORMULAS 示例页:www.jquick.org/jquick-pdf/...
- github 地址:github.com/paohaijiao/...
摘要(meta description):贷款五级分类要在一页纸上说清资产质量,饼图是最省字的选择,但它也最容易误用。本文先回答三个选型问题------为什么用它、什么时候不用、集成成本多大,再展开 jquick-pdf 的实现:JPie 加 JData 装配、item 级触发、红主题模板的左肩线标题与三色风险状态列,末尾把这 9,080 笔的数核了一遍(jquick-pdfx 4.7.0+,Apache-2.0)。
标签 :jquick-pdf、Java PDF、报表开发、饼图
真实渲染效果预览
以下为上述代码 + 模板真实运行生成的 PDF 首屏预览(由 jquick-pdf 渲染输出,非示意图):

三句话先把选型定了
做信贷风险报告的人看到饼图,第一反应通常是"又要画个饼"。所以我先把三个会被追问的问题摆出来,答案先给,理由后面展开。
用不用它?用。五个风险档位是互斥且穷尽的分类,加起来正好是全体资产,这种"部分对整体"的关系是饼图唯一真正擅长的场景。
什么时候不用?某档占比低于 3% 的时候。这份数据里损失类只有 150 笔、1.7%,扇形角度不到 6 度,画出来是一条缝,标签还得往外拉。
成本多大?二十来行代码,两个坑。一个在数据装配的命名上,一个在触发方式上,都在下面。
素材是 JQuickBiz19OverdueLoanPieTest,模板 report/biz/19_overdue_loan_pie.txt,输出 D:\test\biz\19_overdue_loan_pie.pdf。(效果预览图 alt:jquick-pdf 生成的贷款五级分类资产质量报告 PDF,浅红抬头卡片、饼图居中、红底表头的五级分类明细表)
问一:这个场景为什么选饼图
五级分类有一个别的图表替代不了的性质:五档之和就是 100%。正常、关注、次级、可疑、损失,一笔贷款必属其一,不存在第六种情况,也不存在一笔记两档。
这决定了读者看图的方式。柱状图里读者比的是柱高,关心"哪个多";饼图里读者比的是扇形面积占整个圆的比例,关心"占多少"。风险报告里真正要回答的是后者------不良率 10.9% 这个数字,比"次级比可疑多一倍"重要得多。
JPie 的装配是这样的:
java
JPie pie = new JPie("逾期账龄分布");
pie.data(
new JData().name("正常").value(6850),
new JData().name("关注").value(1240),
new JData().name("次级").value(560),
new JData().name("可疑").value(280),
new JData().name("损失").value(150)
);
option.series(pie);
JPie 构造收一个字符串,是系列名。data() 收变长的 JData,每个 JData 链式给 name 和 value。扇形的先后顺序就是这里的书写顺序,正常类 75.4% 一个人占了四分之三个圆,是整个饼的底色。
这里有个我先入为主踩到的点:那个构造参数写的是"逾期账龄分布",而这组数据是五级分类,两者不是一回事。账龄是逾期 30 天、60 天、90 天那套口径,五级分类是监管口径的资产质量分档。系列名会跟着图例和提示信息走,名字挂错了,图对但标签是错的。我在自己项目里改成了"贷款五级分类构成"。
option.tooltip().trigger(JTrigger.item) 这一行和柱图、折线图不一样,用的是 item 而不是 axis。饼图没有坐标轴可以挂,鼠标落在哪个扇形上就是哪个数据项,按项触发是唯一合理的做法。PDF 里没有悬停,这行留着不影响出图,但配置语义别照抄柱图那份。
问二:什么时候我会把饼图换掉
三种情况。
第一种是档位太多。五档是饼图的上限,六档以上扇形开始挤,颜色也不够分。真到了七八档,我会换成横向条形图按值排序,条形长度比扇形角度好读得多。
第二种是小占比档位有业务含义。这份数据里损失类 150 笔,占 1.7%,但"损失类"恰恰是风险报告最需要盯的一档,把它压成一条 6 度的缝,等于把最重要的信息藏起来。这种时候的处理有两个方向:一是把次级、可疑、损失合并成"不良"一个大扇形,再在表里拆开;二是干脆换成条形图,让 1.7% 也有完整的一行。
第三种是要看变化。饼图只能表达某一时点的构成,两张饼并排放也很难比出差异------人对扇形面积差的敏感度远不如对长度差。月度、季度对比必须上柱图或者折线。
这份报告的处理方式是折中的:图还是饼图,但在图上方的"核心结论"里把 990 笔、10.9% 这个合并值直接写成一句话。饼图负责让读者看到"正常类占了大半",文字负责把"不良 10.9%"这个必须被看见的数字摁在读者眼前。图和文各干一件事,谁都不勉强。
问三:集成成本到底多大
去掉 import 和目录创建,核心就三段:装配、渲染、落盘。
java
String svg = JChartRendererFactory.renderChart(JChartType.PIE, option);
枚举 PIE 全大写,返回 SVG 字符串。
java
JPdfConfig config = new JPdfConfig();
config.getLayoutConfig().setFlexLayout(true);
JQuickPdfFactory factory = new JQuickPdfFactory(config);
factory.bind("svg", svg);
byte[] bytes = factory.executeResource("report/biz/19_overdue_loan_pie.txt");
这段是所有图表单据的固定动作,setFlexLayout(true) 要在构造工厂之前。这份模板里 display:flex 只有图表容器一处,开关不打开,图会贴左。
两个坑都在前面提过:系列名"逾期账龄分布"和数据口径不符,JTrigger.item 不要照柱图的 axis 写。除此之外没有别的坑,饼图是这套 API 里配置最少的一种图------没有轴、没有网格、没有刻度,JOption 上只挂了 series 和 tooltip 两样东西。
红主题的克制:一份报告只用一种红
模板的配色值得单独说。整份文档只用了一个色系:#B71C1C 深红做主色,#FBECEC 浅红做底,#E8C4C4 做边框,#BF360C 做预警文字。四个色号,全在红色系里。
抬头是一张卡片:
html
<div style="background:#FBECEC; border:1px solid #E8C4C4; borderLeft:6px solid #B71C1C; borderRadius:4px; padding:12px; marginBottom:10px; keepTogether:true">
<h1 style="color:#B71C1C; fontSize:17pt; bold:true; marginBottom:4px">'贷款五级分类资产质量报告'</h1>
<p style="color:#5A6B7C; fontSize:9pt">'报告编号:CR-2024-Q3-019 | 生成日期:2024年10月8日 | 数据截止:2024年9月30日'</p>
</div>
浅红底加 1px 浅红边,左肩压一根 6px 深红竖线。风险类文档用红,但不能满页红------满页红读者会麻木,反而感觉不到风险。这里的处理是把红压进边框和文字,底色只用最浅的一档,整页看过去是"白纸上有红字",不是"红纸"。
章节标题是另一种形态:
html
<div style="borderLeft:4px solid #B71C1C; paddingLeft:8px; marginBottom:6px">
<h2 style="color:#B71C1C; fontSize:13pt; bold:true; marginTop:0px; marginBottom:0px">'二、五级分类明细'</h2>
</div>
h2 外面包一层带左肩线的 div,标题自己 margin:0,靠外层 paddingLeft:8px 把文字推离竖线。三个一级标题全是这个结构,左肩线 4px、和抬头那根 6px 形成主次------抬头是文档级,章节是区块级,线宽差 2px 就把层级说清楚了。
表格是标准的红底表头加全边框,四列 150 / 140 / 130 / 140 合计 560。最后一列"风险状态"是全表唯一带颜色的数据列:正常类标"达标"用绿色 #1B5E20,关注类标"关注"用橙色 #BF360C,次级、可疑标"预警",损失标"超标",后三个都是橙色加粗。
这一列是整张表的信息浓缩。读者不用去记 6.2% 算不算高,直接看"预警"两个字就知道该不该紧张。绿色只出现一次,橙色出现四次,一绿四橙的分布和图上的构成是一致的------正常类是多数,风险档位是少数但需要盯着。
第三个区块的列表用的是 <list style="paddingLeft:18px; fontSize:10pt" listType="ordered">,靠 listType="ordered" 出数字序号,不是 listStyleType。这两套写法在模板里都出现过,改的时候看清楚手上这份用的是哪个。
核对一遍:9,080 笔是怎么来的
模板里所有百分比都是硬编码,我把它们从代码里的五个数算了一遍。
总数:6850 + 1240 + 560 + 280 + 150 = 9,080,和抬头下面那句"三季度末信贷资产 9,080 笔"、明细表合计行、以及核心结论里的 9,080 全部一致。
占比:6850 ÷ 9080 = 75.44%,表里 75.4%;1240 ÷ 9080 = 13.66%,13.7%;560 ÷ 9080 = 6.17%,6.2%;280 ÷ 9080 = 3.08%,3.1%;150 ÷ 9080 = 1.65%,1.7%。
不良合计:560 + 280 + 150 = 990,990 ÷ 9080 = 10.90%,结论里写 10.9%,对。但表里三项占比 6.2 + 3.1 + 1.7 = 11.0%,和 10.9% 差 0.1 个百分点------这是三个数各自四舍五入后相加的尾差,不是算错。明细表里逐项列百分比、结论里给合并值,两处口径不同,读者拿计算器一加会觉得对不上。想彻底避免,就在表下加一句"占比四舍五入保留一位小数,合计可能存在 0.1 个百分点尾差"。
还有一个数:关注类比正常类低 61.7 个百分点,75.4 - 13.7 = 61.7,结论第一条里引用的就是这个差值。
五个数改动任意一个,模板里至少要跟着动六处:四个百分比、不良合计的笔数与占比、结论里的 61.7。这类单据最怕的就是只改图不改文,PDF 出来图上正常类是 70%,表里还写着 75.4%。
复现清单
- 依赖
io.github.paohaijiao:jquick-pdfx:4.7.0+(Apache-2.0 协议); new JOption(),设标题与副标,tooltip().trigger(JTrigger.item);new JPie("贷款五级分类构成"),data()里按正常到损失顺序给五个new JData().name(...).value(...);option.series(pie),renderChart(JChartType.PIE, option)取 SVG;JPdfConfig先setFlexLayout(true),再构造JQuickPdfFactory;bind("svg", svg)后executeResource("report/biz/19_overdue_loan_pie.txt"),写D:\test\biz\19_overdue_loan_pie.pdf;- 核对 9,080 总数、75.4 / 13.7 / 6.2 / 3.1 / 1.7 五个占比与 990 笔不良合计。
完整源码附录
java
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* Copyright (c) [2025-2099] Martin (goudingcheng@gmail.com)
*/
package com.github.paohaijiao.biz.chart;
import com.github.paohaijiao.JOption;
import com.github.paohaijiao.code.JTrigger;
import com.github.paohaijiao.config.JPdfConfig;
import com.github.paohaijiao.data.JData;
import com.github.paohaijiao.demo.constant.JQuickConstant;
import com.github.paohaijiao.enums.JChartType;
import com.github.paohaijiao.executor.JQuickPdfFactory;
import com.github.paohaijiao.factory.JChartRendererFactory;
import com.github.paohaijiao.series.JPie;
import org.junit.Test;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
/**
* 业务场景 Demo 19:逾期贷款占比(金融 / 信贷平台)
*
* <p>业务场景:信贷平台按贷款五级分类(正常、关注、次级、可疑、损失)统计资产笔数构成,
* 用于资产质量月度例会与风险预警,主色采用风险主题的低饱和深红 #B71C1C。</p>
* <p>实际图表类型:{@link JChartType#PIE} 饼图(展示各风险档位的资产占比)。</p>
*
* <p>模板:{@code report/biz/19_overdue_loan_pie.txt};图表以 SVG 文本形式绑定到模板的
* {@code ${svg}} 占位符,输出到 {@code D:\test\biz\19_overdue_loan_pie.pdf}。</p>
*
* <p>图表尺寸说明:图表按 700pt × 400pt(宽 × 高)的方形比例设计;模板中 {@code <svg>}
* 只声明宽度、不声明高度,因此宽度被页面自动收窄时高度会按原比例同步缩放,不会纵向拉伸变形。</p>
*
* @author Martin
* @version 1.0.0
*/
public class JQuickBiz19OverdueLoanPieTest {
public static final String path = JQuickConstant.path;
@Test
public void biz19OverdueLoanPie() throws IOException {
JOption option = new JOption();
option.title().text("贷款五级分类构成").subtext("2024年三季度末信贷资产(单位:笔)");
option.tooltip().trigger(JTrigger.item);
JPie pie = new JPie("逾期账龄分布");
// 五级分类笔数:正常占绝对多数,次级及以下为关注类风险敞口
pie.data(
new JData().name("正常").value(6850),
new JData().name("关注").value(1240),
new JData().name("次级").value(560),
new JData().name("可疑").value(280),
new JData().name("损失").value(150)
);
option.series(pie);
String svg = JChartRendererFactory.renderChart(JChartType.PIE, option);
File dir = new File(path + "biz");
if (!dir.exists()) {
dir.mkdirs();
}
FileOutputStream fileOutputStream = new FileOutputStream(path + "biz/19_overdue_loan_pie.pdf");
JPdfConfig config = new JPdfConfig();
// 开启 flex 行布局,使模板中的 display:flex 生效(图表居中)
config.getLayoutConfig().setFlexLayout(true);
JQuickPdfFactory factory = new JQuickPdfFactory(config);
factory.bind("svg", svg);
byte[] bytes = factory.executeResource("report/biz/19_overdue_loan_pie.txt");
fileOutputStream.write(bytes);
fileOutputStream.close();
}
}
html
<pdf>
<body>
<div style="background:#FBECEC; border:1px solid #E8C4C4; borderLeft:6px solid #B71C1C; borderRadius:4px; padding:12px; marginBottom:10px; keepTogether:true">
<h1 style="color:#B71C1C; fontSize:17pt; bold:true; marginBottom:4px">'贷款五级分类资产质量报告'</h1>
<p style="color:#5A6B7C; fontSize:9pt">'报告编号:CR-2024-Q3-019 | 生成日期:2024年10月8日 | 数据截止:2024年9月30日'</p>
</div>
<div style="marginBottom:10px">
<div style="borderLeft:4px solid #B71C1C; paddingLeft:8px; marginBottom:6px">
<h2 style="color:#B71C1C; fontSize:13pt; bold:true; marginTop:0px; marginBottom:0px">'一、资产质量概览'</h2>
</div>
<div style="background:#FBECEC; borderLeft:4px solid #B71C1C; padding:10px; borderRadius:3px; marginBottom:10px">
<p style="fontSize:10pt; color:#333; italic:true">'核心结论:三季度末信贷资产 9,080 笔,正常类占比 75.4%、关注类 13.7%,次级及以下不良资产合计 990 笔、占比 10.9%,整体质量可控,但不良处置压力仍然存在。'</p>
</div>
<div style="display:flex; justifyContent:center">
<svg style="width:700pt">${svg}</svg>
</div>
<p style="fontSize:9pt; color:#6c757d; italic:true; marginTop:6px">'图 1 2024年三季度末贷款五级分类构成(数据来源:信贷资产管理系统)'</p>
</div>
<div style="marginBottom:10px">
<div style="borderLeft:4px solid #B71C1C; paddingLeft:8px; marginBottom:6px">
<h2 style="color:#B71C1C; fontSize:13pt; bold:true; marginTop:0px; marginBottom:0px">'二、五级分类明细'</h2>
</div>
<table style="width:560px; fontSize:10pt">
<tr>
<th style="width:150px; backgroundColor:#B71C1C; color:white; padding:6px; textAlign:center; border:1px solid #E8C4C4">'五级分类'</th>
<th style="width:140px; backgroundColor:#B71C1C; color:white; padding:6px; textAlign:center; border:1px solid #E8C4C4">'笔数(笔)'</th>
<th style="width:130px; backgroundColor:#B71C1C; color:white; padding:6px; textAlign:center; border:1px solid #E8C4C4">'占比'</th>
<th style="width:140px; backgroundColor:#B71C1C; color:white; padding:6px; textAlign:center; border:1px solid #E8C4C4">'风险状态'</th>
</tr>
<tr>
<td style="padding:5px; border:1px solid #E8C4C4">'正常类'</td>
<td style="padding:5px; textAlign:right; border:1px solid #E8C4C4">'6,850'</td>
<td style="padding:5px; textAlign:right; border:1px solid #E8C4C4">'75.4%'</td>
<td style="padding:5px; textAlign:center; color:#1B5E20; bold:true; border:1px solid #E8C4C4">'达标'</td>
</tr>
<tr>
<td style="padding:5px; border:1px solid #E8C4C4">'关注类'</td>
<td style="padding:5px; textAlign:right; border:1px solid #E8C4C4">'1,240'</td>
<td style="padding:5px; textAlign:right; border:1px solid #E8C4C4">'13.7%'</td>
<td style="padding:5px; textAlign:center; color:#BF360C; bold:true; border:1px solid #E8C4C4">'关注'</td>
</tr>
<tr>
<td style="padding:5px; border:1px solid #E8C4C4">'次级类'</td>
<td style="padding:5px; textAlign:right; border:1px solid #E8C4C4">'560'</td>
<td style="padding:5px; textAlign:right; border:1px solid #E8C4C4">'6.2%'</td>
<td style="padding:5px; textAlign:center; color:#BF360C; bold:true; border:1px solid #E8C4C4">'预警'</td>
</tr>
<tr>
<td style="padding:5px; border:1px solid #E8C4C4">'可疑类'</td>
<td style="padding:5px; textAlign:right; border:1px solid #E8C4C4">'280'</td>
<td style="padding:5px; textAlign:right; border:1px solid #E8C4C4">'3.1%'</td>
<td style="padding:5px; textAlign:center; color:#BF360C; bold:true; border:1px solid #E8C4C4">'预警'</td>
</tr>
<tr>
<td style="padding:5px; border:1px solid #E8C4C4">'损失类'</td>
<td style="padding:5px; textAlign:right; border:1px solid #E8C4C4">'150'</td>
<td style="padding:5px; textAlign:right; border:1px solid #E8C4C4">'1.7%'</td>
<td style="padding:5px; textAlign:center; color:#BF360C; bold:true; border:1px solid #E8C4C4">'超标'</td>
</tr>
<tr>
<td style="padding:6px; bold:true; border:1px solid #E8C4C4">'合计'</td>
<td style="padding:6px; textAlign:right; bold:true; border:1px solid #E8C4C4">'9,080'</td>
<td style="padding:6px; textAlign:right; bold:true; border:1px solid #E8C4C4">'100.0%'</td>
<td style="padding:6px; textAlign:center; bold:true; border:1px solid #E8C4C4">'---'</td>
</tr>
</table>
</div>
<div style="marginBottom:10px">
<div style="borderLeft:4px solid #B71C1C; paddingLeft:8px; marginBottom:6px">
<h2 style="color:#B71C1C; fontSize:13pt; bold:true; marginTop:0px; marginBottom:0px">'三、风险管控建议'</h2>
</div>
<list style="paddingLeft:18px; fontSize:10pt" listType="ordered">
<li style="marginBottom:5px">'正常类占据绝对主导:正常类 6,850 笔、占比 75.4%,较关注类高出 61.7 个百分点,基础资产整体稳健。'</li>
<li style="marginBottom:5px">'关注类需前置管控:关注类 1,240 笔、占比 13.7%,一旦向下迁徙将直接推高不良,须按月跟踪迁徙率变化。'</li>
<li style="marginBottom:5px">'不良处置压力集中:次级 560 笔、可疑 280 笔、损失 150 笔,合计 990 笔、占比 10.9%,建议加快核销与清收进度。'</li>
<li style="marginBottom:5px">'总体目标:将关注类占比压降至 12% 以内,次级及以下不良占比控制在 9% 以内。'</li>
<li style="marginBottom:5px">'操作建议:对次级及以下 990 笔资产建立"一户一策"清收台账,按季度考核现金回收率。'</li>
</list>
</div>
<p style="textAlignment:center; color:#8a939b; fontSize:8pt; italic:true; marginTop:10px">'本报告仅供信贷资产质量分析使用 | 数据来源:信贷资产管理系统 | 编制部门:风险管理部'</p>
</body>
</pdf>