zoukankan      html  css  js  c++  java
  • java jfreechart 折线图数据量大,X 轴刻度密密麻麻显示不下,或者省略号的解决办法

    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

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

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

  • 相关阅读:
    EJB 3.0持久化规范之实体类的要求
    Java正则表达式应用总结
    Java正则表达式教程
    JS正则表达式大全
    Sql Server 配置 及 Hibernate 驱动配置
    EJB QL查询语言
    解决多个jquery文件,造成jquery文件冲突的方法
    ps之网页设计企业站建设推荐
    zencart程序文件目录
    Database server does not support the InnoDB storage engine MAGENTO 安装提示错误
  • 原文地址:https://www.cnblogs.com/lovling/p/14583853.html
Copyright © 2011-2022 走看看