JavaScript is required

图谱手绘风格切换

这个示例保持一棵小型居中层级图不变,并在运行时在普通风格与手绘风格之间切换。它结合了自定义节点插槽包裹层、可选边框与纹理变体、对内置图层表面的 SVG 滤镜样式、全图节点与连线更新、小地图,以及共享画布导出控制。

将 Relation Graph 切换为手绘呈现模式

这个示例构建了什么

这个示例构建了一个居中层级查看器,可以在运行时在普通卡片呈现和类似草图的手绘皮肤之间切换。画布中展示了七个 KPI 风格节点、六条带标签的连线、一个浮动控制窗口,以及一个内置 minimap。

用户可以开启或关闭手绘模式、修改节点边框样式,并选择纸张、斜线、网格或点状等纹理叠加效果。图结构本身不会变化。相反,这个示例保持同一份已加载的层级数据不变,并原地重新设定其节点、连线、标签和箭头的主题样式。

这个示例最值得关注的一点在于,草图效果来自多层 relation-graph 功能的组合,而不是依赖单独的渲染器。自定义节点插槽、SVG filters、SCSS 覆盖以及图实例更新 API 协同工作,使同一张已有图能够呈现出截然不同的插画风格外观。

数据是如何组织的

数据来自 getJsonData(),返回一个 RGJsonData 对象,其中包含 rootId: "a"、一个扁平的 nodes 数组,以及一个扁平的 lines 数组。每条节点记录都带有 data.namedata.myicon,而六条连线最初只定义了 fromto

在调用 setJsonData() 之前有一个预处理步骤。初始化期间,代码会遍历每一条连线,并注入统一的标签(Line Text)、随机的连接点偏移、一个随机的 junctionOffset,以及自定义的 my-hand-drawn-arrow-end marker。数据加载完成后,后续的样式变化不会重建 JSON,而是通过 updateNodeData()updateLine() 更新已经渲染出来的节点与连线。

在真实产品中,这种结构同样可以表示小型组织架构图、团队仪表盘、能力地图或功能层级。data.name 可以映射为人员、部门、产品或服务,而运行时写入的这些样式字段则可以代表主题预设、展示模式,或品牌定制的插画风格变体。

relation-graph 是如何使用的

index.tsx 通过 RGProvider 包裹整个示例,MyGraph.tsx 则通过 RGHooks.useGraphInstance() 使用共享的图上下文。图被配置为 center 布局,设置了明确的 levelGaps,在两个轴向上都使用居中对齐,并设置 defaultLineWidth: 2。同时,通过 defaultNodeColor: 'transparent'defaultNodeBorderWidth: 0 有意弱化 relation-graph 内置的节点填充和边框渲染,让可见的卡片主体改由自定义插槽内容来定义。

主要扩展点是 RGSlotOnNode。每个节点都会渲染一个大图标、一个名称以及三行固定 KPI,然后根据 enableHandDrawn,将这些内容包裹在 MyNodeContentBoxHandDrawnBox 中。HandDrawnBox 会应用不对称的 border-radius 数值和可选的基于 SVG 的纹理,而 IconSwitcher 会将 node.data.myicon 映射到 Lucide 图标,若未匹配则回退到 HelpCircle

这个示例还使用了 RGSlotOnView 来挂载 RGMiniView,因此查看器无需编写自定义视口代码就获得了一个内置总览面板。运行时行为由图实例 API 驱动:setJsonData() 负责加载图,getNodes() 配合 updateNodeData() 传播边框和纹理状态,getLines() 配合 updateLine() 切换线条形状,而 moveToCenter()zoomToFit()zoomToFitWithAnimation() 则在视觉变化后保持视口对齐。

手绘效果并不止于节点插槽。MySvgFilters 注入了 static-hand-drawn SVG filter 和自定义箭头 marker,而 my-relation-graph.scss 会在 .node-filter-hand-drawn 包裹层下定向作用于 relation-graph 内置的节点、连线和标签层。该 SCSS 添加了滤镜扭曲效果、便签风格的虚线标签,以及围绕自定义内容的选中态覆盖,而不是使用默认图阴影。

浮动控制面板来自共享组件 DraggableWindow。它在本示例中直接承载样式选择器,并且其设置面板使用 RGHooks.useGraphStore() 配合 setOptions() 来调整滚轮与拖拽行为。同一个共享辅助组件还通过 prepareForImageGeneration()domToImageByModernScreenshot()restoreAfterImageGeneration() 提供截图导出能力。

关键交互

核心交互是手绘模式开关。切换它会更改根包裹层 class,在 RGLineShape.Curve8RGLineShape.StandardStraight 之间重写所有连线,并重新适配视口。

当手绘模式启用后,会额外出现两个选择器,用于设置边框样式和背景纹理。任意一个发生变化,都会触发一次全图遍历,把 nodeBorderStylenodeBackgroundStyle 写入每个节点的数据中,从而让整个层级一起改变外观。

浮动窗口本身也是可交互的。用户可以通过标题栏拖动它、将其最小化、打开设置面板,并在浏览当前展示时始终让它悬浮在图上方。

设置面板增加的是查看器级别的控制,而不是内容编辑能力。它可以切换滚轮行为、切换画布拖拽行为,并将当前图 DOM 导出为图片。

关键代码片段

下面这个 options 配置块说明,该示例保留了 relation-graph 原生的中心布局,同时将默认节点主体尽量简化,从而让插槽内容来定义可见的卡片样式。

const graphOptions: RGOptions = {
    debug: true,
    defaultJunctionPoint: RGJunctionPoint.border,
    defaultNodeColor: 'transparent',
    defaultNodeShape: RGNodeShape.rect,
    defaultNodeBorderWidth: 0,
    defaultLineWidth: 2,
    layout: {
        layoutName: 'center',
        levelGaps: [500, 400, 400],
        alignItemsX: 'center',
        alignItemsY: 'center'
    }
};

这个初始化步骤说明,在图加载之前就已经注入了连线标签、偏移量以及自定义草图箭头 marker。

const myJsonData: RGJsonData = await getJsonData();
myJsonData.lines.forEach(line => {
    line.text = 'Line Text';
    line.fromJunctionPointOffsetX = Math.random() * 10;
    line.fromJunctionPointOffsetY = Math.random() * 10;
    line.toJunctionPointOffsetX = Math.random() * 10;
    line.toJunctionPointOffsetY = Math.random() * 10;
    line.junctionOffset = Math.random() * 20 - 10;
    line.endMarkerId = 'my-hand-drawn-arrow-end';
});
await graphInstance.setJsonData(myJsonData);

这个运行时更新函数是该演示的核心:它把选定的节点样式和线条几何形态传播到已经渲染出来的图中。

graphInstance.getNodes().forEach(node => {
    graphInstance.updateNodeData(node, {
        nodeBorderStyle,
        nodeBackgroundStyle
    });
});
graphInstance.getLines().forEach(line => {
    graphInstance.updateLine(line, {
        lineShape: enableHandDrawn ? RGLineShape.Curve8 : RGLineShape.StandardStraight,
    });
});

这个控制片段展示了手绘开关是入口点,并且只有在草图模式启用后,边框和纹理选择器才会出现。

<SimpleUISelect data={[
    { value: false, text: 'None' },
    { value: true, text: 'Hand Drawn Style' }
]} currentValue={enableHandDrawn} onChange={setEnableHandDrawn} />
{
    enableHandDrawn && <>
        <SimpleUISelect data={[
            { value: 'rough', text: 'Rough' },
            { value: 'pencil', text: 'Pencil' },
            { value: 'thick', text: 'Thick' }
        ]} currentValue={nodeBorderStyle} onChange={setNodeBorderStyle} />
    </>
}

这个节点插槽片段展示了同一个 KPI 卡片模板如何在不改变图数据结构的前提下,被包裹进普通盒子或手绘盒子中。

<RGSlotOnNode>
    {({ node }) => {
        const nodeContent = <div>{/* icon, name, KPI rows */}</div>;
        if (enableHandDrawn) {
            return <HandDrawnBox variant={node.data.nodeBorderStyle} texture={node.data.nodeBackgroundStyle}>
                {nodeContent}
            </HandDrawnBox>
        } else {
            return <MyNodeContentBox>{nodeContent}</MyNodeContentBox>
        }
    }}
</RGSlotOnNode>

这个包裹组件说明,手绘卡片并不只是颜色主题变化。边框几何形态和描边粗细都会随变体而改变。

const getWobbleStyle = () => {
  switch (variant) {
    case "pencil":
      return {
        borderRadius: "255px 15px 225px 15px/15px 225px 15px 255px",
        borderWidth: "1px",
        borderStyle: "solid",
        borderColor: color,
      };
    case "thick":
      return {
        borderRadius: "4px 6px 4px 10px / 8px 4px 10px 5px",
        borderWidth: "4px",
        borderStyle: "solid",
        borderColor: color,
      };

这个 SCSS 片段展示了该示例如何把手绘模式作用到 relation-graph 内置的节点、连线和标签 DOM 层上。

.node-filter-hand-drawn {
    .relation-graph {
        .rg-node-peel {
            .rg-node {
                filter: url(#static-hand-drawn);
            }
        }
        .rg-line-peel {
            .rg-line {
                filter: url(#static-hand-drawn);
            }
            .rg-line-label {
                filter: url(#static-hand-drawn);
                background: #fff;
                border: 2px dashed #666;
            }
        }
    }
}

这段共享辅助代码说明,截图导出是通过图实例的准备和恢复流程完成的,而不是直接截取任意页面 DOM。

const downloadImage = async () => {
    const canvasDom = await graphInstance.prepareForImageGeneration();
    let graphBackgroundColor = graphInstance.getOptions().backgroundColor;
    if (!graphBackgroundColor || graphBackgroundColor === 'transparent') {
        graphBackgroundColor = '#ffffff';
    }
    const imageBlob = await domToImageByModernScreenshot(canvasDom, {
        backgroundColor: graphBackgroundColor
    });
    if (imageBlob) {
        downloadBlob(imageBlob, 'my-image-name');
    }

这个示例的独特之处

对比数据表明,这个示例与 custom-line-animationcustom-line-stylenodedeep-each 较为接近,但它的关注重点与这些相邻示例都不同。最突出的区别在于,它把一个固定的居中层级图变成了一个完整的图外观切换器。通过同一个控制窗口,可以统一切换节点包裹层、边框变体、纹理叠加、线条形状、连线抖动、标签处理方式以及箭头样式。

custom-line-animation 相比,这个示例的重点不在于维护一整套动画或线条预设,而在于构建一种覆盖节点、连线、标签和包裹层的统一草图展示模式。与 custom-line-style 相比,它不只是切换 CSS 连线类名,还会通过 updateNodeData() 进一步传播节点级别的边框和纹理状态。

与主要并排比较不同节点渲染技术的 node 示例相比,这个示例保持同一个卡片模板不变,而是在运行时对整张图进行重设主题。与 deep-each 相比,它的图实例更新重点是整场景的展示切换,而不是子树强调或聚焦行为。

稀有性数据也支持一个更具体的判断:HandDrawnBox、注入式 SVG filters、逐节点纹理选择器、自定义箭头 marker,以及 Curve8 与直线之间的切换,这一组合对于一个偏样式导向的演示来说相当丰富。对于那些需要插画式或品牌化展示层、但又不想替换 relation-graph 渲染器的团队来说,它是一个更强的起点。

这种模式还适用于哪里

这种模式适用于图数据保持稳定、但视觉语言需要切换的仪表盘和展示视图。典型场景包括投资人叙事页面、产品战略图、教育内容、工作坊看板、白标租户主题,以及面向营销场景的组织图或能力地图。

它也适用于团队只需要一个临时展示模式,而不是永久性的自定义渲染器的情况。产品可以保留正常的 relation-graph 数据模型和查看器行为,然后在导出、现场演示、干系人评审或特殊报告模式下叠加一层风格化主题。