1,背景

1,原因

项目开发中,一般情况下,统计图什么的都是前端来实现的,后端只需要返回数据就好,但是在一些特殊情况下,比如发送邮件报表什么的,这时候不经过前端,不能让前端渲染之后把图片传回来,这时候我们就需要一个比较好的报表绘制工具了

2,依赖

<!-- 报表绘制 -->
<dependency>
    <groupId>org.jfree</groupId>
    <artifactId>jfreechart</artifactId>
    <version>1.5.2</version>
</dependency>

2,折线图

1,实现折线图代码片段

下面就来绘制一个折线图

package com.hwq.admin.back.service;

import com.hwq.admin.back.config.jfree.IntervalCategoryAxis;
import com.hwq.common.component.service.MokeService;
import com.hwq.common.model.vo.ChartVO;
import org.jfree.chart.ChartFactory;
import org.jfree.chart.ChartUtils;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.axis.CategoryAxis;
import org.jfree.chart.axis.NumberAxis;
import org.jfree.chart.axis.NumberTickUnit;
import org.jfree.chart.labels.StandardCategorySeriesLabelGenerator;
import org.jfree.chart.plot.CategoryPlot;
import org.jfree.chart.plot.PlotOrientation;
import org.jfree.chart.renderer.category.LineAndShapeRenderer;
import org.jfree.chart.title.LegendTitle;
import org.jfree.data.category.DefaultCategoryDataset;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.awt.*;
import java.io.ByteArrayOutputStream;
import java.text.DecimalFormat;
import java.util.List;

import static java.awt.BasicStroke.CAP_ROUND;
import static java.awt.BasicStroke.JOIN_ROUND;

@Service
public class LineChartService {

    @Autowired
    private MokeService mokeService; // 用于读取 自己造的假数据

    public byte[] getLine() {
        List<ChartVO> chartsL = mokeService.mokes("/mokes/走势图左.json", ChartVO.class);  // 读取假数据
        List<ChartVO> chartsR = mokeService.mokes("/mokes/走势图右.json", ChartVO.class);  // 读取假数据

        // 实例化数据集
        DefaultCategoryDataset dataset1 = new DefaultCategoryDataset();
        DefaultCategoryDataset dataset2 = new DefaultCategoryDataset();
        double minValue = chartsL.get(0).getValue(), maxValue = minValue;
        for (ChartVO vo : chartsL) {
            dataset1.addValue(vo.getValue(), "组合", vo.getDate());
            minValue = Math.min(minValue, vo.getValue());
            maxValue = Math.max(maxValue, vo.getValue());
        }
        for (ChartVO vo : chartsR) {
            dataset2.addValue(vo.getValue() - 1, vo.getTitle(), vo.getDate());
            minValue = Math.min(minValue, vo.getValue());
            maxValue = Math.max(maxValue, vo.getValue());
        }

        // 实例化图表
        JFreeChart chart = ChartFactory.createLineChart("", "", "", null, PlotOrientation.VERTICAL, true, false, false);
        CategoryPlot plot = (CategoryPlot) chart.getPlot();

        setLine(chart, plot);
        setRenderer(plot, dataset1, dataset2);
        setX(plot);
        setY(plot, minValue, maxValue);
        try (
                ByteArrayOutputStream bto = new ByteArrayOutputStream();
        ) {
            ChartUtils.writeChartAsJPEG( bto, chart, 900, 500);
            return bto.toByteArray();
        } catch (Exception ex) {
            ex.printStackTrace();
            throw new RuntimeException(ex.getMessage());
        }
    }

    /**
     * 设置折线图样式
     */
    public void setLine(JFreeChart chart, CategoryPlot plot) {
        LegendTitle legend = chart.getLegend();
        legend.setItemFont(new Font("SimHei", Font.PLAIN, 16));       // 设置标示图的字体
        chart.getTitle().setFont(new Font("SimHei", Font.BOLD, 20));  // 设置标题字体
        // plot.setRangeAxisLocation(AxisLocation.BOTTOM_OR_RIGHT);      // 设置坐标轴在下方或者右侧
        plot.setBackgroundPaint(new Color(255, 255, 255));            // 设置绘图区的颜色
        plot.setRangeGridlinePaint(new Color(200, 200, 200));         // 设置水平方向背景线颜色
        plot.setRangeGridlineStroke(new BasicStroke(1.0f));           // 设置水平方向背景线粗细
        plot.setRangeGridlinesVisible(true);                          // 设置是否显示水平方向背景线
    }

    /**
     * 设置两个体系绘制
     */
    public void setRenderer(CategoryPlot plot, DefaultCategoryDataset dataset1, DefaultCategoryDataset dataset2) {
        LineAndShapeRenderer renderer1 = new LineAndShapeRenderer();
        renderer1.setLegendItemLabelGenerator(new StandardCategorySeriesLabelGenerator());
        renderer1.setSeriesStroke(0, new BasicStroke(3.0f, CAP_ROUND, JOIN_ROUND, 2000));
        renderer1.setSeriesPaint(0, new Color(191, 0 ,0));
        renderer1.setSeriesShapesVisible(0, false);

        LineAndShapeRenderer renderer2 = new LineAndShapeRenderer();
        renderer2.setLegendItemLabelGenerator(new StandardCategorySeriesLabelGenerator());
        renderer2.setSeriesStroke(0, new BasicStroke(3.0f, CAP_ROUND, JOIN_ROUND, 2000));
        renderer2.setSeriesPaint(0, new Color(54, 75, 151));
        renderer2.setSeriesShapesVisible(0, false);
        renderer2.setSeriesStroke(1, new BasicStroke(3.0f, CAP_ROUND, JOIN_ROUND, 2000));
        renderer2.setSeriesPaint(1, new Color(181, 125, 69));
        renderer2.setSeriesShapesVisible(1, false);

        plot.setDataset(0, dataset1);
        plot.setRenderer(0, renderer1);

        plot.setDataset(1, dataset2);
        plot.setRenderer(1, renderer2);
    }

    /**
     * 配置 X 轴
     */
    public void setX(CategoryPlot plot) {
        CategoryAxis axis = plot.getDomainAxis();
        axis.setTickLabelFont(new Font("SimHei", Font.BOLD, 16)); // 设置 X 轴每个刻度的字体
        axis.setLabelFont(new Font("SimHei", Font.BOLD, 20));     // 设置 X 轴测的字体
        axis.setAxisLineVisible(false);                           // 设置 X 轴轴线不显示
        axis.setTickMarksVisible(false);                          // 设置 X 轴刻度不显示

        // axis.setCategoryLabelPositions(CategoryLabelPositions.DOWN_45);  // 横坐标旋转一个角度
        axis.setUpperMargin(0);
        axis.setLowerMargin(0);
    }

    /**
     * 设置 Y 轴
     */
    public void setY(CategoryPlot plot, double minValue, double maxValue) {
        double tickUnit = (maxValue - minValue) / 10;
        // NumberAxis axis = (NumberAxis) plot.getRangeAxis();
        NumberAxis axis1 = new NumberAxis();
        axis1.setNumberFormatOverride(new DecimalFormat("0.0000"));     // 设置数据的展示模式
        axis1.setAutoTickUnitSelection(false);                     // 取消自动分配间距
        axis1.setTickUnit(new NumberTickUnit(tickUnit));           // 设置间隔距离
        axis1.setRange(minValue, maxValue);             // 设置显示范围
        // axis1.setUpperMargin(tickUnit1);                            // 设置最大值到顶端的距离
        axis1.setTickLabelFont(new Font("SimHei", Font.BOLD, 16)); // 设置 Y 轴每个刻度的字体
        axis1.setLabelFont(new Font("SimHei", Font.BOLD, 20));     // 设置 Y 轴侧的字体
        axis1.setAxisLineVisible(false);                           // 设置 Y 轴轴线不显示
        axis1.setTickMarksVisible(false);

        NumberAxis axis2 = new NumberAxis();
        axis2.setNumberFormatOverride(new DecimalFormat("0.00%"));    // 设置数据的展示模式
        axis2.setAutoTickUnitSelection(false);                     // 取消自动分配间距
        axis2.setTickUnit(new NumberTickUnit(tickUnit));           // 设置间隔距离
        axis2.setRange(minValue - 1, maxValue - 1);             // 设置显示范围
        // axis2.setUpperMargin(tickUnit2);                            // 设置最大值到顶端的距离
        axis2.setTickLabelFont(new Font("SimHei", Font.BOLD, 16)); // 设置 Y 轴每个刻度的字体
        axis2.setLabelFont(new Font("SimHei", Font.BOLD, 20));     // 设置 Y 轴侧的字体
        axis2.setAxisLineVisible(false);                           // 设置 Y 轴轴线不显示
        axis2.setTickMarksVisible(false);

        plot.setRangeAxis(0, axis1);       // 新增一个 Y 轴
        plot.setRangeAxis(1, axis2);       // 新增一个 Y 轴
        plot.mapDatasetToRangeAxis(1, 1);  // 设置为分别渲染,左右坐标轴 不互相影响
    }
}

2,效果

image-20210326191620912

恩!看着很完美,但是随着数据量一大,坑爹的问题就来了,看下图

image-20210326192317926

MD,横坐标不见了,全部变成了省略号,作为一个成熟的报表绘制工具,居然没有自动踩点的功能,简直失败,然后只能去查文档,查百度,看看有没有设置步数什么的,结果居然发现没有,只有一个 时序图什么的才能设置步数

笔者也试了一下,首先那个需要横坐标是日期对象,到是能自动踩点了,但是可能自动采到的点并不是你存在的数据,这时我们只能通过设置步数获取自己想要的坐标,但是设置了步数更坑,第一个坐标居然不见了,此时笔者的内心是崩溃的!!!

3,改源码

百度了一圈,实在没办法,只能从源码下手了

笔者 debug 了一下,发现 CategoryAxis 对象获取 X轴 横坐标点方法是以下这个方法

image-20210326192956538

那就好办了,继承,重写(拷贝原方法,修改)

package com.hwq.admin.back.config.jfree;

import org.jfree.chart.axis.*;
import org.jfree.chart.plot.CategoryPlot;
import org.jfree.chart.text.TextBlock;
import org.jfree.chart.ui.RectangleEdge;

import java.awt.*;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
import java.util.List;

public class IntervalCategoryAxis extends CategoryAxis {

    private final int stepNum;  // 步数

    public IntervalCategoryAxis(int stepNum) {
        this.stepNum = stepNum;
    }

    /**
     * 重写获取横坐标的方法,根据步数踩点展示,防止横坐标密密麻麻
     */
    @Override
    public List<Tick> refreshTicks(Graphics2D g2, AxisState state, Rectangle2D dataArea, RectangleEdge edge) {
        List<Tick> ticks = new ArrayList<>();
        if (dataArea.getHeight() <= 0.0 || dataArea.getWidth() < 0.0) {
            return ticks;
        }
        CategoryPlot plot = (CategoryPlot) getPlot();
        List<?> categories = plot.getCategoriesForAxis(this);
        double max = 0.0;
        if (categories != null) {
            CategoryLabelPosition position = super.getCategoryLabelPositions().getLabelPosition(edge);
            int categoryIndex = 0;
            for (Object o : categories) {
                Comparable<?> category = (Comparable<?>) o;
                g2.setFont(getTickLabelFont(category));
                TextBlock label = new TextBlock();
                label.addLine(category.toString(), getTickLabelFont(category), getTickLabelPaint(category));
                if (edge == RectangleEdge.TOP || edge == RectangleEdge.BOTTOM) {
                    max = Math.max(max, calculateTextBlockHeight(label, position, g2));
                } else if (edge == RectangleEdge.LEFT || edge == RectangleEdge.RIGHT) {
                    max = Math.max(max, calculateTextBlockWidth(label, position, g2));
                }
                if (categoryIndex % stepNum == 0) {
                    Tick tick = new CategoryTick(category, label, position.getLabelAnchor(), position.getRotationAnchor(), position.getAngle());
                    ticks.add(tick);
                }
                categoryIndex = categoryIndex + 1;
            }
        }
        state.setMax(max);
        return ticks;
    }

}

主要思路大致就是传入一个步数,让列表下标正好是这个步数的整数倍的时候,才像返回值列表里添加坐标,并且添加的时候,把默认的省略号重新赋予正常的数值,最后在调用的地方,使用这个继承类

image-20210326193558861

4,最后的效果

image-20210326193704067

终于实现了,间隔踩点的功能

心得:看来绘图渲染什么的,还是得前端来,后端渲染什么的框架真的是不是很靠谱,当然也可能我用的不恰当,如果大家有更好的办法,欢迎指正

posted on 2021-03-26 19:39  被遗忘的优雅  阅读(3463)  评论(4编辑  收藏  举报