Java新手求助:用Java几何基元绘制带数据的Graphics2D折线图
嘿,作为Java新手,不用JFreeChart直接用Graphics2D撸折线图确实得绕开现成工具的便利,但拆解成基础步骤后其实挺直观的。我给你整理了一套完整的实现思路+可运行代码,你可以跟着一步步来:
核心实现步骤拆解
先明确几个关键问题:屏幕坐标系是y轴向下的,而我们的折线图数据y轴是向上的,所以得做坐标转换;另外要把数据范围映射到面板的绘图区域(留边距放坐标轴和标签)。
1. 准备数据与参数定义
首先得把你的数据整理成可处理的格式,比如用自定义类存每个点的x/y值,同时定义绘图区域的边距、坐标轴长度这些参数:
// 自定义数据点类 class DataPoint { double x; double y; public DataPoint(double x, double y) { this.x = x; this.y = y; } } // 示例数据 List<DataPoint> data = Arrays.asList( new DataPoint(1, 20), new DataPoint(2, 35), new DataPoint(3, 28), new DataPoint(4, 45), new DataPoint(5, 30) ); // 绘图区域参数 int margin = 50; // 四周留边 int graphWidth = getWidth() - 2 * margin; int graphHeight = getHeight() - 2 * margin;
2. 计算数据范围与坐标映射
先找出数据中x和y的最大最小值,然后把数据值映射到屏幕坐标:
// 计算数据范围 double minX = data.stream().mapToDouble(p -> p.x).min().orElse(0); double maxX = data.stream().mapToDouble(p -> p.x).max().orElse(1); double minY = data.stream().mapToDouble(p -> p.y).min().orElse(0); double maxY = data.stream().mapToDouble(p -> p.y).max().orElse(1); // 坐标转换方法:数据值转屏幕坐标 private int dataXToScreen(double x) { return margin + (int) ((x - minX) * graphWidth / (maxX - minX)); } private int dataYToScreen(double y) { // 反转y轴,因为屏幕y向下 return margin + graphHeight - (int) ((y - minY) * graphHeight / (maxY - minY)); }
3. 绘制坐标轴与刻度
用Graphics2D的drawLine画坐标轴,然后计算刻度位置并绘制刻度线和标签:
Graphics2D g2 = (Graphics2D) g; g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); // 抗锯齿 // 画坐标轴 g2.drawLine(margin, margin, margin, margin + graphHeight); // y轴 g2.drawLine(margin, margin + graphHeight, margin + graphWidth, margin + graphHeight); // x轴 // 画x轴刻度与标签 int xTickCount = 5; for (int i = 0; i <= xTickCount; i++) { double xValue = minX + (maxX - minX) * i / xTickCount; int screenX = dataXToScreen(xValue); g2.drawLine(screenX, margin + graphHeight, screenX, margin + graphHeight + 5); // 刻度线 g2.drawString(String.format("%.1f", xValue), screenX - 10, margin + graphHeight + 20); // 标签 } // 画y轴刻度与标签 int yTickCount = 5; for (int i = 0; i <= yTickCount; i++) { double yValue = minY + (maxY - minY) * i / yTickCount; int screenY = dataYToScreen(yValue); g2.drawLine(margin - 5, screenY, margin, screenY); // 刻度线 g2.drawString(String.format("%.1f", yValue), margin - 30, screenY + 5); // 标签 }
4. 绘制折线与数据点
用GeneralPath来绘制折线(比多次drawLine更流畅),然后给每个数据点画个小圆点标记:
// 绘制折线 GeneralPath path = new GeneralPath(); DataPoint firstPoint = data.get(0); path.moveTo(dataXToScreen(firstPoint.x), dataYToScreen(firstPoint.y)); for (int i = 1; i < data.size(); i++) { DataPoint p = data.get(i); path.lineTo(dataXToScreen(p.x), dataYToScreen(p.y)); } g2.setColor(Color.BLUE); g2.draw(path); // 绘制数据点与标签 g2.setColor(Color.RED); for (DataPoint p : data) { int screenX = dataXToScreen(p.x); int screenY = dataYToScreen(p.y); g2.fillOval(screenX - 3, screenY - 3, 6, 6); // 圆点标记 g2.drawString(String.format("%.1f", p.y), screenX + 5, screenY - 5); // 数据标签 }
5. 整合到Swing组件
把这些逻辑放到JPanel的paintComponent方法里,然后加到JFrame中运行:
public class LineChartPanel extends JPanel { private List<DataPoint> data; public LineChartPanel(List<DataPoint> data) { this.data = data; setPreferredSize(new Dimension(600, 400)); } @Override protected void paintComponent(Graphics g) { super.paintComponent(g); // 上面的所有绘图逻辑都放到这里 int margin = 50; int graphWidth = getWidth() - 2 * margin; int graphHeight = getHeight() - 2 * margin; double minX = data.stream().mapToDouble(p -> p.x).min().orElse(0); double maxX = data.stream().mapToDouble(p -> p.x).max().orElse(1); double minY = data.stream().mapToDouble(p -> p.y).min().orElse(0); double maxY = data.stream().mapToDouble(p -> p.y).max().orElse(1); Graphics2D g2 = (Graphics2D) g; g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); // 绘制坐标轴 g2.drawLine(margin, margin, margin, margin + graphHeight); g2.drawLine(margin, margin + graphHeight, margin + graphWidth, margin + graphHeight); // 绘制x轴刻度 int xTickCount = 5; for (int i = 0; i <= xTickCount; i++) { double xValue = minX + (maxX - minX) * i / xTickCount; int screenX = margin + (int) ((xValue - minX) * graphWidth / (maxX - minX)); g2.drawLine(screenX, margin + graphHeight, screenX, margin + graphHeight + 5); g2.drawString(String.format("%.1f", xValue), screenX - 10, margin + graphHeight + 20); } // 绘制y轴刻度 int yTickCount = 5; for (int i = 0; i <= yTickCount; i++) { double yValue = minY + (maxY - minY) * i / yTickCount; int screenY = margin + graphHeight - (int) ((yValue - minY) * graphHeight / (maxY - minY)); g2.drawLine(margin - 5, screenY, margin, screenY); g2.drawString(String.format("%.1f", yValue), margin - 30, screenY + 5); } // 绘制折线 GeneralPath path = new GeneralPath(); DataPoint firstPoint = data.get(0); path.moveTo(margin + (int) ((firstPoint.x - minX) * graphWidth / (maxX - minX)), margin + graphHeight - (int) ((firstPoint.y - minY) * graphHeight / (maxY - minY))); for (int i = 1; i < data.size(); i++) { DataPoint p = data.get(i); path.lineTo(margin + (int) ((p.x - minX) * graphWidth / (maxX - minX)), margin + graphHeight - (int) ((p.y - minY) * graphHeight / (maxY - minY))); } g2.setColor(Color.BLUE); g2.draw(path); // 绘制数据点和标签 g2.setColor(Color.RED); for (DataPoint p : data) { int screenX = margin + (int) ((p.x - minX) * graphWidth / (maxX - minX)); int screenY = margin + graphHeight - (int) ((p.y - minY) * graphHeight / (maxY - minY)); g2.fillOval(screenX - 3, screenY - 3, 6, 6); g2.drawString(String.format("%.1f", p.y), screenX + 5, screenY - 5); } } public static void main(String[] args) { SwingUtilities.invokeLater(() -> { JFrame frame = new JFrame("Custom Line Chart"); List<DataPoint> data = Arrays.asList( new DataPoint(1, 20), new DataPoint(2, 35), new DataPoint(3, 28), new DataPoint(4, 45), new DataPoint(5, 30) ); frame.add(new LineChartPanel(data)); frame.pack(); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setLocationRelativeTo(null); frame.setVisible(true); }); } }
额外提示
- 如果数据范围有负数,只需要调整坐标转换的逻辑,确保最小值能映射到正确的屏幕位置
- 可以通过
g2.setStroke(new BasicStroke(2f))设置折线的粗细 - 标签的位置可以根据实际情况微调,避免重叠
- 如果需要绘制多条折线,只需要重复折线绘制步骤,换不同颜色即可
内容的提问来源于stack exchange,提问作者Wildan Mufid Taufiq
相关产品推荐
相关产品推荐

