You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.25 07:40:05