前言
Java语言所具有的面向对象特性,使许多复杂的问题可以分解成相对独立的对象来处理。本文用面向对象的方法,将一个图表组件从分解到如何组合,以及如何进行扩展作了详细的讲解。从简单的折线图到稍复杂的多种形状组合的图表,读者可以学到构建一个可扩展的图表组件是多么的容易。
常见的图表类型
图表具有很直观的视觉效果,可以方便的用来比较数据的差异、图案和趋势等。
从外观上来看,常用到的图表主要有散点图、(折)曲线图、柱状图等。本文主要讨论这几种图形样式。其中这每种图又可以与其它的类型组合产生更多的形式。下面以图例来说明:
先来看散点图:
图1-1
图1-1是一个典型的散点图,它是由一组X值和一组Y值在二维坐标中两两成对描绘而成。一般这种图形反映两组数据的相关性。例如,要考查钢的硬度与淬火温度的关系,假设上图的横轴表示淬火的温度,纵轴表示同时测出的钢的硬度,这时我们可从上图看出一个趋势,即淬火的温度越高,钢的硬度越大。
再来看一个折线图:
图1-2 |
图1-3 |
在图1-2的折线图中,假设横轴表示周一到周日,纵轴表示某商场的日销售额。我们可以看出其临近周末的销售额呈急剧上升趋势,到周日开始回落,而最惨淡的是周四。通常折线图也可以表示成柱状图的形式,如图1-3。
复杂一点的图形
上图三个图形的数据都是同样的,但它们所能够直观表达的意思又不尽相同。诸如此类的图表,形式多种多样,但它们都是由这几种基本图表组合而成的。
接下来的一节,我们来看一下组成图表的基本元素有哪些。
图表的主要元素
图表的组成
从前面的例子中我们可以看出,每种图表都是由横坐标轴,纵坐标轴,还有不同的绘图形状组成。为了更容易理解,大家看一下下面的分解图:
上图2-1 下图2-2
是一个柱赐己驼巯咄嫉淖楹贤急恚颐墙纸庵螅ㄍ?-2),可以清晰的看到,它是由图表区、坐标轴、网格线、图表形状等组成:
图表区(Chart):包含所有其它的图表元素。
坐标轴(Axis):提供绘图形状的坐标参考。一个图表中通常有一个垂直和一个水平坐标轴。而网格线是以坐标轴的刻度为参考,贯穿整个绘图区。网格线同坐标轴一样也可分为水平和垂直网格线。
图表形状(Plot):也是以坐标轴为参考,按一定的比例将数据按相应形状绘制出来。
所以,从根本上来说,一个图表的是由三种基本的可视元素组成的:图表区,坐标轴,图表形状。
图表的主要元素
图表的组成
从前面的例子中我们可以看出,每种图表都是由横坐标轴,纵坐标轴,还有不同的绘图形状组成。为了更容易理解,大家看一下下面的分解图:
上图2-1 下图2-2
是一个柱赐己驼巯咄嫉淖楹贤急恚颐墙纸庵螅ㄍ?-2),可以清晰的看到,它是由图表区、坐标轴、网格线、图表形状等组成:
图表区(Chart):包含所有其它的图表元素。
坐标轴(Axis):提供绘图形状的坐标参考。一个图表中通常有一个垂直和一个水平坐标轴。而网格线是以坐标轴的刻度为参考,贯穿整个绘图区。网格线同坐标轴一样也可分为水平和垂直网格线。
图表形状(Plot):也是以坐标轴为参考,按一定的比例将数据按相应形状绘制出来。
所以,从根本上来说,一个图表的是由三种基本的可视元素组成的:图表区,坐标轴,图表形状。
图表的主要元素
图表的组成
从前面的例子中我们可以看出,每种图表都是由横坐标轴,纵坐标轴,还有不同的绘图形状组成。为了更容易理解,大家看一下下面的分解图:
上图2-1 下图2-2
是一个柱赐己驼巯咄嫉淖楹贤急恚颐墙纸庵螅ㄍ?-2),可以清晰的看到,它是由图表区、坐标轴、网格线、图表形状等组成:
图表区(Chart):包含所有其它的图表元素。
坐标轴(Axis):提供绘图形状的坐标参考。一个图表中通常有一个垂直和一个水平坐标轴。而网格线是以坐标轴的刻度为参考,贯穿整个绘图区。网格线同坐标轴一样也可分为水平和垂直网格线。
图表形状(Plot):也是以坐标轴为参考,按一定的比例将数据按相应形状绘制出来。
所以,从根本上来说,一个图表的是由三种基本的可视元素组成的:图表区,坐标轴,图表形状。
图表形状(Plot)
组成图表还有一个最重要的类,负责描述数据的图表形状,我们称之为Plot。Plot应能绘制多组数据,而这组数据呢,我们专门用一个模型来描述它,这就是DataSeries。由于我们在这里讨论的是二维图表,所以DataSeries应能提供两组分别代表X和Y坐标的数据。还是来看看它们的类图(图2-6):
图2-6
为了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类:
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); } } |
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模式的图表。
分享到:
相关推荐
在当今的数据驱动型社会中,图表组件成为了数据分析与展示不可或缺的一部分。选择合适的图表组件对于确保数据的有效传达至关重要。本文将深入探讨几款市场上广受欢迎的图表组件,旨在帮助读者更好地理解这些工具的...
4. 图表组件:提供轴(Axis)、图例(Legend)、图例项(LegendItem)等组件,方便构建复杂的图表结构。 5. 数据处理:可以处理各种数据源,如数组、列表、数据库查询结果等。 6. 国际化支持:支持多语言,满足全球...
JFreeChart的核心设计理念是灵活性和可扩展性,允许开发者根据项目需求进行深度定制。源码中的关键组件包括ChartFactory,用于生成各种类型的图表;Dataset接口及其实现,用于存储和管理图表数据;Renderer,负责...
7. **可扩展性和自定义性** 为了满足特定需求,开发者通常需要扩展这些基础图表功能,如添加自定义的鼠标事件监听器、实现动态更新数据等功能。Java的面向对象特性使得这样的扩展变得相对容易。 8. **响应式设计**...
它提供了丰富的功能,让开发者能够轻松地构建可交互的、动态的图形模型,适用于数据可视化、流程图、组织结构图等多种场景。JGraph不仅支持基本的图形绘制,还支持拖放操作、事件处理、布局算法等高级特性,使得图形...
2. **图表组件**:图表是数据可视化的基石,包括柱状图、折线图、饼图等。React数据可视化库支持多种图表类型,这些图表通常包含动态数据绑定、交互式功能如悬停显示数据详情、可配置的图表样式等。开发者可以根据...
组件设计在J2EE应用开发中尤为重要,因为它们是构建大型、可扩展企业应用的基础。文档提到了服务接口、组件控制器工厂等概念,这些都是J2EE组件设计的关键部分。 5. **服务接口**: 文档提到了服务接口的定义,即使...
同时,遵循良好的代码组织和设计模式,如MVC(Model-View-Controller)架构,可以确保代码的清晰性和可扩展性。 在提供的压缩包文件中,"精通Java+Web动态图表编程6-10章.rar"和"精通Java+Web动态图表编程1-5章.rar...
它的灵活性和可扩展性使其成为Java GUI开发中的一个宝贵工具。如果你正在寻找一种方式在Java应用程序中呈现动态、交互的图形信息,那么JGraph是一个值得考虑的选择。 总结来说,JGraph是一个基于Java的图形组件库,...
Java 扩展包是 Java 语言生态中不可或缺的一部分,它们为开发者提供了丰富的功能和工具,以增强标准Java API的功能。在Java中,扩展包通常被称为“包”(Package),是组织类和接口的一种方式,使得代码更加有序和可...
JFreeChart是一个开源的Java图表库,它为开发者提供了丰富的API接口来构建各类专业图表。该项目具有以下特点: 1. **广泛的图表支持**:支持饼图、柱状图(包括堆栈柱状图)、线图、区域图、分布图、混合图、甘特图...
JGraph的核心类是`JGraph`,它是一个可扩展的图形模型视图控制器(MVC)实现。模型(Model)存储图形元素的数据,视图(View)负责渲染这些元素,而控制器(Controller)处理用户交互。开发者可以通过自定义这些组件...
总的来说,这个Java项目旨在提供一个灵活、可扩展的数据分析平台,通过集成多种数据源,实现了数据的多样化处理和展示,对于需要实时监控和深入洞察数据的企业来说,这是一个非常有价值的工具。
这种设计思路有助于我们在实际项目中快速创建出满足需求的图表组件,同时保持代码的整洁和高效。 总结来说,设计和实现Web图表生成引擎的关键在于理解Web环境下的图表绘制原理,选择合适的实现方式(如JavaBean),...
**Swing**是JFC的核心部分,它是AWT的增强版,提供了更多的组件和功能,且完全由Java编写,实现了100%的纯Java,因此具有更好的可移植性。Swing引入了“轻量级”组件,减少了对操作系统资源的依赖,同时提供了可插拔...
8. **图表组件化**:G2Plot采用组件化的设计,每个图表都是独立的组件,可以方便地在项目中复用和组合,提高开发效率。 9. **社区与生态**:AntV有一个活跃的开发者社区,提供了详细的文档、示例和教程,以及丰富的...
可能使用了MVC(模型-视图-控制器)架构,将业务逻辑、界面展示和数据处理分离,以提高代码的可维护性和可扩展性。 总的来说,"java股票实时查询小软件"结合了JavaFX的GUI设计、网络编程(HTTP请求和HTML解析)、...
JavaFX还包括3D图形支持、媒体播放、图表组件以及Scenegraph API,这使得开发者能够创建更加动态和互动的用户界面。 在下载的源代码中,`javaSrc199.zip`很可能是包含了一系列Java GUI程序的源代码文件。这些源代码...