利用d3.js制作連線動(dòng)畫圖與編輯器的方法實(shí)例

連線動(dòng)畫圖

編輯器
效果如上圖所示。
本項(xiàng)目使用主要d3.jsv4制作,分兩部分,一個(gè)是實(shí)際展示的連線動(dòng)畫圖,另一個(gè)是管理人員使用鼠標(biāo)編輯連線的頁(yè)面。對(duì)于d3.js如何引入圖片,如何畫線等基礎(chǔ)功能,這里就不再介紹了,大家可以找一些入門文章看一下。這里主要介紹一下重點(diǎn)問(wèn)題。
1.連線動(dòng)畫圖
此圖的主要功能是每隔給定時(shí)間,通過(guò)ajax請(qǐng)求后臺(tái)數(shù)據(jù),并根據(jù)返回的數(shù)據(jù)動(dòng)態(tài)改變每個(gè)圖片下方的數(shù)值,動(dòng)態(tài)改變連線上的動(dòng)畫流動(dòng)方向和是否流動(dòng)。
首先,確定圖表中需要配置的內(nèi)容,如各圖片存儲(chǔ)位置,連線和動(dòng)畫顏色,圖片和連線的坐標(biāo)等。這些數(shù)據(jù)需要在html中進(jìn)行配置,最好寫成object對(duì)象,賦值給我們自己的圖表類的函數(shù)。比如:
var data = {
element:[{
image: 'img/work.png',
pos:[1,1], // 圖片位置
linePoint:[], // 圖片發(fā)出線段坐標(biāo)數(shù)組
lineDir:0, // 線段動(dòng)畫方向
title: '工作'
}],
lineColor:'black', // 連線顏色
animateColor: 'red', // 動(dòng)畫顏色
};
var chart = new Myd3chart('#chart');
chart.lineChart(data);
其中圖片發(fā)出的線段坐標(biāo)數(shù)組,使用外部文件提供,此文件由之后介紹的編輯器生成。
在設(shè)計(jì)我們自己的圖表函數(shù)時(shí),最好把每個(gè)功能劃分成獨(dú)立的函數(shù),這樣方便以后的維護(hù)和擴(kuò)展。
動(dòng)畫線段采用css的方式,有動(dòng)畫的線段添加此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; /* 如果反向移動(dòng)改為-500 */
}
}
這個(gè)圖表的難點(diǎn)在于動(dòng)態(tài)改變連線上的流動(dòng)動(dòng)畫,因?yàn)锳線段的終點(diǎn)會(huì)連接到B線段上,如果B線段動(dòng)畫停止,則A線段上的動(dòng)畫仍然要從B上經(jīng)過(guò),而不能簡(jiǎn)單停止B線段上的動(dòng)畫。而且如果B線段上的接入點(diǎn)不止一個(gè),還要判斷接入點(diǎn)之間的順序,只顯示最靠近B起始點(diǎn)的接入點(diǎn)的動(dòng)畫。另外還要判斷接入線段上是否有接入線段,層級(jí)關(guān)系里面如果有1個(gè)線段有動(dòng)畫,則此接入點(diǎn)就有動(dòng)畫流出。(這里說(shuō)起來(lái)有點(diǎn)繞)
我的方法是:
1)統(tǒng)計(jì)每個(gè)線段上的所有接入點(diǎn),這里就是圖片名稱,用于判斷此線段是否有動(dòng)畫流出。
2)接收后臺(tái)傳來(lái)的數(shù)據(jù)時(shí),判斷每個(gè)線段是否有動(dòng)畫,如果有動(dòng)畫,則直接恢復(fù)其動(dòng)畫線段的起始點(diǎn)坐標(biāo);如果沒(méi)有動(dòng)畫,則判斷最靠近起始點(diǎn)的接入點(diǎn)是否有動(dòng)畫,如果有動(dòng)畫則將動(dòng)畫線段的起始點(diǎn)改為此接入點(diǎn)坐標(biāo)。
// 統(tǒng)計(jì)接入點(diǎn)
function findAccessPoint() {
var accessPoints = [];
// 記錄每個(gè)線段上的接入點(diǎn),data為配置數(shù)據(jù)
data.eles.forEach(function(d, i){
if(d.line.length == 0){
return;
}
var acsp = {
name: d.title.text,
ap: [], // 接入點(diǎn),按順序排列,頭部離開(kāi)始點(diǎn)近
};
// 本線段上,每?jī)上噜彽狞c(diǎn)作為一個(gè)元素存入數(shù)組
var linePair = [];
// 本線段起始點(diǎn)
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);
}
});
// 對(duì)每?jī)上噜彽狞c(diǎn),查找接入點(diǎn)
linePair.forEach(function(dd, di){
chartData.eles.forEach(function(ddd, ddi){
// 排除自己,查找自己線段上的接入點(diǎn)
if(i != ddi && ddd.line.length > 1){
// 得到此線段終點(diǎn)
var pos = ddd.line[ddd.line.length - 1];
// dd.start開(kāi)始點(diǎn),dd.end結(jié)束點(diǎn)
// 用x坐標(biāo)計(jì)算在本線段上的y坐標(biāo),再和實(shí)際的y坐標(biāo)比較
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以內(nèi),并且此線終點(diǎn)在當(dāng)前線起點(diǎn)和終點(diǎn)之間
// 認(rèn)為此點(diǎn)為接入點(diǎn)
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, // 距離起始點(diǎn)的距離
allNames: [], // 所有通過(guò)此接入點(diǎn)的站點(diǎn)名稱
}
acsp.ap.push(ap);
}
}
});
})
accessPoints.push(acsp);
});
//對(duì)所有的接入點(diǎn),按與起始點(diǎn)的距離排序,并查找此接入點(diǎn)的上層站點(diǎn)
accessPoints.forEach(function(d, i){
// 按distance由小到大排序
d.ap.sort(function(a, b){
return a.distance - b.distance;
});
// 查找每個(gè)接入點(diǎn)的上層站點(diǎn)
d.ap.forEach(function(dd, di){
findPoint(dd.name, dd.allNames);
});
});
// name是接入點(diǎn)名稱,arr是該接入點(diǎn)的allNames
function findPoint(name, arr){
accessPoints.forEach(function(d, i){
// 在數(shù)組中找到指定名稱的項(xiàng)
if(d.name === name){
if(d.ap.length>0){
// 把該項(xiàng)下面的ap中的名稱加入給定arr
d.ap.forEach(function(dd, di){
arr.push(dd.name);
// 如果該點(diǎn)內(nèi)的allNames已經(jīng)有值則直接加入
if(dd.allNames.length>0){
dd.allNames.forEach(function(d, i){
arr.push(d);
});
} else{
// 遞歸查找子接入點(diǎn)
findPoint(dd.name, arr);
}
});
} else {
return;
}
}else{
return;
}
});
}
}
以上函數(shù)的運(yùn)行結(jié)果會(huì)產(chǎn)生一個(gè)對(duì)象,存儲(chǔ)每個(gè)接入線段上‘掛載'的接入點(diǎn),目的就是改變動(dòng)畫時(shí)方便判斷。
// 更新線條動(dòng)畫
aniLine.each(function(d, i){
var curLine = d3.select(this);
// 找到對(duì)應(yīng)的動(dòng)畫line
if (dd.name === curLine.attr('tag')) {
// 處理動(dòng)畫是否運(yùn)行
if (dd.ani) {
// 此線條動(dòng)畫運(yùn)行
curLine.style('animation-play-state', 'running');
curLine.style('display', 'inline');
// 如果動(dòng)畫運(yùn)行,則恢復(fù)原始動(dòng)畫路徑
curLine.attr('d', function(d){
return line(chartData.eles[i].line);
});
} else {
// 此線條動(dòng)畫停止
// 先查找離本線段開(kāi)始點(diǎn)最近的接入點(diǎn)
var acp = accessPoints;
// 從accessPoints中找到本節(jié)點(diǎn)的接入點(diǎn)集合
var ap = [];
acp.forEach(function(acd, aci){
if(acd.name === dd.name){
ap = acd.ap;
}
});
// 最近有動(dòng)畫接入點(diǎn)序號(hào)
var acIndex = -1;
// 找到最近的有動(dòng)畫接入點(diǎn),遠(yuǎn)近按數(shù)組序號(hào)遞增
for(var j=0;j<ap.length;j++){
// 復(fù)制所有子接入點(diǎn)數(shù)組
var allNames = ap[j].allNames.concat();
// 將接入點(diǎn)名稱也加入
allNames.push(ap[j].name);
// 判斷此接入點(diǎn)樹(shù)中是否有動(dòng)畫,如果1個(gè)有就可以
allNames.forEach(function(name,ani){
data.forEach(function(datad, datai){
if(datad.name === name){
if(datad.ani){
acIndex = j;
return;
}
}
});
});
if(acIndex != -1) {
break;
}
}
// 如果存在有動(dòng)畫接入點(diǎn)
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();
// 接入節(jié)點(diǎn)與開(kāi)始點(diǎn)的距離
var disAp = Math.pow((accp[0] - curLine[0][0]),2) +
Math.pow((accp[1] - curLine[0][1]),2);
// 如果當(dāng)前線段中有離開(kāi)始節(jié)點(diǎn)比接入點(diǎn)近的節(jié)點(diǎn)
// 則刪除此節(jié)點(diǎn)
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){
// 刪除此點(diǎn)
curLine.splice(curli,1);
}
}
});
// 從此接入點(diǎn)處開(kāi)始動(dòng)畫
curLine.splice(0,1,accp);
// debugger;
return line(curLine);
});
}else{
// 此線條動(dòng)畫停止
curLine.style('animation-play-state', 'paused');
curLine.style('display', 'none');
}
}
}
2.編輯器
由于本圖表需要配置大量坐標(biāo),如果手動(dòng)填寫的話效率十分低下,所以需要開(kāi)發(fā)一個(gè)編輯器用來(lái)修改圖表。
編輯器的主要使用方法為,使用鼠標(biāo)拖動(dòng)圖標(biāo),雙擊確定起始位置并開(kāi)始實(shí)時(shí)畫線狀態(tài),隨著鼠標(biāo)移動(dòng)動(dòng)態(tài)畫出線段,單擊確定臨時(shí)終點(diǎn),再單擊確定下一個(gè)終點(diǎn),右擊結(jié)束動(dòng)態(tài)畫線狀態(tài)。如果鼠標(biāo)單擊其他圖標(biāo),則終點(diǎn)為該圖標(biāo)的起始坐標(biāo)。本程序的實(shí)時(shí)畫線部分進(jìn)行了傾斜的約束,即左傾或右傾30度角。
編輯器比展示圖要簡(jiǎn)單一些,復(fù)雜部分在事件處理。
// 拖動(dòng)圖標(biāo)
var draging = d3.drag()
.on('drag', function () {
// 當(dāng)長(zhǎng)寬相同時(shí),iconSize是圖標(biāo)大小[寬,高]
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;
// 設(shè)定圖標(biāo)位置
g.select('.image')
.attr('x', eventX)
.attr('y', eventY);
})
// 拖拽結(jié)束
.on('end', function () {
var g = d3.select(this);
g.select('.subBg')
.attr('transform', function (d, i) {
// 對(duì)子標(biāo)簽的處理,自動(dòng)符合字符串長(zhǎng)度
var x = parseFloat(d3.select(this).attr('x')) + parseFloat(d3.select(this).attr('width')) / 2,
// y沒(méi)被縮放,所以不用處理
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 + ')';
});
});
// 圖標(biāo)組增加拖動(dòng)事件
imageGs.call(draging);
以上拖動(dòng)事件,只是調(diào)用基本方法。
實(shí)時(shí)畫線功能需要提前定義臨時(shí)存儲(chǔ)對(duì)象,用來(lái)存儲(chǔ)鼠標(biāo)移動(dòng)時(shí)線段的終點(diǎn)坐標(biāo)。
// 鼠標(biāo)移動(dòng)時(shí),實(shí)時(shí)畫線到鼠標(biāo)當(dāng)前位置,_bodyRect為主區(qū)域
_bodyRect.on('mousemove', function(){
// 如果不處于實(shí)時(shí)畫線狀態(tài)
if(!_chartData.drawing){
return;
}
// 如果沒(méi)有端點(diǎn)名稱
if (!_chartData.linePrePare.name) {
return;
}
/* 實(shí)時(shí)畫線 */
// 判斷線段傾斜方向,linePrePare為線段臨時(shí)存儲(chǔ)
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為實(shí)時(shí)畫線的臨時(shí)存儲(chǔ)
_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);
});
// 當(dāng)鼠標(biāo)移入某個(gè)建筑圖標(biāo)范圍時(shí)
_chartData.imageGs.on('mouseenter', function(d, i){
// 移除舊線
if(_chartData.tempLine.line){
_chartData.tempLine.pos = [];
_chartData.tempLine.line.remove();
}
// 得到圖標(biāo)中心點(diǎn)坐標(biāo)
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;
// 將此建筑圖標(biāo)的中心點(diǎn)坐標(biāo)作為終點(diǎn)坐標(biāo)畫線
_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);
});
});
// 當(dāng)鼠標(biāo)移出圖標(biāo)區(qū)域
_chartData.imageGs.on('mouseleave', function(d, i){
// 移除舊線
if(_chartData.tempLine.line){
_chartData.tempLine.pos = [];
_chartData.tempLine.line.remove();
}
});
// 對(duì)圖標(biāo)單擊鼠標(biāo),保存線
_chartData.imageGs.on('click', function (d, i) {
// 保存臨時(shí)線
drawLine();
// 停止實(shí)時(shí)畫線
exitDrawing();
});
});
// 點(diǎn)擊鼠標(biāo)右鍵,停止實(shí)時(shí)畫線
_bodyRect.on('contextmenu', function(){
// 停止實(shí)時(shí)畫線
exitDrawing();
d3.event.preventDefault();
});
});
}
在此只貼出部分代碼,如果大家有任何建議和問(wèn)題,還請(qǐng)留言,謝謝。
總結(jié)
以上就是這篇文章的全部?jī)?nèi)容了,希望本文的內(nèi)容對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,謝謝大家對(duì)腳本之家的支持。
- JS實(shí)現(xiàn)網(wǎng)頁(yè)煙花動(dòng)畫效果
- JavaScript canvas動(dòng)畫實(shí)現(xiàn)時(shí)鐘效果
- js通過(guò)循環(huán)多張圖片實(shí)現(xiàn)動(dòng)畫效果
- JS實(shí)現(xiàn)水平移動(dòng)與垂直移動(dòng)動(dòng)畫
- p5.js實(shí)現(xiàn)故宮橘貓賞秋圖動(dòng)畫
- jquery輕量級(jí)數(shù)字動(dòng)畫插件countUp.js使用詳解
- 利用d3.js實(shí)現(xiàn)蜂巢圖表帶動(dòng)畫效果
- JavaScript變速動(dòng)畫函數(shù)封裝添加任意多個(gè)屬性
- JS div勻速移動(dòng)動(dòng)畫與變速移動(dòng)動(dòng)畫代碼實(shí)例
- JavaScript實(shí)現(xiàn)沿五角星形線擺動(dòng)的小圓實(shí)例詳解
相關(guān)文章
微信小程序template模板引入的問(wèn)題小結(jié)
這篇文章主要介紹了微信小程序template模板引入的問(wèn)題小結(jié),本文通過(guò)示例代碼給大家介紹的非常詳細(xì),感興趣的朋友跟隨小編一起看看吧2024-07-07
javascript應(yīng)用:Iframe自適應(yīng)其加載的內(nèi)容高度
javascript應(yīng)用:Iframe自適應(yīng)其加載的內(nèi)容高度...2007-04-04
javascript中typeof操作符和constucor屬性檢測(cè)
這篇文章主要介紹了javascript中typeof操作符和constucor屬性檢測(cè)的相關(guān)資料,需要的朋友可以參考下2015-02-02
從jQuery.camelCase()學(xué)習(xí)string.replace() 函數(shù)學(xué)習(xí)
camelCase函數(shù)的功能就是將形如background-color轉(zhuǎn)化為駝峰表示法:backgroundColor。2011-09-09
微信小程序使用scroll-view標(biāo)簽實(shí)現(xiàn)自動(dòng)滑動(dòng)到底部功能的實(shí)例代碼
本文通過(guò)實(shí)例代碼給大家介紹了微信小程序使用scroll-view標(biāo)簽實(shí)現(xiàn)自動(dòng)滑動(dòng)到底部功能,代碼簡(jiǎn)單易懂,非常不錯(cuò),具有一定的參考借鑒價(jià)值,需要的朋友參考下吧2018-11-11
移動(dòng)端H5頁(yè)面返回并刷新頁(yè)面(BFcache)的方法
這篇文章主要給大家介紹了關(guān)于移動(dòng)端H5頁(yè)面返回并刷新頁(yè)面(BFcache)的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2018-11-11

