d3.js 地铁轨道交通项目实战
上一章说了如何制作一个线路图,当然上一章是手写的JSON数据,当然手写的json数据有非常多的好处,例如可以应对客户的各种BT需求,但是大多数情况下我们都是使用地铁公司现成的JSON文件,话不多说我们先看一下百度官方线路图。
就是这样的,今天我们就来完成它的大部分需求,以及地铁公司爸爸提出来的需求。
需求如下:
1.按照不同颜色显示地铁各线路,显示对应站点。
2.用户可以点击手势缩放和平移(此项目为安卓开发)。
3.用户在线路menu里点击线路,对应线路平移值屏幕中心并高亮。
4.根据后台数据,渲染问题路段。
5.点击问题路段站点,显示问题详情。
大致需求就是这些,下面看看看代码
1.定义一些常量和变量
复制代码
const dataset = subwayData; //线路图数据源
let subway = new Subway(dataset); //线路图的类文件
let baseScale = 2; //基础缩放倍率
let deviceScale = 1400 / 2640; //设备与画布宽度比率
let width = 2640; //画布宽
let height = 1760; //画布高
let transX = 1320 + 260; //地图X轴平移(将画布原点X轴平移)
let transY = 580; //地图X轴平移(将画布原点Y轴平移)
let scaleExtent = [0.8, 4]; //缩放倍率限制
let currentScale = 2; //当前缩放值
let currentX = 0; //当前画布X轴平移量
let currentY = 0; //当前画布Y轴平移量
let selected = false; //线路是否被选中(在右上角的线路菜单被选中)
let scaleStep = 0.5; //点击缩放按钮缩放步长默认0.5倍
let tooltip = d3.select('#tooltip'); //提示框
let bugArray = []; //问题路段数组
let svg = d3.select('#sw').append('svg'); //画布
let group = svg.append('g').attr('transform', `translate(${transX}, ${transY}) scale(1)`);//定义组并平移
let whole = group.append('g').attr('class', 'whole-line') //虚拟线路(用于点击右上角响应线路可以定位当视野中心,方法不唯一)
let path = group.append('g').attr('class', 'path'); //定义线路
let point = group.append('g').attr('class', 'point'); //定义站点
const zoom = 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.构造自己的类方法
官方给了我们数据,但是并不是我们能直接使用的,所以我们需要构造自己的方法类
复制代码
class Subway {
constructor(data) {
this.data = data;
this.bugLineArray = [];
}
getInvent() {} //获取虚拟线路数据
getPathArray() {} //获取路径数据
getPointArray() {} //获取站点数组
getCurrentPathArray() {} //获取被选中线路的路径数组
getCurrentPointArray() {} //获取被选中线路的站点数组
getLineNameArray() {} // 获取线路名称数组
getBugLineArray() {} //获取问题路段数组
}
复制代码
下面是我们方法内容,里面的操作不是很优雅(大家将就看啦)
复制代码
getInvent() {
let lineArray = [];
this.data.forEach(d => {
let { loop, lc, lbx, lby, lb, lid} = d.l_xmlattr;
let allPoints = d.p.slice(0);
loop && allPoints.push(allPoints[0]);
let path = this.formatPath(allPoints, 0, allPoints.length - 1);
lineArray.push({
lid: lid,
path: path,
})
})
return lineArray;
}
getPathArray() {
let pathArray = [];
this.data.forEach(d => {
let { loop, lc, lbx, lby, lb, lid} = d.l_xmlattr;
let allPoints = d.p.slice(0);
loop && allPoints.push(allPoints[0])
let allStations = [];
allPoints.forEach((item, index) => item.p_xmlattr.st && allStations.push({...item.p_xmlattr, index}))
let arr = [];
for(let i = 0; i < allStations.length - 1; i++) {
let path = this.formatPath(allPoints, allStations[i].index, allStations[i + 1].index);
arr.push({
lid: lid,
id: `${allStations[i].sid}_${allStations[i + 1].sid}`,
path: path,
color: lc.replace(/0x/, '#')
})
}
pathArray.push({
path: arr,
lc: lc.replace(/0x/, '#'),
lb,lbx,lby,lid
})
})
return pathArray;
}
getPointArray() {
let pointArray = [];
let tempPointsArray = [];
this.data.forEach(d => {
let {lid,lc,lb} = d.l_xmlattr;
let allPoints = d.p;
let allStations = [];
allPoints.forEach(item => {
if(item.p_xmlattr.st && !item.p_xmlattr.ex) {
allStations.push({...item.p_xmlattr, lid, pn: lb, lc: lc.replace(/0x/, '#')})
} else if (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);
})
return pointArray;
}
getCurrentPathArray(name) {
let d = this.data.filter(d => d.l_xmlattr.lid == name)[0];
let { loop, lc, lbx, lby, lb, lid} = d.l_xmlattr;
let allPoints = d.p.slice(0);
loop && allPoints.push(allPoints[0])
let allStations = [];
allPoints.forEach((item, index) => item.p_xmlattr.st && allStations.push({...item.p_xmlattr, index}))
let arr = [];
for(let i = 0; i < allStations.length - 1; i++) {
let path = this.formatPath(allPoints, allStations[i].index, allStations[i + 1].index);
arr.push({
lid: lid,
id: `${allStations[i].sid}_${allStations[i + 1].sid}`,
path: path,
color: lc.replace(/0x/, '#')
})
}
return {
path: arr,
lc: lc.replace(/0x/, '#'),
lb,lbx,lby,lid
}
}
getCurrentPointArray(name) {
let d = this.data.filter(d => d.l_xmlattr.lid == name)[0];
let {lid,lc,lb} = d.l_xmlattr;
let allPoints = d.p;
let allStations = [];
allPoints.forEach(item => {
if(item.p_xmlattr.st && !item.p_xmlattr.ex) {
allStations.push({...item.p_xmlattr, lid, pn: lb, lc: lc.replace(/0x/, '#')})
} else if (item.p_xmlattr.ex) {
allStations.push({...item.p_xmlattr, lid, pn: lb, lc: lc.replace(/0x/, '#')})
}
});
return allStations;
}
getLineNameArray() {
let nameArray = this.data.map(d => {
return {
lb: d.l_xmlattr.lb,
lid: d.l_xmlattr.lid,
lc: d.l_xmlattr.lc.replace(/0x/, '#')
}
})
return nameArray;
}
getBugLineArray(arr) {
if(!arr || !arr.length) return [];
this.bugLineArray = [];
arr.forEach(item => {
let { start, end, cause, duration, lid, lb } = item;
let lines = [];
let points = [];
let tempObj = this.data.filter(d => d.l_xmlattr.lid == lid)[0];
let loop = tempObj.l_xmlattr.loop;
let lc = tempObj.l_xmlattr.lc;
let allPoints = tempObj.p;
let allStations = [];
allPoints.forEach(item => {
if(item.p_xmlattr.st) {
allStations.push(item.p_xmlattr.sid)
}
});
loop && allStations.push(allStations[0]);
for(let i=allStations.indexOf(start); i<=allStations.lastIndexOf(end); i++) {
points.push(allStations[i])
}
for(let i=allStations.indexOf(start); i d.path)
.attr('class', d => d.lid)
.attr('stroke', 'none')
.attr('fill', 'none')
}
function renderAllLine() {
for (let i = 0; i < pathArray.length; i++) {
path.append('g')
.selectAll('path')
.data(pathArray[i].path)
.enter()
.append('path')
.attr('d', d => d.path)
.attr('lid', d => d.lid)
.attr('id', d => d.id)
.attr('class', 'lines origin')
.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-text origin')
.attr('font-size', 14)
.attr('font-weight', 'bold')
.text(pathArray[i].lb)
}
}
function renderAllPoint() {
for (let i = 0; i < pointArray.length; i++) {
for (let j = 0; j < pointArray[i].length; j++) {
let item = pointArray[i][j];
let box = point.append('g');
if (item.ex) {
box.append('image')
.attr('href', './trans.png')
.attr('class', 'points origin')
.attr('id', item.sid)
.attr('x', item.x - 8)
.attr('y', item.y - 8)
.attr('width', 16)
.attr('height', 16)
} else {
box.append('circle')
.attr('cx', item.x)
.attr('cy', item.y)
.attr('r', 5)
.attr('class', 'points origin')
.attr('id', item.sid)
.attr('stroke', item.lc)
.attr('stroke-width', 1.5)
.attr('fill', '#ffffff')
}
box.append('text')
.attr('x', item.x + item.rx)
.attr('y', item.y + item.ry)
.attr('dx', '0.3em')
.attr('dy', '1.1em')
.attr('font-size', 11)
.attr('class', 'point-text origin')
.attr('lid', item.lid)
.attr('id', item.sid)
.text(item.lb)
}
}
}
function renderCurrentLine(name) {
let arr = subway.getCurrentPathArray(name);
path.append('g')
.attr('class', 'temp')
.selectAll('path')
.data(arr.path)
.enter()
.append('path')
.attr('d', d => 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)
}
function renderCurrentPoint(name) {
let arr = subway.getCurrentPointArray(name);
for (let i = 0; i < arr.length; i++) {
let item = arr[i];
let box = point.append('g').attr('class', 'temp');
if (item.ex) {
box.append('image')
.attr('href', './trans.png')
.attr('x', item.x - 8)
.attr('y', item.y - 8)
.attr('width', 16)
.attr('height', 16)
.attr('id', item.sid)
} else {
box.append('circle')
.attr('cx', item.x)
.attr('cy', item.y)
.attr('r', 5)
.attr('id', item.sid)
.attr('stroke', item.lc)
.attr('stroke-width', 1.5)
.attr('fill', '#ffffff')
}
box.append('text')
.attr('class', 'temp')
.attr('x', item.x + item.rx)
.attr('y', item.y + item.ry)
.attr('dx', '0.3em')
.attr('dy', '1.1em')
.attr('font-size', 11)
.attr('lid', item.lid)
.attr('id', item.sid)
.text(item.lb)
}
}
function renderBugLine(modal) {
let bugLineArray = subway.getBugLineArray(modal);
d3.selectAll('.origin').remove();
renderAllLine();
renderAllPoint();
bugLineArray.forEach(d => {
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 () {
let id = d3.select(this).attr('id');
let bool = judgeBugPoint(bugLineArray, id);
if (bool) {
let x, 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');
}
let toolX = (x * currentScale + transX - ((1 - currentScale) * transX - currentX)) * deviceScale;
let toolY = (y * currentScale + transY - ((1 - currentScale) * transY - currentY)) * deviceScale;
let toolH = document.getElementById('tooltip').offsetHeight;
let toolW = 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`);
}
}
});
}
function judgeBugPoint(arr, id) {
if (!arr || !arr.length || !id) return false;
let bugLine = arr.filter(d => {
return d.point