d3.js 地鐵軌道交通項(xiàng)目實(shí)戰(zhàn)
上一章說了如何制作一個(gè)線路圖,當(dāng)然上一章是手寫的JSON數(shù)據(jù),當(dāng)然手寫的json數(shù)據(jù)有非常多的好處,例如可以應(yīng)對(duì)客戶的各種BT需求,但是大多數(shù)情況下我們都是使用地鐵公司現(xiàn)成的JSON文件,話不多說我們先看一下百度官方線路圖。

就是這樣的,今天我們就來完成它的大部分需求,以及地鐵公司爸爸提出來的需求。
需求如下:
1.按照不同顏色顯示地鐵各線路,顯示對(duì)應(yīng)站點(diǎn)。
2.用戶可以點(diǎn)擊手勢(shì)縮放和平移(此項(xiàng)目為安卓開發(fā))。
3.用戶在線路menu里點(diǎn)擊線路,對(duì)應(yīng)線路平移值屏幕中心并高亮。
4.根據(jù)后臺(tái)數(shù)據(jù),渲染問題路段。
5.點(diǎn)擊問題路段站點(diǎn),顯示問題詳情。
大致需求就是這些,下面看看看代碼
1.定義一些常量和變量
const dataset = subwayData; //線路圖數(shù)據(jù)源
let subway = new Subway(dataset); //線路圖的類文件
let baseScale = 2; //基礎(chǔ)縮放倍率
let deviceScale = 1400 / 2640; //設(shè)備與畫布寬度比率
let width = 2640; //畫布寬
let height = 1760; //畫布高
let transX = 1320 + 260; //地圖X軸平移(將畫布原點(diǎn)X軸平移)
let transY = 580; //地圖X軸平移(將畫布原點(diǎn)Y軸平移)
let scaleExtent = [0.8, 4]; //縮放倍率限制
let currentScale = 2; //當(dāng)前縮放值
let currentX = 0; //當(dāng)前畫布X軸平移量
let currentY = 0; //當(dāng)前畫布Y軸平移量
let selected = false; //線路是否被選中(在右上角的線路菜單被選中)
let scaleStep = 0.5; //點(diǎn)擊縮放按鈕縮放步長默認(rèn)0.5倍
let tooltip = d3.select('#tooltip'); //提示框
let bugArray = []; //問題路段數(shù)組
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') //虛擬線路(用于點(diǎn)擊右上角響應(yīng)線路可以定位當(dāng)視野中心,方法不唯一)
let path = group.append('g').attr('class', 'path'); //定義線路
let point = group.append('g').attr('class', 'point'); //定義站點(diǎn)
const zoom = d3.zoom().scaleExtent(scaleExtent).on("zoom", zoomed); //定義縮放事件
這就是我們需要使用的一些常量和變量。注意transX不是寬度的一半,是因?yàn)楸本┑罔F線路網(wǎng)西線更密集。
2.讀官方JSON
使用d3.js數(shù)據(jù)必不可少,然而官方的數(shù)據(jù)并不通俗易懂,我們先解讀一下官方JSON數(shù)據(jù)。

每條線路對(duì)象都有一個(gè)l_xmlattr屬性和一個(gè)p屬性,l_xmlattr是整條線路的屬性,p是站點(diǎn)數(shù)組,我們看一下站點(diǎn)中我們需要的屬性。ex是否是中轉(zhuǎn)站,lb是站名,sid是站的id,rx、ry是文字偏移量,st是是否為站點(diǎn)(因?yàn)橛械狞c(diǎn)不是站點(diǎn)而是為了渲染貝塞爾曲線用的),x、y是站點(diǎn)坐標(biāo)。
3.構(gòu)造自己的類方法
官方給了我們數(shù)據(jù),但是并不是我們能直接使用的,所以我們需要構(gòu)造自己的方法類
class Subway {
constructor(data) {
this.data = data;
this.bugLineArray = [];
}
getInvent() {} //獲取虛擬線路數(shù)據(jù)
getPathArray() {} //獲取路徑數(shù)據(jù)
getPointArray() {} //獲取站點(diǎn)數(shù)組
getCurrentPathArray() {} //獲取被選中線路的路徑數(shù)組
getCurrentPointArray() {} //獲取被選中線路的站點(diǎn)數(shù)組
getLineNameArray() {} // 獲取線路名稱數(shù)組
getBugLineArray() {} //獲取問題路段數(shù)組
}
下面是我們方法內(nèi)容,里面的操作不是很優(yōu)雅(大家將就看啦)
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文件都寫完了,這里的操作就方便了很多,主要就是下面幾個(gè)人方法,
renderInventLine(); //渲染虛擬新路 renderAllStation(); //渲染所有的線路名稱(右上角) renderBugLine(); //渲染問題路段 renderAllLine(); //渲染所有線路 renderAllPoint(); //渲染所有點(diǎn) renderCurrentLine() //渲染當(dāng)前選中的線路 renderCurrentPoint() //渲染當(dāng)前選中的站點(diǎn) zoomed() //縮放時(shí)執(zhí)行的方法 getCenter() //獲取虛擬線中心點(diǎn)的坐標(biāo) scale() //點(diǎn)擊縮放按鈕時(shí)執(zhí)行的方法
下面是對(duì)應(yīng)的方法體
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">封路時(shí)間</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或代碼的朋友們,請(qǐng)移步至原文http://www.bettersmile.cn
總結(jié)
以上所述是小編給大家介紹的d3.js 地鐵軌道交通項(xiàng)目實(shí)戰(zhàn),希望對(duì)大家有所幫助,如果大家有任何疑問請(qǐng)給我留言,小編會(huì)及時(shí)回復(fù)大家的。在此也非常感謝大家對(duì)腳本之家網(wǎng)站的支持!
如果你覺得本文對(duì)你有幫助,歡迎轉(zhuǎn)載,煩請(qǐng)注明出處,謝謝!
相關(guān)文章
Echarts折線圖設(shè)置線條顏色及線條以下代碼示例
最近項(xiàng)目需要,一直在使用Echarts視圖,現(xiàn)在遇到一個(gè)要修改echarts折線圖顏色的需求,下面這篇文章主要給大家介紹了關(guān)于Echarts折線圖設(shè)置線條顏色及線條以下區(qū)域漸變顏色的相關(guān)資料,需要的朋友可以參考下2024-02-02
Js實(shí)現(xiàn)中國公民身份證號(hào)碼有效性驗(yàn)證實(shí)例代碼
這篇文章主要介紹了Js實(shí)現(xiàn)中國公民身份證號(hào)碼有效性驗(yàn)證實(shí)例代碼,可以識(shí)別身份證號(hào)碼的正確性,有興趣的可以了解一下2017-05-05
js實(shí)現(xiàn)的萬能flv網(wǎng)頁播放器代碼
這篇文章主要介紹了js實(shí)現(xiàn)的萬能flv網(wǎng)頁播放器代碼,以簡(jiǎn)單示例形式分析了JavaScript使用swfobject.js實(shí)現(xiàn)在線播放flv視頻的相關(guān)技巧,需要的朋友可以參考下2016-04-04
利用 Chrome Dev Tools 進(jìn)行頁面性能分析的步驟說明(前端性能優(yōu)化)
這篇文章主要介紹了利用 Chrome Dev Tools 進(jìn)行頁面性能分析的步驟說明(前端性能優(yōu)化),本文給大家介紹的非常想詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-02-02
值得分享和收藏的xmlplus組件學(xué)習(xí)教程
值得分享和收藏的xmlplus組件學(xué)習(xí)教程,xmlplus是一個(gè)設(shè)計(jì)非常獨(dú)特 JavaScript 框架,用于快速開發(fā)前后端項(xiàng)目,感興趣的小伙伴們可以參考一下2017-05-05
javascript獲取不重復(fù)的隨機(jī)數(shù)的方法比較
js永不重復(fù)隨機(jī)數(shù)實(shí)現(xiàn)代碼比較2008-09-09

