VML: Vector Markup Language ,单看名字可以看出,首先它绘制出来的图形是矢量图 (Vector) ,这就意味着在浏览器里面可以任意缩放而不失真。其次是标签式的语言 (Markup) ,类似于 xml 或 html 标签。
一、基础资源(资源已在附件上传)
1 、维基百科有关于 VML 的其它介绍: http://zh.wikipedia.org/wiki/VML 语言
2 、这篇由网友“美洲豹”整理的基础教程也比较经典: http://www.itlearner.com/code/vml/index.html
3 、另外一个非常好的教程是网友“风云舞”整理的《 VML 极道教程》,以及他自己开发的工具《 FlashVml 璀灿之星》。
4 、一个德国人做的 VML 组件,可以模拟出 3D 的效果,里面有绘制三维柱体、高阶曲线方程和魔方的曲线。
注意点:
1 、 VML 只有 IE5+ 以上的版本支持,因为 VML 是由 IE 直接支持的,所以使用它来画图不需要安装插件,缺点是其它浏览器不支持。
2 、命名空间信息一定要写对
<html xmlns:v="urn:schemas-microsoft-com:vml">
还有一句固定的 CSS 信息最好也加上,避免莫名奇妙的问题。
<STYLE>
v\:* { BEHAVIOR: url(#default#VML) }
</STYLE>
这些东西虽小,资源教程里面都有,但是如果没有注意到的话,可能要浪费一些时间来调,不太值得。
3 、最基础的东西请对照“美洲豹”的基础教程,这些东西必须自己去看,也花不了多少时间,这里不再重复。
比如嵌入如下的标签:
<v:line style="position:relative" from="0,0" to="100,0" >
<v:stroke dashstyle="Dot"/>
</v:line>
浏览器就会画出一条黑色的线。
二、 VML 曲线图实例代码剖析
1 、实例背景
这是为某供电局电压监测系统的数据展示部分定制的一个曲线展示组件,对于变电站或者某个线路上的单个的监测装置,它的数据频率是每 5 分钟上传一个电压值到监测系统的服务器。则这个组件的 XY 坐标对应的意义分别是时间( 5 分钟为间隔)和电压值。
2 、实例具体需求:
⑴对曲线外观的要求:能控制曲线的线条粗细、线条样式、线条颜色;能在单张图中绘制多条曲线;
⑵可以指定最大最小两个阀值,如果获取到的数据超过阀值,能够屏蔽这些数据,而不在曲线上把这些异常数据绘制出来;
⑶能够实时地获取数据;
⑷鼠标移动到曲线上的某个点时,能够给出该点的数据值。
3 、实例说明
由于这里是解析 VML 的绘图,以上的需求⑴⑵⑶条简化成如下:
(1) 线条的粗细都一样、颜色由自己指定;
(2) 数据由脚本随机生成,先绘出一个正弦和一个余弦曲线;
相信在完成这个简化的模型之后,再来做出更复杂的功能就只是一个体力活,而不会有技术上的障碍和困惑。然后去为你所在的项目顺手定制一个小组件,要求你的老板给多点薪水,就更不在话下了。(千万别说是我让你这么干的!)
4 、最终效果截图
5 、代码分析 完整的代码清单如下(附件有对应文件,curve.rar)
<html xmlns:v="urn:schemas-microsoft-com:vml"> <link href="../fnpic/main.css" rel="stylesheet" type="text/css" /> <title></title> <STYLE type="text/css"> <!-- v\:* { BEHAVIOR: url(#default#VML) } /*标题显示样式*/ .Title { font-family:"宋体", "华文仿宋"; font-size:16px; text-align:center; font-weight:bold; color:#996600; vertical-align:middle; } --> </STYLE> <script language="javascript"> /* 说明: javascript/vml 曲线图 * 作者:大漠穷秋 * 申明:转载,使用,修改请保存版权申明 */ var width=700; //视图区宽度 var height=350; //视图区高度 var yMin=0; //y最小值 var yMax=100; //y最大值 var yPerStep=10; //标签步长 var ySteps=(yMax-yMin)/yPerStep;//标签总步数 var xSteps=288; //x轴步数 var leftBlank=80; //左边距 var rightBlank=95; //右边距 var topBlank=40; //上边距 var bottomBlank=20; //下边距 var labelLeft=35; //标签左边距 var xstep=(width-leftBlank-rightBlank)/xSteps;//x每步像素数 var ystep=(height-topBlank-bottomBlank)/ySteps;//y每步像素数 /** *曲线数组,用于保存所有的曲线对象 *创建一个曲线对象后必须把曲线对象加入到这个数组 *否则,在鼠标移动是,无法获得曲线对应当前鼠标位置的y值 */ var curves=[]; //TipBox实际到该数组记录的曲线中取当前y值(为了实现当一条曲线被隐藏时TipBox里面不显示这条曲线的当前值) var tempCurves=[]; /******************************** *以下函数完成绘图区初始化工作 ********************************/ //绘图区初始化、重新设置全局参数 function init(yMin,yMax,yStep,x,y,width,height,dragAble){ if(yMin&&yMax&&yStep){ yMin=yMin; yMax=yMax; yPerStep=yStep; } //重新计算相关参数 ySteps=(yMax-yMin)/yPerStep;//标签总步数 ystep=(height-topBlank-bottomBlank)/ySteps;//y每步像素数 //首先创建可以拖动的层 createDiv({ x:x, y:y, width:width, height:height, dragAble:dragAble }); containerGroup.style.width=width; containerGroup.style.height=height; containerGroup.coordsize=width+","+height; back_ground.style.width=width; back_ground.style.height=height; } //创建可拖动的层 function createDiv(config){ var div1=document.createElement("div"); div1.id='outerDiv'; div1.style.position='absolute'; div1.style.left=config.x; div1.style.top=config.y; div1.style.height=config.height; div1.style.width=config.width+12; div1.style.backgroundColor='#FFFFFF'; div1.style.border='solid black'; div1.style.borderWidth='1'; div1.attachEvent('onmousemove',moveline); var div2=document.createElement("div"); div2.style.position='relative'; div2.style.padding='10px'; div2.style.backgroundColor='#C4E1FF'; div2.style.borderBottom='solid black'; div2.style.borderWidth='1'; div2.innerHTML="<font color='red' align='center'>按住这里可以拖拽</font>"; if(config.dragAble){//如果是可拖拽的,为第二个div添加一个事件监听器 div2.onmousedown=function(){drag(this.parentNode,event)}; } var group=document.createElement("<v:group id='containerGroup' style='position:relative;top:0;left:0;Z-INDEX:100'/>"); group.style.width=width; group.style.height=height; group.coordsize=width+","+height; var rect=document.createElement("<v:Rect id='back_ground' fillcolor='white' style='position:relative'/>"); var fill=document.createElement("<v:fill rotate='t' type='gradient' color2='#C4E1FF'/>"); var shadow=document.createElement("<v:shadow on='t' type='single' color='silver' offset='3pt,3pt'/>"); rect.appendChild(fill); rect.appendChild(shadow); group.insertBefore(rect); div1.appendChild(div2); div1.appendChild(group); document.body.appendChild(div1); } //创建坐标轴 function createAxis(){ //y轴 var y_axis=document.createElement("<v:line id='y_axis' from='("+leftBlank+","+(height-bottomBlank)+")' to='("+leftBlank+","+(topBlank-20)+")' strokeColor='red' strokeweight='1' style='Z-INDEX:9;POSITION:relative'/>"); y_axis.insertBefore(document.createElement("<v:stroke EndArrow='classic' />")); containerGroup.insertBefore(y_axis); //绘制x轴 if(yMin>0){//在底部绘制坐标轴 var point1; var point2; point1=leftBlank+","+(height-bottomBlank); point2=(width-rightBlank+10)+","+(height-bottomBlank); var tempLine = document.createElement("<v:line from='("+point1+")'to='("+point2+")' strokeColor='red' strokeweight='1' style='Z-INDEX:8;POSITION:relative'/>"); containerGroup.insertBefore(tempLine); tempLine.insertBefore(document.createElement("<v:stroke EndArrow='classic'/>")); //x轴单位 temp_obj=document.createElement("<v:textbox style='font-size:15;z-index:9;left:"+(width-rightBlank+5)+";color:black;word-break:break-all;position:absolute;top:"+(height-bottomBlank+5)+"'></v:textbox>"); temp_obj.innerHTML="<font color='navy'>时间t</font>"; containerGroup.insertBefore(temp_obj); }else if(yMax<0){//在顶部绘制坐标轴 var point1; var point2; point1=leftBlank+","+topBlank; point2=(width-rightBlank+10)+","+topBlank; var tempLine = document.createElement("<v:line from='("+point1+")'to='("+point2+")' strokeColor='red' strokeweight='1' style='Z-INDEX:8;POSITION:relative'/>"); containerGroup.insertBefore(tempLine); tempLine.insertBefore(document.createElement("<v:stroke EndArrow='classic'/>")); //x轴单位 temp_obj=document.createElement("<v:textbox style='font-size:15;z-index:9;left:"+(width-rightBlank+5)+";color:black;word-break:break-all;position:absolute;top:"+(topBlank-10)+"'></v:textbox>"); temp_obj.innerHTML="<font color='navy'>时间t</font>"; containerGroup.insertBefore(temp_obj); }else{ var temp; var y_position; var point1; var point2; for(var i=0;i<=ySteps;i++){ temp=yMin+yPerStep*i;//标签数值 y_position=(height-bottomBlank)-i*ystep;//标签y轴位置 point1=leftBlank+","+y_position; point2=(width-rightBlank+10)+","+y_position; if(temp==0){ var tempLine = document.createElement("<v:line from='("+point1+")'to='("+point2+")' strokeColor='red' strokeweight='1' style='Z-INDEX:8;POSITION:relative'/>"); containerGroup.insertBefore(tempLine); tempLine.insertBefore(document.createElement("<v:stroke EndArrow='classic'/>")); //x轴单位 temp_obj=document.createElement("<v:textbox style='font-size:15;z-index:9;left:"+(width-rightBlank+5)+";color:black;word-break:break-all;position:absolute;top:"+(y_position+5)+"'></v:textbox>"); temp_obj.innerHTML="<font color='navy'>时间t</font>"; containerGroup.insertBefore(temp_obj); break; } } } } //绘制Tip线和Tip框 function createTip(){ //提示线 var tip_line=document.createElement("<v:line id='line1' from='("+leftBlank+",0)' to='("+leftBlank+","+height+")' strokeColor='red' strokeweight='1' style='Z-INDEX:1000;POSITION:relative'/>"); containerGroup.insertBefore(tip_line); //提示框 var tip_box=document.createElement("<v:Rect id='msg_box'fillcolor='#C4E1FF' strokeColor='red' style='Z-INDEX:1001;position:relative;width:110;height:60;left:80;top:40' filled=t/>"); containerGroup.insertBefore(tip_box); } //绘制网格线 function createGrid(){ var x1,y1,x2,y2,point1,point2; //绘制x轴网格 for(var i=0;i<=xSteps;i++){ point1=leftBlank+xstep*i+","+topBlank; point2=leftBlank+xstep*i+","+(height-bottomBlank); var tempLine = document.createElement("<v:line from='"+point1+"' to='"+point2+"' strokeColor='#CCCCCC' style='Z-INDEX:7;POSITION:relative'/>"); containerGroup.insertBefore(tempLine); } //绘制y轴网格 for(var i=0;i<ySteps;i++){ var y1=topBlank+i*ystep; point1=leftBlank+","+y1; point2=(width-rightBlank)+","+y1; var tempLine = document.createElement("<v:line from='"+point1+"' to='"+point2+"' strokeColor='#CCCCCC' style='Z-INDEX:7;POSITION:relative'/>"); containerGroup.insertBefore(tempLine); } } //绘制标签 function createLabel(){ var temp; var y_position; for(var i=0;i<=ySteps;i++){ temp=yMin+yPerStep*i; y_position=(height-bottomBlank)-i*ystep;//标签y轴位置 var temp_obj=document.createElement("<v:textbox id='text_box' style='font-size:15;z-index:9;left:"+labelLeft+";color:black;word-break:break-all;position:absolute;top:"+y_position+"'></v:textbox>"); temp_obj.innerHTML="<font size=1>"+temp+"</font>"; containerGroup.insertBefore(temp_obj); } var temp_obj=document.createElement("<v:textbox style='font-size:15;z-index:9;left:"+(labelLeft-20)+";color:black;word-break:break-all;position:absolute;top:"+(topBlank-20)+"'></v:textbox>"); temp_obj.innerHTML="<font color='navy'>单位:V</font>"; containerGroup.insertBefore(temp_obj); } //绘制标题 function createTitle(arg){ var title_left=width/2-150; var title_top=10; var tempObj = document.createElement("<div class=\"Title\" style=\"POSITION:absolute;Z-INDEX:8;visibility:visible;LEFT:"+title_left+";TOP:"+title_top+";width:"+350+";height:"+20+";/>"); tempObj.innerHTML =arg; containerGroup.insertBefore(tempObj); } /******************************** *以下是一组工具函数 ********************************/ //简单的拖拽工具函数 function drag(elementToDrag,event){ var startX=event.clientX; var startY=event.clientY; var origX=elementToDrag.offsetLeft; var origY=elementToDrag.offsetTop; var deltaX=startX-origX; var deltaY=startY-origY; if(document.attachEvent){ elementToDrag.setCapture(); elementToDrag.attachEvent("onmousemove",moveHandler); elementToDrag.attachEvent("onmouseup",upHandler); elementToDrag.attachEvent("onlosecapture",upHandler); }else{ var oldMoveHandler=document.onmousemove; var oldUpHandler=document.onmouseup; document.onmousemove=moveHandler; document.onmouseup=upHandler; } if(event.stopPropagation){ event.stopPropagation; }else{ event.cancelBubble=true; } if(event.preventDefault){ event.preventDefault; }else{ event.returnValue=false; } function moveHandler(e){ if(!e) e=window.event; elementToDrag.style.left=(e.clientX-deltaX)+'px'; elementToDrag.style.top=(e.clientY-deltaY)+'px'; if(e.stopPropagation) e.stopPropagation; else e.cancelBubble=true; } function upHandler(e){ if(!e) e=window.event; if(document.detachEvent){ elementToDrag.detachEvent("onlosecapture",upHandler); elementToDrag.detachEvent("onmouseup",upHandler); elementToDrag.detachEvent("onmousemove",moveHandler); elementToDrag.releaseCapture(); }else{ document.onmouseup=oldUpHandler; document.onmousemove=oldMoveHandler; } if(e.stopPropagation) e.stopPropagation; else e.cancelBubble=true; } } //格式化传入的曲线y值,超过标签范围的值全部置为标签最大/最小值 function y_data_format(value){ var result; if(value>=yMax){ result=yMax; }else if(value<=yMin){ result=yMin } else{ result=value; } return result; } //格式化传入的曲线y值,超过x范围的值全部置为x轴最大/最小值 function x_data_format(value){ var result; if(value<0){ result=0; } else if(value>xSteps){ result=xSteps; } else{ result=value; } return result; } //获取x值对应图中的x像素值 function get_x(value){ var temp=leftBlank+value*xstep; return temp; } //获取y值对应途中的y像素值 function get_y(value){ var temp=(height-bottomBlank-topBlank)/(yMax-yMin);//每像素代表的y值 var result=height-bottomBlank-(value-yMin)*temp; return result; } //数据精度格式化 function format(source,n){ var val=Math.round(source*Math.pow(10,n))/Math.pow(10,n); return parseFloat(val); } //根据鼠标当前像素值计算x坐标值 function cal_x(line_in){ var result=parseInt((line_in-leftBlank)/xstep); return result; } //根据x值计算时间字符串 function get_time(x_value){ var all=x_value*5; return parseInt(all/60)+"时"+all%60+"分"; } /******************************************** *以下是鼠标移动事件处理函数: *鼠标移动时提示线和数据显示框跟随鼠标移动 ********************************************/ //鼠标移动事件响应:鼠标位置内部值=(当前鼠标真实位置-div层对window的偏移量)/放大倍率 function moveline(){ var box_width=80; var box_top=60; var cursorX=event.clientX; var cursorY=event.clientY; //这里的鼠标当前x和y坐标需要减去最外层的DIV对window对应的偏移量 //这样在拖动div和产生滚动条时tip线的位置才正确 var innerX=cursorX-outerDiv.offsetLeft; var innerY=cursorY-outerDiv.offsetTop; if(innerX>=leftBlank&&innerX<=(width-rightBlank)){//控制边界 //线跟随 line1.style.left=innerX-leftBlank; //框跟随,同时动态获取对应于x值的每条曲线的y值 var xvalue=cal_x(innerX); var val_msg=''; for(var i=0;i<curves.length;i++){ val_msg+=curves[i].getYValue(x_value); } msg_box.innerHTML=" "+get_time(x_value)+"<br>"+val_msg; //以下调整数据提示框的位置,避免提示框超出绘图区边界 if(innerX<width-rightBlank-box_width-10){ msg_box.style.left=innerX+5; }else{ msg_box.style.left=innerX-box_width-35; } if(innerY>=topBlank&&innerY<=(height-bottomBlank)){ if(innerY<=height-bottomBlank-box_top-10){ msg_box.style.top=innerY; }else{ msg_box.style.top=innerY-box_top; } } } } /********************************************************** *曲线类: *color *xValues(Array)、yValues(Array) *dataPresition **********************************************************/ var idCounter=0; Curve=function(config){ this.id='__auto_gen'+idCounter++;//id,为保证唯一性,加自定义字符串 this.curveIndex=idCounter; //内部使用,记录这条曲线是第几个被创建出来的 this.name=''; //曲线名称 this.color='blue'; //曲线颜色 this.xValues=[]; //x轴坐标数组 this.yValues=[]; //y轴坐标数组 this.dataPrecision=2; //数据精度,精确到小数点后的位数,默认为2位 this.maskBadData=false; //创建曲线时是否屏蔽异常值 this.upLimit=0; //如果屏蔽异常值标志为true,则必须给出比较上限和下限,否则将出错 this.downLimit=0; this.whitLabel=true; //是否创建右侧标签标志位 //拷贝参数 apply(this,config); //绘制曲线 this.create=function(){ var x1,y1,x2,y2,point1,point2; //先创建一个组来容纳曲线 //容器组,用来容纳这条曲线(因为一条曲线实际上是由多条细小的线段连接起来的,所以必须用一个组来做容器,否则要整体隐藏这条曲线比较困难) var group=document.createElement("<v:group id="+this.id+" style='position:relative;top:0;left:0;Z-INDEX:100'/>"); group.style.width=width; group.style.height=height; group.coordsize=width+","+height; flag: for(var i=0;i<xSteps;i++){ x1=this.xValues[i]; x2=this.xValues[i+1]; y1=this.yValues[i]; y2=this.yValues[i+1]; if(this.maskBadData){//屏蔽异常值标志 if(y1<this.downLimit||y2<this.downLimit){ continue flag; } if(y1>this.upLimit||y2>this.upLimit){ continue flag; } } if(x1!=null&&x2!=null&&y1!=null&&y2!=null){ x1=x_data_format(x1); x2=x_data_format(x2); y1=y_data_format(y1); y2=y_data_format(y2); var x_temp1=get_x(x1); var x_temp2=get_x(x2); var y_temp1=get_y(y1); var y_temp2=get_y(y2); point1=x_temp1+","+y_temp1; point2=x_temp2+","+y_temp2; var tempLine = document.createElement("<v:line from='"+point1+"'to='"+point2+"' strokeColor='"+this.color+"' strokeweight='2' style='Z-INDEX:8;POSITION:relative'/>"); group.insertBefore(tempLine); } } containerGroup.insertBefore(group); if(this.whitLabel){ this.createLabel();//创建右侧标签 } }; //显示、隐藏曲线 this.hide=function(e){ var labelId=e.srcElement.id; var r=document.getElementById(labelId); if(r.filled){ r.filled=false; }else{ r.filled=true; } labelId=labelId.replace('label_',''); var g=document.getElementById(labelId); var flag=g.style.visibility; if(!flag){ g.style.visibility="hidden"; }else if(flag=="hidden"){ g.style.visibility="visible"; }else if(flag=="visible"){ g.style.visibility="hidden"; }else{ g.style.visibility="visible"; } }; //创建右侧标签 this.createLabel=function(){ var limit_textbox_left=width-rightBlank+15; var limit_rect_left=limit_textbox_left-15; var temp_obj=document.createElement("<v:textbox style='font-size:15;z-index:9;left:"+limit_textbox_left+";color:black;word-break:break-all;position:absolute;top:"+(20+this.curveIndex*20)+"'></v:textbox>"); temp_obj.innerHTML="<font color="+this.color+">"+this.name+"</font>"; containerGroup.insertBefore(temp_obj); temp_obj=document.createElement("<v:rect id='label_"+this.id+"' fillcolor='"+this.color+"' filled=t strokeColor='blue' strokeweight='1' style='width:10;height:10;z-index:9;left:"+limit_rect_left+";color:black;position:absolute;top:"+(20+this.curveIndex*20)+"'></v:rect>"); //因为VML只是IE支持,这里注册事件就不需要判断,直接写attachEvent了 temp_obj.attachEvent('onclick',this.hide); containerGroup.insertBefore(temp_obj); }; //查找x处对应的y值 this.getYValue=function find_y(index){ var result=this.yValues[index]; if(result==null){//没有值 result="没有数据"; }else{ result=result.toFixed(this.dataPrecision); result=result+"v";//单位kv } result="<font color="+this.color+">"+this.name+">"+result+"</font><br>"; return result; }; curves.push(this); tempCurves.push(this); } //********************************函数调用入口*************************************** //初始化绘图背景 function initBg(config){ init(config.yMin,config.yMax,config.yStep, config.x,config.y,config.width,config.height, config.dragAble); //绘图区初始化 createAxis(); //绘制坐标轴 createTip(); //绘制跟随鼠标的tip线和tip框 createGrid(); //绘制网格 createLabel(); //绘制y轴刻度标签 createTitle(config.title); //绘制标题 } </script> <body> </body> <script language="javascript" type="text/javascript"> //初始化绘图背景 initBg({ yMin:-30, yMax:100, yStep:10, title:'和平供电公司 2008-10-22 实时曲线', x:200, y:50, width:700, height:350, dragAble:true }); //正弦曲线 var xVal=[]; var yVal=[]; for(var i=0;i<288;i++){//随机测试值 xVal.push(i); yVal.push(Math.sin(20*Math.PI/360*i)*20+40); } var curve1=new Curve({ name:'正弦', color:"green", xValues:xVal, yValues:yVal }); curve1.create(); //余弦曲线 xVal=[]; yVal=[]; for(var i=0;i<288;i++){ xVal.push(i); yVal.push(Math.cos(20*Math.PI/360*i)*20+40); } var curve2=new Curve({ name:'余弦', color:"red", xValues:xVal, yValues:yVal, whitLabel:false }); curve2.create(); //alert(window.curve1.id); </script> </html>
如果把基础的教程看过一遍,对于这个代码,从函数调用入口扫一遍代码应该没有什么困难。另外,曲线的绘图是图表组件里面(曲线、饼图、柱状图)里面相对复杂的一种,如果这个代码能顺利看过,那么画饼图、柱状图应该都不是问题。
这个例子是去年为公司做的,没有完全采用面向对象的写法,只定义了 Cruve 一个类,其它都是零散的函数。对 vml 整体的 js 封包在 ext 的论坛里面好像有人做过。
关于拿 VML 直接来绘图就先到这里,下一篇:《 VML 的轻量级 GIS 应用》。
可加 qun:88403922 参与讨论。
这里有做得非常绚丽的图表应用(基于flash,后篇讨论到flash的时候再来看如何自己定制flash图表):http://www.fusioncharts.com/,可惜是收费的,不过参考参考也不错。如果你的老板实在是不差钱的话就建议他去买人家的组件吧!!