内容


在 SVG 中呈现动态图形

创建随内容动态缩放的 SVG 图形

Comments

我在 IBM Hursley UK 做测试和开发时遇到这样一个问题。于是我便写了一些代码,以便能够精细地使用 SVG 绘制柱状图、线图和散点图。但是每次测试所要处理的数据都要跨越多个数量级。使用同一比例来绘制每个图是没有用处的,很多图要么非常大,要么非常小,数据要么拥挤在一个角上,要么分散在整个页面上。

我需要一种能够自动确定每组数据使用的最佳比例的方法。

注意,要查看本文中的 SVG 图,需要一个 SVG 查看器,可以从 参考资料中找到该查看器(还有一个 .zip 压缩包,它包括所有相关的文件)。

一点高中数学知识

从问题中我发现,自动缩放图形的最佳方式是观察需要绘制的数据集,确定数据集中的最大值,并使用该值作为其他将绘制的数据的比例。

最好通过 实际的例子来说明我采用的技术。假设测试生成三个值:A、B 和 C。每次测试后都需要将这些值绘制到柱状图中,但是每次测试中数据的范围都是动态变化的。

第一次测试生成的数据,如下所示:

  • A=100
  • B=50
  • C=25

在这次测试中,A 的值最大,为 100。第一项任务(也是最重要的任务)是计算图形的 比例因子,就是说在绘图时每单位的值在高度上要占用多少像素。比例因子可以用图轴的高度除以数据的最大值来获得。通过下面的计算就可以清楚如何获得比例因子。

  • 为了简单起见,Y 轴的高度设为 1000 像素,这样计算起来很方便。
  • 数据集中的最大值是 100(A)。
  • 对于该图,比例因子应该是: 1000/100 = 10
  • 因此对于这个柱状图,每单位的值在高度上用 10 个像素表示。
  • 要得到每个条的高度,只需要用比例因子乘上数据集中的值即可,就是说每个条的高度为:
    图 1. 使用比例因子,最大值为 100(A)
    使用比例因子,最大值为 100(A)
    使用比例因子,最大值为 100(A)

假设下一次测试得到如下所示的值:

  • A=2000
  • B=5000
  • C=800

要记住,Y 轴仍然是 1000 个像素高。这次测试中的最大值在 B 栏,为 5000。 因此比例因子就是 1000/5000=0.2

因此每个条的高度为:

图 2. 使用比例因子,最大值为 5000(B)
使用比例因子,最大值为 5000(B)
使用比例因子,最大值为 5000(B)

可以看出,因为条的高度是相对于最大值计算得到的,条的高度不会超过 1000 像素,因此图不会画得太高。SVG 动态缩放的关键就是比例因子。要记住,比例因子是根据最大值计算得到的,而其他数据都根据比例因子值来计算。

现在已经介绍了比例因子这个重要概念,下面来说明如何使用它。下面的例子是一个小型的 Java 语言程序,从命令行中得到一组数据,然后按照 上例所述的方式绘制 SVG 柱状图。虽然使用 Java 代码进行编写,但其中的关键是数学计算。需要的话,可以参照这里的 Java 代码用其他任何语言改写这个程序。

本文不再列出完整的代码,仅仅介绍其中的要点。建议您下载这个例子(请参阅 参考资料),然后使用带有对行进行编号功能的编辑器打开 SVG_barchart.java 文件。

首先,第 38-48 行确定传入的数据的最大值。

最重要的是第 51 行,它计算 Y 轴的比例因子:

清单 1. 计算 Y 轴比例因子
51.	YAxisScalingFactor = 1000/(double)largestNumber;

第 58 和第 59 行创建了 SVG 输出文件。SVG 使用 SVGout.write(-SVG-) 写入该文件。主要必须使用各种转义字符,如双引号用 \" 表示。有时候这样使 SVG 难以阅读,但必需如此,否则 Java 编译器就不能正确地解释它,代码也无法编译。此外,还要注意每个 SVG 文本行都以 \n 开始,这样可以保证使用 SVG 查看器的“查看源代码”功能时,SVG 更容易阅读。

第 70-79 行绘制 X 和 Y 轴。X 轴是从 [x=0, y=1000] 到 [x=1000, y=1000] 的一条直线,Y 轴是从 [x=0, y=0] 到 [x=0, y=1000],这样就建立了一个简单的笛卡儿坐标系。

第 82 和 83 行在 Y 轴的顶端和中间各划一条水平虚线,这样做只是为了让查看图时更方便。

第 86 和 87 行在顶端和中间标记 Y 轴。注意,要使用前面计算的最大值来标记 Y 轴。

清单 2. 标记 Y 轴
86.	SVGout.write("\n <text style=\"fill:black; stroke:none\" 
	x=\"-10\" y=\"0\" >" + largestNumber + "</text>");
87.	SVGout.write("\n <text style=\"fill:black; stroke:none\" 
	x=\"-10\" y=\"500\" >" + (largestNumber/2) + "</text>");

值是动态变化的,因此这里使用 largestNumber 来标记轴。如果需要,添加其他的基准线和标记也很方便。

绘制柱状图从第 90 行开始:

清单 3. 绘制条
90.// The graph is ready to be rendered with the values.
91.for(i=0;i<barChartValues.length;i++){
92.  
93. // Calculate the Y position. First work out how high the bar 
94. // will be by multiplying the value by the scaling factor.
94. // calculated earlier
95. double barHeight = 
96.    Integer.parseInt(barChartValues[i]) * YAxisScalingFactor;
97.		
98. System.out.println("Bar Height is =" + barHeight);
99.    
100. // You now have the height that the bar will be. Need to work 
101. // out now where to place the bar. With Y values running 
102. // positively down, and the Y-axis being 1000 pixels tall, 
103. // simply subtract the  bar height from 1000 to get the position
104. // of where to place the bar.
105. 
106. double YStart = 1000 - barHeight;
107.                
108. // Each of the bars is 100 pixels wide. So to space them out 
109. //(with a 10-pixel gap between them), multiply the readings position
110. // in the array by 110.
111.	 
112. double XPosition = (i*110);
113.    
114. // Generate some random numbers for your bar colours
115. int randomRed = random.nextInt(255);
116. int randomGreen = random.nextInt(255);
117. int randomBlue = random.nextInt(255);
118.    
119. // You now have all your values ready. Draw the rectangle. 
120. SVGout.write("\n<rect x=\""+XPosition+"\" y=\""+
121.  YStart+"\" width =\""+ 100 +"\" height=\""+barHeight+
122.  "\" style=\"fill:rgb("+randomRed+"
123.  ,"+randomGreen+","+randomBlue+");\" /> ");
124.    	                             
125.}

可以下载并自己来运行该程序,传递不同量级的数据,查看图形的变化和自动缩放功能,图 3 给出了一些例子。

图 3. 动态生成的 SVG 条形图
动态生成的 SVG 条形图
动态生成的 SVG 条形图

线图

目前为止,我们已经介绍了如何动态缩放柱状图。但这并不是惟一的图形,某些形式的数据,特别是基于时间的数据(如股票价格或者地震数据),最好用线图来表示。也可以使用与柱状图类似的方法缩放线图。

假设您监控数据库中的行数。 每 30 秒读一次行数,并记录在一个数组中,最后得到包含以下 10 个值的数组:

10,20,30,50,90,25,45,60,70,10

这次测试同样需要计算每次测试的比例因子。但是您可能不希望局限在只能绘制包含特定个数据的图,因此需要同时在 X 轴和 Y 轴两个方向上进行缩放。

绘制线图最好的办法是使用称为 polyline 的 SVG 元素。polyline 接受成对的 X 和 Y 坐标值并在相邻的两点之间划一条线,比如:

<polyline points="0 0, 10 10, 20 20">

将绘制一条线,其中三个点在 [X=0,Y=0]、[X=10,Y=10] 和 [X=20,Y=20]。这就是需要缩放和计算的点。

与上面的描述相同,也提供了一个 Java 示例程序,根据输入的数据缩放和呈现折线图(请参阅 参考资料)。

然后分析代码中的要点。

首先计算 X 轴的比例因子。它由要呈现的读入次数决定,因此要用 X 轴的像素数除以读入的次数。比方说,如果读取了 50 次,X 轴的比例因子就是 1000/50 = 20。因此每次读取操作的结果在 X 轴上间隔 20 像素进行绘制。读取的结果以数组传入时,只需要用数组的元素个数去除以 1000 即可。

清单 4. 计算 X 轴的比例因子
40. XAxisScalingFactor = 1000/(double)valuesToPlot.length;

第 41-49 行确定数组中的最大值。

第 54-100 行准备输出文件并绘制 X 和 Y 坐标轴。

Y 轴比例因子是在第 104 行计算的,方法和上面的柱状图使用的方法一样(请参阅 清单 2)。

第 108 到 125 行呈现了要绘制的线图:

图 5. 绘制折线
108. // Render the line
109. SVGout.write("\n<polyline points=\"0 1000,");
110.    	    
111. for(i=0;i<valuesToPlot.length;i++){
112.    
113.  // Calculate the X position by determining which 
114.  //value in the array you are dealing with.
115.  XValue = ((i+1)*XAxisScalingFactor);
116.
117.  YValue = Integer.parseInt(valuesToPlot[i]);
118.  YValue = YValue*YAxisScalingFactor;
119.  YValue = 1000-YValue;
120.    
121.  // You now have your polyline point.
122.  SVGout.write(" " + XValue + " " + YValue +",\n");
123.                
124. }           	           
125. // Close off the polyline.
126. SVGout.write("\" style=\"stroke:red; stroke-width: 3; fill : 
127. none;\"/>");

第 109 行是 polyline 元素的起点,它与坐标轴的原点重合。

然后依次处理数组中的值。首先计算 X,用元素在数组中的位置乘上 X 轴比例因子。比如,如果数组中有 50 个元素,X 轴比例因子就是:

  • XAxisScalingFactor = 1000/(double)valuesToPlot.length;
  • XAxisScalingFactor = 1000/(double)50;
  • XAxisScalingFactor = 20

因此,两个值以 20 像素的间距绘制,全部数组的计算结果为:

  • 第 1 个点: x=20,<1st value>
  • 第 2 个点: x=40,<2nd value>
  • 第 3 个点: x=60,<3rd value>
  • ...
  • ...
  • 第 49 个点: x=980,<49th value>
  • 第 50 个点: x=1000,<50th value>

注意,这里使用了 (i+1),因为数组中第一个元素的索引号是 0,要从第一个元素开始,必须加上 1

然后计算 Y 值:

  1. 第 117 行从数组中提取数据并转化成整数。
  2. 第 118 行将该值乘以 Y 轴的比例因子,以确定其位置。
  3. 最后从 1000 中减去这个值,这是因为 Y 轴是从页面的上方向下增长,X 轴从 Y 轴的第 1000 个像素的位置开始绘制,因此,要确定计算得到的 Y 值在 X 轴的上方多远的位置上,就必须从 1000 中减去它。

现在计算得到了 X 和 Y 位置,然后要将该坐标点添加到 polyline 参数表中。

处理完数组中的值后封闭折线并应用适当的样式。

试一试以下这个例子:传入不同量级的数据。您会看到图形也能根据传入的最大值自动缩放。还要试一试传入不同数量的数据,比如 10 个或者 1000 个。X 轴将会按照传入的数据个数自动缩放。图 4 给出了一些例子。

图 4. 动态生成的 SVG 线图
动态生成的 SVG 线图
动态生成的 SVG 线图

散点图

最后介绍的是散点图。这种图形适用于在 X 维和 Y 维上都相差很大的数据。我们采用与前面相同的方法,但是这一次要绘制的数据同时提供 X 和 Y 坐标值。

该例中,数据以 (X-value),(Y-value) 的形式传入,比如: [1,1]、[3,5]、[50,2] 和 [10,34]。

我同样提供了一个 Java 实例程序以供下载(请参阅 参考资料),请分析下面这些代码。

第 31-57 行确定 X 和 Y 的最大值。

第 60-61 行计算 X 轴和 Y 轴的比例因子。

第 67-119 行绘制坐标轴,这里画了 4 条辅助虚线,并相应地做了标记。

第 124-157 行绘制数据:

清单 6. 计算和绘制散点图
122. // The axis and the guide lines are ready; now draw the data.
123. SVGout.write("\n    <g style=\"fill:none; 
124. stroke:red; stroke-width:3\">");
125.
126. for(i=0;i<dataToPlot.length;i++){
127.                 
128.    // Get the value out of the array.
129.	String value = dataToPlot[i];
130.                
131.	// The data is in the form (X-Value),(Y-Value), so find
132.	// the comma and get the values on either side of it.
133.	index = value.indexOf (',');
134.	String X_Pos = value.substring(0,index);
135.	String Y_Pos = value.substring(index+1);
136.
137.	// Change them to numbers
138.	XValue = Integer.parseInt(X_Pos);
139.	YValue = Integer.parseInt(Y_Pos);
140.
141.	// Calculate the point's position by using the scaling 
142.	// factor calculated earlier
143.	XValue = XValue*XAxisScalingFactor;     
144.	YValue = YValue*YAxisScalingFactor;
145.	YValue = 1000-YValue;
146.
147.	// You now have your point. As it's a scatter plot, it 
148.	// would look nice with an X, so use the point to draw 
149	// a line from the top left to the bottom right, and from
150.	// the top right to the bottom left.
151. 
152.	SVGout.write("\n  <line x1=\""+ (int)(XValue-5) + 
153		"\" y1=\""+(int)(YValue+5)+ "\" x2=\""+(int)(XValue+5)+
154.	"\" y2=\""+(int)(YValue-5)+"\" />"); 
155.	SVGout.write("\n  <line x1=\""+ (int)(XValue+5) + 
156.	"\" y1=\""+(int)(YValue+5)+ "\" x2=\""+(int)(XValue-5)+
157.	"\" y2=\""+(int)(YValue-5)+"\" />");    
158.}

首先要定义这一组的样式(第 123 行)。

然后开始处理数据,第 129 行获得了数据。

根据逗号分隔 X 和 Y 值(第 133-135 行),并将这些值转化成数值(第 138-139 行)。

计算点的位置(第 143 - 145 行)。

现在可以画点了,但是这一次不是简单地画一个点,而是要用 X 来标记。也就是说要画两条线,一条从左上角到右下角,一条从右上角到左下角,偏移量都是 5 个像素,从而得到以计算点为中心的 X。

下载并运行这个例子。

图 5. 动态生成的散点图
动态生成的散点图
动态生成的散点图

进一步改进

为了简化和保持代码的可读性,我在上述例子中使用了最少的 Java 代码生成 SVG。当然,您还可以改进这些例子,增加新的颜色、效果和信息。例子中包含两个柱状图生成器,一个像前面所述的那样绘制简单的矩形(参见 图 3)。另一个稍微复杂一点,使用斜线和点填充图形,但计算条形的方法不变,如图 6 所示。

图 6. 改进的柱状图
改进的柱状图
改进的柱状图

图 7 是一个线图,显示了测试的数据库中的一些信息,添加了一个徽标、阴影效果和测试的统计表。

图 7. 改进的 SVG 线图
改进的 SVG 线图
改进的 SVG 线图

图 8 是一个散点图,测试过程中我希望比较从数据库中删除一条记录所用的时间,一组数据使用 JDBC 执行这项操作,另一组则使用准备好的语句执行该操作。两组数据都提供了徽标、阴影效果和状态表。

图 8. 改进的 SVG 散点图
改进的 SVG 散点图
改进的 SVG 散点图

这些图的完整 SVG 版本包含在示例文件中。

结束语

本文通过例子说明了如何根据数据缩放 SVG 图形。通过这些技术和提供的示例代码,您可以呈现自己的图形,并根据需要定制图形。


下载资源


相关主题


评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=XML
ArticleID=49153
ArticleTitle=在 SVG 中呈现动态图形
publish-date=11012004