利用 VML 制作统计图全攻略

2022-08-10 11:05

文档管理软件,文档管理系统,知识管理系统,档案管理系统的技术资料:

提要:
VML是Microsoft IE 5.0及其后续版本内嵌的矢量图形实现,虽然MS也提倡日后使用SVG,但是作为一个IE内嵌的标记语言,在某些时候的应用还是比较方便的。本文通过完整的描述一个统计饼图的建立过程,来展现VML在Web方面的魅力。文章通过实现一个JavaScript类,读者能够完整地看到整个饼图的制作过程。
VML(Vector Markup Language)自从问世以来似乎都处于默默无闻的地步,直到现在为止,情况依然没有改变。其实细心的一点你就可以发现,在Web方面,MS得很多产品还是内置使用了,最典型的就是Office的自选图形,将word或者ppt文档存储成html,如果文档内部使用了自选图形,你就可以看到那些图形使用过VML来表述的,另外一个典型的应用就是Visio的导出到web这个工具。
前段时间,James在CSDN发表了使用ASP生成统计图的例子,我也认真拜读了其中的代码,并且也针对一些问题找他请教了,James的颜色感觉非常好,可惜我没有那样的功底,因此在代码实现中我就使用了随机颜色来实现,可能整体的界面看起来会稍微差劲一点。
统计图比较典型的是饼图,柱状图,曲线图,本文着重讲解Pie的制作过程,文章采用了JavaScript实现了一个类,如果相关Javascript面向对象不是特别了解的,可以参考我另外的文章《面向对象的Javascript编程》和《再论面向对象的JavaScript编程》。

暂且不考虑如何实现,我们先看看代码最终的使用如何。
objPie=new VMLPie("600px","450px","人口统计图"); //初始化宽度,高度,标题
objPie.BorderWidth=3; //图表边框
objPie.BorderColor="blue"; //图表边框颜色
objPie.Width="800px"; //定义图表宽度
objPie.Height="600px"; //定义图表高度
objPie.backgroundColor="#ffffff"; //定义背景颜色
objPie.Shadow=true; //是否需要阴影 true为是 false为不要阴影
//添加图表数据
//顺序为名称,值,描述
objPie.AddData("北京",50,"北京的人口");
objPie.AddData("上海",52,"上海的固定人口");
objPie.AddData("天津",30,"天津的外地人口");
objPie.AddData("西安",58,"西安城市人口");
objPie.AddData("武汉",30,"武汉的外地人口");
objPie.AddData("重庆",58,"重庆城市人口");
result.innerHTML=objPie.Draw(); //生成VML数据

这段代码就是最终的调用,我封装了一个VMLPie的JavaScript类,而本文的重点也就是详细地描述类的具体实现过程,另外要使用VML必须作如下的声明。
1. HTML Tag的名字空间声明

2. 之间的style声明

完成了上述工作之后,代码就能够完整的工作了。

现在我们开始讲述这个VMLPie的实现了,首先,我将那些实现的函数作一个简单的说明。
//VMLPic主函数,提供创建一个VMLPie实例
function VMLPie(pWidth,pHeight,pCaption,pContainer){}
//开始画图,将图形画到指定的容器上面
VMLPie.prototype.Draw=function(){}
//画饼图的各个块
VMLPie.prototype.CreatePie=function(){}
//接口功能:
// 放大或者缩小图形
//参数说明:
// iValue:放大的或者缩小的倍数,1为原图大小,0.5原图的50%,2为原图的
// 两倍,以此类推
VMLPie.prototype.Zoom=function (iValue){}
//接口功能:
// 添加饼图数据
//参数说明:
// sName:数据标签名称
// sValue:数据值
// sTooltipText:数据描述
VMLPie.prototype.AddData=function(sName,sValue,sTooltipText){}
//接口功能:
// 清除所有数据
VMLPie.prototype.Clear=function(){}
//以下四个函数是扩展使用,就是提供一些VML的交互
function HoverPie(el){}
function RestorePie(el){}
function LegendMouseOverEvent(){}
function LegendMouseOutEvent(){}
看完这些函数的说明,我想读者对于整个类的结构有一个大致的想法思路了吧,可是对于VML的制作,可能还是没有一个很清晰的思路吧,那么,下面我就着重介绍几个VML元素,我只是大致的介绍一些用法,具体的在 美洲豹的 《VML中文教程》有比较详细地介绍,如果有兴趣的话可以去参考参考。

1. V:Group
作为VML其它元素的容器,其属性coordsize定义其坐标大小,内部的元素的位置都只是相对于group元素所定义的coordsize,假设coordsize定义为21600,21600,就是定义了21600 * 21600的画布,如果内部有一个v:shape或者其它元素, shape.style.left=”2160px”,其实际位置只是在v:group的1/10宽度的位置。
2. V:Rect
定义一个矩形元素,fillcolor表示填充的背景颜色,stokecolor表示边框颜色,strokeweight表示边框宽度
3. V:Shape
VML提供的默认形状元素,通过定义path可以定义出需要的任何形状,至于path的用法,可以参考w3c的文档。
4. V:Fill
作为shape的子元素,用来设置shape的背景效果,通过type来设置填充的方法,具体用法如下
1) solid:实心填充,通过color设置填充颜色
2) gradient:线状渐变,这个时候需要color和color2这两个参数来设置渐变的开始颜色和结束颜色,Angle则设置渐变方向。
3) gradientradial:圆心渐变,其他的使用方法和gradient类似
4) tile:使用图片平铺,src设置图片
5) pattern:使用图片作为一个图章填充模式
6) frame:使用图片拉神填充

另外Opacity则用来设置透明度
5. V:Shadow
设置shape是否需要阴影,主要使用如下参数
ON:设置是否启用阴影
Color:阴影的颜色
Offset:阴影的偏离位置
6. V:TextBox
定义shape的文字区域

以上的这些元素是我在制作VMLPie需要使用到的,所以做了一个简单的介绍,具体的可以参考豹子的《VML中文教程》,另外MSDN里头的Vector Markup Language(VML)的SDK Document是一个非常好的参考资料。W3c的note比较抽象,在做一些比较深入的开发的时候,也是一个非常重要的参考。
做完了一系列的准备工作,那么现在应该开始真正介绍如何绘制Pie了吧。首先看看下面的一个示意图,清楚地描述了VMLPie的元素结构。


为了更加清楚的显示构成VML的文本结构,我使用XML的格式给出了如下的方式。



VML饼图










重庆(50)



完成了制图的基本思路之后,剩下的就是通过DOM逐步完成建立过程了,为了简单起见,我们安装函数的功能进行逐步分析。

1. VMLPie
this.Container=pContainer;
this.Width= pWidth || "400px";
this.Height=pHeight || "250px";
this.Caption = pCaption || "VML Chart";
this.backgroundColor="";
this.Shadow=false;
this.BorderWidth=0;
this.BorderColor=null;
this.all=new Array();
this.id=document.uniqueID;
this.RandColor=function(){
return "rgb("+ parseInt( Math.random() * 255) +"," +parseInt( Math.random() * 255) +"," +parseInt( Math.random() * 255)+")";
}
this.VMLObject=null;
this.LegendObject=null;

这个函数只是简单的初始化了一些变量,将class可以使用的一些属性在这里做了声明。RandColor函数提供了生成随机颜色的作用,这个也就是我在前文提到的,对于饼图的各个块的颜色,我都采用随机颜色,这样带来的一个问题就是会出现有些时候两个颜色比较接近,如果随能够给我提供几个基础的颜色列表,我将感激不尽。
2. VMLPie.prototype.AddData
添加图表数据,在这里我采用了prototype滞后加载的方式实现,如果读者认为这样不够清晰,可以修改成this.RandColor那样的内置形式。
实现代码如下:
VMLPie.prototype.AddData=function(sName,sValue,sTooltipText){
var oData=new Object();
oData.Name=sName;
oData.Value=sValue;
oData.TooltipText=sTooltipText;
var iCount=this.all.length;
this.all[iCount]=oData;
}
这里使用一个Object来存储每一个数据项,然后放到this.all这个数组中。
3. VMLPie.prototype.Draw
整个类实现最关键的函数,也就是在这个函数和后续的CreatePie函数中,一步一步地实现了图形的绘制工作。
//画外框
var o=document.createElement("v:group");
o.id=this.id;
o.style.width=this.Width;
o.style.height=this.Height;
o.coordsize="21600,21600";
//添加一个背景层
var vRect=document.createElement("v:rect");
vRect.style.width="21600px"
vRect.style.height="21600px"
o.appendChild(vRect);
//添加标题
var vCaption=document.createElement("v:textbox");
vCaption.style.fontSize="24px";
vCaption.style.height="24px"
vCaption.preSize="24";
vCaption.style.fontWeight="bold";
vCaption.innerHTML=this.Caption;
vCaption.style.textAlign="center";

vRect.appendChild(vCaption);
//设置边框大小
if(this.BorderWidth){
vRect.strokeweight=this.BorderWidth;
}
//设置边框颜色
if(this.BorderColor){
vRect.strokecolor=this.BorderColor;
}
//设置背景颜色
if(this.backgroundColor){
vRect.fillcolor=this.backgroundColor;
}
//设置是否出现阴影
if(this.Shadow){
var vShadow=document.createElement("v:shadow");
vShadow.on="t";
vShadow.type="single";
vShadow.color="graytext";
vShadow.offset="4px,4px";
vRect.appendChild(vShadow);
}
this.VMLObject=o;

完成上述工作之后,已经构建出了一个canvas(画布),完成了图形外框及其标题的制作,剩下的也就是最最重要的工作调用CreatePie来画出各个块,并且作出图例。
4. VMLPie.prototype.CreatePie
CreatePie提供了一个参数,也就是饼图制作的容器,我们通过Draw的上述代码已经建立了一个V:Group元素,这个也就是饼图绘制的容器了。
var mX=Math.pow(2,16) * 360;
//这个参数是划图形的关键
//AE x y width height startangle endangle
//x y表示圆心位置
//width height形状的大小
//startangle endangle的计算方法如下
// 2^16 * 度数
var vTotal=0;
var startAngle=0;
var endAngle=0;
var pieAngle=0;
var prePieAngle=0;

//计算数据的总和
for(i=0;i vTotal+=this.all[i].Value;
}
//建立图例容器
//这里子元素的left,top或者width都是针对于容器设置的坐标系统而言的
//例如
//图表容器我设置了coordsize为 21600,21600,那么objLengendGroup的位置都是相对于这个坐标系统而言的
//和实际图形显示的大小没有直接关系
var objLegendGroup=document.createElement("v:group");
with(objLegendGroup){
style.left="17000px";
style.top="4000px";
style.width="4000px";
style.height=1400 * this.all.length +"px";
coordsize="21600,21600";
}
//做图例的背景填充并且设置阴影
var objLegendRect=document.createElement("v:rect");
objLegendRect.fillcolor=" #EBF1F9";
objLegendRect.strokeweight=1;
with(objLegendRect){
//设置为21600,21600,就是保证完全覆盖group客户区
style.width="21600px";
style.height="21600px";
}
//对于图例加入阴影
var vShadow=document.createElement("v:shadow");
vShadow.on="t";
vShadow.type="single";
vShadow.color="graytext";
vShadow.offset="4px,4px";
objLegendRect.appendChild(vShadow);

//将其放到图例的容器中
objLegendGroup.appendChild(objLegendRect);

this.LegendObject=objLegendGroup;
vGroup.appendChild(objLegendGroup);

这个时候,我们已经完成了各个区域位置的绘制,通过如上的代码,我绘制了一个LegendGroup,将其作为图例的显示位置,另外主的V:group就作为pie的绘制容器,如果出于规范的考虑,也应该将Pie的各个shape放到一个group中,那样在日后的编程控制中会更加方便一点。
下面的这段代码也就是我要讲述的,因为代码比较关键,除了给出代码,我还着重的说明各个语句的作用。
for(i=0;i var vPieEl=document.createElement("v:shape");
var vPieId=document.uniqueID;
vPieEl.style.width="15000px";
vPieEl.style.height="14000px";
vPieEl.style.top="4000px";
vPieEl.style.left="1000px";
vPieEl.adj="0,0";
vPieEl.coordsize="1500,1400";
vPieEl.strokecolor="white";
vPieEl.id=vPieId;
vPieEl.style.zIndex=1;
vPieEl.onmouseover="HoverPie(this)";
vPieEl.onmouseout="RestorePie(this)";
pieAngle= this.all[i].Value / vTotal;
startAngle+=prePieAngle;
prePieAngle=pieAngle;
endAngle=pieAngle;
vPieEl.path="M 750 700 AE 750 700 750 700 " + parseInt(mX * startAngle) +" " + parseInt(mX * endAngle) +" xe";
vPieEl.title=this.all[i].Name +"\n所占比例:"+ endAngle * 100 +"%\n详细描述:" +this.all[i].TooltipText;
vPieEl._scale=parseInt( 360 * (startAngle + endAngle /2));

var objFill=document.createElement("v:fill");
objFill.rotate="t";
objFill.focus="100%";
objFill.type="gradient";
objFill.angle=parseInt( 360 * (startAngle + endAngle /2));

var objTextbox=document.createElement("v:textbox");
objTextbox.innerHTML=this.all[i].Name +":" + this.all[i].Value;
objTextbox.inset="5px 5px 5px 5px";
objTextbox.style.width="100px";
objTextbox.style.height="20px";

var vColor=this.RandColor();
vPieEl.fillcolor=vColor; //设置颜色
//开始画图例
p.innerText=vPieEl.outerHTML;
var colorTip=document.createElement("v:rect");

var iHeight=parseInt(21600 / (this.all.length * 2));
var iWidth=parseInt(iHeight * parseInt(objLegendGroup.style.height) /parseInt(objLegendGroup.style.width) /1.5 );

colorTip.style.height= iHeight;
colorTip.style.width=iWidth;
colorTip.style.top=iHeight * i * 2 + parseInt(iHeight /2);
colorTip.style.left=parseInt(iWidth /2);
colorTip.fillcolor=vColor;
colorTip.element=vPieId;
//colorTip.attachEvent("onmouse",LegendMouseOverEvent);
colorTip.onmouseover="LegendMouseOverEvent()";
colorTip.onmouseout="LegendMouseOutEvent()";

var textTip=document.createElement("v:rect");
textTip.style.top=parseInt(colorTip.style.top)- 500;
textTip.style.left= iWidth * 2;
textTip.innerHTML="" + this.all[i].Name +"("+ this.all[i].Value+")";

objLegendGroup.appendChild(colorTip);
objLegendGroup.appendChild(textTip);

vGroup.appendChild(vPieEl);
}
我们现在就开始来看如上代码的实现。

1.首先建立一个v:shape,left,top,width,height分别设置为1000px,4000px,15000px;14000px;这个的数字是我根据大致的位置确定的。
2.设置每个shape的id,我采用了document.uniqueID,就是用DOM的方法提供的随机id,本来我没有打算设置这个ID,但是后来考虑到和legend的交互,所以就设置了每个shape的ID.
3.设置shape的onmouseover,onmouseout事件,开始的时候我是采用attachEvent的方式来实现的,后来发现无法起作用(到现在我也没有找到原因),求出每个数据所占的比例。
4.设置Path,这个也就是比较令人难以看懂的部分了,我首先求出了startAngle和endAngle,startAngle的意义是这样的,假设有3个数,0.2,0.2,0.4,对于第二项来说,startAngle应该是 0.25,endAngle是0.25,对于第三项来说,startAngle是0.5,endAngle是0.5,总而言之,startAngle可以表示为前面数据所占的比例,engAngle表示当前数据所占的比例。Path有很多指令,对于其他指令,我就不多作解释,而这里使用的是M 750 700 AE 750 700 750 700 start end 。对于shape我重新定义了coordsize=”1500,1400”,m 750,700则表示移动到 750,700,就是移动到shape定义的中心,AE用来画曲线,总共有6个参数,w3c的note描述如下center (x,y) size(w,h) start-angle, end-angle,前面四个参数不难理解,剩下的两个参数我不是特别明白意思,但是在国外的一篇文章看到其中的算法如下应该是 2^16 * 度数,对应于程序,就应该是startAngle * 2^16 * 360,因为我的startAngle是比例。
5.通过设置fillcolor来设置饼图块的颜色,同时设置title提供一些提示信息.
6.对于图例开始着色,同时添加图例的文字说明.
7.因为所有的坐标系统都是相对的,为了保证美观,对于objLegendGroup我通过1400 * this.all.length +"px"来求得,而对于图例的colortip,高度和宽度就是动态计算的。
到目前为止,通过程序已经建立了一个基于VML的Pie,剩下的就是输出的问题了,前文我也提到过,我通过DOM方式建立的VML在显示方面没有任何问题,可是在动态交互方面,似乎不起作用,obj.VMLObject就是一个DOM对象,可是对于onmouseover,onmouseout等等的设置如果直接使用appendChild的方式的时候,根本无法起作用。而通过设置innerHTML来生成VML图形的时候,那些鼠标事件却可以响应,有过这个方面编程的朋友麻烦告诉我具体的原因如何。
function HoverPie(el){}
function RestorePie(el){}
function LegendMouseOverEvent(){}
function LegendMouseOutEvent(){}
这四个函数就是提供了简单的交互作用。具体的代码如下:
function HoverPie(el){
var v_length=500;
var x_cur=Math.cos( el._scale * Math.PI /180);
var y_cur=Math.sin (el._scale * Math.PI /180);
x=parseInt(x_cur * v_length);
y=parseInt(y_cur * v_length);
el.style.top=4000 -y;
el.style.left=1000 +x;
el.strokecolor="black";

}
function RestorePie(el){
el.style.top=4000;
el.style.left=1000;
el.strokecolor="white";

}
function LegendMouseOverEvent(){

var eSrc=window.event.srcElement;
var vPie=document.all(eSrc.element);
HoverPie(vPie);
}
function LegendMouseOutEvent(){
var eSrc=window.event.srcElement;
var vPie=document.all(eSrc.element);
RestorePie(vPie);
}
HoverPie提供了简单的图形相对位移的功能.

5.VMLPie.prototype.Zoom
提供了图形的放大和缩小的功能,既然是作为矢量图形,那么其放大和缩小就应该是没有任何图像损失的,实现的原理很简单,只需要修改group的坐标系统就可以实现,这个也就是我一直强调的需要将shape都放到group中的主要原因,对于文字,指能够使用和html Dom相同的方式进行设置。

VMLPie.prototype.Zoom=function (iValue){
var vX=21600;
var vY=21600;
this.VMLObject=document.all(this.id);
this.VMLObject.coordsize=parseInt(vX / iValue) +","+parseInt(vY /iValue);
var texts=this.VMLObject.getElementsByTagName("TEXTBOX");
for(var i=0;i if (texts[i].preSize){
texts[i].style.fontSize= parseInt(texts[i].preSize) * iValue +"px";
}
else{
texts[i].style.fontSize= 12 * iValue + "px";
}
}
}
到目前为止,已经全部介绍完了VMLPie的全部制作过程,我是第一次写这样的文章,可能在很多东西的表述方面不是特别清楚,希望可以和大家共同探讨,另外完整的源代码我已经贴在CSDN的论坛里头,大家可以去下载。