DEV Community

paohaijiao
paohaijiao

Posted on

Heatmap of Monthly Revenue Across Regions

jquick-pdf by example — demo #10 of 184. Every article in this series ships one complete, runnable demo: a template file and a Java class. No snippets, no omissions.

Twelve months by eight regions, coloured by revenue band — the fastest way to spot seasonality.

This document belongs to the Credit report — chart series set. Like every demo in the series it has its own colour scheme and layout combination, derived deterministically from the demo number — so the look is stable across runs and two neighbouring documents never look alike.

What you'll build

  • A PDF written to D:/test/creditreport/10_heatmap.pdf
  • A template file (report/credit_report_10_heatmap.txt) with 1 bound placeholder
  • A detail grid of 12 rows × 6 columns, laid out with width:100% so it fills the text area
  • One self-contained Java class (JQuickCreditReport10HeatmapTest) that binds the data and renders the file

Bound placeholders

${svg}
Enter fullscreen mode Exit fullscreen mode

Prerequisites

Java 8+ and one dependency — jquick-pdf is built on Apache PDFBox, not iText:

<dependency>
    <groupId>io.github.paohaijiao</groupId>
    <artifactId>jquick-pdfx</artifactId>
    <version>4.2.0</version>
</dependency>
Enter fullscreen mode Exit fullscreen mode

Coordinates: io.github.paohaijiao:jquick-pdfx:4.2.0.

Step 1 — the template file

JPdfX is jquick-pdf's HTML-like template language. It lives in its own file (src/test/resources/report/credit_report_10_heatmap.txt) instead of a Java string, so designers can edit it without touching code.

<pdf>
<body>
  <div style="background:linear-gradient(135deg, #311B92 0%, #673AB7 100%); borderRadius:8px; paddings:16px; marginBottom:18px">
    <h1 style="fontSize:21; color:white; textAlignment:center; bold:true; marginBottom:12px">'企业信用评级报告'</h1>
    <div style="display:flex; justifyContent:space-around">
      <div style="width:180px; textAlignment:center">
        <div style="fontSize:8; color:#d1c4e9; textAlignment:center; marginBottom:3px">'报告编号'</div>
        <div style="fontSize:10; color:white; textAlignment:center">'CR-2023-08975'</div>
      </div>
      <div style="width:180px; textAlignment:center">
        <div style="fontSize:8; color:#d1c4e9; textAlignment:center; marginBottom:3px">'评估日期'</div>
        <div style="fontSize:10; color:white; textAlignment:center">'2023年11月15日'</div>
      </div>
      <div style="width:180px; textAlignment:center">
        <div style="fontSize:8; color:#d1c4e9; textAlignment:center; marginBottom:3px">'评估机构'</div>
        <div style="fontSize:10; color:white; textAlignment:center">'四川省企业信用评估中心'</div>
      </div>
    </div>
  </div>

  <div style="marginBottom:18px">
    <h2 style="fontSize:14; color:#311B92; bold:true; marginBottom:10px">'四川省泡海椒科技有限公司'</h2>
    <table style="width:600px; fontSize:10">
      <tr>
        <th style="backgroundColor:#512DA8; color:white; bold:true; padding:6px; textAlignment:left; width:160px">'统一信用代码'</th>
        <td style="padding:6px; textAlignment:left; width:440px">'91110108MA01XX1234'</td>
      </tr>
      <tr>
        <th style="backgroundColor:#ede7f6; color:#311B92; bold:true; padding:6px; textAlignment:left">'成立日期'</th>
        <td style="padding:6px; textAlignment:left">'2018年5月20日'</td>
      </tr>
      <tr>
        <th style="backgroundColor:#ede7f6; color:#311B92; bold:true; padding:6px; textAlignment:left">'注册资本'</th>
        <td style="padding:6px; textAlignment:left">'1000万元人民币'</td>
      </tr>
      <tr>
        <th style="backgroundColor:#ede7f6; color:#311B92; bold:true; padding:6px; textAlignment:left">'法定代表人'</th>
        <td style="padding:6px; textAlignment:left">'泡海椒'</td>
      </tr>
      <tr>
        <th style="backgroundColor:#ede7f6; color:#311B92; bold:true; padding:6px; textAlignment:left">'所属行业'</th>
        <td style="padding:6px; textAlignment:left">'软件和信息技术服务业'</td>
      </tr>
      <tr>
        <th style="backgroundColor:#ede7f6; color:#311B92; bold:true; padding:6px; textAlignment:left">'企业规模'</th>
        <td style="padding:6px; textAlignment:left">'中型(150-200人)'</td>
      </tr>
    </table>
  </div>

  <div style="marginBottom:18px">
    <h2 style="fontSize:13; color:#311B92; bold:true; marginBottom:10px">'历年财务数据(单位:万元)'</h2>
    <table style="width:600px; fontSize:10">
      <tr>
        <th style="backgroundColor:#512DA8; color:white; bold:true; padding:6px; textAlignment:left">'财务指标'</th>
        <th style="backgroundColor:#512DA8; color:white; bold:true; padding:6px; textAlignment:right">'2020年'</th>
        <th style="backgroundColor:#512DA8; color:white; bold:true; padding:6px; textAlignment:right">'2021年'</th>
        <th style="backgroundColor:#512DA8; color:white; bold:true; padding:6px; textAlignment:right">'2022年'</th>
        <th style="backgroundColor:#512DA8; color:white; bold:true; padding:6px; textAlignment:right">'2023年'</th>
        <th style="backgroundColor:#512DA8; color:white; bold:true; padding:6px; textAlignment:right">'增长率'</th>
      </tr>
      <tr>
        <td style="padding:6px; textAlignment:left; borderBottom:1px solid #ede7f6; bold:true">'营业收入'</td>
        <td style="padding:6px; textAlignment:right; borderBottom:1px solid #ede7f6">'8,560'</td>
        <td style="padding:6px; textAlignment:right; borderBottom:1px solid #ede7f6">'12,340'</td>
        <td style="padding:6px; textAlignment:right; borderBottom:1px solid #ede7f6">'18,920'</td>
        <td style="padding:6px; textAlignment:right; borderBottom:1px solid #ede7f6">'25,680'</td>
        <td style="padding:6px; textAlignment:right; borderBottom:1px solid #ede7f6; color:#512DA8; bold:true">'+44.2%'</td>
      </tr>
      <tr>
        <td style="padding:6px; textAlignment:left; borderBottom:1px solid #ede7f6; bold:true; backgroundColor:#f6f4fb">'净利润'</td>
        <td style="padding:6px; textAlignment:right; borderBottom:1px solid #ede7f6; backgroundColor:#f6f4fb">'1,240'</td>
        <td style="padding:6px; textAlignment:right; borderBottom:1px solid #ede7f6; backgroundColor:#f6f4fb">'2,150'</td>
        <td style="padding:6px; textAlignment:right; borderBottom:1px solid #ede7f6; backgroundColor:#f6f4fb">'3,890'</td>
        <td style="padding:6px; textAlignment:right; borderBottom:1px solid #ede7f6; backgroundColor:#f6f4fb">'5,420'</td>
        <td style="padding:6px; textAlignment:right; borderBottom:1px solid #ede7f6; backgroundColor:#f6f4fb; color:#512DA8; bold:true">'+63.8%'</td>
      </tr>
      <tr>
        <td style="padding:6px; textAlignment:left; borderBottom:1px solid #ede7f6; bold:true">'总资产'</td>
        <td style="padding:6px; textAlignment:right; borderBottom:1px solid #ede7f6">'15,800'</td>
        <td style="padding:6px; textAlignment:right; borderBottom:1px solid #ede7f6">'22,450'</td>
        <td style="padding:6px; textAlignment:right; borderBottom:1px solid #ede7f6">'31,200'</td>
        <td style="padding:6px; textAlignment:right; borderBottom:1px solid #ede7f6">'42,800'</td>
        <td style="padding:6px; textAlignment:right; borderBottom:1px solid #ede7f6; color:#512DA8; bold:true">'+39.5%'</td>
      </tr>
      <tr>
        <td style="padding:6px; textAlignment:left; borderBottom:1px solid #ede7f6; bold:true; backgroundColor:#f6f4fb">'净资产'</td>
        <td style="padding:6px; textAlignment:right; borderBottom:1px solid #ede7f6; backgroundColor:#f6f4fb">'9,200'</td>
        <td style="padding:6px; textAlignment:right; borderBottom:1px solid #ede7f6; backgroundColor:#f6f4fb">'13,800'</td>
        <td style="padding:6px; textAlignment:right; borderBottom:1px solid #ede7f6; backgroundColor:#f6f4fb">'19,500'</td>
        <td style="padding:6px; textAlignment:right; borderBottom:1px solid #ede7f6; backgroundColor:#f6f4fb">'26,400'</td>
        <td style="padding:6px; textAlignment:right; borderBottom:1px solid #ede7f6; backgroundColor:#f6f4fb; color:#512DA8; bold:true">'+42.1%'</td>
      </tr>
      <tr>
        <td style="padding:6px; textAlignment:left; borderBottom:1px solid #ede7f6; bold:true">'毛利率'</td>
        <td style="padding:6px; textAlignment:right; borderBottom:1px solid #ede7f6">'42.5%'</td>
        <td style="padding:6px; textAlignment:right; borderBottom:1px solid #ede7f6">'45.8%'</td>
        <td style="padding:6px; textAlignment:right; borderBottom:1px solid #ede7f6">'48.2%'</td>
        <td style="padding:6px; textAlignment:right; borderBottom:1px solid #ede7f6">'51.6%'</td>
        <td style="padding:6px; textAlignment:right; borderBottom:1px solid #ede7f6; color:#512DA8; bold:true">'+9.1%'</td>
      </tr>
    </table>
  </div>

  <div style="marginBottom:18px">
    <h2 style="fontSize:13; color:#311B92; bold:true; marginBottom:10px">'关键经营指标'</h2>
    <div style="display:flex; justifyContent:space-between">
      <div style="width:190px; padding:10px; border:1px solid #673AB7; borderRadius:6px; fontSize:10">
        '营收增长率:连续三年保持40%以上增长,2023年达到25,680万元。'
        '利润率提升:净利率从14.5%提升至21.1%,盈利能力显著增强。'
      </div>
      <div style="width:190px; padding:10px; border:1px solid #673AB7; borderRadius:6px; fontSize:10">
        '研发投入:研发费用年均增长38.9%,占营收比例达13.3%。'
        '人才结构:技术研发人员占比42%,硕士以上学历占35%。'
      </div>
      <div style="width:190px; padding:10px; border:1px solid #673AB7; borderRadius:6px; fontSize:10">
        '客户质量:服务15家上市公司,客户续约率92%,大客户满意度4.8/5.0。'
        '资产质量:资产负债率维持在38%左右,流动比率2.8,偿债能力良好。'
      </div>
    </div>
  </div>

  <div style="marginBottom:18px">
    <h2 style="fontSize:13; color:#311B92; bold:true; textAlignment:center; marginBottom:10px">'月度经营热力图'</h2>
    <div style="textAlignment:center">
      <svg style="width:450px;height:400px">${svg}</svg>
    </div>
    <div style="marginTop:10px">
      <h3 style="fontSize:12; color:#311B92; bold:true; textAlignment:center; marginBottom:8px">'评级指标说明'</h3>
      <div style="display:flex; justifyContent:space-between">
        <div style="width:290px">
          <div style="fontSize:10; marginBottom:6px">
            <span style="color:#512DA8; bold:true">'● 财务状况(90分): '</span>
            '营收稳定增长'
          </div>
          <div style="fontSize:10; marginBottom:6px">
            <span style="color:#2ecc71; bold:true">'● 经营能力(95分): '</span>
            '客户留存率高'
          </div>
        </div>
        <div style="width:290px">
          <div style="fontSize:10; marginBottom:6px">
            <span style="color:#e74c3c; bold:true">'● 偿债能力(85分): '</span>
            '负债率合理'
          </div>
          <div style="fontSize:10; marginBottom:6px">
            <span style="color:#f39c12; bold:true">'● 发展潜力(92分): '</span>
            '研发投入增加'
          </div>
        </div>
      </div>
    </div>
  </div>

  <div style="marginBottom:18px; background:linear-gradient(135deg, #ede7f6 0%, #d1c4e9 100%); borderRadius:10px; padding:16px; borderLeft:4px solid #673AB7">
    <h2 style="fontSize:13; color:#311B92; bold:true; marginBottom:8px">'综合评价'</h2>
    <p style="fontSize:11; textAlignment:left; marginBottom:8px">
      '四川省泡海椒科技有限公司作为国家级高新技术企业和专精特新"小巨人"企业,在云计算和大数据领域表现出色。'
      '公司财务状况健康,经营能力突出,技术创新能力强,具备良好的发展潜力。信用评级为AAA级,展望稳定。'
    </p>
    <p style="fontSize:8; color:#512DA8; textAlignment:right">'认证通过'</p>
  </div>

  <div style="textAlignment:center; marginTop:20px">
    <lineSeparator style="strokeColor:#512DA8"></lineSeparator>
    <p style="fontSize:9; color:#6c757d; textAlignment:center; marginTop:8px">'本报告依据公开信息和专业评估模型生成,仅供参考 | 报告生成时间: 2025年09月06日'</p>
    <p style="fontSize:9; color:#6c757d; textAlignment:center">'© 2023 企业信用评估中心 版权所有 | 认证编号: CE-2023-001'</p>
  </div>
</body>
</pdf>
Enter fullscreen mode Exit fullscreen mode

Step 2 — the Java class

jquick-pdfx/src/test/java/com/github/paohaijiao/biz/report/JQuickCreditReport10HeatmapTest.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.report;

import com.github.paohaijiao.JOption;
import com.github.paohaijiao.axis.JCategoryAxis;
import com.github.paohaijiao.config.JPdfConfig;
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.JHeatmap;
import org.junit.Test;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;

/**
 * 统一信用报告 —— 风格 10:热力图(深紫)
 *
 * <p>以 {@code report/credit_report_10_heatmap.txt} 为模板,把热力图渲染成 SVG 后
 * 绑定到模板中的 {@code ${svg}} 占位符,输出到 {@code D:\test\creditreport\10_heatmap.pdf}。</p>
 *
 * @author Martin
 * @version 1.0.0
 */
public class JQuickCreditReport10HeatmapTest {

    public static final String path = JQuickConstant.path;

    @Test
    public void report10Heatmap() throws IOException {
        JOption option = new JOption();
        option.title("月度经营热力图", "各业务线月度营收(万元)");
        option.xAxis(new JCategoryAxis().data("1月", "2月", "3月", "4月", "5月", "6月"));
        option.yAxis(new JCategoryAxis().data("软件产品", "技术服务", "云服务", "其他业务"));
        JHeatmap heatmap = new JHeatmap();
        heatmap.data(
                new Object[]{0, 0, 860}, new Object[]{0, 1, 1020}, new Object[]{0, 2, 1180},
                new Object[]{0, 3, 1340}, new Object[]{0, 4, 1520}, new Object[]{0, 5, 1680},
                new Object[]{1, 0, 520}, new Object[]{1, 1, 640}, new Object[]{1, 2, 780},
                new Object[]{1, 3, 890}, new Object[]{1, 4, 1010}, new Object[]{1, 5, 1160},
                new Object[]{2, 0, 380}, new Object[]{2, 1, 460}, new Object[]{2, 2, 540},
                new Object[]{2, 3, 620}, new Object[]{2, 4, 710}, new Object[]{2, 5, 820},
                new Object[]{3, 0, 260}, new Object[]{3, 1, 310}, new Object[]{3, 2, 360},
                new Object[]{3, 3, 420}, new Object[]{3, 4, 470}, new Object[]{3, 5, 540}
        );
        option.series(heatmap);

        String svg = JChartRendererFactory.renderChart(JChartType.HEATMAP, option);

        File dir = new File(path + "creditreport");
        if (!dir.exists()) {
            dir.mkdirs();
        }
        FileOutputStream fileOutputStream = new FileOutputStream(path + "creditreport/10_heatmap.pdf");
        JPdfConfig config = new JPdfConfig();
        // 开启 flex 行布局:图表与其右侧的指标说明面板在宽度允许时并排,而不是换行堆叠。
        config.getLayoutConfig().setFlexLayout(true);
        JQuickPdfFactory factory = new JQuickPdfFactory(config);
        factory.bind("svg", svg);
        byte[] bytes = factory.executeResource("report/credit_report_10_heatmap.txt");
        fileOutputStream.write(bytes);
        fileOutputStream.close();
    }
}
Enter fullscreen mode Exit fullscreen mode

Step 3 — run it

The class is a JUnit test, so run it with Maven:

mvn -pl jquick-pdfx test -Dtest=JQuickCreditReport10HeatmapTest
Enter fullscreen mode Exit fullscreen mode

…or call the test method from a main wrapper if you want a plain entry point:

public static void main(String[] args) throws Exception {
    new JQuickCreditReport10HeatmapTest().report10Heatmap();
}
Enter fullscreen mode Exit fullscreen mode

Output: D:/test/creditreport/10_heatmap.pdf

How it works

Binding a chart SVG into the template

The chart is not a bitmap — JChartRendererFactory.renderChart(...) returns an SVG string, and the template embeds it with a single <svg> element bound to ${svg}. That means the chart stays vector-sharp at any zoom level, and it participates in normal flow layout instead of being stamped on top of the page.

  • Declare only the width on <svg>; the height is derived from the chart's aspect ratio, so the chart never stretches vertically.
  • Open config.getLayoutConfig().setFlexLayout(true) if the template centers the chart with display:flex.
  • Bind the SVG string with factory.bind("svg", svg) — bind() is pure data injection and never re-parses markup.
String svg = JChartRendererFactory.renderChart(JChartType.LINE, option);
JQuickPdfFactory factory = new JQuickPdfFactory(config);
factory.bind("svg", svg);
byte[] bytes = factory.executeResource(TEMPLATE);
Enter fullscreen mode Exit fullscreen mode

Making the table fill the text area

Tables default to content width, which leaves a ragged right margin on a formal document. Setting width:100% on the <table> makes it span the full text area; percentage widths on the first row's cells then distribute the remaining space across columns.

  • Set width:100% on <table> and give explicit widths only to the leading columns.
  • Label columns need around 100px so labels like Taxpayer ID do not wrap.
  • Leave the last column without a width so it absorbs rounding remainder.
<table style="width:100%; fontSize:9pt">
  <tr>
    <td style="width:100px; padding:5px">'Taxpayer ID'</td>
    <td style="padding:5px">${taxNo}</td>
  </tr>
</table>
Enter fullscreen mode Exit fullscreen mode

Template syntax cheat sheet

Syntax Meaning
<table style="width:100%"> Spans the full text area; give explicit widths only to the leading columns.
<areaBreak></areaBreak> Force the next block onto a fresh page (CSS page-break is not supported).
<image src='D:/.../x.png'> Embed an image. src is a raw path — it is not evaluated as an expression.
<svg style="width:479pt">${svg}</svg> Embed a vector chart produced by JChartRendererFactory.
// comment Line comment. Keep <!-- ... --> comments short — long ones can blank the render.

Gotchas worth remembering

  • <image src="..."> never evaluates ${...}; write a literal path.
  • A <td> holds text only — nested <p>, <image> or <table> is dropped.
  • There is no colspan; emit one <td> per column and use '—' as filler.
  • setFontSize takes a float (8.5f); setMargins takes List<Integer>.

Output

Wrap up

You now have a template file, a Java class and a PDF — all three reproducible from a clean clone. Swap the placeholder blocks for real data (or for real seal images) and the layout does not move.

Series navigation: ← #9 Gauge Chart for the Composite Credit Score · #11 Correlation Matrix of Operating KPIs →

Full index: README.md · Source: jquick-pdf


Appendix — files in this demo

Role Path
Java class jquick-pdfx/src/test/java/com/github/paohaijiao/biz/report/JQuickCreditReport10HeatmapTest.java
Template jquick-pdfx/src/test/resources/report/credit_report_10_heatmap.txt
Output PDF D:/test/creditreport/10_heatmap.pdf
Preview image images/10.png

Top comments (2)

Collapse
 
fgrandon profile image
Francisco Grandón •

Binding the vector ${svg} heatmap directly into the layout template via io.github.paohaijiao:jquick-pdfx:4.2.0 and letting Apache PDFBox handle the rendering is a really clean design choice, especially for keeping credit and financial PDFs crisp without pulling in the heavyweight licensing baggage of iText. The 12x6 grid layout filling the text area (width: 100%) in JQuickCreditReport10HeatmapTest makes monthly regional seasonality immediately legible at a glance.

One practical refinement for production financial reporting would be introducing an adaptive luminance check for the cell value labels (evaluating against WCAG's 4.5:1 contrast threshold) so text flips between dark and light ink depending on cell saturation. Alternatively, if regions have wildly different baselines, allowing an optional diverging scale centered on each region's median or budget target (rather than a purely global min-max sequential scale) would keep high-earning territories from washing out seasonal anomalies in smaller regional branches.

Plus, generating heatmaps without waking up corporate legal over AGPL dual-licensing audit alerts might just be the highest ROI metric in the entire quarterly ledger.

Collapse
 
paohaijiao profile image
paohaijiao •

Good points, both doable. One note: the demo grid is 6x4 rather than 12x6, and 4.2.0 is Apache-2.0 — AGPL only covers 4.0.0 and earlier.