d3.js 地铁轨道交通项目实战


Posted in Javascript onNovember 27, 2019

上一章说了如何制作一个线路图,当然上一章是手写的JSON数据,当然手写的json数据有非常多的好处,例如可以应对客户的各种BT需求,但是大多数情况下我们都是使用地铁公司现成的JSON文件,话不多说我们先看一下百度官方线路图。

d3.js 地铁轨道交通项目实战

就是这样的,今天我们就来完成它的大部分需求,以及地铁公司爸爸提出来的需求。

需求如下:

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数据。

d3.js 地铁轨道交通项目实战

每条线路对象都有一个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 地铁轨道交通项目实战,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对三水点靠木网站的支持!
如果你觉得本文对你有帮助,欢迎转载,烦请注明出处,谢谢!

Javascript 相关文章推荐
JavaScript 对象链式操作测试代码
Apr 25 Javascript
juqery 学习之三 选择器 简单 内容
Nov 25 Javascript
JS TextArea字符串长度限制代码集合
Oct 31 Javascript
Javascript高级技巧分享
Feb 25 Javascript
Javascript闭包实例详解
Nov 29 Javascript
分享使用AngularJS创建应用的5个框架
Dec 05 Javascript
JavaScript中的Object对象学习教程
May 20 Javascript
Vue.js实现拖放效果的实例
Sep 30 Javascript
socket在egg中的使用实例代码详解
May 30 Javascript
微信小程序里引入SVG矢量图标的方法
Sep 20 Javascript
javascript实现页面的实时时钟显示示例
Aug 06 Javascript
详解JavaScript中的数据类型,以及检测数据类型的方法
Sep 17 Javascript
微信小程序表单验证WxValidate的使用
Nov 27 #Javascript
vue(2.x,3.0)配置跨域代理
Nov 27 #Javascript
微信小程序加载机制及运行机制图解
Nov 27 #Javascript
webgl实现物体描边效果的方法介绍
Nov 27 #Javascript
three.js利用卷积法如何实现物体描边效果
Nov 27 #Javascript
Angular8引入百度Echarts进行图表分析的实现代码
Nov 27 #Javascript
vue实现浏览器全屏展示功能
Nov 27 #Javascript
You might like
THINKPHP+JS实现缩放图片式截图的实现
2010/03/07 PHP
ThinkPHP处理Ajax返回的方法
2014/11/22 PHP
Symfony2实现在doctrine中内置数据的方法
2016/02/05 PHP
PHP中FTP相关函数小结
2016/07/15 PHP
比较全的JS checkbox全选、取消全选、删除功能代码
2008/12/19 Javascript
Javascript 的addEventListener()及attachEvent()区别分析
2009/05/21 Javascript
js中eval()函数和trim()去掉字符串左右空格应用
2013/02/02 Javascript
javascript实现链接单选效果的方法
2015/05/13 Javascript
jQuery移动web开发中的页面初始化与加载事件
2015/12/03 Javascript
Node.js实用代码段之正确拼接Buffer
2016/03/17 Javascript
IE8 内存泄露(内存一直增长 )的原因及解决办法
2016/04/06 Javascript
Angular 中 select指令用法详解
2016/09/29 Javascript
js document.getElementsByClassName的使用介绍与自定义函数
2016/11/25 Javascript
详解vue-validator(vue验证器)
2017/01/16 Javascript
AngularJs 利用百度地图API 定位当前位置 获取地址信息
2017/01/18 Javascript
ajax接收后台数据在html页面显示
2017/02/19 Javascript
Angular-Ui-Router+ocLazyLoad动态加载脚本示例
2017/03/02 Javascript
详解如何在 vue 项目里正确地引用 jquery 和 jquery-ui的插件
2017/06/01 jQuery
详细介绍RxJS在Angular中的应用
2017/09/23 Javascript
使用原生js编写一个简单的框选功能方法
2019/05/13 Javascript
vue-cli配置flexible过程详解
2019/07/04 Javascript
原生js实现可兼容PC和移动端的拖动滑块功能详解【测试可用】
2019/08/15 Javascript
在Layui 的表格模板中,实现layer父页面和子页面传值交互的方法
2019/09/10 Javascript
[45:16]完美世界DOTA2联赛循环赛 IO vs FTD BO2第二场 11.05
2020/11/06 DOTA
使用wxPython获取系统剪贴板中的数据的教程
2015/05/06 Python
批量获取及验证HTTP代理的Python脚本
2017/04/23 Python
使用Python写一个贪吃蛇游戏实例代码
2017/08/21 Python
详解Python Matplot中文显示完美解决方案
2019/03/07 Python
python等待10秒执行下一命令的方法
2020/07/19 Python
辩论赛主持词
2014/03/18 职场文书
2014年高二班主任工作总结
2014/12/16 职场文书
世界遗产导游词
2015/02/13 职场文书
劳务派遣管理制度(样本)
2019/08/23 职场文书
用Python制作灯光秀短视频的思路详解
2021/04/13 Python
详解thinkphp的Auth类认证
2021/05/28 PHP
你真的会用Mysql的explain吗
2022/03/31 MySQL