|
|
|
@ -10,47 +10,123 @@ import org.jfree.chart.plot.PiePlot; |
|
|
|
import org.jfree.chart.plot.PlotOrientation; |
|
|
|
import org.jfree.chart.plot.PlotOrientation; |
|
|
|
import org.jfree.chart.renderer.category.BarRenderer; |
|
|
|
import org.jfree.chart.renderer.category.BarRenderer; |
|
|
|
import org.jfree.chart.title.LegendTitle; |
|
|
|
import org.jfree.chart.title.LegendTitle; |
|
|
|
|
|
|
|
import org.jfree.chart.ui.HorizontalAlignment; |
|
|
|
|
|
|
|
import org.jfree.chart.ui.RectangleEdge; |
|
|
|
import org.jfree.data.category.DefaultCategoryDataset; |
|
|
|
import org.jfree.data.category.DefaultCategoryDataset; |
|
|
|
import org.jfree.data.general.DefaultPieDataset; |
|
|
|
import org.jfree.data.general.DefaultPieDataset; |
|
|
|
|
|
|
|
|
|
|
|
import java.awt.*; |
|
|
|
import java.awt.BasicStroke; |
|
|
|
|
|
|
|
import java.awt.Color; |
|
|
|
|
|
|
|
import java.awt.Font; |
|
|
|
|
|
|
|
import java.awt.GraphicsEnvironment; |
|
|
|
import java.io.ByteArrayOutputStream; |
|
|
|
import java.io.ByteArrayOutputStream; |
|
|
|
import java.text.NumberFormat; |
|
|
|
import java.text.DecimalFormat; |
|
|
|
|
|
|
|
import java.util.LinkedHashMap; |
|
|
|
|
|
|
|
import java.util.LinkedHashSet; |
|
|
|
|
|
|
|
import java.util.List; |
|
|
|
import java.util.Map; |
|
|
|
import java.util.Map; |
|
|
|
|
|
|
|
import java.util.Set; |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
/** |
|
|
|
* 图表渲染工具(PNG) |
|
|
|
* 图表渲染工具(PNG) |
|
|
|
* - 适配报告输出:白底、抗锯齿、中文字体、标签更清晰、柱状图显示数值 |
|
|
|
* <p> |
|
|
|
|
|
|
|
* 适用场景: |
|
|
|
|
|
|
|
* 1. JFreeChart 生成图表 |
|
|
|
|
|
|
|
* 2. poi-tl 插入 Word 模板 |
|
|
|
|
|
|
|
* 3. 报表类固定输出场景 |
|
|
|
*/ |
|
|
|
*/ |
|
|
|
public class ChartRenderUtil { |
|
|
|
public class ChartRenderUtil { |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private ChartRenderUtil() { |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
|
|
* 饼图配色 |
|
|
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
private static final Color[] PIE_COLORS = new Color[]{ |
|
|
|
|
|
|
|
new Color(91, 155, 213), |
|
|
|
|
|
|
|
new Color(237, 125, 49), |
|
|
|
|
|
|
|
new Color(165, 165, 165), |
|
|
|
|
|
|
|
new Color(255, 192, 0), |
|
|
|
|
|
|
|
new Color(68, 114, 196), |
|
|
|
|
|
|
|
new Color(112, 173, 71), |
|
|
|
|
|
|
|
new Color(38, 68, 120), |
|
|
|
|
|
|
|
new Color(158, 72, 14), |
|
|
|
|
|
|
|
new Color(99, 99, 99), |
|
|
|
|
|
|
|
new Color(153, 115, 0) |
|
|
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
|
|
* 柱状图系列配色 |
|
|
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
private static final Color[] BAR_COLORS = new Color[]{ |
|
|
|
|
|
|
|
new Color(91, 155, 213), |
|
|
|
|
|
|
|
new Color(237, 125, 49), |
|
|
|
|
|
|
|
new Color(112, 173, 71), |
|
|
|
|
|
|
|
new Color(165, 165, 165), |
|
|
|
|
|
|
|
new Color(255, 192, 0), |
|
|
|
|
|
|
|
new Color(68, 114, 196), |
|
|
|
|
|
|
|
new Color(158, 72, 14), |
|
|
|
|
|
|
|
new Color(38, 68, 120) |
|
|
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private static final String DEFAULT_OTHER_NAME = "其余问题类型"; |
|
|
|
|
|
|
|
private static final String DEFAULT_EMPTY_NAME = "暂无数据"; |
|
|
|
|
|
|
|
|
|
|
|
// =========================
|
|
|
|
// =========================
|
|
|
|
// 对外 API:根据数据生成图
|
|
|
|
// 对外 API:饼图(唯一入口)
|
|
|
|
// =========================
|
|
|
|
// =========================
|
|
|
|
|
|
|
|
|
|
|
|
public static byte[] piePng(Map<String, Number> data, int w, int h) { |
|
|
|
/** |
|
|
|
|
|
|
|
* 生成饼图 PNG |
|
|
|
|
|
|
|
* |
|
|
|
|
|
|
|
* @param title 标题 |
|
|
|
|
|
|
|
* @param data 数据,key=名称,value=数值 |
|
|
|
|
|
|
|
* @param topN 前N项,<=0表示不截断,超出部分合并为“其他” |
|
|
|
|
|
|
|
* @param w 宽 |
|
|
|
|
|
|
|
* @param h 高 |
|
|
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
public static byte[] piePng(String title, Map<String, Number> data, int topN, int w, int h) { |
|
|
|
|
|
|
|
Map<String, Number> safeData = normalizePieData(data); |
|
|
|
|
|
|
|
Map<String, Number> chartData = mergeTopNAsOther(safeData, topN, DEFAULT_OTHER_NAME); |
|
|
|
|
|
|
|
|
|
|
|
DefaultPieDataset<String> dataset = new DefaultPieDataset<>(); |
|
|
|
DefaultPieDataset<String> dataset = new DefaultPieDataset<>(); |
|
|
|
data.forEach(dataset::setValue); |
|
|
|
chartData.forEach(dataset::setValue); |
|
|
|
|
|
|
|
|
|
|
|
JFreeChart chart = ChartFactory.createPieChart( |
|
|
|
JFreeChart chart = ChartFactory.createPieChart( |
|
|
|
"占比统计", // 你不想要标题可以传 null
|
|
|
|
title, |
|
|
|
dataset, |
|
|
|
dataset, |
|
|
|
true, |
|
|
|
true, |
|
|
|
false, |
|
|
|
false, |
|
|
|
false |
|
|
|
false |
|
|
|
); |
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
beautifyPie(chart); |
|
|
|
beautifyPie(chart, chartData); |
|
|
|
return toPng(chart, w, h, "生成饼图失败"); |
|
|
|
return toPng(chart, w, h, "生成饼图失败"); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
public static byte[] barPng(Map<String, Number> data, String xLabel, String yLabel, int w, int h) { |
|
|
|
// =========================
|
|
|
|
|
|
|
|
// 对外 API:柱状图(单系列)
|
|
|
|
|
|
|
|
// =========================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
|
|
* 单系列柱状图 |
|
|
|
|
|
|
|
* |
|
|
|
|
|
|
|
* @param title 标题 |
|
|
|
|
|
|
|
* @param data 数据,key=类目,value=数值 |
|
|
|
|
|
|
|
* @param xLabel X轴标题 |
|
|
|
|
|
|
|
* @param yLabel Y轴标题 |
|
|
|
|
|
|
|
* @param w 宽 |
|
|
|
|
|
|
|
* @param h 高 |
|
|
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
public static byte[] barPng(String title, Map<String, Number> data, String xLabel, String yLabel, int w, int h) { |
|
|
|
DefaultCategoryDataset dataset = new DefaultCategoryDataset(); |
|
|
|
DefaultCategoryDataset dataset = new DefaultCategoryDataset(); |
|
|
|
// series 用一个固定值即可(yLabel 也行)
|
|
|
|
|
|
|
|
data.forEach((k, v) -> dataset.addValue(v, yLabel, k)); |
|
|
|
Map<String, Number> safeData = normalizeBarData(data); |
|
|
|
|
|
|
|
safeData.forEach((category, value) -> dataset.addValue(value, yLabel, category)); |
|
|
|
|
|
|
|
|
|
|
|
JFreeChart chart = ChartFactory.createBarChart( |
|
|
|
JFreeChart chart = ChartFactory.createBarChart( |
|
|
|
"统计分析", // 你不想要标题可以传 null
|
|
|
|
title, |
|
|
|
xLabel, |
|
|
|
xLabel, |
|
|
|
yLabel, |
|
|
|
yLabel, |
|
|
|
dataset, |
|
|
|
dataset, |
|
|
|
@ -60,75 +136,161 @@ public class ChartRenderUtil { |
|
|
|
false |
|
|
|
false |
|
|
|
); |
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
beautifyBar(chart); |
|
|
|
beautifyBar(chart, 1); |
|
|
|
return toPng(chart, w, h, "生成柱状图失败"); |
|
|
|
return toPng(chart, w, h, "生成柱状图失败"); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// =========================
|
|
|
|
// =========================
|
|
|
|
// Demo:随便生成一个图(可留可删)
|
|
|
|
// 对外 API:柱状图(多系列)
|
|
|
|
|
|
|
|
// =========================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
|
|
* 多系列分组柱状图 |
|
|
|
|
|
|
|
* |
|
|
|
|
|
|
|
* @param title 标题 |
|
|
|
|
|
|
|
* @param seriesData 多系列数据 |
|
|
|
|
|
|
|
* 外层key = 系列名,例如“下发问题数”“查实问题数” |
|
|
|
|
|
|
|
* 内层key = 类目名,例如“单位A”“单位B” |
|
|
|
|
|
|
|
* 内层value = 数值 |
|
|
|
|
|
|
|
* @param xLabel X轴标题 |
|
|
|
|
|
|
|
* @param yLabel Y轴标题 |
|
|
|
|
|
|
|
* @param w 宽 |
|
|
|
|
|
|
|
* @param h 高 |
|
|
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
public static byte[] groupedBarPng(String title, |
|
|
|
|
|
|
|
Map<String, Map<String, Number>> seriesData, |
|
|
|
|
|
|
|
String xLabel, |
|
|
|
|
|
|
|
String yLabel, |
|
|
|
|
|
|
|
int w, |
|
|
|
|
|
|
|
int h) { |
|
|
|
|
|
|
|
DefaultCategoryDataset dataset = new DefaultCategoryDataset(); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Map<String, Map<String, Number>> safeSeriesData = normalizeGroupedBarData(seriesData); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
for (Map.Entry<String, Map<String, Number>> seriesEntry : safeSeriesData.entrySet()) { |
|
|
|
|
|
|
|
String seriesName = seriesEntry.getKey(); |
|
|
|
|
|
|
|
Map<String, Number> categoryData = seriesEntry.getValue(); |
|
|
|
|
|
|
|
for (Map.Entry<String, Number> categoryEntry : categoryData.entrySet()) { |
|
|
|
|
|
|
|
dataset.addValue(categoryEntry.getValue(), seriesName, categoryEntry.getKey()); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
JFreeChart chart = ChartFactory.createBarChart( |
|
|
|
|
|
|
|
title, |
|
|
|
|
|
|
|
xLabel, |
|
|
|
|
|
|
|
yLabel, |
|
|
|
|
|
|
|
dataset, |
|
|
|
|
|
|
|
PlotOrientation.VERTICAL, |
|
|
|
|
|
|
|
true, |
|
|
|
|
|
|
|
false, |
|
|
|
|
|
|
|
false |
|
|
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
beautifyBar(chart, safeSeriesData.size()); |
|
|
|
|
|
|
|
return toPng(chart, w, h, "生成分组柱状图失败"); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// =========================
|
|
|
|
|
|
|
|
// Demo
|
|
|
|
// =========================
|
|
|
|
// =========================
|
|
|
|
|
|
|
|
|
|
|
|
public static byte[] createPieChart() { |
|
|
|
public static byte[] createPieChart() { |
|
|
|
return piePng(Map.of("查实", 40, "基本属实", 30, "不属实", 30), 900, 520); |
|
|
|
Map<String, Number> data = new LinkedHashMap<>(); |
|
|
|
|
|
|
|
data.put("查实", 40); |
|
|
|
|
|
|
|
data.put("基本属实", 30); |
|
|
|
|
|
|
|
data.put("不属实", 30); |
|
|
|
|
|
|
|
return piePng("查实情况分布", data, 0, 900, 520); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
public static byte[] createBarChart() { |
|
|
|
public static byte[] createBarChart() { |
|
|
|
return barPng(Map.of("市局", 120, "分县市局", 80, "基层单位", 50), "单位", "数量", 900, 520); |
|
|
|
Map<String, Map<String, Number>> seriesData = new LinkedHashMap<>(); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Map<String, Number> issued = new LinkedHashMap<>(); |
|
|
|
|
|
|
|
issued.put("单位A", 120); |
|
|
|
|
|
|
|
issued.put("单位B", 80); |
|
|
|
|
|
|
|
issued.put("单位C", 50); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Map<String, Number> verified = new LinkedHashMap<>(); |
|
|
|
|
|
|
|
verified.put("单位A", 100); |
|
|
|
|
|
|
|
verified.put("单位B", 60); |
|
|
|
|
|
|
|
verified.put("单位C", 30); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
seriesData.put("下发问题数", issued); |
|
|
|
|
|
|
|
seriesData.put("查实问题数", verified); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return groupedBarPng("单位问题分布", seriesData, "单位", "数量", 1000, 520); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// =========================
|
|
|
|
// =========================
|
|
|
|
// 美化:饼图
|
|
|
|
// 美化:饼图
|
|
|
|
// =========================
|
|
|
|
// =========================
|
|
|
|
|
|
|
|
|
|
|
|
private static void beautifyPie(JFreeChart chart) { |
|
|
|
private static void beautifyPie(JFreeChart chart, Map<String, Number> data) { |
|
|
|
chart.setAntiAlias(true); |
|
|
|
chart.setAntiAlias(true); |
|
|
|
chart.setTextAntiAlias(true); |
|
|
|
chart.setTextAntiAlias(true); |
|
|
|
chart.setBackgroundPaint(Color.WHITE); |
|
|
|
chart.setBackgroundPaint(Color.WHITE); |
|
|
|
chart.setBorderVisible(false); |
|
|
|
chart.setBorderVisible(false); |
|
|
|
|
|
|
|
|
|
|
|
Font titleFont = pickFont(Font.BOLD, 16); |
|
|
|
Font titleFont = pickFont(Font.BOLD, 18); |
|
|
|
Font legendFont = pickFont(Font.PLAIN, 12); |
|
|
|
Font legendFont = pickFont(Font.PLAIN, 12); |
|
|
|
Font labelFont = pickFont(Font.PLAIN, 12); |
|
|
|
Font labelFont = pickFont(Font.PLAIN, 11); |
|
|
|
|
|
|
|
|
|
|
|
if (chart.getTitle() != null) { |
|
|
|
if (chart.getTitle() != null) { |
|
|
|
chart.getTitle().setFont(titleFont); |
|
|
|
chart.getTitle().setFont(titleFont); |
|
|
|
|
|
|
|
chart.getTitle().setPaint(new Color(51, 51, 51)); |
|
|
|
|
|
|
|
chart.getTitle().setHorizontalAlignment(HorizontalAlignment.CENTER); |
|
|
|
|
|
|
|
chart.getTitle().setMargin(0, 0, 12, 0); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
LegendTitle legend = chart.getLegend(); |
|
|
|
LegendTitle legend = chart.getLegend(); |
|
|
|
if (legend != null) { |
|
|
|
if (legend != null) { |
|
|
|
legend.setItemFont(legendFont); |
|
|
|
legend.setItemFont(legendFont); |
|
|
|
legend.setBackgroundPaint(Color.WHITE); |
|
|
|
legend.setBackgroundPaint(Color.WHITE); |
|
|
|
|
|
|
|
legend.setBorder(0, 0, 0, 0); |
|
|
|
|
|
|
|
legend.setPosition(RectangleEdge.TOP); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
PiePlot plot = (PiePlot) chart.getPlot(); |
|
|
|
PiePlot<?> plot = (PiePlot<?>) chart.getPlot(); |
|
|
|
plot.setBackgroundPaint(Color.WHITE); |
|
|
|
plot.setBackgroundPaint(Color.WHITE); |
|
|
|
plot.setOutlineVisible(false); |
|
|
|
plot.setOutlineVisible(false); |
|
|
|
|
|
|
|
plot.setShadowPaint(null); |
|
|
|
|
|
|
|
plot.setInteriorGap(0.08); |
|
|
|
|
|
|
|
|
|
|
|
// label 样式:白底半透明,边框淡一点,去阴影
|
|
|
|
|
|
|
|
plot.setLabelFont(labelFont); |
|
|
|
plot.setLabelFont(labelFont); |
|
|
|
|
|
|
|
plot.setLabelPaint(new Color(51, 51, 51)); |
|
|
|
plot.setLabelBackgroundPaint(new Color(255, 255, 255, 235)); |
|
|
|
plot.setLabelBackgroundPaint(new Color(255, 255, 255, 235)); |
|
|
|
plot.setLabelOutlinePaint(new Color(220, 220, 220)); |
|
|
|
plot.setLabelOutlinePaint(new Color(220, 220, 220)); |
|
|
|
plot.setLabelShadowPaint(null); |
|
|
|
plot.setLabelShadowPaint(null); |
|
|
|
|
|
|
|
plot.setLabelLinkPaint(new Color(160, 160, 160)); |
|
|
|
|
|
|
|
plot.setLabelLinkStroke(new BasicStroke(1.0f)); |
|
|
|
|
|
|
|
plot.setSimpleLabels(false); |
|
|
|
|
|
|
|
|
|
|
|
// 标签内容:名称 + 百分比(例:查实 40.0%)
|
|
|
|
|
|
|
|
plot.setLabelGenerator(new StandardPieSectionLabelGenerator( |
|
|
|
plot.setLabelGenerator(new StandardPieSectionLabelGenerator( |
|
|
|
"{0} {2}", |
|
|
|
"{0} {2}({1})", |
|
|
|
NumberFormat.getNumberInstance(), |
|
|
|
new DecimalFormat("0"), |
|
|
|
NumberFormat.getPercentInstance() |
|
|
|
new DecimalFormat("0.0%") |
|
|
|
)); |
|
|
|
)); |
|
|
|
|
|
|
|
|
|
|
|
// 让饼图别贴边
|
|
|
|
plot.setSectionOutlinesVisible(true); |
|
|
|
plot.setInteriorGap(0.03); |
|
|
|
plot.setForegroundAlpha(0.95f); |
|
|
|
|
|
|
|
|
|
|
|
// 分离效果(可选:更“立体”,如果你觉得花哨可以注释)
|
|
|
|
// 分类太多时隐藏扇区标签,只保留图例
|
|
|
|
// plot.setExplodePercent("查实", 0.04);
|
|
|
|
if (data.size() > 6) { |
|
|
|
|
|
|
|
plot.setLabelGenerator(null); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
int index = 0; |
|
|
|
|
|
|
|
for (String key : data.keySet()) { |
|
|
|
|
|
|
|
plot.setSectionPaint(key, PIE_COLORS[index % PIE_COLORS.length]); |
|
|
|
|
|
|
|
index++; |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// =========================
|
|
|
|
// =========================
|
|
|
|
// 美化:柱状图
|
|
|
|
// 美化:柱状图
|
|
|
|
// =========================
|
|
|
|
// =========================
|
|
|
|
|
|
|
|
|
|
|
|
private static void beautifyBar(JFreeChart chart) { |
|
|
|
private static void beautifyBar(JFreeChart chart, int seriesCount) { |
|
|
|
chart.setAntiAlias(true); |
|
|
|
chart.setAntiAlias(true); |
|
|
|
chart.setTextAntiAlias(true); |
|
|
|
chart.setTextAntiAlias(true); |
|
|
|
chart.setBackgroundPaint(Color.WHITE); |
|
|
|
chart.setBackgroundPaint(Color.WHITE); |
|
|
|
@ -137,45 +299,221 @@ public class ChartRenderUtil { |
|
|
|
Font titleFont = pickFont(Font.BOLD, 16); |
|
|
|
Font titleFont = pickFont(Font.BOLD, 16); |
|
|
|
Font legendFont = pickFont(Font.PLAIN, 12); |
|
|
|
Font legendFont = pickFont(Font.PLAIN, 12); |
|
|
|
Font axisFont = pickFont(Font.PLAIN, 12); |
|
|
|
Font axisFont = pickFont(Font.PLAIN, 12); |
|
|
|
Font valueFont = pickFont(Font.PLAIN, 12); |
|
|
|
Font valueFont = pickFont(Font.PLAIN, 11); |
|
|
|
|
|
|
|
|
|
|
|
if (chart.getTitle() != null) { |
|
|
|
if (chart.getTitle() != null) { |
|
|
|
chart.getTitle().setFont(titleFont); |
|
|
|
chart.getTitle().setFont(titleFont); |
|
|
|
|
|
|
|
chart.getTitle().setPaint(new Color(51, 51, 51)); |
|
|
|
|
|
|
|
chart.getTitle().setHorizontalAlignment(HorizontalAlignment.CENTER); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
LegendTitle legend = chart.getLegend(); |
|
|
|
LegendTitle legend = chart.getLegend(); |
|
|
|
if (legend != null) { |
|
|
|
if (legend != null) { |
|
|
|
legend.setItemFont(legendFont); |
|
|
|
legend.setItemFont(legendFont); |
|
|
|
legend.setBackgroundPaint(Color.WHITE); |
|
|
|
legend.setBackgroundPaint(Color.WHITE); |
|
|
|
|
|
|
|
legend.setBorder(0, 0, 0, 0); |
|
|
|
|
|
|
|
legend.setPosition(RectangleEdge.TOP); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
CategoryPlot plot = chart.getCategoryPlot(); |
|
|
|
CategoryPlot plot = chart.getCategoryPlot(); |
|
|
|
plot.setBackgroundPaint(Color.WHITE); |
|
|
|
plot.setBackgroundPaint(Color.WHITE); |
|
|
|
plot.setOutlineVisible(false); |
|
|
|
plot.setOutlineVisible(false); |
|
|
|
|
|
|
|
|
|
|
|
// 网格线淡一点(更干净)
|
|
|
|
|
|
|
|
plot.setRangeGridlinePaint(new Color(230, 230, 230)); |
|
|
|
plot.setRangeGridlinePaint(new Color(230, 230, 230)); |
|
|
|
plot.setRangeGridlinesVisible(true); |
|
|
|
plot.setRangeGridlinesVisible(true); |
|
|
|
|
|
|
|
|
|
|
|
// 轴字体
|
|
|
|
|
|
|
|
plot.getDomainAxis().setTickLabelFont(axisFont); |
|
|
|
plot.getDomainAxis().setTickLabelFont(axisFont); |
|
|
|
plot.getDomainAxis().setLabelFont(axisFont); |
|
|
|
plot.getDomainAxis().setLabelFont(axisFont); |
|
|
|
plot.getRangeAxis().setTickLabelFont(axisFont); |
|
|
|
plot.getRangeAxis().setTickLabelFont(axisFont); |
|
|
|
plot.getRangeAxis().setLabelFont(axisFont); |
|
|
|
plot.getRangeAxis().setLabelFont(axisFont); |
|
|
|
|
|
|
|
|
|
|
|
// 柱体渲染:更清爽
|
|
|
|
|
|
|
|
BarRenderer renderer = (BarRenderer) plot.getRenderer(); |
|
|
|
BarRenderer renderer = (BarRenderer) plot.getRenderer(); |
|
|
|
renderer.setDrawBarOutline(false); |
|
|
|
renderer.setDrawBarOutline(false); |
|
|
|
renderer.setShadowVisible(false); |
|
|
|
renderer.setShadowVisible(false); |
|
|
|
renderer.setMaximumBarWidth(0.12); |
|
|
|
|
|
|
|
renderer.setItemMargin(0.10); |
|
|
|
renderer.setItemMargin(0.10); |
|
|
|
|
|
|
|
|
|
|
|
// 在柱子上方显示数值
|
|
|
|
// 多系列时适当放宽柱宽
|
|
|
|
|
|
|
|
if (seriesCount <= 1) { |
|
|
|
|
|
|
|
renderer.setMaximumBarWidth(0.12); |
|
|
|
|
|
|
|
} else if (seriesCount == 2) { |
|
|
|
|
|
|
|
renderer.setMaximumBarWidth(0.18); |
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
renderer.setMaximumBarWidth(0.25); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < seriesCount; i++) { |
|
|
|
|
|
|
|
renderer.setSeriesPaint(i, BAR_COLORS[i % BAR_COLORS.length]); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
renderer.setDefaultItemLabelGenerator(new StandardCategoryItemLabelGenerator()); |
|
|
|
renderer.setDefaultItemLabelGenerator(new StandardCategoryItemLabelGenerator()); |
|
|
|
renderer.setDefaultItemLabelsVisible(true); |
|
|
|
renderer.setDefaultItemLabelsVisible(true); |
|
|
|
renderer.setDefaultItemLabelFont(valueFont); |
|
|
|
renderer.setDefaultItemLabelFont(valueFont); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// =========================
|
|
|
|
|
|
|
|
// 数据预处理:饼图
|
|
|
|
|
|
|
|
// =========================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
|
|
* 过滤无效数据,并按数值倒序 |
|
|
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
private static Map<String, Number> normalizePieData(Map<String, Number> data) { |
|
|
|
|
|
|
|
Map<String, Number> filtered = new LinkedHashMap<>(); |
|
|
|
|
|
|
|
if (data != null) { |
|
|
|
|
|
|
|
data.forEach((k, v) -> { |
|
|
|
|
|
|
|
if (k != null && !k.isBlank() && v != null && v.doubleValue() > 0) { |
|
|
|
|
|
|
|
filtered.put(k.trim(), v); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
}); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (filtered.isEmpty()) { |
|
|
|
|
|
|
|
filtered.put(DEFAULT_EMPTY_NAME, 1); |
|
|
|
|
|
|
|
return filtered; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
LinkedHashMap<String, Number> sorted = new LinkedHashMap<>(); |
|
|
|
|
|
|
|
filtered.entrySet().stream() |
|
|
|
|
|
|
|
.sorted((a, b) -> Double.compare(b.getValue().doubleValue(), a.getValue().doubleValue())) |
|
|
|
|
|
|
|
.forEachOrdered(e -> sorted.put(e.getKey(), e.getValue())); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return sorted; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
|
|
* 仅保留前 topN 项,其余合并为“其他” |
|
|
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
private static Map<String, Number> mergeTopNAsOther(Map<String, Number> data, int topN, String otherName) { |
|
|
|
|
|
|
|
if (data == null || data.isEmpty()) { |
|
|
|
|
|
|
|
Map<String, Number> result = new LinkedHashMap<>(); |
|
|
|
|
|
|
|
result.put(DEFAULT_EMPTY_NAME, 1); |
|
|
|
|
|
|
|
return result; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (topN <= 0 || data.size() <= topN) { |
|
|
|
|
|
|
|
return data; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
String finalOtherName = (otherName == null || otherName.isBlank()) |
|
|
|
|
|
|
|
? DEFAULT_OTHER_NAME |
|
|
|
|
|
|
|
: otherName.trim(); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
List<Map.Entry<String, Number>> sorted = data.entrySet().stream().toList(); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Map<String, Number> result = new LinkedHashMap<>(); |
|
|
|
|
|
|
|
double otherTotal = 0D; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < sorted.size(); i++) { |
|
|
|
|
|
|
|
Map.Entry<String, Number> entry = sorted.get(i); |
|
|
|
|
|
|
|
if (i < topN) { |
|
|
|
|
|
|
|
result.put(entry.getKey(), entry.getValue()); |
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
otherTotal += entry.getValue().doubleValue(); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (otherTotal > 0D) { |
|
|
|
|
|
|
|
if (isIntegerLike(otherTotal)) { |
|
|
|
|
|
|
|
result.put(finalOtherName, (long) otherTotal); |
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
result.put(finalOtherName, otherTotal); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return result; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// =========================
|
|
|
|
|
|
|
|
// 数据预处理:单系列柱状图
|
|
|
|
|
|
|
|
// =========================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private static Map<String, Number> normalizeBarData(Map<String, Number> data) { |
|
|
|
|
|
|
|
Map<String, Number> result = new LinkedHashMap<>(); |
|
|
|
|
|
|
|
if (data != null) { |
|
|
|
|
|
|
|
data.forEach((k, v) -> { |
|
|
|
|
|
|
|
if (k != null && !k.isBlank() && v != null && v.doubleValue() >= 0) { |
|
|
|
|
|
|
|
result.put(k.trim(), v); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
}); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (result.isEmpty()) { |
|
|
|
|
|
|
|
result.put(DEFAULT_EMPTY_NAME, 0); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return result; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// =========================
|
|
|
|
|
|
|
|
// 数据预处理:多系列柱状图
|
|
|
|
|
|
|
|
// =========================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private static Map<String, Map<String, Number>> normalizeGroupedBarData(Map<String, Map<String, Number>> seriesData) { |
|
|
|
|
|
|
|
Map<String, Map<String, Number>> result = new LinkedHashMap<>(); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (seriesData == null || seriesData.isEmpty()) { |
|
|
|
|
|
|
|
Map<String, Number> emptySeries = new LinkedHashMap<>(); |
|
|
|
|
|
|
|
emptySeries.put(DEFAULT_EMPTY_NAME, 0); |
|
|
|
|
|
|
|
result.put("数量", emptySeries); |
|
|
|
|
|
|
|
return result; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 收集所有类目,保证多系列类目对齐
|
|
|
|
|
|
|
|
Set<String> allCategories = new LinkedHashSet<>(); |
|
|
|
|
|
|
|
for (Map.Entry<String, Map<String, Number>> seriesEntry : seriesData.entrySet()) { |
|
|
|
|
|
|
|
Map<String, Number> item = seriesEntry.getValue(); |
|
|
|
|
|
|
|
if (item == null) { |
|
|
|
|
|
|
|
continue; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
item.forEach((k, v) -> { |
|
|
|
|
|
|
|
if (k != null && !k.isBlank()) { |
|
|
|
|
|
|
|
allCategories.add(k.trim()); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
}); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (allCategories.isEmpty()) { |
|
|
|
|
|
|
|
Map<String, Number> emptySeries = new LinkedHashMap<>(); |
|
|
|
|
|
|
|
emptySeries.put(DEFAULT_EMPTY_NAME, 0); |
|
|
|
|
|
|
|
result.put("数量", emptySeries); |
|
|
|
|
|
|
|
return result; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
for (Map.Entry<String, Map<String, Number>> seriesEntry : seriesData.entrySet()) { |
|
|
|
|
|
|
|
String seriesName = seriesEntry.getKey(); |
|
|
|
|
|
|
|
if (seriesName == null || seriesName.isBlank()) { |
|
|
|
|
|
|
|
continue; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Map<String, Number> rawCategoryData = seriesEntry.getValue(); |
|
|
|
|
|
|
|
Map<String, Number> normalizedCategoryData = new LinkedHashMap<>(); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
for (String category : allCategories) { |
|
|
|
|
|
|
|
Number value = 0; |
|
|
|
|
|
|
|
if (rawCategoryData != null && rawCategoryData.containsKey(category)) { |
|
|
|
|
|
|
|
Number raw = rawCategoryData.get(category); |
|
|
|
|
|
|
|
if (raw != null && raw.doubleValue() >= 0) { |
|
|
|
|
|
|
|
value = raw; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
normalizedCategoryData.put(category, value); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
result.put(seriesName.trim(), normalizedCategoryData); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (result.isEmpty()) { |
|
|
|
|
|
|
|
Map<String, Number> emptySeries = new LinkedHashMap<>(); |
|
|
|
|
|
|
|
emptySeries.put(DEFAULT_EMPTY_NAME, 0); |
|
|
|
|
|
|
|
result.put("数量", emptySeries); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return result; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private static boolean isIntegerLike(double value) { |
|
|
|
|
|
|
|
return Math.abs(value - Math.rint(value)) < 0.0000001D; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// =========================
|
|
|
|
// =========================
|
|
|
|
// 输出 PNG
|
|
|
|
// 输出 PNG
|
|
|
|
// =========================
|
|
|
|
// =========================
|
|
|
|
@ -194,6 +532,7 @@ public class ChartRenderUtil { |
|
|
|
// =========================
|
|
|
|
// =========================
|
|
|
|
|
|
|
|
|
|
|
|
private static Font pickFont(int style, int size) { |
|
|
|
private static Font pickFont(int style, int size) { |
|
|
|
|
|
|
|
try { |
|
|
|
// 按优先级尝试(Windows 常见:微软雅黑;Linux 常见:Noto/文泉驿/DejaVu)
|
|
|
|
// 按优先级尝试(Windows 常见:微软雅黑;Linux 常见:Noto/文泉驿/DejaVu)
|
|
|
|
String[] candidates = new String[]{ |
|
|
|
String[] candidates = new String[]{ |
|
|
|
"Microsoft YaHei", "微软雅黑", |
|
|
|
"Microsoft YaHei", "微软雅黑", |
|
|
|
@ -214,6 +553,10 @@ public class ChartRenderUtil { |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
} catch (Throwable e) { |
|
|
|
|
|
|
|
// 字体系统初始化失败时兜底
|
|
|
|
|
|
|
|
System.err.println("Font detection failed, fallback to SansSerif: " + e.getMessage()); |
|
|
|
|
|
|
|
} |
|
|
|
return new Font("SansSerif", style, size); |
|
|
|
return new Font("SansSerif", style, size); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |