d3.js制作连线动画图和编辑器
此文章为原创文章,原文地址:https://www.cnblogs.com/eagle1098/p/11431679.html
连线动画图
编辑器
效果如上图所示。
本项目使用主要d3.jsv4制作,分两部分,一个是实际展示的连线动画图,另一个是管理人员使用鼠标编辑连线的页面。对于d3.js如何引入图片,如何画线等基础功能,这里就不再介绍了,大家可以找一些入门文章看一下。这里主要介绍一下重点问题。
1.连线动画图
此图的主要功能是每隔给定时间,通过ajax请求后台数据,并根据返回的数据动态改变每个图片下方的数值,动态改变连线上的动画流动方向和是否流动。
首先,确定图表中需要配置的内容,如各图片存储位置,连线和动画颜色,图片和连线的坐标等。这些数据需要在html中进行配置,最好写成object对象,赋值给我们自己的图表类的函数。比如:
- var data = {
- element:[{
- image: 'img/work.png',
- pos:[1,1], // 图片位置
- linePoint:[], // 图片发出线段坐标数组
- lineDir:0, // 线段动画方向
- title: '工作'
- }],
- lineColor:'black', // 连线颜色
- animateColor: 'red', // 动画颜色
- };
- var chart = new Myd3chart('#chart');
- chart.lineChart(data);
其中图片发出的线段坐标数组,使用外部文件提供,此文件由之后介绍的编辑器生成。
在设计我们自己的图表函数时,最好把每个功能划分成独立的函数,这样方便以后的维护和扩展。
动画线段采用css的方式,有动画的线段添加此css即可:
- .animate-line{
- fill: none;
- stroke-width: 1;
- stroke-dasharray: 50 100;
- stroke-dashoffset: 0;
- animation: stroke 6s infinite linear;
- }
- @keyframes stroke {
- 100% {
- stroke-dashoffset: 500; /* 如果反向移动改为-500 */
- }
- }
这个图表的难点在于动态改变连线上的流动动画,因为A线段的终点会连接到B线段上,如果B线段动画停止,则A线段上的动画仍然要从B上经过,而不能简单停止B线段上的动画。而且如果B线段上的接入点不止一个,还要判断接入点之间的顺序,只显示最靠近B起始点的接入点的动画。另外还要判断接入线段上是否有接入线段,层级关系里面如果有1个线段有动画,则此接入点就有动画流出。(这里说起来有点绕)
我的方法是:
1)统计每个线段上的所有接入点,这里就是图片名称,用于判断此线段是否有动画流出。
2)接收后台传来的数据时,判断每个线段是否有动画,如果有动画,则直接恢复其动画线段的起始点坐标;如果没有动画,则判断最靠近起始点的接入点是否有动画,如果有动画则将动画线段的起始点改为此接入点坐标。
- // 统计接入点
- function findAccessPoint() {
- var accessPoints = [];
- // 记录每个线段上的接入点,data为配置数据
- data.eles.forEach(function(d, i){
- if(d.line.length == 0){
- return;
- }
- var acsp = {
- name: d.title.text,
- ap: [], // 接入点,按顺序排列,头部离开始点近
- };
- // 本线段上,每两相邻的点作为一个元素存入数组
- var linePair = [];
- // 本线段起始点
- var startPos = d.line[0];
- d.line.forEach(function(dd, di){
- if(d.line[di+1]){
- var pair = {
- start: dd,
- end: d.line[di+1]
- };
- linePair.push(pair);
- }
- });
- // 对每两相邻的点,查找接入点
- linePair.forEach(function(dd, di){
- chartData.eles.forEach(function(ddd, ddi){
- // 排除自己,查找自己线段上的接入点
- if(i != ddi && ddd.line.length > 1){
- // 得到此线段终点
- var pos = ddd.line[ddd.line.length - 1];
- // dd.start开始点,dd.end结束点
- // 用x坐标计算在本线段上的y坐标,再和实际的y坐标比较
- var computeY = dd.start[1] +
- (pos[0] - dd.start[0])*(dd.end[1] - dd.start[1])/(dd.end[0] - dd.start[0]);
- var dif = Math.abs(computeY - pos[1]);
- // 如果误差在2以内,并且此线终点在当前线起点和终点之间
- // 认为此点为接入点
- if(dif < 2 && (
- (
- ((pos[0] > dd.start[0]) && (pos[0] < dd.end[0])) ||
- ((pos[0] < dd.start[0]) && (pos[0] > dd.end[0]))
- ) && (
- ((pos[1] > dd.start[1]) && (pos[1] < dd.end[1])) ||
- ((pos[1] < dd.start[1]) && (pos[1] > dd.end[1]))
- )
- )) {
- var dis = Math.pow((pos[0] - startPos[0]),2) + Math.pow((pos[1] - startPos[1]),2);
- var ap = {
- name: ddd.title.text,
- ap: pos,
- distance: dis, // 距离起始点的距离
- allNames: [], // 所有通过此接入点的站点名称
- }
- acsp.ap.push(ap);
- }
- }
- });
- })
- accessPoints.push(acsp);
- });
- //对所有的接入点,按与起始点的距离排序,并查找此接入点的上层站点
- accessPoints.forEach(function(d, i){
- // 按distance由小到大排序
- d.ap.sort(function(a, b){
- return a.distance - b.distance;
- });
- // 查找每个接入点的上层站点
- d.ap.forEach(function(dd, di){
- findPoint(dd.name, dd.allNames);
- });
- });
- // name是接入点名称,arr是该接入点的allNames
- function findPoint(name, arr){
- accessPoints.forEach(function(d, i){
- // 在数组中找到指定名称的项
- if(d.name === name){
- if(d.ap.length>0){
- // 把该项下面的ap中的名称加入给定arr
- d.ap.forEach(function(dd, di){
- arr.push(dd.name);
- // 如果该点内的allNames已经有值则直接加入
- if(dd.allNames.length>0){
- dd.allNames.forEach(function(d, i){
- arr.push(d);
- });
- } else{
- // 递归查找子接入点
- findPoint(dd.name, arr);
- }
- });
- } else {
- return;
- }
- }else{
- return;
- }
- });
- }
- }
以上函数的运行结果会产生一个对象,存储每个接入线段上‘挂载’的接入点,目的就是改变动画时方便判断。
- // 更新线条动画
- aniLine.each(function(d, i){
- var curLine = d3.select(this);
- // 找到对应的动画line
- if (dd.name === curLine.attr('tag')) {
- // 处理动画是否运行
- if (dd.ani) {
- // 此线条动画运行
- curLine.style('animation-play-state', 'running');
- curLine.style('display', 'inline');
- // 如果动画运行,则恢复原始动画路径
- curLine.attr('d', function(d){
- return line(chartData.eles[i].line);
- });
- } else {
- // 此线条动画停止
- // 先查找离本线段开始点最近的接入点
- var acp = accessPoints;
- // 从accessPoints中找到本节点的接入点集合
- var ap = [];
- acp.forEach(function(acd, aci){
- if(acd.name === dd.name){
- ap = acd.ap;
- }
- });
- // 最近有动画接入点序号
- var acIndex = -1;
- // 找到最近的有动画接入点,远近按数组序号递增
- for(var j=0;j<ap.length;j++){
- // 复制所有子接入点数组
- var allNames = ap[j].allNames.concat();
- // 将接入点名称也加入
- allNames.push(ap[j].name);
- // 判断此接入点树中是否有动画,如果1个有就可以
- allNames.forEach(function(name,ani){
- data.forEach(function(datad, datai){
- if(datad.name === name){
- if(datad.ani){
- acIndex = j;
- return;
- }
- }
- });
- });
- if(acIndex != -1) {
- break;
- }
- }
- // 如果存在有动画接入点
- if(acIndex != -1){
- curLine.style('animation-play-state', 'running');
- curLine.style('display', 'inline');
- curLine.attr('d', function(d){
- var accp = ap[acIndex].ap;
- var curLine = data.element[i].line.concat();
- // 接入节点与开始点的距离
- var disAp = Math.pow((accp[0] - curLine[0][0]),2) +
- Math.pow((accp[1] - curLine[0][1]),2);
- // 如果当前线段中有离开始节点比接入点近的节点
- // 则删除此节点
- curLine.forEach(function(curld, curli){
- if(curli > 0){
- var dis = Math.pow((curld[0] - curLine[0][0]),2) +
- Math.pow((curld[1] - curLine[0][1]),2);
- if(dis < disAp){
- // 删除此点
- curLine.splice(curli,1);
- }
- }
- });
- // 从此接入点处开始动画
- curLine.splice(0,1,accp);
- // debugger;
- return line(curLine);
- });
- }else{
- // 此线条动画停止
- curLine.style('animation-play-state', 'paused');
- curLine.style('display', 'none');
- }
- }
- }
此文章为原创文章,原文地址:https://www.cnblogs.com/eagle1098/p/11431679.html
2.编辑器
由于本图表需要配置大量坐标,如果手动填写的话效率十分低下,所以需要开发一个编辑器用来修改图表。
编辑器的主要使用方法为,使用鼠标拖动图标,双击确定起始位置并开始实时画线状态,随着鼠标移动动态画出线段,单击确定临时终点,再单击确定下一个终点,右击结束动态画线状态。如果鼠标单击其他图标,则终点为该图标的起始坐标。本程序的实时画线部分进行了倾斜的约束,即左倾或右倾30度角。
编辑器比展示图要简单一些,复杂部分在事件处理。
- // 拖动图标
- var draging = d3.drag()
- .on('drag', function () {
- // 当长宽相同时,iconSize是图标大小[宽,高]
- var move = iconSize[0] / 2,
- moveSubBg = [25, 53.5], moveTitle = [25, 50];
- var g = d3.select(this),
- eventX = d3.event.x - move,
- eventY = d3.event.y - move;
- // 设定图标位置
- g.select('.image')
- .attr('x', eventX)
- .attr('y', eventY);
- })
- // 拖拽结束
- .on('end', function () {
- var g = d3.select(this);
- g.select('.subBg')
- .attr('transform', function (d, i) {
- // 对子标签的处理,自动符合字符串长度
- var x = parseFloat(d3.select(this).attr('x')) + parseFloat(d3.select(this).attr('width')) / 2,
- // y没被缩放,所以不用处理
- y = d3.select(this).attr('y'),
- dsl = (d.title.subTitle.text + '').length;
- var scaleX = dsl * 5.5;
- return 'translate(' + x + ' ' + y + ') scale(' + scaleX + ', 1) translate(' + -x + ' ' + -y + ')';
- });
- });
- // 图标组增加拖动事件
- imageGs.call(draging);
以上拖动事件,只是调用基本方法。
实时画线功能需要提前定义临时存储对象,用来存储鼠标移动时线段的终点坐标。
- // 鼠标移动时,实时画线到鼠标当前位置,_bodyRect为主区域
- _bodyRect.on('mousemove', function(){
- // 如果不处于实时画线状态
- if(!_chartData.drawing){
- return;
- }
- // 如果没有端点名称
- if (!_chartData.linePrePare.name) {
- return;
- }
- /* 实时画线 */
- // 判断线段倾斜方向,linePrePare为线段临时存储
- var preLines = linePrePare.lines;
- var mousePos = d3.mouse(_bodyRect.node()),
- beforePos = preLines[preLines.length - 1], newy,
- newPos = [];
- if((mousePos[0]>beforePos[0] && mousePos[1]>beforePos[1]) || (mousePos[0]<beforePos[0] && mousePos[1]<beforePos[1])){
- // 向左倾斜\ 左上到右下:y = cy + 0.7*(x-cx)
- newy = beforePos[1] + 0.7 * (mousePos[0] - beforePos[0]);
- } else {
- // 向右倾斜/ 左下到右上:y = cy - 0.7*(cx-x)
- newy = beforePos[1] - 0.7 * (mousePos[0] - beforePos[0]);
- }
- newPos = [mousePos[0], newy];
- // 移除旧线
- if(_chartData.tempLine.line){
- _chartData.tempLine.pos = [];
- _chartData.tempLine.line.remove();
- }
- // 画新线,tempLine为实时画线的临时存储
- _chartData.tempLine.line = _chartData.lineRootG.append('path')
- .attr('class', 'line-path')
- .attr('stroke', chartData.line.color)
- .attr('stroke-width', chartData.line.width)
- .attr('fill', 'none')
- .attr('d', function () {
- var newLine = [
- preLines[preLines.length - 1],
- newPos
- ];
- _chartData.tempLine.pos = newPos;
- return line(newLine);
- });
- // 当鼠标移入某个建筑图标范围时
- _chartData.imageGs.on('mouseenter', function(d, i){
- // 移除旧线
- if(_chartData.tempLine.line){
- _chartData.tempLine.pos = [];
- _chartData.tempLine.line.remove();
- }
- // 得到图标中心点坐标
- var posX = parseFloat(d3.select(this).select('.image').attr('x')) + _chartConf.baseSize[0] / 2;
- var posY = parseFloat(d3.select(this).select('.image').attr('y')) + _chartConf.baseSize[1] / 2;
- // 将此建筑图标的中心点坐标作为终点坐标画线
- _chartData.tempLine.line = _chartData.lineRootG.append('path')
- .attr('class', 'line-path')
- .attr('stroke', chartData.line.color)
- .attr('stroke-width', chartData.line.width)
- .attr('fill', 'none')
- .attr('d', function () {
- var newLine = [
- preLines[preLines.length - 1],
- [posX,posY]
- ];
- _chartData.tempLine.pos = [posX,posY];
- return line(newLine);
- });
- });
- // 当鼠标移出图标区域
- _chartData.imageGs.on('mouseleave', function(d, i){
- // 移除旧线
- if(_chartData.tempLine.line){
- _chartData.tempLine.pos = [];
- _chartData.tempLine.line.remove();
- }
- });
- // 对图标单击鼠标,保存线
- _chartData.imageGs.on('click', function (d, i) {
- // 保存临时线
- drawLine();
- // 停止实时画线
- exitDrawing();
- });
- });
- // 点击鼠标右键,停止实时画线
- _bodyRect.on('contextmenu', function(){
- // 停止实时画线
- exitDrawing();
- d3.event.preventDefault();
- });
- });
- }
在此只贴出部分代码,如果大家有任何建议和问题,还请留言,谢谢。
原文地址:https://www.cnblogs.com/eagle1098/p/11431679.html
d3.js制作连线动画图和编辑器的更多相关文章
- D3.js 制作中国地图 .net 公共基础类
D3.js 制作中国地图 from: http://d3.decembercafe.org/pages/map/index.html GeoJSON is a format for encoding ...
- d3.js制作蜂巢图表带动画效果
以上是效果图,本图表使用d3.js v4制作.图表主要功能是在六边形格子中显示数据,点击底部图标可以切换指定格子高亮显示,图表可以随浏览器任意缩放. 1.图表的主体结构是由正六边形组成,使用d3生成六 ...
- d3.js 教程 模仿echarts折线图
今天我们来仿echarts折线图,这个图在echarts是折线图堆叠,但是我用d3改造成了普通的折线图,只为了大家学习(其实在简单的写一个布局就可以).废话不多说商行代码. 1 制作 Line 类 c ...
- d3.js制作条形时间范围选择器
此文章为原创文章,原文地址:https://www.cnblogs.com/eagle1098/p/12146688.html 效果如上图所示. 本项目使用主要d3.js v4制作,可以用来选择两年的 ...
- 【D3.js】Focus + Context 折线图
利用D3.js库实现Focus+Context的折线图.读取data.tsv文件数据 index.html <!DOCTYPE html> <meta charset="u ...
- d3.js 制作简单的俄罗斯方块
d3.js是一个不错的可视化框架,同时对于操作dom也是十分方便的.今天我们使用d3.js配合es6的类来制作一个童年小游戏--俄罗斯方块.话不多说先上图片. 1. js tetris类 由于方法拆分 ...
- d3.js 制作简单的贪吃蛇
d3.js是一个不错的可视化框架,同时对于操作dom也是十分方便的.今天我们使用d3.js配合es6的类来制作一个童年小游戏–贪吃蛇.话不多说先上图片. 1. js snaker类 class Sna ...
- D3.js系列——布局:打包图和地图
一.打包图 打包图( Pack ),用于表示包含与被包含的关系,也可表示各对象的权重,通常用一圆套一圆来表示前者,用圆的大小来表示后者. 1.布局(数据转换) var pack = d3.layout ...
- D3.js系列——布局:弦图和集群图/树状图
一.弦图 1.弦图是什么 弦图(Chord),主要用于表示两个节点之间的联系的图表.两点之间的连线,表示谁和谁具有联系. 2.数据 初始数据为: var city_name = [ "北京& ...
随机推荐
- PTA 打印沙漏
https://pintia.cn/problem-sets/17/problems/260 #include <bits/stdc++.h> using namespace std; i ...
- [leetcode] 87. Scramble String (Hard)
题意: 判断两个字符串是否互为Scramble字符串,而互为Scramble字符串的定义: 字符串看作是父节点,从字符串某一处切开,生成的两个子串分别是父串的左右子树,再对切开生成的两个子串继续切开, ...
- Windows系统配置java环境
1:下载jdk 网址:http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html 2:下载 ...
- FTP文件传输服务器原理
FTP服务器,全称File Transfer Protocol Server,是在互联网上提供文件存储和访问服务的计算机,它们依照FTP协议提供服务.FTP,文件传输协议(File Transfer ...
- django第三次(转自刘江)
所有的模型字段都可以接收一定数量的参数,比如CharField至少需要一个max_length参数.下面的这些参数是所有字段都可以使用的,并且是可选的. null 该值为True时,Django在数据 ...
- PageHelper分页实战(SSM整合)
步骤一:引入SSM相关的jar包,包列表如下: 步骤二:创建或修改配置文件,配置文件清单如下: applicationContext.xml <?xml version="1.0&qu ...
- isMemberOfClass、isKindOfClass原理分析
isMemberOfClass - 调用者必须是传入的类的实例对象才返回YES- 判断调用者是否是传入对象的实例,别弄反了,如 [s1 isMemberOfClass:p1] ,意思是s1是否是p1的 ...
- How to check all timestamps of a file
A friend of mine she asked me how to check all timestamps of a file on an NTFS volume. She did not h ...
- MemCached的工具类。获取cached中的所有key
package com.ibs.auth.controller; import java.io.UnsupportedEncodingException; import java.util.Date; ...
- Redis优化建议
优化的一些建议 1.尽量使用短的key 当然在精简的同时,不要完了key的"见名知意".对于value有些也可精简,比如性别使用0.1. 2.避免使用keys * keys *, ...