首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

構建可擴展的 Java 圖表元件:從架構設計到實戰擴展

     編輯:Java 相關知識

前言:為什麼要自製圖表元件?

商業圖表庫功能強大但笨重、授權昂貴。利用 Java 面向物件特性,我們可以從零構建一個輕量、可擴展、易理解的圖表元件。本文將展示如何將圖表分解為核心元素,透過介面與抽象類定義契約,實現折線圖、長條圖、組合圖與即時繪圖。

圖表類型與視覺元素分析

基礎圖表類型

類型視覺特徵典型用途
散點圖X-Y 座標點分佈兩變數相關性分析(如:淬火溫度 vs 鋼硬度)
折線圖點連成線趨勢變化(如:每日銷售額)
長條圖矩形條高度代表數值類別比較、離散資料

組合圖表範例

同一資料集可呈現為折線圖、長條圖、或兩者疊加(折線+長條),甚至包含負值長條。

組合圖表範例1 組合圖表範例2 組合圖表範例3

核心元素解構與介面設計

三大可視基元

  1. Chart Area (圖表區):容器,擁有位置、寬高,包含所有子元素
  2. Axis (座標軸):提供座標參考,繪製刻度、網格線,持有 Scale(比例尺)
  3. Plot (繪圖形狀):根據資料與座標軸繪製具體圖形(線、條、點)
圖表元素分解圖

介面契約定義

ChartWidget:所有可視元素的共同介面


public interface ChartWidget {
    int getX(); int getY();
    int getWidth(); int getHeight();
    void draw(Graphics g);
}

Scale:座標轉換模型(MVC 中的 Model)


public abstract class Scale {
    protected double min, max;       // 實際資料範圍
    protected int screenMin, screenMax; // 螢幕像素範圍
    
    public abstract int getScreenCoordinate(double value); // 實際值 → 像素
    
    public double getActualValue(int pixel) { // 像素 → 實際值
        double range = max - min;
        double ratio = (pixel - screenMin) / (double)(screenMax - screenMin);
        return min + ratio * range;
    }
    // getter/setter 略
}

Axis:座標軸視圖(MVC 中的 View)


public abstract class Axis implements ChartWidget {
    protected Scale scale;
    protected Axis peerAxis;           // 正交軸(用於網格線終點)
    protected boolean drawGrid = false;
    protected Color gridColor = Color.LIGHT_GRAY;
    protected Color axisColor = Color.BLACK;
    protected int tickLength = 5;
    protected int tickCount;
    // 位置、尺寸、顏色等屬性與 getter/setter 略
    
    protected abstract int calculateTickLabelSize(Graphics g);
}

Plot:繪圖邏輯封裝


public abstract class Plot implements ChartWidget {
    protected XAxis xAxis;
    protected YAxis yAxis;
    protected List dataSeries = new ArrayList<>();
    
    public void addDataSeries(DataSeries ds) { dataSeries.add(ds); }
    public void removeDataSeries(DataSeries ds) { dataSeries.remove(ds); }
    
    // 總繪製入口:遍歷所有資料序列
    public void draw(Graphics g) {
        for (int i = 0; i < dataSeries.size(); i++) {
            draw(g, dataSeries.get(i), i);
        }
    }
    
    // 子類實現單一資料序列的繪製
    public abstract void draw(Graphics g, DataSeries ds, int index);
}

DataSeries:資料模型


public class DataSeries {
    protected List xData = new ArrayList<>();
    protected List yData = new ArrayList<>();
    
    public void add(double x, double y) { xData.add(x); yData.add(y); }
    public double getX(int i) { return xData.get(i); }
    public double getY(int i) { return yData.get(i); }
    public int size() { return xData.size(); }
}

具體實作:X/Y 軸與折線圖

X 軸實作


public class XAxis extends Axis {
    public XAxis(Scale scale, int tickCount) {
        this.scale = scale; this.tickCount = tickCount;
    }
    
    @Override
    public void draw(Graphics g) {
        if (!(scale instanceof XScale)) return;
        int ticks = getTickCount();
        int tickDist = (scale.getScreenMax() - scale.getScreenMin()) / (ticks + 1);
        int tickY = peerAxis.getScale().getScreenMin();
        int gridLength = peerAxis.getScale().getScreenMax();
        
        g.setColor(axisColor);
        g.drawLine(scale.getScreenMin(), tickY, scale.getScreenMax(), tickY); // 軸線
        
        for (int i = 0; i < ticks; i++) {
            int tickX = scale.getScreenMin() + tickDist * (i + 1);
            if (isDrawGrid()) { g.setColor(gridColor); g.drawLine(tickX, tickY, tickX, gridLength); }
            g.setColor(axisColor);
            g.drawLine(tickX, tickY, tickX, tickY + tickLength); // 刻度線
            g.drawString(String.valueOf(i+1), tickX - 5, tickY + tickLength + 12); // 標籤
        }
    }
}

折線圖 Plot 實作


public class LinePlot extends Plot {
    private Color lineColor = Color.BLUE;
    
    public LinePlot(DataSeries ds, XAxis xAxis, YAxis yAxis) {
        addDataSeries(ds); this.xAxis = xAxis; this.yAxis = yAxis;
    }
    
    @Override
    public void draw(Graphics g, DataSeries ds, int index) {
        if (ds == null) return;
        g.setColor(lineColor);
        
        int n = ds.size();
        int[] xPoints = new int[n];
        int[] yPoints = new int[n];
        
        for (int i = 0; i < n; i++) {
            xPoints[i] = xAxis.getScale().getScreenCoordinate(ds.getX(i));
            yPoints[i] = yAxis.getScale().getScreenCoordinate(ds.getY(i));
        }
        g.drawPolyline(xPoints, yPoints, n); // 一行核心代碼繪製折線
    }
}

完整折線圖組裝


double[] y = {12.5, 14.1, 13.2, 11.4, 13.25, 12.32};
DataSeries ds = new DefaultDataSeries(y); // 僅提供 Y 值,X 自動 1,2,3...

XAxis xAxis = new XAxis(new XScale(0, y.length+1), y.length);
YAxis yAxis = new YAxis(new YScale(10, 15), 4);
xAxis.setDrawGrid(true); yAxis.setDrawGrid(true);

LinePlot plot = new LinePlot(ds, xAxis, yAxis);
Chart chart = new Chart(xAxis, yAxis, plot);

JFrame frame = new JFrame("Line Plot Demo");
frame.setSize(400, 300);
frame.getContentPane().add(chart);
frame.setVisible(true);
單組資料折線圖 多組資料折線圖

擴展長條圖

只需實作 BarPlot extends Plot,核心差異在於計算每個長條的 x, y, width, height 並呼叫 g.fillRect()。支援多組長條分組、負值處理。


// 關鍵計算邏輯節錄
int barWidth = xAxis.getWidth() / (ds.size() + 1) / dataSeries.size();
int barX = xAxis.getScale().getScreenCoordinate(i+1) - groupWidth/2 + index*barWidth;
int barY = yAxis.getScale().getScreenCoordinate(value);
int barH = yAxis.getScale().getScreenCoordinate(0) - barY; // 簡化版
g.fillRect(barX, barY, barWidth, barH);
單組長條圖 多組長條圖 含負值長條圖

組合圖表:一行代碼疊加


Chart chart = new Chart(xAxis, yAxis, barPlot); // 先建長條圖
chart.addPlot(linePlot);                        // 再加折線圖

即時繪圖:觀察者模式驅動自動重繪

架構設計

  • DataSeries → 被觀察者,資料變更時通知監聽器
  • Chart → 觀察者,實作 DataChangeListener,收到通知呼叫 repaint()
即時繪圖類結構 通知流程序列圖

關鍵代碼


// 監聽者介面
public interface DataChangeListener {
    void dataChanged();
}

// 可觀察的資料序列
public class RealtimeDataSeries extends DataSeries {
    private List listeners = new ArrayList<>();
    
    public void registerDataChangeListener(DataChangeListener l) { listeners.add(l); }
    
    @Override
    public void add(double x, double y) {
        super.add(x, y);
        notifyListeners();
    }
    
    private void notifyListeners() {
        for (DataChangeListener l : listeners) l.dataChanged();
    }
}

// 即時圖表
public class RealtimeChart extends Chart implements DataChangeListener {
    @Override
    public void dataChanged() { repaint(); } // 極簡:觸發重繪
}

模擬即時資料源


new Thread(() -> {
    RealtimeDataSeries ds = new RealtimeDataSeries();
    RealtimeChart chart = new RealtimeChart(xAxis, yAxis, new LinePlot(ds, xAxis, yAxis));
    chart.registerDataChangeListener(chart);
    
    Random r = new Random();
    while (true) {
        ds.add(System.currentTimeMillis(), 10 + r.nextDouble() * 5);
        Thread.sleep(500);
    }
}).start();

擴展建議與總結

此架構極易擴展:

  • LinePlot:增加線型(虛線)、線寬、資料點標記(圓/方/三角)
  • BarPlot:漸層填充、圖案填充、3D 效果
  • Axis:對數座標、時間軸、多座標軸、3D 投影
  • Chart:圖例、標題、工具提示、滑鼠互動、匯出圖片

掌握「介面定義契約、抽象類提供骨架、具體類實現變化」的分層設計,即可構建出專屬、輕量、高度可定製的圖表元件。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved