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 相关文章推荐
UpdatePanel和Jquery冲突的解决方法
Apr 01 Javascript
js实现用户注册协议倒计时的方法
Jan 21 Javascript
js树插件zTree获取所有选中节点数据的方法
Jan 28 Javascript
JavaScript toUpperCase()方法使用详解
Aug 26 Javascript
jquery实现手机端单店铺购物车结算删除功能
Feb 22 Javascript
利用NPM淘宝的node.js镜像加速nvm
Mar 27 Javascript
兼容浏览器的js事件绑定函数(详解)
May 09 Javascript
javascript实现Emrips反质数枚举的示例代码
Dec 06 Javascript
vue 根据数组中某一项的值进行排序的方法
Aug 30 Javascript
微信小程序实现基于三元运算验证手机号/姓名功能示例
Jan 19 Javascript
js实现小时钟效果
Mar 25 Javascript
详解vue组件之间的通信
Aug 30 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
星际争霸中的对战模式介绍
2020/03/04 星际争霸
php标签云的实现代码
2012/10/10 PHP
真正根据utf8编码的规律来进行截取字符串的函数(utf8版sub_str )
2012/10/24 PHP
使用PHP获取当前url路径的函数以及服务器变量
2013/06/29 PHP
ThinkPHP使用PHPExcel实现Excel数据导入导出完整实例
2014/07/22 PHP
php array_keys 返回数组的键名
2016/10/25 PHP
PHP数组式访问接口ArrayAccess用法分析
2017/12/28 PHP
php下的原生ajax请求用法实例分析
2020/02/28 PHP
实例:尽可能写友好的Javascript代码
2006/10/09 Javascript
javascript dom 基本操作小结
2010/04/11 Javascript
JS二维数组的定义说明
2014/03/03 Javascript
IE下双击checkbox反应延迟问题的解决方法
2014/03/27 Javascript
Area 区域实现post提交数据的js写法
2014/04/22 Javascript
JavaScript实现简单图片翻转的方法
2015/04/17 Javascript
基于jQuery实现拖拽图标到回收站并删除功能
2015/11/25 Javascript
node.js使用cluster实现多进程
2016/03/17 Javascript
jquery  实现轮播图详解及实例代码
2016/10/12 Javascript
React-Native之定时器Timer的实现代码
2017/10/04 Javascript
快速解决vue在ios端下点击响应延时的问题
2018/08/27 Javascript
小程序实现左滑删除的效果的实例代码
2020/10/19 Javascript
[46:27]DOTA2上海特级锦标赛主赛事日 - 1 胜者组第一轮#2LGD VS MVP.Phx第一局
2016/03/02 DOTA
wxPython使用系统剪切板的方法
2015/06/16 Python
Python正则获取、过滤或者替换HTML标签的方法
2016/01/28 Python
CentOS中升级Python版本的方法详解
2017/07/10 Python
python opencv 直方图反向投影的方法
2018/02/24 Python
PyTorch快速搭建神经网络及其保存提取方法详解
2018/04/28 Python
Opencv+Python 色彩通道拆分及合并的示例
2018/12/08 Python
讲解Python3中NumPy数组寻找特定元素下标的两种方法
2019/08/04 Python
pycharm设置python文件模板信息过程图解
2020/03/10 Python
美国LOGO设计公司:The Logo Company
2018/07/16 全球购物
公司法人授权委托书范本
2014/09/12 职场文书
教师党员个人整改措施材料
2014/09/16 职场文书
医院领导班子整改方案
2014/10/01 职场文书
休学证明范本
2015/06/19 职场文书
劳务派遣管理制度(样本)
2019/08/23 职场文书
《童年》读后感(三篇)
2019/08/27 职场文书