【React】基于Echarts实现关系图(图谱&graph)

效果

环境

  • echarts: ^5.5.0
  • lodash: ^4.17.21
  • next: 14.1.3
  • react: ^18

目录

仅包含涉及到的文件

复制代码
| - app
   |- page.tsx
| - components
    |- echarts
       |- graph
          |- echarts.ts
          |- index.tsx

实操

创建EchartsGraph组件

  • components/echarts/graph/index.tsx
typescript 复制代码
"use client"

import react, {useEffect, useRef} from "react";
import EchartsContainer from "./echarts"

type Props = {
    nodes: object[],
    edges: object[],
    categories: object[]
}

const ChartBoxStyles:react.CSSProperties = {
    width: '100%',
    height: '100%'
}

const EchartsGraph = function (props: Props) {
    let echartsRef = useRef<HTMLDivElement>(null);

    useEffect(()=>{
        const chart = EchartsContainer(echartsRef.current);
        chart.setOption({
            legend: [
                {
                    data: props.categories.map(function (a:any) {
                        return a.name;
                    })
                }
            ],
            series: [
                {
                    nodes: props.nodes,
                    edges: props.edges,
                    categories: props.categories,
                }
            ]
        });
        // 绑定鼠标松开触发事件,用于固定节点
        chart.on('mouseup', function (params: any) {
            let option = chart.getOption();
            option.series[0].nodes[params.dataIndex].x = params.event.offsetX || option.series[0].nodes[params.dataIndex].x;
            option.series[0].nodes[params.dataIndex].y = params.event.offsetY || option.series[0].nodes[params.dataIndex].y;
            option.series[0].nodes[params.dataIndex].fixed = true;
            chart.setOption(option);
        })

    }, [])

    return (
        <div ref={echartsRef} style={ChartBoxStyles}/>
    )
}

export default EchartsGraph
  • components/echarts/graph/echarts.ts
typescript 复制代码
import type { EChartsType } from "echarts"
import _ from "lodash"
import * as echarts from "echarts"

class EchartsContainer {
    protected _echarts: EChartsType|null = null;
    protected _option:any = {}

    constructor(dom?:HTMLElement) {
        if (!window || !dom)throw Error('Not Set HTMLElement')

        this._echarts = echarts.init(dom);
        this._echarts?.showLoading();
        this._initOption();
    }

    private _initOption(){
        this._option.tooltip = {};
        this._option.legend = [];
        this._option.series = [
            {
                type: 'graph',
                // 布局: none、circular、force
                layout: 'force',
                // 开启动画
                animation: true,
                // 初始动画的时长
                animationDuration: 1500,
                // 数据更新动画的缓动效果
                animationEasingUpdate: 'quinticInOut',
                // 边两端的标记类型
                // @link https://echarts.apache.org/zh/option.html#series-graph.edgeSymbol
                edgeSymbol: ['arrow', ''],
                // @link https://echarts.apache.org/zh/option.html#series-graph.force
                force: {
                    // 边的两个节点之间的距离,这个距离也会受 repulsion。
                    // 值最大的边长度会趋向于 10,值最小的边长度会趋向于 50
                    edgeLength: 120,
                    // 节点之间的斥力因子
                    repulsion: 500,
                    // 节点受到的向中心的引力因子(该值越大节点越往中心点靠拢)。
                    gravity: .2,
                    // 减缓节点的移动速度(取值范围 0 到 1)
                    friction: .6
                },
                // 是否开启鼠标缩放和平移漫游
                roam: true,
                // 节点是否可拖拽
                draggable: true,
                // 高亮状态的图形样式
                // @link https://echarts.apache.org/zh/option.html#series-graph.emphasis
                // DEPRECATED: itemStyle.emphasis has been changed to emphasis.itemStyle since 4.0
                emphasis:{
                    //鼠标放上去有阴影效果
                    itemStyle: {
                        shadowColor: '#cccccc',
                        shadowOffsetX: 0,
                        shadowOffsetY: 0,
                        shadowBlur: 40,
                    },
                },
                // 文本标签配置
                // @link https://echarts.apache.org/zh/option.html#series-graph.label
                label: {
                    show: true,
                    position: 'right',
                    formatter: '{b}'
                },
                // 标签的统一布局配置
                // @link https://echarts.apache.org/zh/option.html#series-graph.labelLayout
                // labelLayout: {
                //     hideOverlap: true,
                // },
                // 针对节点之间存在多边的情况,自动计算各边曲率,默认不开启。
                // @link https://echarts.apache.org/zh/option.html#series-graph.autoCurveness
                // autoCurveness: true,
                // 滚轮缩放的极限控制
                // @link https://echarts.apache.org/zh/option.html#series-graph.scaleLimit
                scaleLimit: {
                    min: 0.4,
                    max: 2
                },
                // 关系边的公用线条样式
                // @link https://echarts.apache.org/zh/option.html#series-graph.lineStyle
                lineStyle: {
                    color: 'source',
                    // 边的曲度,支持从 0 到 1 的值,值越大曲度越大。
                    curveness: .3
                },
                categories: [],
            }
        ];

        this.setOption(this._option);
        this._echarts?.hideLoading();
    }

    public setOption(option: any){
        if (Object.keys(option).length>0){
            this._echarts?.setOption(_.merge({}, this._option, option))
        }
    }

    public getOption(){
        return this._echarts?.getOption();
    }

    public on(event: string, handler: any){
        this._echarts?.on(event, handler)
    }
}

const getSingle = (className: any) => {
    if (typeof className !== 'function') {
        throw('参数必须为一个类或一个函数')
    }
    const single = (fn: Function) => {
        let instance:any = null
        return (...args:any[]) => {
            if (!instance) {
                instance = fn.call(null, ...args)
            }
            return instance;
        }
    }
    const fn = (...args: any[]) => {
        return new className(...args);
    };
    return single(fn);
}

export default getSingle(EchartsContainer)

页面调用

demo数据源于官方的一个演示接口数据:
https://echarts.apache.org/examples/data/asset/data/les-miserables.json

typescript 复制代码
import EchartsGraph from "@/components/echarts/graph";
import demoData from "./demo"

export default function Home() {

  return (
    <main style={{
        width: '100%',
        height: '100vh'
    }}>
      <EchartsGraph nodes={demoData.nodes}
                    edges={demoData.links}
                    categories={demoData.categories} />
    </main>
  );
}
相关推荐
浪浪山_大橙子3 分钟前
使用Electron+Vue3开发Qwen3 2B桌面应用:从想法到实现的完整指南
前端·人工智能
狗哥哥6 分钟前
聊聊设计模式在 Vue 3 业务开发中的落地——从一次代码重构说起
前端·架构
用户479492835691515 分钟前
CVE-2025-55182:React 史上最严重漏洞,CVSS 满分 10.0
安全·react.js·全栈
shenzhenNBA17 分钟前
如何在python文件中使用日志功能?简单版本
java·前端·python·日志·log
掘金泥石流19 分钟前
分享下我创业烧了 几十万的 AI Coding 经验
前端·javascript·后端
用户479492835691521 分钟前
JavaScript 为什么选择原型链?从第一性原理聊聊这个设计
前端·javascript
new code Boy26 分钟前
vscode左侧栏图标及目录恢复
前端·javascript
唐诗26 分钟前
Git提交信息太乱?AI一键美化!一行命令拯救你的项目历史🚀
前端·ai编程
涔溪42 分钟前
有哪些常见的Vite插件及其作用?
前端·vue.js·vite
糖墨夕42 分钟前
从一行代码看TypeScript的精准与陷阱:空值合并vs逻辑或
前端·typescript