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<allStations.lastIndexOf(end); i++) {

lines.push(`${allStations[i]}_${allStations[i+1]}`)

}

this.bugLineArray.push({cause,duration,lid,lb,lines,points,lc: lc.replace(/0x/, '#'),start: points[0],end:points[points.length - 1]});

})

return this.bugLineArray;


这种方法大家也不必看懂,知道传入了什么,输入了什么即可,这就是我们的方法类。

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));

let pathArray = subway.getPathArray();

let pointArray = subway.getPointArray();

renderInventLine();

renderAllStation();

renderBugLine();

function renderInventLine() {

let arr = 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')

}

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.points.indexOf(id) > -1

});

if (bugLine.length) {

removeTooltip()

tooltip.select('#tool-head').html(`<span>${id}</span><div class="deletes" onclick="removeTooltip()">×</div>`);

bugLine.forEach(d => {

let item = tooltip.select('#tool-body').append('div').attr('class', 'tool-item');

item.html(`

<div class="tool-content">

<div style="color: #ffffff;border-bottom: 2px solid ${d.lc};">

<span style="background: ${d.lc};padding: 4px 6px;">${d.lb}</span>

</div>

<div>

<div class="content-left">封路时间</div><div class="content-right">${d.duration}</div>

</div>

<div>

<div class="content-left">封路原因</div><div class="content-right">${d.cause}</div>

</div>

<div>

<div class="content-left">封路路段</div><div class="content-right">${d.start}-${d.end}</div>

</div>

</div>

`)

})

d3.select('#tooltip').style('display', 'block');

return true;

} else {

return false;

}

}

function removeTooltip() {

d3.selectAll('.tool-item').remove();

d3.select('#tooltip').style('display', 'none');

}

function zoomed() {

removeTooltip();

let {x, y, k} = d3.event.transform;

currentScale = k;

currentX = x;

currentY = y;

group.transition().duration(50).ease(d3.easeLinear).attr("transform", () => `translate(${x + transX * k}, ${y + transY * k}) scale(${k})`)

}

function getCenter(str) {

if (!str) return null;

let x, y;

let tempArr = [];

let tempX = [];

let tempY = [];

str.split(' ').forEach(d => {

if (!isNaN(d)) {

tempArr.push(d)

}

})

tempArr.forEach((d, i) => {

if (i % 2 == 0) {

tempX.push(parseFloat(d))

} else {

tempY.push(parseFloat(d))

}

})

x = (d3.min(tempX) + d3.max(tempX)) / 2;

y = (d3.min(tempY) + d3.max(tempY)) / 2;

return [x, y]

}

function renderAllStation() {

let nameArray = subway.getLineNameArray();

let len = Math.ceil(nameArray.length / 5);

let box = d3.select('#menu').append('div')

.attr('class', 'name-box')

for (let i = 0; i < len; i++) {

let subwayCol = box.append('div')

.attr('class', 'subway-col')

let item = subwayCol.selectAll('div')

.data(nameArray.slice(i * 5, (i + 1) * 5))

.enter()

.append('div')

.attr('id', d => d.lid)

.attr('class', 'name-item')

item.each(function (d) {

d3.select(this).append('span').attr('class', 'p_mark').style('background', d.lc);

d3.select(this).append('span').attr('class', 'p_name').text(d.lb);

d3.select(this).on('click', d => {

selected = true;

d3.selectAll('.origin').style('opacity', 0.1);

d3.selectAll('.temp').remove();

renderCurrentLine(d.lid);

renderCurrentPoint(d.lid);

let arr = getCenter(d3.select(`path.${d.lid}`).attr('d'));

svg.call(zoom.transform, d3.zoomIdentity.translate((width / 2 - transX) - arr[0] - (arr[0] + transX) * (currentScale - 1), (height / 2 - transY) - arr[1] - (arr[1] + transY) * (currentScale - 1)).scale(currentScale));

})

})

}

}

function scale(type) {

if (type && currentScale + scaleStep <= scaleExtent[1]) {

svg.call(zoom.transform, d3.zoomIdentity.translate((1 - currentScale - scaleStep) * transX - ((1 - currentScale) * transX - currentX) * (currentScale + scaleStep) / currentScale, (1 - currentScale - scaleStep) * transY - ((1 - currentScale) * transY - currentY) * (currentScale + scaleStep) / currentScale).scale(currentScale + scaleStep));

} else if (!type && currentScale - scaleStep >= scaleExtent[0]) {

svg.call(zoom.transform, d3.zoomIdentity.translate((1 - (currentScale - scaleStep)) * transX - ((1 - currentScale) * transX - currentX) * (currentScale - scaleStep) / currentScale, (1 - (currentScale - scaleStep)) * transY - ((1 - currentScale) * transY - currentY) * (currentScale - scaleStep) / currentScale).scale(currentScale - scaleStep));

}

}

上面是大部分代码,想看全部的可以查看demo。

想查看demo或代码的朋友们,请移步至原文http://www.bettersmile.cn

总结

以上所述是小编给大家介绍的d3.js 地铁轨道交通项目实战,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对网站的支持!

如果你觉得本文对你有帮助,欢迎转载,烦请注明出处,谢谢!

以上是 d3.js 地铁轨道交通项目实战 的全部内容, 来源链接: utcz.com/z/351866.html

回到顶部