JavaScript is required

节点样式实现方式

这个示例基于一个静态 relation-graph 场景,提供紧凑的节点样式参考。它对比了按节点 JSON 覆盖样式、自定义节点插槽、CSS 驱动动画、固定定位、小地图,以及交互模式切换和图片导出等共享画布工具。

在同一张图中比较节点样式技术

这个示例构建了什么

这个示例构建了一个紧凑的 relation-graph 场景,主要目的是在同一个画布中比较多种节点呈现技术。查看者可以看到圆形和矩形节点、显式覆盖尺寸的节点、fit-content 节点、一个固定位置节点、一个以 logo 为背景的自定义节点,以及一个带 CSS 动画的节点。

这个图以只读样式参考的形式呈现,而不是业务工作流。用户可以查看预设的变体、打开浮动设置面板、切换滚轮和拖拽行为、拖动或最小化辅助窗口、将图导出为图片,并使用 mini view 导航。节点点击和连线点击都已接线,但在这个示例中它们只会记录被选中的对象。

数据是如何组织的

图数据在 initializeGraph() 内部以内联方式组装为一个 RGJsonData 对象。它设置了 rootId: 'a',定义了十一个节点,并在通过 setJsonData() 加载这份数据之前连接了十一条连线。

关键点在于,这份数据集本身已经携带了许多视觉差异。各个节点记录会覆盖 widthheightborderWidthborderColorcolorfontColornodeShapefixedxytypeclassName 等字段。因此,在任何 slot 逻辑运行之前,数据层就已经成为样式对比的直接载体。

在真实产品中,同样的模式可以编码业务含义,而不是示例标签。每个节点上的字段可以表示强调等级、类别颜色、头像或 logo 占位、固定参考节点、紧凑摘要卡片,或者需要动画效果的告警状态。

relation-graph 是如何使用的

这个示例使用内置的 center 布局,并为整张图设置了圆形节点、曲线连线、边框连接点以及默认 80 x 80 节点尺寸等全局默认值。这些默认值建立了统一的基线,而 JSON 数据只覆盖那些需要不同处理的节点。

RGHooks.useGraphInstance() 驱动图的生命周期。在挂载时,组件通过 setJsonData() 加载内联数据,然后通过 moveToCenter()zoomToFit() 规范化视口。同一个 hook 也被复用在共享设置面板中,使这个示例可以更新运行时选项,并为图片导出准备画布。

自定义被拆分在 slots、共享子组件和 CSS 之间。RGSlotOnNode 挂载了一个 NodeSlot 渲染器,它根据 node.idnode.type 切换,以生成特殊的节点主体。RGSlotOnViewRGMiniView 作为覆盖层加入。RGProvider 包裹了整个示例,从而让 hooks 可以访问当前激活的图实例。浮动的 DraggableWindow 提供说明外壳,并承载 CanvasSettingsPanel,后者会读取当前 store 状态,并通过 setOptions() 修改 wheelEventActiondragEventAction。动画节点变体来自导入的 simple-node-animations.scss 样式表,而图片背景节点和纯文本节点变体则使用节点 slot 内的内联样式。

关键交互

  • 图会在挂载时加载一次,并立即将预设数据集居中并适配到视口,因此无需手动设置就能看到完整的样式展示。
  • 点击节点或连线会触发处理器,但这些处理器只会记录被选中的对象。它们是诊断钩子,而不是功能入口。
  • 浮动辅助窗口可以通过其标题栏拖动,并可在展开和最小化状态之间切换。
  • 打开设置覆盖层后,会暴露图级别控件,用于控制滚轮行为(scrollzoomnone)以及画布拖拽行为(selectionmovenone)。
  • Download Image 操作会先准备图画布,将其捕获为 blob,触发下载,然后恢复图状态。
  • RGMiniView 为该示例提供了一个小地图覆盖层。由于场景中混合了不同尺寸的节点,并且包含一个带显式坐标的固定节点,这一点尤其有用。

关键代码片段

这个片段说明,基础图配置被刻意保持得很简单,因此节点级覆盖更容易比较。

const graphOptions: RGOptions = {
    layout: {
        layoutName: 'center'
    },
    defaultNodeShape: RGNodeShape.circle,
    defaultLineShape: RGLineShape.StandardCurve,
    defaultJunctionPoint: RGJunctionPoint.border,
    defaultNodeWidth: 80,
    defaultNodeHeight: 80,
};

这个片段说明,这个示例如何在图渲染前,直接在 JSON 载荷中编码视觉差异。

const myJsonData: RGJsonData = {
    rootId: 'a',
    nodes: [
        { id: 'a', text: 'Border color', width: 150, height: 150, borderColor: '#f43ce5', borderWidth: 3 },
        { id: 'a1-1', text: 'Plain Text Node', borderWidth: 0, width: 0, height: 0, nodeShape: RGNodeShape.rect },
        { id: 'e', text: 'Node width/height fit-content ', width: 0, height: 0, nodeShape: RGNodeShape.rect },
        { id: 'f1', text: 'Fixed', fixed: true, x: -660, y: -160 },
        { id: 'g', type: 'my-animation', text: 'Css Animation', width: 0, height: 0, className: 'my-node-flash-style' }
    ],
    // ...
};

这个片段说明了挂载时的图生命周期:加载预设数据,然后规范化视口。

const initializeGraph = async () => {
    // ...
    await graphInstance.setJsonData(myJsonData);
    graphInstance.moveToCenter();
    graphInstance.zoomToFit();
};

useEffect(() => {
    initializeGraph();
}, []);

这个片段说明,RGSlotOnNode 并不是用于一套统一皮肤。它通过分支在同一张图里渲染多种彼此无关的节点主体变体。

const NodeSlot: React.FC<RGNodeSlotProps> = ({ node }) => {
    if (node.id === 'a1-1') {
        return <div style={{ color: '#ff8c00', display: 'flex', alignItems: 'center', justifyContent: 'center', width: '100%', height: '100%' }}>{node.text}</div>;
    }
    if (node.id === 'a1-4') {
        return <div style={{ border: '#ff8c00 solid 6px', height: '100%', width: '100%', borderRadius: '40px', backgroundImage: `url(${rgLogoUrl})`, backgroundPosition: 'center center', backgroundColor: '#fff' }} />;
    }
    if (node.type === 'my-animation') {
        return <div className="my-node-animation-01 h-32 w-32 rounded-full relative text-lg flex place-items-center justify-center overflow-hidden">{/* ... */}</div>;
    }
    return <div style={{ padding: '5px 10px', height: '100%', width: '100%', display: 'flex', justifyContent: 'center', alignItems: 'center', color: node.fontColor || 'inherit' }}>{node.text}</div>;
};

这个片段说明,动画节点外观来自可复用的 CSS 类,而不是重复的内联样式声明。

.my-node-animation-01 {
    background: linear-gradient(
            to bottom,
            #00ffff,
            #ff00ff,
            #00ffff
    );
    background-size: 100% 200%;
    animation: gradient-flow 6s ease-in-out infinite;
}

这个片段说明,共享设置面板如何让这个示例变成一个带运行时交互切换和图片导出的查看器工具壳层。

<SettingRow
    label="Wheel Event:"
    options={[
        { label: 'Scroll', value: 'scroll' },
        { label: 'Zoom', value: 'zoom' },
        { label: 'None', value: 'none' },
    ]}
    value={wheelMode}
    onChange={(newValue: string) => { graphInstance.setOptions({ wheelEventAction: newValue }); }}
/>
<SettingRow
    label="Canvas Drag Event:"
    options={[
        { label: 'Selection', value: 'selection' },
        { label: 'Move', value: 'move' },
        { label: 'none', value: 'none' },
    ]}
    value={dragMode}
    onChange={(newValue: string) => { graphInstance.setOptions({ dragEventAction: newValue }); }}
/>
<SimpleUIButton onClick={downloadImage}>
    Download Image
</SimpleUIButton>

这个示例的独特之处

与附近的 node-style3node-style4 等示例相比,这个示例覆盖面更广,也不那么强调单一主题。这些示例倾向于围绕一套连贯的自定义 slot 皮肤展开,而 node 则在一个画布中并排比较多条样式路径:每节点 JSON 覆盖、定向 slot 渲染例外、fit-content 矩形、图片背景节点、CSS 动画节点,以及固定位置节点。

line 相比,整体教学模式相似,但对比目标不同。line 用同样的静态展示方式呈现边的几何形态和标记,而 node 则把重点转向节点外观、尺寸、形状、文本样式以及自定义主体渲染。

layout-tree 相比,共享查看器外壳并不是主要看点。这个示例确实包含浮动辅助窗口和 RGMiniView,但从对比数据可以看出,它真正的独特价值在于稳定布局上的固定样式对比,而不是运行时重新布局或方向切换。

最不寻常的组合在于,它在正常查看器路径上,同时把多种节点样式技术混合进一张小图中。它并不把自己呈现为完整编辑器,也不只依赖 slots。可见结果来自内置的每节点视觉字段、slot 级例外、CSS 动画以及轻量查看器工具的组合。

这种模式还适用于哪里

这种模式非常适合图形 UI 的设计系统文档。团队可以使用一张预设图,来比较状态徽标、严重级别颜色、固定节点、头像节点、媒体背景节点以及动画告警状态,然后再把这些样式应用到生产工作流中。

它也适合那些相关方需要在不编辑图结构的前提下验证节点处理方式的内部工具。例如知识图谱主题评审、网络监控仪表盘、依赖地图以及组织架构图,在这些场景中,核心问题是不同类别的节点应该如何呈现,而不是布局应当如何行为。

同样的结构也适合作为回归面板。由于这张图会加载带有已知尺寸、形状和 slot 变体的固定样例节点,因此每当团队修改 relation-graph 主题规则、slot 渲染器或导出行为时,它都可以充当一个紧凑的可视化测试表面。