第八章 構建可擴展的Java圖表組件
前言
Java語言所具有的麵向對象特性,使許多複雜的問題可以分解成相對獨立的對象來處理。本文用麵向對象的方法,將一個圖表組件從分解到如何組合,以及如何進行擴展作了詳細的講解。從簡單的折線圖到稍複雜的多種形狀組合的圖表,讀者可以學到構建一個可擴展的圖表組件是多麼的容易。
常見的圖表類型
圖表具有很直觀的視覺效果,可以方便的用來比較數據的差異、圖案和趨勢等。
從外觀上來看,常用到的圖表主要有散點圖、(折)曲線圖、柱狀圖等。本文主要討論這幾種圖形樣式。其中這每種圖又可以與其它的類型組合產生更多的形式。下麵以圖例來說明:
先來看散點圖:
圖1-1
圖1-1是一個典型的散點圖,它是由一組X值和一組Y值在二維坐標中兩兩成對描繪而成。一般這種圖形反映兩組數據的相關性。例如,要考查鋼的硬度與淬火溫度的關係,假設上圖的橫軸表示淬火的溫度,縱軸表示同時測出的鋼的硬度,這時我們可從上圖看出一個趨勢,即淬火的溫度越高,鋼的硬度越大。
再來看一個折線圖:
圖1-2
圖1-3
在圖1-2的折線圖中,假設橫軸表示周一到周日,縱軸表示某商場的日銷售額。我們可以看出其臨近周末的銷售額呈急劇上升趨勢,到周日開始回落,而最慘淡的是周四。通常折線圖也可以表示成柱狀圖的形式,如圖1-3。
複雜一點的圖形
圖1-4
圖1-5
圖1-6
上圖三個圖形的數據都是同樣的,但它們所能夠直觀表達的意思又不盡相同。諸如此類的圖表,形式多種多樣,但它們都是由這幾種基本圖表組合而成的。
圖表的主要元素
圖表的組成
從前麵的例子中我們可以看出,每種圖表都是由橫坐標軸,縱坐標軸,還有不同的繪圖形狀組成。為了更容易理解,大家看一下下麵的分解圖:
上圖2-1 下圖2-2
是一個柱狀圖和折線圖的組合圖表,我們將它分解之後(圖2-2),可以清晰的看到,它是由圖表區、坐標軸、網格線、圖表形狀等組成:
圖表區(Chart):包含所有其它的圖表元素。
坐標軸(Axis):提供繪圖形狀的坐標參考。一個圖表中通常有一個垂直和一個水平坐標軸。而網格線是以坐標軸的刻度為參考,貫穿整個繪圖區。網格線同坐標軸一樣也可分為水平和垂直網格線。
圖表形狀(Plot):也是以坐標軸為參考,按一定的比例將數據按相應形狀繪製出來。
所以,從根本上來說,一個圖表的是由三種基本的可視元素組成的:圖表區,坐標軸,圖表形狀。
實現基本圖表元素
基本圖表元素的特征
我們已經知道了圖表的主要組成元素,現在再來看看這些元素有哪些特征。
還是來看一個圖:
圖2-3
從圖上我們可以看出,一個位於屏幕坐標係中的圖表具有寬度(Wc)和高度(Hc)以及坐標位置(x,y)。圖表中的坐標軸也有高度Ha、寬度Wa及坐標位置(x,y)。同樣,圖表形狀也有相應的高度Hp和寬度Wp和坐標位置。
一個圖表通常擁有一個橫坐標軸和縱坐標軸。所有的繪圖數據的坐標都要轉化成適當的屏幕坐標,於是我們需要一個新的元素:比例尺。比例尺應負責完成實際坐標值到屏幕坐標值以及屏幕坐標值到實際坐標值的相互轉化。而坐標軸是用來描繪刻度用的,它應與比例尺成對使用。
一個圖表還可以有多個圖表形狀(如圖1-6和圖2-1),並且我們可以往圖表裏麵增加或移除形狀。一個圖表形狀應可以表示至少一組以上的數據(如圖1-5)。由於圖表形狀要在圖表上描繪數據,它需要有一個東西來記錄數據,我們將它稱之為數據序列。
基本圖表元素的設計實現
我們的目標是用程序來實現一個圖表。前麵的討論我們已經知道構成圖表的基本的元素和它們的特性了。由此我們可以為這幾個圖表元素設計幾個接口類。在設計之前,要首先說明一下,我們不打算實現類似於商業化圖表組件的強大交互功能,我們所有的設計,隻是為了能闡明問題。
圖表元素接口(ChartWidget)
因為所有的圖表可視元素都有一些共同的屬性:位置,寬度和高度,它們還要負責繪製自己本身。所以我們設計一個ChartWidget接口,其它所有可視元素都要繼承於這個接口。這個接口的類圖如圖2-4:
圖2-4
由這個類圖,我們可以很容易的寫出它的代碼:
public interface ChartWidget{
public int getX();
public int getY();
public int getWidth();
public int getHeight();
public void draw(Graphics g);
}
坐標軸(Axis)
接下來的一個類是坐標軸Axis。坐標軸主要任務是繪製軸及其刻度(Tick)和刻度值,因為它繪製時是按一定的比例繪製的,所以它需要有一個比例尺將實際坐標值轉換值成屏幕坐標值。這就引出了Scale這個類。Scale類主要完成實際坐標值到屏幕坐標值以及屏幕坐標值到實際坐標值的相互轉化。由此,Axis與Scale是一對相互依賴的類。從設計模式的角度來看,Axis是視圖(View),負責界麵繪製,Scale就是它的模型(Model),負責提供相應的數據。它們的類圖見圖2-5:
圖2-5
下麵來分別看看Axis類與Scale類的代碼:
public abstract class Axis implements ChartWidget
{
protected Scale scale;
protected int x;
protected int y;
protected int width;
protected int height;
protected Axis peerAxis;
protected boolean drawGrid;
protected Color gridColor;
protected Color axisColor;
protected int tickLength;
protected int tickCount;
public Axis()
{
gridColor = Color.LIGHT_GRAY;
axisColor = Color.BLACK;
tickLength = 5;
drawGrid = false;
}
public int getTickCount(){ return tickCount;}
public void setTickCount(int tickCount){this.tickCount=tickCount;}
public Scale getScale(){ return scale;}
public void setScale(Scale scale){ this.scale = scale;}
public int getX(){ return x;}
public void setX(int x){this.x = x;}
public int getY(){ return y;}
public void setY(int y){this.y = y;}
public int getHeight(){ return height;}
public void setHeight(int height){this.height = height;}
public int getWidth(){ return width;}
public void setWidth(int width){this.width = width;}
public boolean isDrawGrid(){return drawGrid;}
public void setDrawGrid(boolean drawGrid){this.drawGrid=drawGrid;}
public Color getAxisColor(){return axisColor;}
public void setAxisColor(Color axisColor){ this.axisColor=axisColor;}
public Color getGridColor(){return gridColor;}
public void setGridColor(Color gridColor){this.gridColor=gridColor;}
public int getTickLength(){return tickLength;}
public void setTickLength(int tickLength){this.tickLength=tickLength;}
public Axis getPeerAxis(){return peerAxis;}
public void setPeerAxis(Axis peerAxis){this.peerAxis = peerAxis;}protected abstract int calculateTickLabelSize(Graphics g);}
public abstract class Scale{
protected double min;
protected double max;
protected int screenMin;
protected int screenMax;
public abstract int getScreenCoordinate(double value);
public double getActualValue(int value)
{
double vrange = max - min;
if(min < 0.0 && max < 0.0)
vrange = (min - max) * -1.0;
double i = screenMax - screenMin;
i = ((double)(value - screenMin) * vrange) / i;
i += min;
return i;
}
public void setMax(double max){this.max = max;}
public void setMin(double min){this.min = min;}
public double getMax(){return max;}
public double getMin(){return min;}
public int getScreenMax(){return screenMax;}
public int getScreenMin(){return screenMin;}
public void setScreenMax(int screenMax){this.screenMax =screenMax;}