jquick-pdf 饼图(JPie)实战:Java 生成贷款五级分类资产质量报告

jquick-pdf 逾期贷款饼图实战:三个选型问题先回答,再谈五级分类怎么画

官网示例:

摘要(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%。

复现清单

  1. 依赖 io.github.paohaijiao:jquick-pdfx:4.7.0+(Apache-2.0 协议);
  2. new JOption(),设标题与副标,tooltip().trigger(JTrigger.item);
  3. new JPie("贷款五级分类构成"),data() 里按正常到损失顺序给五个 new JData().name(...).value(...);
  4. option.series(pie),renderChart(JChartType.PIE, option) 取 SVG;
  5. JPdfConfig 先 setFlexLayout(true),再构造 JQuickPdfFactory;
  6. bind("svg", svg) 后 executeResource("report/biz/19_overdue_loan_pie.txt"),写 D:\test\biz\19_overdue_loan_pie.pdf;
  7. 核对 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>
相关推荐
学长毕设1 小时前
基于SpringBoot的景点管理系统-附源码
java·spring boot·后端
生锈的键盘2 小时前
从实战搞懂 Docker:一个 Python 项目的开发环境容器化笔记
后端
九零HTTP3 小时前
RFC 7230 把请求行分了四种,网关只用得上两种
后端
SomeOtherTime4 小时前
Rust生命周期介绍(AI回答)
开发语言·后端·rust
无责任此方_修行中4 小时前
「不止技术」AIE 2026 会后感悟
人工智能·后端·ai编程
SimonKing5 小时前
Workbuddy太占C盘空间了,写个迁移工具迁走10G的数据
java·后端·程序员
喵了几个咪6 小时前
GoWind Flow 拆解:一个 Go 工作流引擎与它的双模式设计器
开发语言·人工智能·后端·golang
倔强的石头_6 小时前
AI数据库是什么?我把金仓KES的库内三件套拆开看了一遍
后端