詳解如何在react中搭建d3力導(dǎo)向圖
D3js力導(dǎo)向圖搭建
d3js是一個可以基于數(shù)據(jù)來操作文檔的JavaScript庫??梢允褂肏TML,CSS,SVG以及Canvas來展示數(shù)據(jù)。力導(dǎo)向圖能夠用來表示節(jié)點間多對多的關(guān)系。
實現(xiàn)效果:連線有箭頭,點擊節(jié)點能改變該節(jié)點顏色和所連接的線的粗細,縮放、拖拽。
版本:4.X
安裝和導(dǎo)入
npm安裝:npm install d3
前端導(dǎo)入:import * as d3 from 'd3';
一、完整代碼
import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import { push } from 'react-router-redux'; import * as d3 from 'd3'; import { Row, Form } from 'antd'; import { chartReq} from './actionCreator'; import './Chart.less'; const WIDTH = 1900; const HEIGHT = 580; const R = 30; let simulation; class Chart extends Component { constructor(props, context) { super(props, context); this.print = this.print.bind(this); this.forceChart = this.forceChart.bind(this); this.state = { }; } componentWillMount() { this.props.dispatch(push('/Chart')); } componentDidMount() { this.print(); } print() { let callback = (res) => { // callback獲取后臺返回的數(shù)據(jù),并存入state let nodeData = res.data.nodes; let relationData = res.data.rels; this.setState({ nodeData: res.data.nodes, relationData: res.data.rels, }); let nodes = []; for (let i = 0; i < nodeData.length; i++) { nodes.push({ id: (nodeData[i] && nodeData[i].id) || '', name: (nodeData[i] && nodeData[i].name) || '', type: (nodeData[i] && nodeData[i].type) || '', definition: (nodeData[i] && nodeData[i].definition) || '', }); } let edges = []; for (let i = 0; i < relationData.length; i++) { edges.push({ id: (relationData[i] && (relationData[i].id)) || '', source: (relationData[i] && relationData[i].start.id) || '', target: (relationData[i] && relationData[i].end.id) || '', tag: (relationData[i] && relationData[i].name) || '', }); } this.forceChart(nodes, edges); // d3力導(dǎo)向圖內(nèi)容 }; this.props.dispatch(chartReq({ param: param }, callback)); } // func forceChart(nodes, edges) { this.refs['theChart'].innerHTML = ''; // 函數(shù)內(nèi)其余代碼請看拆解代碼 } render() { return ( <Row style={{ minWidth: 900 }}> <div className="outerDiv"> <div className="theChart" id="theChart" ref="theChart"> </div> </div> </Row> ); } } Chart.propTypes = { dispatch: PropTypes.func.isRequired, }; function mapStateToProps(state) { return { }; } const WrappedChart = Form.create({})(Chart); export default connect(mapStateToProps)(WrappedChart);
二、拆解代碼
1.組件
<div className="theChart" id="theChart" ref="theChart"> </div>
整個圖都將在div里繪制。
2.構(gòu)造節(jié)點和連線
let nodes = []; // 節(jié)點 for (let i = 0; i < nodeData.length; i++) { nodes.push({ id: (nodeData[i] && nodeData[i].id) || '', name: (nodeData[i] && nodeData[i].name) || '', // 節(jié)點名稱 }); } let edges = []; // 連線 for (let i = 0; i < relationData.length; i++) { edges.push({ id: (relationData[i] && (relationData[i].id)) || '', source: (relationData[i] && relationData[i].start.id) || '', // 開始節(jié)點 target: (relationData[i] && relationData[i].end.id) || '', // 結(jié)束節(jié)點 tag: (relationData[i] && relationData[i].name) || '', // 連線名稱 }); }
具體怎么構(gòu)造依據(jù)你們的項目數(shù)據(jù)。
3.定義力模型
const simulation = d3.forceSimulation(nodes) // 指定被引用的nodes數(shù)組 .force('link', d3.forceLink(edges).id(d => d.id).distance(150)) .force('collision', d3.forceCollide(1).strength(0.1)) .force('center', d3.forceCenter(WIDTH / 2, HEIGHT / 2)) .force('charge', d3.forceManyBody().strength(-1000).distanceMax(800));
通過simulation.force()設(shè)置力,可以設(shè)置這幾種力:
- Centering:中心力,設(shè)置圖中心點位置。
- Collision:節(jié)點碰撞作用力,.strength參數(shù)范圍為[0,1]。
- Links:連線的作用力;.distance設(shè)置連線兩端節(jié)點的距離。
- Many-Body:.strength的參數(shù)為正時,模擬重力,為負時,模擬電荷力;.distanceMax的參數(shù)設(shè)置最大距離。
Positioning:給定向某個方向的力。
通過simulation.on監(jiān)聽力圖元素位置變化。
4.繪制svg
const svg = d3.select('#theChart').append('svg') // 在id為‘theChart'的標簽內(nèi)創(chuàng)建svg .style('width', WIDTH) .style('height', HEIGHT * 0.9) .on('click', () => { console.log('click', d3.event.target.tagName); }) .call(zoom); // 縮放 const g = svg.append('g'); // 則svg中創(chuàng)建g
創(chuàng)建svg,在svg里創(chuàng)建g,將節(jié)點連線等內(nèi)容放在g內(nèi)。
- select:選擇第一個對應(yīng)的元素
- selectAll:選擇所有對應(yīng)的元素
- append:創(chuàng)建元素
5.繪制連線
const edgesLine = svg.select('g') .selectAll('line') .data(edges) // 綁定數(shù)據(jù) .enter() // 添加數(shù)據(jù)到選擇集edgepath .append('path') // 生成折線 .attr('d', (d) => { return d && 'M ' + d.source.x + ' ' + d.source.y + ' L ' + d.target.x + ' ' + d.target.y; }) // 遍歷所有數(shù)據(jù),d表示當(dāng)前遍歷到的數(shù)據(jù),返回繪制的貝塞爾曲線 .attr('id', (d, i) => { return i && 'edgepath' + i; }) // 設(shè)置id,用于連線文字 .attr('marker-end', 'url(#arrow)') // 根據(jù)箭頭標記的id號標記箭頭 .style('stroke', '#000') // 顏色 .style('stroke-width', 1); // 粗細
連線用貝塞爾曲線繪制:(M 起點X 起點y L 終點x 終點y)
6.繪制連線上的箭頭
const defs = g.append('defs'); // defs定義可重復(fù)使用的元素 const arrowheads = defs.append('marker') // 創(chuàng)建箭頭 .attr('id', 'arrow') // .attr('markerUnits', 'strokeWidth') // 設(shè)置為strokeWidth箭頭會隨著線的粗細進行縮放 .attr('markerUnits', 'userSpaceOnUse') // 設(shè)置為userSpaceOnUse箭頭不受連接元素的影響 .attr('class', 'arrowhead') .attr('markerWidth', 20) // viewport .attr('markerHeight', 20) // viewport .attr('viewBox', '0 0 20 20') // viewBox .attr('refX', 9.3 + R) // 偏離圓心距離 .attr('refY', 5) // 偏離圓心距離 .attr('orient', 'auto'); // 繪制方向,可設(shè)定為:auto(自動確認方向)和 角度值 arrowheads.append('path') .attr('d', 'M0,0 L0,10 L10,5 z') // d: 路徑描述,貝塞爾曲線 .attr('fill', '#000'); // 填充顏色
- viewport:可視區(qū)域
- viewBox:實際大小,會自動縮放填充viewport
7.繪制節(jié)點
const nodesCircle = svg.select('g') .selectAll('circle') .data(nodes) .enter() .append('circle') // 創(chuàng)建圓 .attr('r', 30) // 半徑 .style('fill', '#9FF') // 填充顏色 .style('stroke', '#0CF') // 邊框顏色 .style('stroke-width', 2) // 邊框粗細 .on('click', (node) => { // 點擊事件 console.log('click'); }) .call(drag); // 拖拽單個節(jié)點帶動整個圖
創(chuàng)建圓作為節(jié)點。
.call()調(diào)用拖拽函數(shù)。
8.節(jié)點名稱
const nodesTexts = svg.select('g') .selectAll('text') .data(nodes) .enter() .append('text') .attr('dy', '.3em') // 偏移量 .attr('text-anchor', 'middle') // 節(jié)點名稱放在圓圈中間位置 .style('fill', 'black') // 顏色 .style('pointer-events', 'none') // 禁止鼠標事件 .text((d) => { // 文字內(nèi)容 return d && d.name; // 遍歷nodes每一項,獲取對應(yīng)的name });
因為文字在節(jié)點上層,如果沒有設(shè)置禁止鼠標事件,點擊文字將無法響應(yīng)點擊節(jié)點的效果,也無法拖拽節(jié)點。
9.連線名稱
const edgesText = svg.select('g').selectAll('.edgelabel') .data(edges) .enter() .append('text') // 為每一條連線創(chuàng)建文字區(qū)域 .attr('class', 'edgelabel') .attr('dx', 80) .attr('dy', 0); edgesText.append('textPath')// 設(shè)置文字內(nèi)容 .attr('xlink:href', (d, i) => { return i && '#edgepath' + i; }) // 文字布置在對應(yīng)id的連線上 .style('pointer-events', 'none') .text((d) => { return d && d.tag; });
10.鼠標移到節(jié)點上有氣泡提示
nodesCircle.append('title') .text((node) => { // .text設(shè)置氣泡提示內(nèi)容 return node.definition; });
11.監(jiān)聽圖元素的位置變化
simulation.on('tick', () => { // 更新節(jié)點坐標 nodesCircle.attr('transform', (d) => { return d && 'translate(' + d.x + ',' + d.y + ')'; }); // 更新節(jié)點文字坐標 nodesTexts.attr('transform', (d) => { return 'translate(' + (d.x) + ',' + d.y + ')'; }); // 更新連線位置 edgesLine.attr('d', (d) => { const path = 'M ' + d.source.x + ' ' + d.source.y + ' L ' + d.target.x + ' ' + d.target.y; return path; }); // 更新連線文字位置 edgesText.attr('transform', (d, i) => { return 'rotate(0)'; }); });
12.拖拽
function onDragStart(d) { // console.log('start'); // console.log(d3.event.active); if (!d3.event.active) { simulation.alphaTarget(1) // 設(shè)置衰減系數(shù),對節(jié)點位置移動過程的模擬,數(shù)值越高移動越快,數(shù)值范圍[0,1] .restart(); // 拖拽節(jié)點后,重新啟動模擬 } d.fx = d.x; // d.x是當(dāng)前位置,d.fx是靜止時位置 d.fy = d.y; } function dragging(d) { d.fx = d3.event.x; d.fy = d3.event.y; } function onDragEnd(d) { if (!d3.event.active) simulation.alphaTarget(0); d.fx = null; // 解除dragged中固定的坐標 d.fy = null; } const drag = d3.drag() .on('start', onDragStart) .on('drag', dragging) // 拖拽過程 .on('end', onDragEnd);
13.縮放
function onZoomStart(d) { // console.log('start zoom'); } function zooming(d) { // 縮放和拖拽整個g // console.log('zoom ing', d3.event.transform, d3.zoomTransform(this)); g.attr('transform', d3.event.transform); // 獲取g的縮放系數(shù)和平移的坐標值。 } function onZoomEnd() { // console.log('zoom end'); } const zoom = d3.zoom() // .translateExtent([[0, 0], [WIDTH, HEIGHT]]) // 設(shè)置或獲取平移區(qū)間, 默認為[[-∞, -∞], [+∞, +∞]] .scaleExtent([1 / 10, 10]) // 設(shè)置最大縮放比例 .on('start', onZoomStart) .on('zoom', zooming) .on('end', onZoomEnd);
三、其它效果
1.單擊節(jié)點時讓連接線加粗
nodesCircle.on('click, (node) => { edges_line.style("stroke-width",function(line){ if(line.source.name==node.name || line.target.name==node.name){ return 4; }else{ return 0.5; } }); })
2.被點擊的節(jié)點變色
nodesCircle.on('click, (node) => { nodesCircle.style('fill', (nodeOfSelected) => { // nodeOfSelected:所有節(jié)點, node: 選中的節(jié)點 if (nodeOfSelected.id === node.id) { // 被點擊的節(jié)點變色 console.log('node') return '#36F'; } else { return '#9FF'; } }); })
四、在react中使用注意事項
componentDidMount() { this.print(); } print() { let callback = (res) => { // callback獲取后臺返回的數(shù)據(jù),并存入state let nodeData = res.data.nodes; let relationData = res.data.rels; this.setState({ nodeData: res.data.nodes, relationData: res.data.rels, }); let nodes = []; for (let i = 0; i < nodeData.length; i++) { nodes.push({ id: (nodeData[i] && nodeData[i].id) || '', name: (nodeData[i] && nodeData[i].name) || '', type: (nodeData[i] && nodeData[i].type) || '', definition: (nodeData[i] && nodeData[i].definition) || '', }); } let edges = []; for (let i = 0; i < relationData.length; i++) { edges.push({ id: (relationData[i] && (relationData[i].id)) || '', source: (relationData[i] && relationData[i].start.id) || '', target: (relationData[i] && relationData[i].end.id) || '', tag: (relationData[i] && relationData[i].name) || '', }); } this.forceChart(nodes, edges); // d3力導(dǎo)向圖內(nèi)容 }; this.props.dispatch(getDataFromNeo4J({ neo4jrun: 'match p=(()-[r]-()) return p limit 300', }, callback)); }
在哪里構(gòu)造圖 因為圖是動態(tài)的,如果渲染多次(render執(zhí)行多次,渲染多次),不會覆蓋前面渲染的圖,反而會造成渲染多次,出現(xiàn)多個圖的現(xiàn)象。把構(gòu)造圖的函數(shù)print()放到componentDidMount()內(nèi)執(zhí)行,則只會渲染一次。
對節(jié)點和連線數(shù)據(jù)進行增刪改操作后,需要再次調(diào)用print()函數(shù),重新構(gòu)造圖。
從哪里獲取數(shù)據(jù) 數(shù)據(jù)不從redux獲取,發(fā)送請求后callback直接獲取。
五、干貨:d3項目查找網(wǎng)址
D3js所有項目檢索.http://blockbuilder.org/search/
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
React動畫實現(xiàn)方案Framer Motion讓頁面自己動起來
這篇文章主要為大家介紹了React動畫實現(xiàn)方案Framer Motion讓頁面自己動起來,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-10-10React?錯誤邊界Error?Boundary使用示例解析
這篇文章主要為大家介紹了React?錯誤邊界Error?Boundary使用示例解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-09-09React路由規(guī)則定義與聲明式導(dǎo)航及編程式導(dǎo)航分別介紹
這篇文章主要介紹了React路由規(guī)則的定義、聲明式導(dǎo)航、編程式導(dǎo)航,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)吧2022-09-09React語法中設(shè)置TS校驗規(guī)則的步驟詳解
這篇文章主要給大家介紹了React語法中如何設(shè)置TS校驗規(guī)則,文中有詳細的代碼示例供大家參考,對大家的學(xué)習(xí)或工作有一定的幫助,需要的朋友可以參考下2023-10-10react-native 完整實現(xiàn)登錄功能的示例代碼
本篇文章主要介紹了react-native 完整實現(xiàn)登錄功能的示例代碼,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-09-09