feiyu02
2025-09-30 94fee0b511279679b43e210878d3d36e5a14384b
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
package com.flightfeather.uav.common.chart
 
import org.jfree.chart.ChartFactory
import org.jfree.chart.ChartUtils
import org.jfree.chart.JFreeChart
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.data.category.DefaultCategoryDataset
import java.awt.BasicStroke
import java.awt.Color
import java.awt.Font
import java.awt.Paint
import java.io.ByteArrayOutputStream
import java.text.DecimalFormat
import kotlin.math.max
import kotlin.math.min
 
/**
 * 图表生成
 * @date 2024/5/31
 * @author feiyu02
 */
object ChartUtil {
 
    private const val FONT_NAME = "SimHei"
    private val defaultColors = listOf(
        Color(191, 0, 0),
        Color(181, 125, 69),
        Color(236, 204, 6),
        Color(84, 151, 57),
        Color(13, 221, 151),
        Color(57, 75, 151),
        Color(115, 51, 206),
    )
 
    // 图表数据值坐标
    data class ChartValue(val x: String?, val y: Double)
    // 图表数据集
    data class ChartDataset(val dataset: DefaultCategoryDataset, val minValue: Double, val maxValue: Double)
 
    /**
     * 创建折线图
     */
    fun line(title: String, chartDatasetList: List<ChartDataset>): JFreeChart? {
        if (chartDatasetList.isEmpty()) return null
 
        val line = ChartFactory.createLineChart(
            title, "时间", "浓度(μg/m3)",
            null, PlotOrientation.VERTICAL, true, false, false
        )
//        val line = ChartFactory.createLineChart(
//            title, "", "浓度(μg/m³)",
//            null, PlotOrientation.VERTICAL, true, false, false
//        )
        var minValue = chartDatasetList[0].minValue
        var maxValue = chartDatasetList[0].maxValue
        val datasetList = mutableListOf<DefaultCategoryDataset>()
        chartDatasetList.forEach {
            minValue = min(minValue, it.minValue)
            maxValue = max(maxValue, it.maxValue)
            datasetList.add(it.dataset)
        }
        val plot = line.categoryPlot
        setLine(line)
        setRenderer(plot, datasetList)
        setX(plot)
        setY(plot, "浓度(μg/m3)", minValue, maxValue)
        return line
    }
 
    /**
     * 新建折线图并转换为byte数组
     */
    fun lineToByteArray(title: String, dataset: List<ChartDataset>): ByteArray {
        val chart = line(title, dataset)
        val output = ByteArrayOutputStream()
        ChartUtils.writeChartAsJPEG(output, chart, 1080, 607)
        val byteArray = output.toByteArray()
        output.flush()
        output.close()
        return byteArray
    }
 
    /**
     * 新建图表数据集
     */
    fun newDataset(dataList: List<ChartValue>, seriesName: String): ChartDataset? {
        if (dataList.isEmpty()) return null
        val dataset = DefaultCategoryDataset()
        var minValue = dataList[0].y
        var maxValue = minValue
        dataList.forEach {
            dataset.setValue(it.y, seriesName, it.x)
            minValue = min(minValue, it.y)
            maxValue = max(maxValue, it.y)
        }
        return ChartDataset(dataset, minValue, maxValue)
    }
 
    /**
     * 设置折线图样式
     */
    private fun setLine(chart: JFreeChart) {
        chart.legend.itemFont = Font(FONT_NAME, Font.PLAIN, 16)
        chart.title.font = Font(FONT_NAME, Font.BOLD, 20)
        chart.categoryPlot.apply {
            backgroundPaint = Color(255, 255, 255)
            rangeGridlinePaint = Color(200, 200, 200)
            rangeGridlineStroke = BasicStroke(1f)
            isRangeGridlinesVisible = true
        }
    }
 
    private fun setRenderer(plot: CategoryPlot, datasetList: List<DefaultCategoryDataset>) {
        datasetList.forEachIndexed { index, v ->
            val renderer = newBasicRenderer(paint = defaultColors[index % defaultColors.size])
            plot.setDataset(index, v)
            plot.setRenderer(index, renderer)
        }
 
    }
 
    private fun setX(plot: CategoryPlot) {
        plot.domainAxis.apply {
            // 设置x轴每个刻度的字体
            tickLabelFont = Font(FONT_NAME, Font.BOLD, 16)
            // 设置x轴标签的字体
            labelFont = Font(FONT_NAME, Font.BOLD, 20)
            // 设置x轴轴线是否显示
            isAxisLineVisible = false
            // 设置x轴刻度是否显示
            isTickMarksVisible = false
 
            upperMargin = .0
            lowerMargin = .0
        }
    }
 
    private fun setY(plot: CategoryPlot, label:String, minValue: Double, maxValue: Double) {
        val tickUnit = (maxValue - minValue) / 10
        val axis1 = NumberAxis().apply {
            this.label = label
            // 刻度展示格式化
            numberFormatOverride = DecimalFormat("0.0")
            if (tickUnit != .0) {
                // 取消自动分配间距
                isAutoTickUnitSelection = false
                // 设置间隔距离
                setTickUnit(NumberTickUnit(tickUnit))
                // 设置显示范围
                setRange(minValue, maxValue)
            }
            // 设置y轴每个刻度的字体
            tickLabelFont = Font(FONT_NAME, Font.BOLD, 16)
            // 设置y轴标签的字体
            labelFont = Font(FONT_NAME, Font.BOLD, 20)
            // 设置y轴轴线不显示
            isAxisLineVisible = false
            // 设置y轴刻度不显示
            isTickMarksVisible = false
        }
        plot.rangeAxis = axis1
    }
 
    private fun newBasicRenderer(series: Int = 0, paint: Paint): LineAndShapeRenderer {
        return LineAndShapeRenderer().apply {
            legendItemLabelGenerator = StandardCategorySeriesLabelGenerator()
            setSeriesStroke(series, BasicStroke(3f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, 2000f))
            setSeriesShapesVisible(series, false)
            setSeriesPaint(series, paint)
        }
    }
}