d3.js 地铁轨道交通项目实战
上一章说了如何制作一个线路图,当然上一章是手写的JSON数据,当然手写的json数据有非常多的好处,例如可以应对客户的各种BT需求,但是大多数情况下我们都是使用地铁公司现成的JSON文件,话不多说我们先看一下百度官方线路图。
就是这样的,今天我们就来完成它的大部分需求,以及地铁公司爸爸提出来的需求。
需求如下:
1.按照不同颜色显示地铁各线路,显示对应站点。
2.用户可以点击手势缩放和平移(此项目为安卓开发)。
3.用户在线路menu里点击线路,对应线路平移值屏幕中心并高亮。
4.根据后台数据,渲染问题路段。
5.点击问题路段站点,显示问题详情。
大致需求就是这些,下面看看看代码
1.定义一些常量和变量
constdataset=subwayData;//线路图数据源 letsubway=newSubway(dataset);//线路图的类文件 letbaseScale=2;//基础缩放倍率 letdeviceScale=1400/2640;//设备与画布宽度比率 letwidth=2640;//画布宽 letheight=1760;//画布高 lettransX=1320+260;//地图X轴平移(将画布原点X轴平移) lettransY=580;//地图X轴平移(将画布原点Y轴平移) letscaleExtent=[0.8,4];//缩放倍率限制 letcurrentScale=2;//当前缩放值 letcurrentX=0;//当前画布X轴平移量 letcurrentY=0;//当前画布Y轴平移量 letselected=false;//线路是否被选中(在右上角的线路菜单被选中) letscaleStep=0.5;//点击缩放按钮缩放步长默认0.5倍 lettooltip=d3.select('#tooltip');//提示框 letbugArray=[];//问题路段数组 letsvg=d3.select('#sw').append('svg');//画布 letgroup=svg.append('g').attr('transform',`translate(${transX},${transY})scale(1)`);//定义组并平移 letwhole=group.append('g').attr('class','whole-line')//虚拟线路(用于点击右上角响应线路可以定位当视野中心,方法不唯一) letpath=group.append('g').attr('class','path');//定义线路 letpoint=group.append('g').attr('class','point');//定义站点 constzoom=d3.zoom().scaleExtent(scaleExtent).on("zoom",zoomed);//定义缩放事件
这就是我们需要使用的一些常量和变量。注意transX不是宽度的一半,是因为北京地铁线路网西线更密集。
2.读官方JSON
使用d3.js数据必不可少,然而官方的数据并不通俗易懂,我们先解读一下官方JSON数据。
每条线路对象都有一个l_xmlattr属性和一个p属性,l_xmlattr是整条线路的属性,p是站点数组,我们看一下站点中我们需要的属性。ex是否是中转站,lb是站名,sid是站的id,rx、ry是文字偏移量,st是是否为站点(因为有的点不是站点而是为了渲染贝塞尔曲线用的),x、y是站点坐标。
3.构造自己的类方法
官方给了我们数据,但是并不是我们能直接使用的,所以我们需要构造自己的方法类
classSubway{ constructor(data){ this.data=data; this.bugLineArray=[]; } getInvent(){}//获取虚拟线路数据 getPathArray(){}//获取路径数据 getPointArray(){}//获取站点数组 getCurrentPathArray(){}//获取被选中线路的路径数组 getCurrentPointArray(){}//获取被选中线路的站点数组 getLineNameArray(){}//获取线路名称数组 getBugLineArray(){}//获取问题路段数组 }
下面是我们方法内容,里面的操作不是很优雅(大家将就看啦)
getInvent(){ letlineArray=[]; this.data.forEach(d=>{ let{loop,lc,lbx,lby,lb,lid}=d.l_xmlattr; letallPoints=d.p.slice(0); loop&&allPoints.push(allPoints[0]); letpath=this.formatPath(allPoints,0,allPoints.length-1); lineArray.push({ lid:lid, path:path, }) }) returnlineArray; } getPathArray(){ letpathArray=[]; this.data.forEach(d=>{ let{loop,lc,lbx,lby,lb,lid}=d.l_xmlattr; letallPoints=d.p.slice(0); loop&&allPoints.push(allPoints[0]) letallStations=[]; allPoints.forEach((item,index)=>item.p_xmlattr.st&&allStations.push({...item.p_xmlattr,index})) letarr=[]; for(leti=0;i{ let{lid,lc,lb}=d.l_xmlattr; letallPoints=d.p; letallStations=[]; allPoints.forEach(item=>{ if(item.p_xmlattr.st&&!item.p_xmlattr.ex){ allStations.push({...item.p_xmlattr,lid,pn:lb,lc:lc.replace(/0x/,'#')}) }elseif(item.p_xmlattr.ex){ if(tempPointsArray.indexOf(item.p_xmlattr.sid)==-1){ allStations.push({...item.p_xmlattr,lid,pn:lb,lc:lc.replace(/0x/,'#')}) tempPointsArray.push(item.p_xmlattr.sid); } } }); pointArray.push(allStations); }) returnpointArray; } getCurrentPathArray(name){ letd=this.data.filter(d=>d.l_xmlattr.lid==name)[0]; let{loop,lc,lbx,lby,lb,lid}=d.l_xmlattr; letallPoints=d.p.slice(0); loop&&allPoints.push(allPoints[0]) letallStations=[]; allPoints.forEach((item,index)=>item.p_xmlattr.st&&allStations.push({...item.p_xmlattr,index})) letarr=[]; for(leti=0;i d.l_xmlattr.lid==name)[0]; let{lid,lc,lb}=d.l_xmlattr; letallPoints=d.p; letallStations=[]; allPoints.forEach(item=>{ if(item.p_xmlattr.st&&!item.p_xmlattr.ex){ allStations.push({...item.p_xmlattr,lid,pn:lb,lc:lc.replace(/0x/,'#')}) }elseif(item.p_xmlattr.ex){ allStations.push({...item.p_xmlattr,lid,pn:lb,lc:lc.replace(/0x/,'#')}) } }); returnallStations; } getLineNameArray(){ letnameArray=this.data.map(d=>{ return{ lb:d.l_xmlattr.lb, lid:d.l_xmlattr.lid, lc:d.l_xmlattr.lc.replace(/0x/,'#') } }) returnnameArray; } getBugLineArray(arr){ if(!arr||!arr.length)return[]; this.bugLineArray=[]; arr.forEach(item=>{ let{start,end,cause,duration,lid,lb}=item; letlines=[]; letpoints=[]; lettempObj=this.data.filter(d=>d.l_xmlattr.lid==lid)[0]; letloop=tempObj.l_xmlattr.loop; letlc=tempObj.l_xmlattr.lc; letallPoints=tempObj.p; letallStations=[]; allPoints.forEach(item=>{ if(item.p_xmlattr.st){ allStations.push(item.p_xmlattr.sid) } }); loop&&allStations.push(allStations[0]); for(leti=allStations.indexOf(start);i<=allStations.lastIndexOf(end);i++){ points.push(allStations[i]) } for(leti=allStations.indexOf(start);i
这种方法大家也不必看懂,知道传入了什么,输入了什么即可,这就是我们的方法类。
4.d3渲染画布并添加方法
这里是js的核心代码,既然class文件都写完了,这里的操作就方便了很多,主要就是下面几个人方法,
renderInventLine();//渲染虚拟新路 renderAllStation();//渲染所有的线路名称(右上角) renderBugLine();//渲染问题路段 renderAllLine();//渲染所有线路 renderAllPoint();//渲染所有点 renderCurrentLine()//渲染当前选中的线路 renderCurrentPoint()//渲染当前选中的站点 zoomed()//缩放时执行的方法 getCenter()//获取虚拟线中心点的坐标 scale()//点击缩放按钮时执行的方法下面是对应的方法体
svg.call(zoom); svg.call(zoom.transform,d3.zoomIdentity.translate((1-baseScale)*transX,(1-baseScale)*transY).scale(baseScale)); letpathArray=subway.getPathArray(); letpointArray=subway.getPointArray(); renderInventLine(); renderAllStation(); renderBugLine(); functionrenderInventLine(){ letarr=subway.getInvent(); whole.selectAll('path') .data(arr) .enter() .append('path') .attr('d',d=>d.path) .attr('class',d=>d.lid) .attr('stroke','none') .attr('fill','none') } functionrenderAllLine(){ for(leti=0;id.path) .attr('lid',d=>d.lid) .attr('id',d=>d.id) .attr('class','linesorigin') .attr('stroke',d=>d.color) .attr('stroke-width',7) .attr('stroke-linecap','round') .attr('fill','none') path.append('text') .attr('x',pathArray[i].lbx) .attr('y',pathArray[i].lby) .attr('dy','1em') .attr('dx','-0.3em') .attr('fill',pathArray[i].lc) .attr('lid',pathArray[i].lid) .attr('class','line-textorigin') .attr('font-size',14) .attr('font-weight','bold') .text(pathArray[i].lb) } } functionrenderAllPoint(){ for(leti=0;i d.path) .attr('lid',d=>d.lid) .attr('id',d=>d.id) .attr('stroke',d=>d.color) .attr('stroke-width',7) .attr('stroke-linecap','round') .attr('fill','none') path.append('text') .attr('class','temp') .attr('x',arr.lbx) .attr('y',arr.lby) .attr('dy','1em') .attr('dx','-0.3em') .attr('fill',arr.lc) .attr('lid',arr.lid) .attr('font-size',14) .attr('font-weight','bold') .text(arr.lb) } functionrenderCurrentPoint(name){ letarr=subway.getCurrentPointArray(name); for(leti=0;i { console.log(d) d.lines.forEach(dd=>{ d3.selectAll(`path#${dd}`).attr('stroke','#eee'); }) d.points.forEach(dd=>{ d3.selectAll(`circle#${dd}`).attr('stroke','#ddd') d3.selectAll(`text#${dd}`).attr('fill','#aaa') }) }) d3.selectAll('.points').on('click',function(){ letid=d3.select(this).attr('id'); letbool=judgeBugPoint(bugLineArray,id); if(bool){ letx,y; if(d3.select(this).attr('href')){ x=parseFloat(d3.select(this).attr('x'))+8; y=parseFloat(d3.select(this).attr('y'))+8; }else{ x=d3.select(this).attr('cx'); y=d3.select(this).attr('cy'); } lettoolX=(x*currentScale+transX-((1-currentScale)*transX-currentX))*deviceScale; lettoolY=(y*currentScale+transY-((1-currentScale)*transY-currentY))*deviceScale; lettoolH=document.getElementById('tooltip').offsetHeight; lettoolW=110; if(toolY<935/2){ tooltip.style('left',`${toolX-toolW}px`).style('top',`${toolY+5}px`); }else{ tooltip.style('left',`${toolX-toolW}px`).style('top',`${toolY-toolH-5}px`); } } }); } functionjudgeBugPoint(arr,id){ if(!arr||!arr.length||!id)returnfalse; letbugLine=arr.filter(d=>{ returnd.points.indexOf(id)>-1 }); if(bugLine.length){ removeTooltip() tooltip.select('#tool-head').html(`${id} ×