构建可扩展的Java图表组件
副标题#e#
媒介
Java语言所具有的面向工具特性,使很多巨大的问题可以解析成相对独立的工具来处理惩罚。本文用面向工具的要领,将一个图表组件从解析到如何组合,以及如何举办扩展作了具体的讲授。从简朴的折线图到稍巨大的多种形状组合的图表,读者可以学到构建一个可扩展的图表组件是何等的容易。
常见的图表范例
图表具有很直观的视觉结果,可以利便的用来较量数据的差别、图案和趋势等。
从外观上来看,常用到的图表主要有散点图、(折)曲线图、柱状图等。本文主要接头这几种图形样式。个中这每种图又可以与其它的范例组合发生更多的形式。下面以图例来说明:
先来看散点图:
图1-1
图1-1是一个典范的散点图,它是由一组X值和一组Y值在二维坐标中两两成对描画而成。一般这种图形反应两组数据的相关性。譬喻,要观察钢的硬度与淬火温度的干系,假设上图的横轴暗示淬火的温度,纵轴暗示同时测出的钢的硬度,这时我们可从上图看出一个趋势,即淬火的温度越高,钢的硬度越大。
再来看一个折线图:
图1-2
图1-3
#p#副标题#e#
在图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)
#p#分页标题#e#
因为所有的图表可视元素都有一些配合的属性:位置,宽度和高度,它们还要认真绘制本身自己。所以我们设计一个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;}
public void setScreenMin(int screenMin){this.screenMin = screenMin;}
}
在上面的Axis类代码中,我们在原有的ChartWidget接口的基本上,为Axis添加了几个其它的属性:轴线的颜色axisColor,网格线的颜色gridColor及网格线的可见属性drawGrid。尚有刻度线的长度和个数tickLength和tickCount。而peerAxis属性是参考坐标轴,在绘制坐标轴时的会用到。 Scale类也是抽象的,因为横轴和纵轴的屏幕坐标的转换方法纷歧样,所以getScreenCoordinate()要领留待子类来实现它。
图表形状(Plot)
构成图表尚有一个最重要的类,认真描写数据的图表形状,我们称之为Plot。Plot应能绘制多组数据,而这组数据呢,我们专门用一个模子来描写它,这就是DataSeries。由于我们在这里接头的是二维图表,所以DataSeries应能提供两组别离代表X和Y坐标的数据。照旧来看看它们的类图(图2-6):
图2-6
#p#分页标题#e#
为了plot能绘制多组数据,除了从ChartWidget担任来的draw(Graphics)要领外,plot还提供了draw(Graphics,DataSeries,int)要领,用来绘制单组的数据。下面的代码更能说明问题:
public abstract class Plot implements ChartWidget
{
protected int x;
protected int y;
protected int width;
protected int height;
protected XAxis xAxis;
protected YAxis yAxis;
protected ArrayList dataSeries;
public int getX(){return x;}
public int getY(){return y;}
public int getWidth(){return width;}
public int getHeight(){return height;}
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)dataSeries.get(i),i);
}
public abstract void draw(Graphics g,DataSeries ds,int index);
}
Plot类也被设计成了抽象类,详细的绘制要领由子类为实现。而DataSeries类的过于简朴,在此我们就不列出代码了。
图表(Chart)
最后就是将上面的元素合成一个完整的图表,即Chart类。一个Chart有一个横轴和一个纵轴以及至少一个Plot,而且可觉得它添加多个Plot。我们最厥后看一下整个Chart及其相关类的UML干系图:
图2-7
由于篇幅有限,在此就不列出Chart类的代码了。
完成一个折线图
由于前面先容的只是一些接口或抽象类,要完成一个图表组件,还必需实现它们,下面我们以一个折线图为例,来完成一个完整的折线图。
实现x轴和y轴
其实前面的Axis抽象类已经完成一个大部门的操纵,余下的就是别离完成x轴和y轴的绘制了。在这里我们就不规划列出完整的类代码,只列出要害的实现部门。
Public class XAxis extends Axis
{
……
public void draw(Graphics g)
{
if ( ! (scale instanceof XScale) )
return;
int ticks = getTickCount();
int tickDist = (int) ((double)(scale.getScreenMax()-scale.getScreenMin())/(double)(ticks+1));
int tickX = scale.getScreenMin();
int tickY = peerAxis.getScale().getScreenMin();
int gridLength = peerAxis.getScale().getScreenMax();
int axisLength = scale.getScreenMax()-scale.getScreenMin();
/*配置轴线颜色*/
g.setColor(axisColor);
/*绘制横轴*/
g.drawLine(tickX, tickY, tickX+axisLength,tickY);
for ( int i = 0 ; i < ticks; i++ )
{
tickX = scale.getScreenMin()+tickDist*(i+1);
if ( isDrawGrid() )
{
/*假如drawGrid属性为true,用gridColor绘制网格线*/
g.setColor(gridColor);
g.drawLine(tickX, tickY , tickX, gridLength );
}
/*绘制刻度线*/
g.setColor(axisColor);
g.drawLine(tickX, tickY , tickX, tickY+tickLength);
int tickLabelWidth = g.getFontMetrics().stringWidth(String.valueOf(i+1));
int tickLabelHeight = g.getFontMetrics().getHeight();
g.drawString(String.valueOf(i+1), tickX-(tickLabelWidth/2), tickY+tickLabelHeight);
}
}
}
public class YAxis extends Axis
{
public void draw(Graphics g)
{
if ( ! (scale instanceof YScale) )
return;
int ticks = getTickCount();
int tickDist = (int) Math.abs((double)(scale.getScreenMax() - scale.getScreenMin())/(double)(ticks+1));
int tickY = scale.getScreenMin();
int tickX = peerAxis.getScale().getScreenMin();
int gridLength = peerAxis.getScale().getScreenMax();
int axisLength = scale.getScreenMax();
/*绘制纵坐标轴*/
g.setColor(axisColor);
g.drawLine(tickX, tickY, tickX, axisLength);
for ( int i = 0 ; i < ticks; i++ )
{
tickY = scale.getScreenMin()-tickDist*(i+1);
if ( isDrawGrid() )
{
/*假如drawGrid属性为true,用gridColor绘制网格线*/
g.setColor(gridColor);
g.drawLine(tickX, tickY , gridLength, tickY );
}
/*绘制刻度线*/
g.setColor(axisColor);
g.drawLine(tickX, tickY , tickX-tickLength, tickY);
int tickLabelWidth = g.getFontMetrics().stringWidth(String.valueOf(i+1));
g.drawString(String.valueOf(i+1), tickX-tickLength-tickLabelWidth, tickY);
}
}
}
实现画折线的LinePlot
由于Plot是由DataSeries为它提供画图数据的,在实现LinePlot之前,先来实现一个DefaultDataSeries类:
#p#分页标题#e#
public class DefaultDataSeries extends DataSeries
{
public DefaultDataSeries(Object[] yData) throws InvalidDataException
{
super();
if ( yData == null || !(yData[0] instanceof Double) )
throw new InvalidDataException();
for ( int i = 0;i<yData.length;i++ )
{
/*将y值添加到序列中*/
this.yData.add(yData[i]);
/*按照y值的个数,从1开始自动添加相应数量的x值*/
this.xData.add(new Double(i+1));
}
}
}
这个DefaultDataSeries提供了一个结构要领,利用者只需提供一组y坐标值,即可结构一个DataSeries了。
下面是很重要的部门了。我们来看看实现一个画折线的LinePlot是何等的简朴:
Public class LinePlot extends Plot
{
……
public void draw(Graphics g, DataSeries ds, int index)
{
if ( ds == null ) return;
g.setColor(lineColor);
double[] x = new double[ds.size()];
double[] y = new double[ds.size()];
int[] xPoints = new int[ds.size()];
int[] yPoints = new int[ds.size()];
for ( int i = 0; i< ds.size(); i++ )
{
x[i] = ((Double)ds.getXData(i)).doubleValue();
y[i] = ((Double)ds.getYData(i)).doubleValue();
/*将ds中的实际值转换成屏幕坐标值*/
xPoints[i] = xAxis.getScale().getScreenCoordinate(x[i]);
yPoints[i] = yAxis.getScale().getScreenCoordinate(y[i]);
}
/*绘制折线*/
g.drawPolyline(xPoints, yPoints, xPoints.length);
}
}
上面可出了LinePlot中绘制折线的代码,我们看到,绘制一个折线是何等的轻松和简朴。
完成折线图
通过前面的实现代码,我们来看一个完整的折线图示例:
double[] y = new double[]
{ 12.5,14.1,13.2,11.4,13.25,12.32 };
try {
DataSeries ds = new DefaultDataSeries(Primary2ObjectUtil.Doulbe2Object(y));
XAxis xaxis = new XAxis(new XScale(0,y.length+1),ds.size());
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);
}
catch (InvalidDataException e)
{
e.printStackTrace();
}
下面是这个措施运行起来的屏幕截图:
(单组数据的折线图)
(有多组数据的折线图)
扩展其它范例的图表
通过前面的例子,我们知道要实现特定范例的图表,只要实现特定的Plot类就可以了。假如数据有非凡名目,只需再扩展一个DataSeries就可以了。为使各人加深领略,我们再以一个柱状图为例子作讲授。
在第一节的图1-2和图1-3中,我们知道,一组数据除了用折线图暗示之外,还可以暗示成柱状图的形式。在这里我们就借用折线图的数据,来实现一个BarPlot。下面列出了BarPlot的要害代码:
public class BarPlot extends Plot
{
……
public void draw(Graphics g, DataSeries ds, int index)
{
if ( ds == null ) return;
/*每组柱子的个数*/
int bars = this.dataSeries.size();
/*出每个柱子应有的宽度*/
int barWidth = (int) ((double)xAxis.width/((double)ds.size()+1)/bars-barSpace);
if ( barWidth <=0 ) barWidth = 1;
int barx,bary,barw,barh;
int barGroupWidth = barWidth*bars;
double ymin = yAxis.getScale().getMin();
for ( int i = 0;i<ds.size(); i++ )
{
barx = (int)(xAxis.getScale().getScreenCoordinate(i+1) - barGroupWidth/2.0d) + index*barWidth;
double val = ((Double)ds.getYData(i)).doubleValue();
bary = yAxis.getScale().getScreenCoordinate(val);
if ( ymin<0) if ( val<0 )
{
barh = bary-yAxis.getScale().getScreenCoordinate(0);
bary = bary-barh;
}
else
{
barh = yAxis.getScale().getScreenCoordinate(0)-bary;
}
else
{
barh = yAxis.getScale().getScreenCoordinate(ymin)-bary;
}
barw = barWidth; g.setColor(barColor);
g.fillRect(barx,bary,barw, barh);
g.setColor(Color.BLACK);
g.drawRect(barx,bary, barw, barh);
}
}
#p#分页标题#e#
BarPlot的实现比LinePlot稍微巨大一点。主要是要计较每个柱子的位置,宽度和高度。由于思量到多组柱子以及柱子的值为负数时坐标差异,所以计较要繁索一点。但总体来说,实现BarPlot也是相当简朴的。由于柱状图运行代码与折线图雷同,这里就不列出演示代码。下面来看看措施在几种环境下的运行画面:
(单组数据的柱状图)
(多组数据的柱状图)
(有负值的柱状图)
此刻我们有了画折线图的类LinePlot和画柱状图的类BarPlot。我们要生成一个折线图与柱状图组合起来的例子。照旧来看看代码是如何实现的:
DataSeries ds = new DefaultDataSeries(Primary2ObjectUtil.Doulbe2Object(y1));
XAxis xaxis = new XAxis(new XScale(0,y1.length+1),ds.size());
YAxis yaxis = new YAxis(new YScale(10,15),4);
xaxis.setDrawGrid(true);
yaxis.setDrawGrid(true);
LinePlot linePlot = new LinePlot(ds,xaxis,yaxis);
BarPlot barPlot = new BarPlot(ds,xaxis,yaxis);
/*先生成Bar Chart*/
Chart chart = new Chart(xaxis,yaxis,barPlot);
/*然后将Line Plot加到Bar Chart中*/
chart.addPlot(linePlot);
代码中,我们先成立了一个Line Plot和一个Bar Plot,再生成了一个Bar Chart,然后再将Line Plot加到Bar Chart中。一个组合图表就简简朴单的完成了。来看看:
及时画图
及时画图最常见的就是股票行情图了。我们不规划在此讲授如何实现这样的股票行情图。为了能说明问题,我们用一个线程按时发生一个数据,模仿及时画图。
在此,我们对前面的图表组件举办扩展。这里我们用到了一个设计模式:Observer模式。利用Observer模式可使一个工具的状态产生改变时,所有依赖于它的工具都获得通知并自动更新。所以在Observer中,要害的工具是被调查方针和调查者。一个调查方针可以有多个调查者。调查者必需事先注册给调查目次。这样当调查目次的状态产生改变时,调查者才有大概被通知到。
在我们的及时画图布局中,DataSeries就是被调查方针,而Chart就是调查者。为此,我们设计了一个DataChangeListener接口作为调查者。我们重载了一个Chart来实现DataChangeListener。相应的,我们也重载了DataSeries类,提供注册调查者的机制。下面是它们的布局:
RealtimeChart工具事先用registerDataChangeListener要领注册给RealtimeDataSeries工具。当RealtimeDataSeries的数据产生改变时,将挪用notifyListener要领通知所有已注册的DataChangeListener。Notify要领将依次挪用每个已注册DataChangeListener工具的dataChanged要领。如下图:
在RealtimeChart中,实现了DataChangeListener接口的dataChanged要领:
public class RealtimeChart extends Chart implements DataChangeListener
{
……
/*实现DataChangeListener的要领*/
public void dataChanged()
{ repaint();
}
}
RealtimeChart的dataChanged要领在这里只需简朴的从头绘制一次本身。绘制时将自动按新的数据来绘制。操作Observer模式,及时画图就这样子简朴的实现了。
借助及时画图的例子,读者可以很容易的自行写一个毗连到数据库可能说网络流的画图措施,在此,我们就不作讲授了。
竣事语
本文已较完整的讲授了一个可扩展的图表组件的构建进程。读者可以在此基本上扩展本身的组件。譬喻扩展LinePlot,使它具有可改变线型,线宽,尚有点样式等成果。或扩展BarPlot,使它可以用差异的填充模式。你甚至可以扩展Axis来实现3D模式的图表。