提要:
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的名字空間聲明
<html xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
2. <head>之間的style聲明
<STYLE>
v\:* { BEHAVIOR: url(#default#VML) }
o\:* { BEHAVIOR: url(#default#VML) }
.shape { BEHAVIOR: url(#default#VML) }
</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的格式給出了如下的方式。
<v:group>
<!--設定餅圖的背景及其標題 -->
<v:rect>
<v:textbox>VML餅圖</v:textbox>
</v:rect>
<!--設定餅圖的各個塊-->
<v:shape path="...">
</v:shape>
<!--圖例元素集合 -->
<v:group>
<!--設定圖例的顏色 -->
<v:rect fillcolor="green"></v:rect>
<v:rect>
<!-- 設定圖例文字-->
<v:textbox>重慶(50)</v:textbox>
</v:rect>
</v:group>
</v:group>
完成了製圖的基本思路之後,剩下的就是通過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<this.all.length;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<this.all.length;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="<v:textbox style=\"font-size:12px;font-weight:bold\">" + this.all[i].Name +"("+ this.all[i].Value+")</v:textbox>";
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<texts.length;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的論壇裡頭,大家可以去下載。
http://expert.csdn.net/Expert/topic/2160/2160456.xml?temp=.4498102