使用Freemarker配合JFreeChart生成图表动态修改word文档数据

 引言

  • 介绍背景:在自动化报告生成场景中,为何需要将实时数据转化为可视化图表插入到Word文档中
  • Freemarker功能及其在该场景下的作用
    • 定义需要动态替换的数据,包含普通数据和图表(图表使用图片代替)
  • JFreeChart功能及其在该场景下的作用
    • 根据数据生成,折线图,饼图,柱形图,环形图

Freemarker模板引擎简介 

主要特性

  • 纯Java实现: 由于完全使用Java编写,FreeMarker能够无缝集成到任何Java应用程序中,尤其适用于Web应用开发框架,例如Spring MVC、Struts等。
  • 强大的表达式语言: 提供了丰富的表达式语言用于访问和操作数据模型,支持条件判断、循环迭代、字符串处理、日期格式化等功能。
  • 模板继承与包含: 支持模板的继承与片段重用,有助于代码复用并维护统一的设计风格。
  • 分离关注点: 通过模板技术实现了视图层与业务逻辑的分离,有利于项目维护和团队协作。

应用场景

  • Web应用: 在MVC架构中,FreeMarker常作为View层技术,负责渲染动态网页内容。
  • 报表生成: 结合业务数据,可以动态生成各种形式的报表,如Excel、PDF或HTML报告。
  • 邮件发送: 自动化邮件系统中,用来构造个性化的邮件内容。
  • 配置文件生成: 根据不同环境或用户需求,自动化生成定制化的配置文件。

JFreeChart库基础 

JFreeChart主要特点:

  1. 丰富多样的图表类型:支持多种预定义图表类型,可以根据实际需求选择合适的图表形式展示数据。
  2. 高度可定制:提供了丰富的自定义选项,可以设置标题、轴标签、图例、颜色方案、网格线、数据标签等属性,以满足个性化设计要求。
  3. 灵活的数据接口:能够处理来自不同来源的数据,包括数组、列表、数据库查询结果以及JFreeChart自己的数据模型类。
  4. 易用性:通过ChartFactory工厂类提供了一系列便捷方法快速生成基本图表,同时也可以通过构造器自定义更复杂的图表结构。
  5. 事件监听:支持图表事件监听,允许用户对鼠标点击、移动等交互行为进行响应。

使用JFreeChart的基本步骤:

  • 获取数据集:首先,需要准备或从某个数据源获取数据,并将其封装到JFreeChart支持的数据集中,例如DefaultPieDataset用于制作饼图,TimeSeriesCollection用于时间序列折线图等。

  • 创建图表实例:调用ChartFactory的静态方法或者直接构造JFreeChart对象来初始化图表。传入参数通常包括图表标题、数据集、使用的图表类型以及其他相关配置信息。

  • 显示图表:对于桌面应用,可以将图表添加到ChartPanel中,进而嵌入到Swing组件中;对于Web应用,则可以通过将图表导出为图像文件(如PNG、JPEG)的方式展示。

  • 高级定制:如果基础图表不能满足需求,还可以进一步操作图表的各种组件(如Plot、Axis、Renderer等),实现更深入的定制化效果。

代码实现 

引入pom

        <dependency>
            <groupId>org.jfree</groupId>
            <artifactId>jfreechart</artifactId>
            <version>1.5.4</version>
        </dependency>

        <dependency>
            <groupId>org.freemarker</groupId>
            <artifactId>freemarker</artifactId>
            <version>2.3.31</version>
        </dependency>

 数据获取与预处理

  • 处理word模板转为ftl文件,在Word模板中定义占位符,将word中动态数据替换为${data}
  • 将在项目的resources下创建templates目录,将ftl文件放这。
  • 将所有文字处理完成,使用word另存为xml格式,然后修改后缀为ftl,在idea格式化,检查各个文字格式是否正常,比如${data}是在一行,等问题。
  • <w:r>
        <w:rPr>
            <w:rFonts w:ascii="微软雅黑" w:h-ansi="微软雅黑"
                      w:fareast="微软雅黑" w:cs="微软雅黑" w:hint="default"/>
            <w:b/>
            <w:b-cs/>
            <w:color w:val="57C1FF"/>
            <w:sz w:val="40"/>
            <w:sz-cs w:val="40"/>
        </w:rPr>
        <w:t>${data}</w:t>
    </w:r>
    

    特殊处理word表格问题

  • 图片处理,需要去搜索<w:binData,这个标签,然后将里面的base64的删除改为占位符
  •  <w:r>
        <w:pict>
         <w:binData w:name="wordml://4.png">${pict4}</w:binData>

  • 表格问题,可以在tr标签的前后加上<#list list as item>和</#list>包围,具体使用如下
  • <#list list as item>
    <w:tr>
    ......
    <w:p>
        <w:pPr>
            <w:widowControl/>
            <w:jc w:val="center"/>
            <w:textAlignment w:val="center"/>
            <w:rPr>
                <w:rFonts w:ascii="微软雅黑" w:h-ansi="微软雅黑"
                          w:fareast="微软雅黑" w:cs="微软雅黑"
                          w:hint="default"/>
                <w:color w:val="000000"/>
                <w:sz w:val="15"/>
                <w:sz-cs w:val="15"/>
            </w:rPr>
        </w:pPr>
        <w:r>
            <w:rPr>
                <w:rFonts w:ascii="微软雅黑" w:h-ansi="微软雅黑"
                          w:fareast="微软雅黑" w:cs="微软雅黑"
                          w:hint="fareast"/>
                <w:color w:val="000000"/>
                <w:sz w:val="15"/>
                <w:sz-cs w:val="15"/>
            </w:rPr>
            <w:t>${item.district}</w:t>
        </w:r>
    </w:p>

    ..........

    </w:tr>
    </#list>

    调用freemark的对象,将占位符的数据以map的形式去存储,包括图片,在表格数据直接使用list去完成

  •         //封装map
            Map<String, Object> part = new HashMap<>();
            //获取数据
            VisitRecordFilterCriteria criteria = reportRequestDTO.getCriteria();
    
            Configuration configuration = new Configuration(Configuration.VERSION_2_3_0);
            configuration.setDefaultEncoding("utf-8");
            configuration.setClassForTemplateLoading(this.getClass(), "/templates");
            Template template = configuration.getTemplate("gzlbg.ftl");
    
            //省略获取数据方法
            。。。。。。。。。。。。。。。。。
    
            //比如封装map数据
        
            part.put("product",product);
            part.put("username",userName);
            part.put("year",year);
            part.put("smm",smm);
            part.put("startDay",startDay);
    
            。。。。。。。。。。。。。。。。。。。。。。。。
            part.put("pict5",top5VisitsDig);
            part.put("list",customerDistribution);
    
    
            // 输出位置
            File outFile = new File(path);
            Writer out = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(outFile),"UTF-8"));
            try {
                template.process(part,out);
                out.flush();
                out.close();
            } catch (TemplateException e) {
                e.printStackTrace();
            }

  • 使用JFreeChart生成图表然后转BASE64替换模板中的占位符

  • 折线图

  • private static String getVisitsGroupedByDayDig(List<Map.Entry<Date, BigInteger>> companyVisitsGroupedByDay) {
            TimeSeries series = new TimeSeries("");
            SimpleDateFormat sdf = new SimpleDateFormat("'Day'dd");
            // 添加数据到时间序列并处理缺失日期
            for (Map.Entry<Date, BigInteger> entry : companyVisitsGroupedByDay) {
                Day day = new Day(entry.getKey());
                series.add(day, entry.getValue().doubleValue());
            }
    
            // 处理缺失的日期,将缺失日期对应的访问量设为 0
            List<Date> allDates = new ArrayList<>();
    
            // 找到日期范围的最小和最大日期
            Date minDate = companyVisitsGroupedByDay.get(0).getKey();
            Date maxDate = companyVisitsGroupedByDay.get(companyVisitsGroupedByDay.size() - 1).getKey();
    
            // 创建 Calendar 实例来处理日期
            Calendar calendar = Calendar.getInstance();
            calendar.setTime(minDate);
    
            // 将日期循环添加到列表中
            while (calendar.getTime().before(maxDate) || calendar.getTime().equals(maxDate)) {
                allDates.add(calendar.getTime());
                calendar.add(Calendar.DATE, 1);
            }
    
            for (Date date : allDates) {
                Day day = new Day(date);
                if (series.getIndex(day) < 0) {
                    series.add(day, 0); // 缺失日期对应的访问量设为 0
                }
            }
    
            TimeSeriesCollection dataset = new TimeSeriesCollection();
            dataset.addSeries(series);
    
            // 创建时间序列折线图
            JFreeChart chartDay = ChartFactory.createTimeSeriesChart(
                    "", // 图表标题
                    "", // X 轴标签
                    "", // Y 轴标签
                    dataset,
                    false, // 显示图例
                    true, // 生成工具提示
                    false // 不生成URL链接
            );
    
            // 设置日期轴每2天显示一个标签,并尝试设置X轴标签倾斜(JFreeChart默认不支持直接旋转日期轴标签)
    
            DateAxis xAxisDay = (DateAxis) ((XYPlot) chartDay.getPlot()).getDomainAxis();
    
    
            xAxisDay.setDateFormatOverride(sdf); // 设置日期格式为只显示日
            xAxisDay.setTickUnit(new DateTickUnit(DateTickUnit.DAY, 3)); // 设置每隔一天显示一个标签
            // 设置X轴标签字体大小
            Font labelFont = new Font("Arial", Font.BOLD, 16); // 创建字体对象,参数分别为字体名称、样式、大小
            xAxisDay.setTickLabelFont(labelFont); // 设置日期轴标签的字体
    
    
            // 尝试模拟X轴标签倾斜效果,但JFreeChart本身不直接支持此功能;若需倾斜标签,可能需要自定义轴标签器实现
            // xAxisDay.setCategoryLabelPositions(CategoryLabelPositions.UP_45); // 对于DateAxis无效
    
            // 设置字体大小
            xAxisDay.setLabelFont(new Font("Sans-serif", Font.PLAIN, 20));
    
            NumberAxis yAxisDay = (NumberAxis) ((XYPlot) chartDay.getPlot()).getRangeAxis();
            yAxisDay.setStandardTickUnits(NumberAxis.createIntegerTickUnits());
            yAxisDay.setLabelFont(new Font("Arial", Font.PLAIN, 16)); // 增大Y轴标签字体
            yAxisDay.setTickLabelFont(new Font("Arial", Font.PLAIN, 24)); // 增大Y轴刻度数字字体
    
            // 现代化样式调整
            chartDay.setBackgroundPaint(Color.WHITE);
            XYPlot plot = (XYPlot) chartDay.getPlot();
            plot.setBackgroundPaint(Color.WHITE);
            plot.setOutlineVisible(false);
            plot.setRangeGridlinePaint(Color.LIGHT_GRAY);
            plot.setDomainGridlinePaint(Color.LIGHT_GRAY);
    
            // 设置背景透明
            chartDay.setBackgroundPaint(new Color(0, 0, 0, 0));
            plot.setBackgroundPaint(new Color(0, 0, 0, 0)); // 绘图区域背景设置为透明
            plot.setRangeGridlinePaint(Color.DARK_GRAY);
            plot.setDomainGridlinePaint(Color.DARK_GRAY);
    
            // 设置线条和点的样式
            XYLineAndShapeRenderer rendererDay = new XYLineAndShapeRenderer(true, false);
            rendererDay.setSeriesPaint(0, new Color(0, 153, 255)); // 设置折线颜色
            rendererDay.setBaseItemLabelFont(new Font("Arial", Font.PLAIN, 20)); // 设置数据点标签的字体
    
            // 设置数据点标签可见及样式
            XYItemLabelGenerator generator = new StandardXYItemLabelGenerator();
            rendererDay.setBaseItemLabelGenerator(generator); // 设置数据点标签生成器
            rendererDay.setBaseItemLabelsVisible(true); // 设置数据点标签可见
            rendererDay.setBaseItemLabelPaint(Color.BLACK); // 数据点标签颜色
    
            plot.setRenderer(rendererDay);
    
            // 将图表转换为 Base64 数据
            String dayPicture = convertChartToBase64(chartDay);
            return dayPicture;
    }

    柱形图

  •  private static String getTopVisitsDig(List<Map<String, Object>> top5PharmaciesByVisits) {
            DefaultCategoryDataset dataset5 = new DefaultCategoryDataset();
            for (Map<String, Object> data : top5PharmaciesByVisits) {
                String pharmacyName = (String) data.get("name");
                int visitCount =  (int)data.get("total_visit_count");
                dataset5.addValue(visitCount, "单人拜访次数", pharmacyName);
            }
    
            JFreeChart barChart = ChartFactory.createBarChart(
                    "", // 图表标题
                    "",         // X 轴标签
                    "",           // Y 轴标签
                    dataset5 ,// 数据集
                    PlotOrientation.HORIZONTAL,
                    true, // 显示图例
                    true, // 生成工具提示
                    false // 不生成URL链接
            );
    
            // 设置背景和边框颜色
            barChart.setBackgroundPaint(Color.WHITE);
            ((CategoryPlot) barChart.getPlot()).setBackgroundPaint(Color.WHITE);
            ((CategoryPlot) barChart.getPlot()).setOutlineVisible(false);
            // 设置绘图区域背景透明
            ((CategoryPlot) barChart.getPlot()).setBackgroundAlpha(0.0f); // 设置绘图区域背景透明
    
            // 设置图表背景透明
            barChart.setBackgroundPaint(new Color(0, 0, 0, 0)); // 设置图表背景为透明色
    
            // 设置绘图区域边框透明
            ((CategoryPlot) barChart.getPlot()).setOutlineVisible(false); // 隐藏绘图区域边框
            // 设置Y轴字体大小
            NumberAxis yAxisBar = (NumberAxis) ((CategoryPlot) barChart.getPlot()).getRangeAxis();
            yAxisBar.setStandardTickUnits(NumberAxis.createIntegerTickUnits());
            yAxisBar.setLabelFont(new Font("Arial", Font.PLAIN, 18));// 增大Y轴标签字体
            yAxisBar.setTickLabelFont(new Font("Arial", Font.PLAIN, 18));// 增大Y轴刻度数字字体
    
    
            // 创建并设置柱形渲染器
            BarRenderer rendererBar = (BarRenderer) ((CategoryPlot) barChart.getPlot()).getRenderer();
            StandardCategoryItemLabelGenerator generator = new StandardCategoryItemLabelGenerator();
            rendererBar.setBaseItemLabelGenerator(generator); // 设置基础项标签生成器
            rendererBar.setBaseItemLabelsVisible(true); // 显示数据标签
            rendererBar.setBaseItemLabelFont(new Font("黑体", Font.PLAIN, 18)); // 设置数据标签字体大小
            // 设置柱形宽度(可选)
            rendererBar.setMaximumBarWidth(0.10); // 根据实际情况调整柱形宽度
            rendererBar.setSeriesPaint(0, new Color(51, 153, 255)); // 设置柱体颜色为 RGB(0, 153, 255)
    
            // 创建图表后...
            CategoryPlot plot = barChart.getCategoryPlot();
            // 获取 X 轴
            CategoryAxis xAxis = plot.getDomainAxis();
            // 设置 X 轴刻度标签字体大小
            Font labelFont = new Font("宋体", Font.BOLD, 14); // 示例为宋体字体
            xAxis.setTickLabelFont(labelFont);
    
            // 将图表转换为 Base64 数据
            String barChart64 = convertChartToBase64(barChart);
            return barChart64;
        }

    环形图

  •  public String createRingChart(Map<String, Double> map) {
            // 创建数据集
            DefaultPieDataset dataset = new DefaultPieDataset();
            for (Map.Entry<String, Double> entry : map.entrySet()) {
                dataset.setValue(entry.getKey(), entry.getValue());
            }
    
    // 创建环形图配置
            JFreeChart chart = ChartFactory.createRingChart(
                    "", // 图表标题
                    dataset, // 数据集
                    true, // 不显示图例
                    true, // 显示工具提示
                    false // 不生成URL
            );
    
            // 设置显示标注
            // 标注位于上侧
            // chart.getLegend().setPosition(RectangleEdge.TOP);
            // 设置标注无边框
            chart.getLegend().setFrame(new BlockBorder(Color.WHITE));
    
            // 环形图
            RingPlot ringplot = (RingPlot) chart.getPlot();
            ringplot.setOutlineVisible(false);
            //{2}表示显示百分比  //{0}:key {1}:value {2}:百分比 {3}:sum
            ringplot.setLabelGenerator(new StandardPieSectionLabelGenerator("{2}"));
            ringplot.setBackgroundPaint(new Color(253, 253, 253));
            ringplot.setOutlineVisible(false);
            ringplot.setLabelFont(new Font("宋体", Font.BOLD, 20));
            ringplot.setSimpleLabels(true);
                /*//设置标签样式
                ringplot.setLabelFont(new Font("宋体", Font.BOLD, 15));
                ringplot.setSimpleLabels(true);
                ringplot.setLabelLinkPaint(Color.WHITE);
                ringplot.setLabelOutlinePaint(Color.WHITE);
                ringplot.setLabelLinksVisible(false);
                ringplot.setLabelShadowPaint(null);
                ringplot.setLabelOutlinePaint(new Color(0,true));
                ringplot.setLabelBackgroundPaint(new Color(0,true));
                ringplot.setLabelPaint(Color.WHITE);*/
    
            ringplot.setSectionOutlinePaint(Color.WHITE);
            ringplot.setSeparatorsVisible(true);
            ringplot.setSeparatorPaint(Color.WHITE);
            ringplot.setShadowPaint(new Color(253, 253, 253));
            // 设置深度,即带颜⾊圆环的宽度
            ringplot.setSectionDepth(0.4);
            ringplot.setStartAngle(90);
            // 指定颜色
            ringplot.setDrawingSupplier(new DefaultDrawingSupplier(
                    new Paint[]{
                            new Color(0, 114, 150),
                            new Color(52, 152, 219),
                            new Color(231, 76, 60),
                            new Color(241, 196, 15),
                            new Color(211, 84, 0),
                            new Color(169, 209, 142),
                            new Color(191, 195, 202),
                            new Color(0, 102, 153),
                            new Color(0, 136, 204),
                            new Color(255, 102, 0),
                            new Color(136, 187, 0),
                            new Color(204, 204, 204)
                    },
                    DefaultDrawingSupplier.DEFAULT_OUTLINE_PAINT_SEQUENCE,
                    DefaultDrawingSupplier.DEFAULT_STROKE_SEQUENCE,
                    DefaultDrawingSupplier.DEFAULT_OUTLINE_STROKE_SEQUENCE,
                    DefaultDrawingSupplier.DEFAULT_SHAPE_SEQUENCE));
       LegendTitle legend = chart.getLegend();
            legend.setPosition(RectangleEdge.BOTTOM); // 将图例放置在底部
            legend.setItemFont(new Font("黑体", Font.PLAIN, 20)); // 设置图例文字字体
    
            // 设置背景透明
            ringplot.setBackgroundPaint(new Color(255, 255, 255, 0));
            chart.setBackgroundPaint(new Color(0, 0, 0, 0)); // 设置图表背景透明
    
            // 将图表绘制到缓冲区并转换为Base64(假设convertChartToBase64方法已实现)
            return convertChartToBase64(chart);
        }

    饼状图

  •  private static String getPie(Map<String, Double> hospitalLevelStats) {
            DefaultPieDataset datasetPie = new DefaultPieDataset();
            // 将数据添加到数据集中
            for (Map.Entry<String, Double> entry : hospitalLevelStats.entrySet()) {
                if (entry.getValue() != 0.0) {
                    datasetPie.setValue(entry.getKey(), entry.getValue());
                }
            }
            // 创建饼状图
            JFreeChart chart2 = ChartFactory.createPieChart(
                    "", // 图表标题
                    datasetPie,
                    true, // 是否显示图例
                    true, // 是否显示工具提示
                    false // 是否显示 URL
            );
            // 获取或创建PiePlot对象
            PiePlot plot = (PiePlot) chart2.getPlot();
    // 设置绘图区域背景透明
            plot.setBackgroundAlpha(0.0f); // 设置绘图区域背景透明
    
            // 设置图表背景透明
            chart2.setBackgroundPaint(new Color(0, 0, 0, 0)); // 设置图表背景为透明色
            plot.setLabelGenerator(new StandardPieSectionLabelGenerator("({2})",
                    NumberFormat.getNumberInstance(), // 数值格式化器
                    new DecimalFormat("0.0%"))
            ); // 百分比格式化器
    
            // 可选地,您可以控制标签是否可见以及其外观
            plot.setSimpleLabels(true); // 简洁模式下标签可能更紧凑
    
            // 设置标签背景色为透明
            plot.setLabelBackgroundPaint(new Color(0, 0, 0, 0)); // 设置标签背景色为透明色
    
            // 设置扇区颜色为 RGB(255, 119, 34)
            Color color1 = new Color(51, 153, 255);
            Color color7 = new Color(238, 102, 102);
            Color color4 = new Color(145, 204, 117);
            Color color3 = new Color(250, 200, 88);
            Color color5 = new Color(115, 192, 222);
            Color color6 = new Color(59, 162, 114);
            Color color2 = new Color(252, 132, 82);
            Color color8 = new Color(154, 96, 180);
    
            List<Color> strings = new ArrayList<>();
            strings.add(color1);
            strings.add(color2);
            strings.add(color3);
            strings.add(color4);
            strings.add(color5);
            strings.add(color6);
            strings.add(color7);
            strings.add(color8);
            // 设置标签颜色
            int index = 0;
            for (Map.Entry<String, Double> entry : hospitalLevelStats.entrySet()) {
                if (entry.getValue() != 0.0) {
                    datasetPie.setValue(entry.getKey(), entry.getValue());
                    plot.setSectionPaint(entry.getKey(), strings.get(index)); // 使用ColorPalette类获取不同颜色
                    index++; // 更新颜色索引
                }
    
            }
    //        // 设置所有扇区标签的字体大小和样式
    //        plot.setLabelFont(new Font("宋体", Font.BOLD, 30));
            Font font = new Font("宋体", Font.PLAIN, 24); // 指定字体和大小
            // 设置标签字体大小
            plot.setLabelFont(font.deriveFont(Font.BOLD,24));
            // 获取图例对象
            LegendTitle legend = chart2.getLegend();
    
            // 设置图例的字体和样式
            Font legendFont = new Font("宋体", Font.PLAIN, 25);
            legend.setItemFont(legendFont);
    
            // 设置图例的背景色
            Paint backgroundPaint = new Color(255, 255, 255, 200);
            legend.setBackgroundPaint(backgroundPaint);
    
            // 设置图例边框颜色和粗细
            Paint outlinePaint = new Color(0, 0, 0);
            Stroke outlineStroke = new BasicStroke(1.5f);
    
            // 设置图例的位置和大小
            legend.setPosition(RectangleEdge.RIGHT); // 可以根据需要设置不同的位置
            legend.setItemLabelPadding(new RectangleInsets(10, 10, 10, 10)); // 设置标签内边距
    
    
            // 将图表转换为 Base64 数据
            String pieBase64 = convertChartToBase64(chart2);
            return pieBase64;
        }

    常见问题与解决方案

  • word模板中的参数如果名称和map对不上,就会直接报错
  • 一定要先调整好模板, 或者熟悉前端的小伙伴也可以直接在ftl文件修改样式

总结与展望

可以完成基本的word生成,数据替换,满足大部分场景的使用

缺点就是想要把生成的图表直接插入到word到,支持图表的修改

  • 15
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值