Изучаем D3js с нуля: построение силового графа d3 в React

React.js d3.js

Автор статьи: BP-Captain

источник:nuggets.capable/post/684490…

Что такое D3js?

  • Это библиотека визуализации данных, которая может создавать красивые и сложные диаграммы.
  • Это управляемая данными библиотека визуализации данных, которая привязывает данные к модели DOM, прежде чем их можно будет отобразить.
  • Это библиотека визуализации данных js, основанная на Html, CSS, svg/canvas.

Этот код реализует эффект:

  • линия со стрелкой
  • Нажмите на узел, чтобы изменить цвет узла и толщину его соединительной линии.
  • возможность масштабирования
  • Можно перетащить.

Версия: 4.Х

установить и импортировать

установка нпм: нпм установить д3

Интерфейсный импорт: импортировать * как d3 из 'd3';

1. Полный код

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获取后台返回的数据,并存入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力导向图内容
    };
    this.props.dispatch(chartReq({ param: param }, callback));
  }

  // func
  forceChart(nodes, edges) {
    this.refs['theChart'].innerHTML = '';

    // 函数内其余代码请看下文的**【拆解代码】**
    
    }

      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);

2. Разобрать код

1. Компоненты

<div className="theChart" id="theChart" ref="theChart">
</div>

Весь график будет нарисован внутри div.

2. Построить узлы и соединения

Узлы и соединения были созданы в [Полном коде], но это основано на фоновых данных, которые могут быть недостаточно интуитивными. Теперь я даю два набора данных, а затем строю данные для узлов и линий.

const nodeData = [
    { id: 1, name: '中国' },
    { id: 2, name: '北京' },
    { id: 3, name: '天津' },
    { id: 4, name: '上海' },
    { id: 5, name: '重庆' },
    { id: 6, name: '福建' },
    { id: 7, name: '广东' },
    { id: 8, name: '广西' },
    { id: 9, name: '浙江' },
    { id: 10, name: '江苏' },
    { id: 11, name: '河北' },
    { id: 12, name: '山西' },
    { id: 13, name: '吉林' },
    { id: 14, name: '辽宁' },
    { id: 15, name: '黑龙江' },
    { id: 16, name: '安徽' },
    { id: 17, name: '江西' },
    { id: 18, name: '山东' },
    { id: 19, name: '河南' },
    { id: 20, name: '湖南' },
    { id: 21, name: '湖北' },
    { id: 22, name: '海南' },
    { id: 23, name: '贵州' },
    { id: 24, name: '云南' },
    { id: 25, name: '新疆' },
    { id: 26, name: '西藏' },
    { id: 27, name: '台湾' },
    { id: 28, name: '澳门' },
    { id: 29, name: '香港' },
    { id: 30, name: '陕西' },
    { id: 31, name: '甘肃' },
    { id: 32, name: '青海' },
    { id: 33, name: '内蒙古' },
    { id: 34, name: '宁夏' },
    { id: 35, name: '四川' },
    
    { id: 36, name: '福州' },
    { id: 37, name: '厦门' },
    { id: 38, name: '漳州' },
    { id: 39, name: '莆田' },
    { id: 40, name: '南平' },
    { id: 41, name: '龙岩' },
    { id: 42, name: '三明' },
    { id: 43, name: '宁德' },
    { id: 44, name: '泉州' },
];
let nodes = [];
for (let i = 0; i < nodeData.length; i++) {
  nodes.push({
    id: (nodeData[i] && nodeData[i].id) || '', // 节点id
    name: (nodeData[i] && nodeData[i].name) || '', // 节点名称
  });
}
const relData = [
    { id: 1, source: 1, target: 2, tag: '省份' },
    { id: 2, source: 1, target: 3, tag: '省份' },
    { id: 3, source: 1, target: 4, tag: '省份' },
    { id: 4, source: 1, target: 5, tag: '省份' },
    { id: 5, source: 1, target: 6, tag: '省份' },
    { id: 6, source: 6, target: 36, tag: '地级市' },
    { id: 7, source: 6, target: 37, tag: '地级市' },
    { id: 8, source: 6, target: 38, tag: '地级市' },
    { id: 9, source: 6, target: 39, tag: '地级市' },
    { id: 10, source: 6, target: 40, tag: '地级市' },
    { id: 11, source: 6, target: 41, tag: '地级市' },
    { id: 12, source: 6, target: 42, tag: '地级市' },
    { id: 13, source: 6, target: 43, tag: '地级市' },
    { id: 14, source: 6, target: 44, tag: '地级市' },
    { id: 15, source: 1, target: 7, tag: '省份' },
    { id: 16, source: 1, target: 8, tag: '省份' },
    { id: 17, source: 1, target: 9, tag: '省份' },
    { id: 18, source: 1, target: 44, tag: '省份' },
    { id: 19, source: 1, target: 10, tag: '省份' },
    { id: 20, source: 1, target: 11, tag: '省份' },
    { id: 21, source: 1, target: 12, tag: '省份' },
    { id: 22, source: 1, target: 13, tag: '省份' },
    { id: 23, source: 1, target: 14, tag: '省份' },
    { id: 24, source: 1, target: 15, tag: '省份' },
    { id: 25, source: 1, target: 16, tag: '省份' },
    { id: 26, source: 1, target: 17, tag: '省份' },
    { id: 27, source: 1, target: 18, tag: '省份' },
    { id: 28, source: 1, target: 19, tag: '省份' },
    { id: 29, source: 1, target: 20, tag: '省份' },
    { id: 23, source: 1, target: 21, tag: '省份' },
    { id: 31 source: 1, target: 22, tag: '省份' },
    { id: 32, source: 1, target: 23, tag: '省份' },
    { id: 33, source: 1, target: 24, tag: '省份' },
    { id: 34, source: 1, target: 25, tag: '省份' },
    { id: 35, source: 1, target: 26, tag: '省份' },
    { id: 36, source: 1, target: 27, tag: '省份' },
    { id: 37, source: 1, target: 28, tag: '省份' },
    { id: 38, source: 1, target: 29, tag: '省份' },
    { id: 39, source: 1, target: 30, tag: '省份' },
    { id: 40, source: 1, target: 31, tag: '省份' },
    { id: 41, source: 1, target: 32, tag: '省份' },
    { id: 42, source: 1, target: 33, tag: '省份' },
    { id: 43, source: 1, target: 34, tag: '省份' },
];
let edges = [];
for (let i = 0; i < relData.length; i++) {
  edges.push({
    id: (relData[i] && (relData[i].id)) || '', // 连线id
    source: relData[i].source, // 开始节点
    target: relData[i].target, // 结束节点
    tag: (relData[i].tag) || '', // 连线名称
  });
}

В зависимости от вашего проекта дайте силовому графу то, что ему нужно.

3. Определите силовую модель

const simulation = d3.forceSimulation(nodes) // 指定被引用的nodes数组
    .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(), вы можете установить эти силы:

  • Center: Точка гравитации, задайте положение центра тяжести графика, направленного силой. После установки, как бы вы ни тянули, центр тяжести силы не изменится, если его не установить, то изменится центр тяжести силы, но будет исходное положение центра тяжести силы. в начале, что означает, что вы можете видеть только 1/4 изображения при первом входе на страницу. Это влияет на опыт.

  • Collision: сила столкновения узла, параметр .strength находится в диапазоне от [0, 1].

  • Ссылки: Сила соединения, .distance устанавливает расстояние между узлами на обоих концах соединения.

  • Many-Body: Когда параметр .strength положительный, он имитирует гравитацию, а когда он отрицательный, он имитирует силу заряда, параметр .distanceMax устанавливает максимальное расстояние.

  • Позиционирование: Дана сила в определенном направлении.

Отслеживайте изменения положения элемента карты усилия с помощью Simulation.on. (Пожалуйста, обратитесь к следующему [Мониторинг изменения положения элементов графика])

4. Нарисуйте SVG

const svg = d3.select('#theChart').append('svg') // 在id为‘theChart’的标签内创建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中创建g

Создайте svg, создайте g в svg и поместите содержимое, такое как соединение узла, в g.

  • select: выберите первый соответствующий элемент

  • selectAll: выбрать все соответствующие элементы

  • добавить: создать элемент

  • стиль: установить стиль

  • on('click', function()): щелчок устанавливает событие ответа на щелчок

  • call(zoom): функция масштабирования, подробности см. в разделе [Zoom] ниже.

5. Нарисуйте соединение

const edgesLine = svg.select('g')
    .selectAll('line')
    .data(edges) // 绑定数据
    .enter() // 为数据添加对应数量的占位符
    .append('path') // 在占位符上面生成折线(用path画)
    .attr('d', (d) => { return d && 'M ' + d.source.x + ' ' + d.source.y + ' L ' + d.target.x + ' ' + d.target.y; }) //遍历所有数据。d表示当前遍历到的数据,返回绘制的贝塞尔曲线
    .attr('id', (d, i) => { return i && 'edgepath' + i; }) // 设置id,用于连线文字
    .attr('marker-end', 'url(#arrow)') // 根据箭头标记的id号标记箭头
    .style('stroke', '#000') // 颜色
    .style('stroke-width', 1); // 粗细
  • data(), enter(), append(): эти три вместе связывают данные для создания графики.

  • атрибут: установить атрибут

  • стиль: установить стиль

  • Линия соединения рисуется кривой Безье: (M начальная точка X начальная точка y L конечная точка x конечная точка y)

Если вы хотите узнать больше о кривых Безье, перейдите по ссылке:Нажмите на меня, чтобы узнать больше о кривых Безье.

6. Имя соединения

const edgesText = svg.select('g').selectAll('.edgelabel')
    .data(edges)
    .enter()
    .append('text') // 为每一条连线创建文字区域
    .attr('class', 'edgelabel')
    .attr('dx', 80)
    .attr('dy', 0);
edgesText.append('textPath')
    .attr('xlink:href', (d, i) => { return i && '#edgepath' + i; }) // 文字布置在对应id的连线上
    .style('pointer-events', 'none') // 禁止鼠标事件
    .text((d) => { return d && d.tag; }); // 设置文字内容
  • attr() помещается после .append(), что означает установку атрибутов для элемента, созданного .append()
  • .style('pointer-events', 'none') отключает события мыши: нельзя выбрать, нельзя щелкнуть, и мышь не станет на ней вертикальной полосой.

7. Нарисуйте стрелку на линии соединения

const defs = g.append('defs'); // defs定义可重复使用的元素
const arrowheads = defs.append('marker') // 创建箭头
    .attr('id', 'arrow')
    // .attr('markerUnits', 'strokeWidth') // 设置为strokeWidth箭头会随着线的粗细进行缩放
    .attr('markerUnits', 'userSpaceOnUse') // 设置为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'); // 绘制方向,可设定为:auto(自动确认方向)和 角度值
arrowheads.append('path')
    .attr('d', 'M0,0 L0,10 L10,5 z') // d: 路径描述,贝塞尔曲线
    .attr('fill', '#000'); // 填充颜色
  • viewport: Видимая область
  • viewBox: Фактический размер будет автоматически масштабироваться до тех пор, пока не заполнит окно просмотра.

Я до сих пор не понимаю отношения между окном просмотра и окном просмотра, пожалуйста, перейдите к:Понимание видового экрана SVG, viewBox, saveAspectRatio.

8. Нарисуйте узлы

const nodesCircle = svg.select('g')
    .selectAll('circle')
    .data(nodes)
    .enter()
    .append('circle') // 创建圆
    .attr('r', 30) // 半径
    .style('fill', '#9FF') // 填充颜色
    .style('stroke', '#0CF') // 边框颜色
    .style('stroke-width', 2) // 边框粗细
    .on('click', (node) => { // 点击事件
        console.log('click');
    })
    .call(drag); // 拖拽单个节点带动整个图

Создайте круги как узлы.

.call() вызывает функцию перетаскивания.

9. Имя узла

const nodesTexts = svg.select('g')
    .selectAll('text')
    .data(nodes)
    .enter()
    .append('text')
    .attr('dy', '.3em') // 偏移量
    .attr('text-anchor', 'middle') // 节点名称放在圆圈中间位置
    .style('fill', 'black') // 颜色
    .style('pointer-events', 'none') // 禁止鼠标事件
    .text((d) => { // 文字内容
        return d && d.name; // 遍历nodes每一项,获取对应的name
    });

Значение отключения событий мыши здесь двоякое:

  • Наведение мыши на текст не превратится в вертикальную полосу, но останется стрелкой, что очень удобно.

  • Задаем событие клика для ноды, если не отключить событие мыши, то текст будет занимать место, а на верхнем слое ноды мы не сможем реагировать на событие ноды при клике по тексту.

10. При перемещении мыши к узлу появляется всплывающая подсказка.

nodesCircle.append('title')
    .text((node) => { // .text设置气泡提示内容
        return node.name; // 气泡提示为node的名称
    });

11. Элемент изменения позиции прослушивания на фиг.

simulation.on('tick', () => {
    // 更新节点坐标
    nodesCircle.attr('transform', (d) => {
        return d && 'translate(' + d.x + ',' + d.y + ')';
    });
    // 更新节点文字坐标
    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) // 设置衰减系数,对节点位置移动过程的模拟,数值越高移动越快,数值范围[0,1]
      .restart();  // 拖拽节点后,重新启动模拟
    }
    d.fx = d.x;    // d.x是当前位置,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的缩放系数和平移的坐标值。
}
function onZoomEnd() {
    // console.log('zoom end');
}
const zoom = d3.zoom()
    // .translateExtent([[0, 0], [WIDTH, HEIGHT]]) // 设置或获取平移区间, 默认为[[-∞, -∞], [+∞, +∞]]
    .scaleExtent([1 / 10, 10]) // 设置最大缩放比例
    .on('start', onZoomStart)
    .on('zoom', zooming)
    .on('end', onZoomEnd);

3. Другие эффекты

1. Сделайте линию соединения полужирной при нажатии на узел

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. Нажатый узел меняет цвет

nodesCircle.on('click, (node) => {
    nodesCircle.style('fill', (nodeOfSelected) => { // nodeOfSelected:所有节点, node: 选中的节点
    if (nodeOfSelected.id === node.id) { // 被点击的节点变色
        console.log('node')
            return '#36F';
        } else {
            return '#9FF';
        }
    });
})

Кстати, в реакции prevState — это значение предыдущего состояния. С состоянием и prevState можно сделать больше суждений и создать больше изменений.

В-четвертых, использование мер предосторожности при реагировании

componentDidMount() {
    this.print();
}
print() {
    let callback = (res) => { // callback获取后台返回的数据,并存入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力导向图内容
    };
    this.props.dispatch(getDataFromNeo4J({
        neo4jrun: 'match p=(()-[r]-()) return p limit 300',
    }, callback));
}

Где построены графики?

Поскольку график является динамическим, если он визуализируется несколько раз (рендеринг выполняется несколько раз и визуализируется несколько раз), он не будет охватывать ранее визуализированный граф, но вызовет несколько визуализаций и несколько графиков. Поместите функцию print() диаграммы построения в componentDidMount() и выполните ее, она будет отображена только один раз.

После добавления, удаления и изменения данных узла и соединения вам нужно снова вызвать функцию print(), чтобы восстановить график.

Откуда брать данные?

Данные не получаются из редукса, а обратный вызов получается сразу после отправки запроса.

Пять, галантерейные товары: URL-адрес поиска проекта d3

Если вы ищете экземпляр D3js, вы можете нажать здесь:Поиск проекта D3js.

Молодцы, давайте больше советов! ! !

Для перепечатки просьба указывать источник в соответствии с форматом:

Автор статьи: BP-Captain

источник:nuggets.capable/post/684490…