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;id.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;id.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}×