G6(五)图形交互 Behavior

335 阅读6分钟

G6 封装了一系列交互方法,方便直接使用。本文将为 G6图形 增加简单的交互:hover 节点、点击节点、点击边、放缩画布、拖拽画布。本节目标效果如下:

G6.gif

先知概念

交互行为 Behavior

G6 中的交互行为。G6 内置了一系列交互行为,可以直接使用。简单地理解,就是可以一键开启这些交互行为:

  • drag-canvas:拖拽画布;
  • zoom-canvas:缩放画布。

交互管理 Mode

Mode 是 G6 交互行为的管理机制,一个 mode 是多种行为 Behavior 的组合,允许通过切换不同的模式进行交互行为的管理。

交互状态 State

状态 State 是 G6 中的状态机制。用户可以为图中的元素(节点/边)设置不同的状态及不同状态下的样式。在状态发生变化时,G6 自动更新元素的样式。

例如,可以为节点设置状态 'click' 为 true 或 false,并为节点设置 'click' 的样式为加粗节点边框。当 'click' 状态被切换为 true 时,节点的边框将会被加粗,'click' 状态被切换为 false 时,节点的样式恢复到默认。

交互实现

拖拽、缩放——内置的交互行为

在 G6 中使用内置 Behavior 的方式非常简单,只需要在图实例化时配置 modes。拖拽和缩放属于 G6 内置交互行为,代码如下:

const graph = new G6.Graph({
    // ...                                          // 其他配置项
    modes: {
        default: ['drag-canvas', 'zoom-canvas', 'drag-node'], // 允许拖拽画布、放缩画布、拖拽节点
    },
});

除了直接使用内置交互名称外,也可以为 Behavior 配置参数,例如放缩画布的敏感度、最大/最小放缩程度等。

上面代码中的 modes 定义了 G6 的模式,default 是默认的模式,还可以允许有其他的模式,比如:编辑模式 edit 等。不同的模式,用户能进行的行为可以不同,比如默认模式能拖拽画布,编辑模式不允许拖拽画布。

Hover、Click 改变样式——状态式交互

有时我们希望通过交互可以将元素样式变成特定样式,如鼠标 hover 节点、点击节点、点击边时,样式发生了变化。这里涉及到了 G6 中 状态 State 的概念。简单地说,是否 hover 、click 、任何操作(可以是自己起的状态名),都可以称为一种状态(state)。

我们可以自由设置不同状态下的元素样式。要达到交互更改元素样式,需要两步:

  • Step 1: 设置各状态下的元素样式;
  • Step 2: 监听事件并切换元素状态。
设置各状态下的元素样式

在实例化图时,通过 nodeStateStyles 和 edgeStateStyles 两个配置项可以配置元素在不同状态下的样式。 为达到目的效果:

  • 鼠标 hover 节点时,该节点颜色变浅;
  • 点击节点时,该节点边框加粗变黑;
  • 点击边时,该边变成蓝色。

下面代码设置了节点分别在 hover 和 click 状态为 true 时的样式,边在 click 状态为 true 时的样式:

const graph = new G6.Graph({
  // ...                           // 其他配置项
  // 节点不同状态下的样式集合
  nodeStateStyles: {
    // 鼠标 hover 上节点,即 hover 状态为 true 时的样式
    hover: {
      fill: 'lightsteelblue',
    },
    // 鼠标点击节点,即 click 状态为 true 时的样式
    click: {
      stroke: '#000',
      lineWidth: 3,
    },
  },
  // 边不同状态下的样式集合
  edgeStateStyles: {
    // 鼠标点击边,即 click 状态为 true 时的样式
    click: {
      stroke: 'steelblue',
    },
  },
});
监听事件并切换元素状态

G6 中所有元素监听都挂载在图实例上,如下代码中的 graph 对象是 G6.Graph 的实例,graph.on() 函数监听了某元素类型(node / edge)的某种事件(click / mouseenter / mouseleave / ... 所有事件参见:Event API)。

// 在图实例 graph 上监听
graph.on('元素类型:事件名', (e) => {
  // do something
});

现在,我们通过下面代码,为 G6图形 增加点和边上的监听事件,并在监听函数里使用 graph.setItemState() 改变元素的状态:

效果代码

import { useRef, useEffect } from "react";
import G6 from "@antv/g6";

const Behavior = () => {
    const containerRef = useRef(null);
    const graphRef = useRef();
    useEffect(() => {
        if (graphRef.current || !containerRef.current) return;
        const graph = new G6.Graph({
            container: containerRef.current, // String | HTMLElement,必须,在 Step 1 中创建的容器 id 或容器本身
            width: 800, // Number,必须,图的宽度
            height: 600, // Number,必须,图的高度
            // 布局配置项
            layout: {
                // Object,可选,布局的方法及其配置项,默认为 random 布局。
                type: "force", // 指定为力导向布局
                preventOverlap: true, // 防止节点重叠
                // nodeSize: 40        // 节点大小,用于算法中防止节点重叠时的碰撞检测。由于已经在上一节的元素配置中设置了每个节点的 size 属性,则不需要在此设置 nodeSize。
                linkDistance: 150, // 指定边距离为150
            },
            // 设置画布的交互模式
            modes: {
                default: ["drag-canvas", "zoom-canvas", "drag-node"], // 允许拖拽画布、放缩画布、拖拽节点
            },
            // 节点在默认状态下的样式配置(style)和其他配置
            defaultNode: {
                size: 40, //节点的大小
                // 节点样式配置
                style: {
                    fill: "steelblue", // 节点填充色
                    stroke: "#666", // 节点描边色
                    lineWidth: 1, // 节点描边粗细
                },
                // 节点上的标签文本配置
                labelCfg: {
                    // 节点上的标签文本样式配置
                    style: {
                        fill: "#fff", // 节点标签文字颜色
                    },
                },
            },
            // 边在默认状态下的样式配置(style)和其他配置
            defaultEdge: {
                // 边上的标签文本配置
                labelCfg: {
                    autoRotate: true, // 边上的标签文本根据边的方向旋转
                },
            },
            // 节点不同状态下的样式集合
            nodeStateStyles: {
                // 鼠标 hover 上节点,即 hover 状态为 true 时的样式
                hover: {
                    fill: "lightsteelblue",
                },
                // 鼠标点击节点,即 click 状态为 true 时的样式
                click: {
                    stroke: "#000",
                    lineWidth: 3,
                },
            },
            // 边不同状态下的样式集合
            edgeStateStyles: {
                // 鼠标点击边,即 click 状态为 true 时的样式
                click: {
                    stroke: "steelblue",
                },
            },
        });

        onLoadData((data) => {
            graph.data(data); // 绑定数据
            graph.render(); // 渲染图
            graphRef.current = graph;

            // 鼠标进入节点
            graph.on("node:mouseenter", (e) => {
                const nodeItem = e.item; // 获取鼠标进入的节点元素对象
                graph.setItemState(nodeItem, "hover", true); // 设置当前节点的 hover 状态为 true
            });
            // 鼠标离开节点
            graph.on("node:mouseleave", (e) => {
                const nodeItem = e.item; // 获取鼠标离开的节点元素对象
                graph.setItemState(nodeItem, "hover", false); // 设置当前节点的 hover 状态为 false
            });

            // 点击节点
            graph.on("node:click", (e) => {
                // 先将所有当前是 click 状态的节点置为非 click 状态
                const clickNodes = graph.findAllByState("node", "click");
                clickNodes.forEach((cn) => {
                    graph.setItemState(cn, "click", false);
                });
                const nodeItem = e.item; // 获取被点击的节点元素对象
                graph.setItemState(nodeItem, "click", true); // 设置当前节点的 click 状态为 true
            });

            // 点击边
            graph.on("edge:click", (e) => {
                // 先将所有当前是 click 状态的边置为非 click 状态
                const clickEdges = graph.findAllByState("edge", "click");
                clickEdges.forEach((ce) => {
                    graph.setItemState(ce, "click", false);
                });
                const edgeItem = e.item; // 获取被点击的边元素对象
                graph.setItemState(edgeItem, "click", true); // 设置当前边的 click 状态为 true
            });
        });
    }, []);

    const onLoadData = async (callback) => {
        const response = await fetch(
            "https://gw.alipayobjects.com/os/basement_prod/6cae02ab-4c29-44b2-b1fd-4005688febcb.json"
        );
        const remoteData = await response.json();
        const nodes = remoteData.nodes;
        nodes.forEach((node) => {
            if (!node.style) {
                node.style = {};
            }
            // 根据节点数据中的 class 属性配置图形
            switch (node.class) {
                case "c0": {
                    node.type = "circle"; // class = 'c0' 时节点图形为 circle
                    break;
                }
                case "c1": {
                    node.type = "rect"; // class = 'c1' 时节点图形为 rect
                    node.size = [35, 20]; // class = 'c1' 时节点大小
                    break;
                }
                case "c2": {
                    node.type = "ellipse"; // class = 'c2' 时节点图形为 ellipse
                    node.size = [35, 20]; // class = 'c2' 时节点大小
                    break;
                }
            }
        });
        const edges = remoteData.edges;
        edges.forEach((edge) => {
            if (!edge.style) {
                edge.style = {};
            }
            edge.style.lineWidth = edge.weight; // 边的粗细映射边数据中的 weight 属性数值
            // 移到此处
            edge.style.opacity = 0.6;
            edge.style.stroke = "grey";
        });
        callback(remoteData);
    };

    return (
        <div
            ref={containerRef}
            style={{ border: "2px solid #000", display: "inline-block" }}
        ></div>
    );
};

export default Behavior;