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 相关文章推荐
Jquery作者John Resig自己封装的javascript 常用函数
Nov 09 Javascript
Jquery判断IE6等浏览器的代码
Apr 05 Javascript
JS/FLASH实现复制代码到剪贴板(兼容所有浏览器)
May 27 Javascript
点击按钮自动加关注的代码(sina微博/QQ空间/人人网/腾讯微博)
Jan 02 Javascript
JavaScript获取元素尺寸和大小操作总结
Feb 27 Javascript
JavaScript代码实现左右上下自动晃动自动移动
Apr 08 Javascript
全面了解构造函数继承关键apply call
Jul 26 Javascript
浅谈JavaScript 中有关时间对象的方法
Aug 15 Javascript
从零学习node.js之详解异步控制工具async(八)
Feb 27 Javascript
JavaScript遍历数组的三种方法map、forEach与filter实例详解
Feb 27 Javascript
使用Vue 实现滑动验证码功能
Jun 27 Javascript
Vuex的热更替如何实现
Jun 05 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
PHP把数字转成人民币大写的函数分享
2014/06/30 PHP
PHP处理JSON字符串key缺少双引号的解决方法
2014/09/16 PHP
php similar_text()函数的定义和用法
2016/05/12 PHP
PHP通过CURL实现定时任务的图片抓取功能示例
2016/10/03 PHP
PHP strcmp()和strcasecmp()的区别实例
2016/11/05 PHP
PHP操作Redis数据库常用方法示例
2018/08/25 PHP
详解JavaScript函数绑定
2013/08/18 Javascript
Jquery中CSS选择器用法分析
2015/02/10 Javascript
JS获取Table中td值的方法
2015/03/19 Javascript
Jquery幻灯片特效代码分享--鼠标点击按钮时切换(1)
2015/08/15 Javascript
vue自定义指令实现v-tap插件
2016/11/03 Javascript
使用Javascript简单计算器
2018/11/17 Javascript
Vue入门之数量加减运算操作示例
2018/12/11 Javascript
JavaScript函数式编程(Functional Programming)高阶函数(Higher order functions)用法分析
2019/05/22 Javascript
redux.js详解及基本使用
2019/05/24 Javascript
[09:33]2015国际邀请赛第四日TOP10
2015/08/08 DOTA
python获取豆瓣电影简介代码分享
2014/01/16 Python
Python简单定义与使用字典dict的方法示例
2017/07/25 Python
Python 实现12306登录功能实例代码
2018/02/09 Python
Matplotlib 生成不同大小的subplots实例
2018/05/25 Python
python实现自动发送邮件
2018/06/20 Python
python和shell监控linux服务器的详细代码
2018/06/22 Python
Python发展简史 Python来历
2019/05/14 Python
Python之pymysql的使用小结
2019/07/01 Python
Python实现打印实心和空心菱形
2019/11/23 Python
python字典的值可以修改吗
2020/06/29 Python
利用CSS3实现平移动画效果示例代码
2016/10/12 HTML / CSS
英国领先的酒类网上商城:TheDrinkShop
2017/03/16 全球购物
戴尔美国官方折扣店:Dell Outlet
2018/02/13 全球购物
美国美食礼品篮网站:Gourmet Gift Baskets
2019/12/15 全球购物
高中生毕业自我鉴定范文
2013/12/22 职场文书
公司成立感言
2014/01/11 职场文书
三年大学自我鉴定
2014/01/16 职场文书
暑期社会实践心得体会
2014/09/02 职场文书
服务承诺书
2015/01/19 职场文书
2016年学校“3.12”植树节活动总结
2016/03/16 职场文书