JFreeChart使用XYSplineRenderer时自动量程无法完整显示样条曲线问题
问题描述
需要绘制平滑形态的图表,因此选用了XYSplineRenderer作为渲染器,同时要求NumberAxis针对数据段自动设置量程。但部分场景下计算生成的样条曲线值会超出自动量程范围,导致曲线无法完整显示。
经排查,自动量程的计算时机早于样条曲线生成时机是问题诱因。曾尝试按比例扩大纵轴量程缓解该问题,代码如下:
double percentOverRange = 0.05;//2% double initalRange = series.getMaxY() - series.getMinY(); double increase = initalRange*percentOverRange; verticalAxis.setRange(new Range(series.getMinY()-increase, series.getMaxY()+increase));
但该方案存在明显缺陷:根据输入数据不同,量程扩展比例最高需达25%,会导致图表曲线适配精度下降。
上述代码生成的图表可复现前两个数据点之间曲线显示不全的问题,注:域轴为DateAxis,采用不含周末数据的日度数据。完整复现代码如下:
public class MyPlotChart { private static Color MetalColor = new Color(255, 152, 0); static double[] yData = new double[] { 0.67, 0.67, 0.69, 0.70, 0.70, 0.71, 0.71 }; static String[] labels = new String[] { "2021-11-09", "2021-11-10", "2021-11-11", "2021-11-12", "2021-11-15", "2021-11-16", "2021-11-17" }; public static void plot(String metal, int samples) throws IOException, ParseException { SimpleDateFormat dateformatyyyy_MM_dd = new SimpleDateFormat("yyyy-MM-dd"); SimpleDateFormat dateformatdd_MM_yyyy = new SimpleDateFormat("dd-MM-yyyy"); XYSeries series = new XYSeries(metal); for (int i = 0; i < yData.length; i++) { Date date = dateformatyyyy_MM_dd.parse(labels[i]); series.add(date.getTime(), yData[i]); } //Configure Vertical Axis NumberAxis verticalAxis = new NumberAxis(null); NumberFormat numberFormat = NumberFormat.getInstance(Locale.getDefault()); numberFormat.setRoundingMode(RoundingMode.HALF_DOWN); numberFormat.setMinimumFractionDigits(2); numberFormat.setMaximumFractionDigits(2); double vericalTickUnit = (series.getMaxY() - series.getMinY()) / 7; NumberTickUnit nt = new NumberTickUnit(vericalTickUnit, numberFormat); verticalAxis.setTickUnit(nt); double percentOverRange = 0.05;// 2% double initalRange = series.getMaxY() - series.getMinY(); double increase = initalRange * percentOverRange; verticalAxis.setRange(new Range(series.getMinY()-increase, series.getMaxY()+increase)); verticalAxis.setAutoRange(true); verticalAxis.setAutoRangeIncludesZero(false); verticalAxis.setTickMarksVisible(true); verticalAxis.setTickMarkInsideLength(3f); //Configure Domain Axis DateAxis domainAxis = new DateAxis(null); domainAxis.setTickUnit(new DateTickUnit(DateTickUnitType.DAY, 1, dateformatdd_MM_yyyy)); //Configure Renderer XYSplineRenderer r = new XYSplineRenderer(10); r.setSeriesPaint(0, MetalColor); r.setDefaultShapesVisible(true); r.setSeriesStroke(0, new BasicStroke(3.0f)); XYDataset dataset = new XYSeriesCollection(series); XYPlot xyplot = new XYPlot(dataset, domainAxis, verticalAxis, r); xyplot.getDomainAxis().setVerticalTickLabels(true); xyplot.setDomainGridlinesVisible(false); xyplot.setBackgroundImage(null); xyplot.setBackgroundPaint(Color.WHITE); Font font = xyplot.getDomainAxis().getTickLabelFont(); Font fontnew = new Font(font.getName(), Font.BOLD, 14); xyplot.getDomainAxis().setTickLabelFont(fontnew); xyplot.getRangeAxis().setTickLabelFont(fontnew); JFreeChart chart = new JFreeChart(xyplot); chart.removeLegend();// Remove legend chart.setBackgroundPaint(Color.WHITE); String fileName = "myChart" + metal + samples + "TEST.png"; ChartUtils.saveChartAsPNG(new File(fileName), chart, 600, 600); } public static void main(String[] args) throws IOException, ParseException { MyPlotChart.plot("metal", 7); } }
补充更新1
以下图表均为上述代码仅修改XYSplineRenderer的precision参数生成,根据官方Javadoc定义:
XYSplineRenderer: 一款使用自然三次样条连接数据点、且/或在每个数据点绘制形状的渲染器。
public XYSplineRenderer(int precision)
使用指定精度创建新渲染器,不对样条下方(0到样条之间)区域进行填充。
参数:precision - 数据点之间的插值点数量。
也就是说自然三次样条仅基于原始数据点计算,precision参数用于定义每对数据点之间的插值点数量,对应公式为:Precision = N - 1,其中N为每个数据段之间的插值点总数。
目前有两种可行优化方案:
- XYSplineRenderer新增返回自然三次样条集合的方法,计算各分段极值后对应设置AutoRange;
- JFreeChart新增基于NURBS(非均匀有理B样条)的渲染器,通过控制点控制曲线形态避免超量程。



补充更新2
当存在数据空缺(如周末无数据)时问题会进一步加剧,DateAxis会在周五和周一之间插入两天间隔,数据点间距更大,样条曲线超出量程的长度也会更长。
内容的提问来源于stack exchange,提问作者J Robes

