批量切换线条 CSS 样式
这个示例构建了一个小型的从左到右树形查看器,主要用途是比较 relation-graph 的线条皮肤。画布中展示了圆形图标节点、绿色渐变背景,以及九条初始就带有不同 `className` 值、标签模式和曲线设置的连线。
在 relation-graph 树图中批量切换 CSS 线条样式
这个示例构建了什么
这个示例构建了一个小型的从左到右树形查看器,主要用途是比较 relation-graph 的线条皮肤。画布中展示了圆形图标节点、绿色渐变背景,以及九条初始就带有不同 className 值、标签模式和曲线设置的连线。
用户可以打开一个浮动控制窗口,并将当前所有线条统一切换为六种预定义 CSS 样式族中的任意一种。可见变化不仅仅是描边不同。线条光晕、虚线模式、独立标签块以及路径文字颜色都会一起变化,而图结构本身保持不变。
这个示例最有价值的地方在于,它保留了 relation-graph 内置的线条渲染器,同时仍然通过 CSS 类和运行时 updateLine() 调用实现了很强的视觉变化。
数据是如何组织的
数据在 initializeGraph() 中以内联方式声明为一个 RGJsonData 对象,其中包含 rootId、扁平的 nodes 数组和扁平的 lines 数组。每个节点都会在 node.data.icon 中保存一个图标键,每条线也可以携带自己的表现元数据,例如 className、useTextOnPath、lineShape、fromJunctionPoint 和 toJunctionPoint。
在调用 setJsonData() 之前只有一个预处理步骤:代码会把每条线的标签重写为 className=...,这样图一开始就能显示每条边使用的是哪个类族。之后 JSON 会被直接加载,不需要外部获取、不需要布局阶段转换,也没有结构编辑步骤。
在真实系统中,同样的数据形态可以表示功能树、服务依赖图、工作流族或产品分类体系。node.data.icon 可以表示类别或状态,而每条线上的元数据则可以表示关系类型、风险级别、传输方式,或其他需要通过 CSS 统一样式化的边类别。
relation-graph 是如何使用的
index.tsx 使用 RGProvider 包裹整个示例,因此图组件和共享工具窗口都能解析到同一个图上下文。在 MyGraph.tsx 中,RelationGraph 采用从左到右生长的树布局,并使用较大的间距(treeNodeGapH: 310、treeNodeGapV: 70),以便更容易比较图标节点和标签变化。
图配置先建立了一个可被 CSS 重新样式化的中性基础。节点默认是圆形、透明背景、白色描边,而被选中的项会获得半透明白色背景标记。线条默认是半透明白色和直线段,其中部分记录又单独覆盖为 RGLineShape.StandardCurve,并配合左右连接点。
这个示例通过 RGHooks.useGraphInstance() 来加载数据集、居中并适配视口、读取当前已渲染的线条,以及在样式切换后更新每条线。RGSlotOnNode 用 node.data.icon 选择的 Lucide 图标替换了默认节点主体。这个示例没有使用自定义线条插槽。相反,my-relation-graph.scss 会直接作用于 relation-graph 的内置元素,例如 .rg-line-peel、.rg-line-bg、.rg-line、.rg-line-label 和 .rg-line-text。
浮动辅助 UI 来自共享的 DraggableWindow 组件。它的设置面板通过 RGHooks.useGraphStore() 反映当前滚轮和拖拽模式,并通过 setOptions(...) 在运行时修改这些模式。这个辅助窗口也通过 prepareForImageGeneration()、getOptions() 和 restoreAfterImageGeneration() 暴露了图片导出能力。
关键交互
核心交互是浮动窗口中的六样式选择器。点击任意选项后,会更新本地状态,遍历 graphInstance.getLines(),并重写当前每条线的 className 和可见文本。整棵树会在一次操作中切换到同一种视觉族。
浮动窗口本身也是可交互的。用户可以拖动它、最小化它,并在不影响图状态的前提下重新打开。
设置面板还增加了两个查看器级别的控制项:将滚轮行为切换为滚动、缩放或无操作,以及将画布拖拽切换为框选、移动或无操作。它还提供了一个下载动作,用于将当前图视图捕获为图片。
关键代码片段
这段配置代码展示了布局和核心几何仍然由 relation-graph 负责,而可见样式则设计为通过 CSS 覆盖。
const graphOptions: RGOptions = {
defaultLineColor: 'rgba(255, 255, 255, 0.6)',
defaultNodeColor: 'transparent',
defaultNodeBorderWidth: 1,
defaultNodeBorderColor: '#fff',
checkedItemBackgroundColor: 'rgba(255,255,255,0.3)',
defaultNodeShape: RGNodeShape.circle,
toolBarDirection: 'h',
toolBarPositionH: 'right',
toolBarPositionV: 'bottom',
defaultLineShape: RGLineShape.StandardStraight,
layout: {
layoutName: 'tree',
from: 'left',
treeNodeGapH: 310,
treeNodeGapV: 70
}
};
这段数据片段表明,线条的表现元数据在图加载之前就直接存放在每条记录上。
const myJsonData: RGJsonData = {
rootId: 'a',
nodes: [
{ id: 'a', text: 'a', data: { icon: 'align_bottom' } },
{ id: 'b', text: 'b', data: { icon: 'basketball' } },
// ...
],
lines: [
{ id: 'line-1', from: 'a', to: 'b', text: 'Relation description', className: 'my-line-class-01' },
{ id: 'line-3', from: 'a', to: 'b2', useTextOnPath: true, text: 'Relation description', className: 'my-line-class-02' },
{ id: 'line-4', from: 'b2', to: 'b2-1', lineShape: RGLineShape.StandardCurve, fromJunctionPoint: RGJunctionPoint.lr, toJunctionPoint: RGJunctionPoint.lr, text: 'Relation description', className: 'my-line-class-02' }
]
};
这个预处理步骤证明,这个示例会把线条标签本身变成当前激活类名的实时引用。
myJsonData.lines.forEach(line => {
line.text = `className=${line.className || ''}`;
});
await graphInstance.setJsonData(myJsonData);
graphInstance.moveToCenter();
graphInstance.zoomToFit();
这个更新函数是核心运行时行为:它会把当前已渲染的每条线批量重写为选中的 CSS 样式族,而不是重新构建 JSON。
const changeAllLineClassName = (newClassName: string) => {
setLineStyle(newClassName);
const allLines = graphInstance.getLines();
allLines.forEach(line => {
graphInstance.updateLine(line.id, {
className: `my-line-class-${newClassName}`,
text: `className=${newClassName}`
});
});
}
这段渲染片段展示了,这个示例将浮动选择器与自定义节点插槽结合在一起,同时把线条渲染本身继续交给 relation-graph。
<DraggableWindow width={500}>
<div className="pb-4 text-base">Define line style via CSS</div>
<div className="pb-2">Change the className property of all lines in batch:</div>
<SimpleUISelect
data={[
{ value: '01', text: 'Style 01' },
{ value: '02', text: 'Style 02' },
{ value: '03', text: 'Style 03' }
]}
onChange={(newValue: string) => changeAllLineClassName(newValue)}
currentValue={lineStyle}
small={true}
/>
</DraggableWindow>
<RelationGraph options={graphOptions}>
<RGSlotOnNode>{/* icon node renderer */}</RGSlotOnNode>
</RelationGraph>
这段 SCSS 片段展示了,一个类族如何同时重设内置渲染器提供的线条层和两种标签模式的样式。
.rg-line-peel.my-line-class-01 {
.rg-line-bg {
stroke: rgba(244, 60, 229, 0.68);
stroke-width: calc(var(--rg-line-width) + 6px);
stroke-dasharray: 20, 20, 20;
opacity: 1;
}
.rg-line-label {
color: rgba(244, 60, 229, 1);
background-color: rgb(205, 204, 204);
}
.rg-line-text {
fill: rgba(244, 60, 229, 1);
}
}
这段共享设置代码展示了,画布行为和导出都是通过图实例 API 调整的,而不是通过单独的页面状态。
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');
}
await graphInstance.restoreAfterImageGeneration();
};
这个示例的独特之处
从对比数据来看,这个示例与 custom-line-animation、line-style1、line-style2、customer-line1 和 ever-changing-tree 最接近,但它的重点更窄,也更偏向 CSS。本示例最突出的区别在于,它保留了 relation-graph 原生的线条渲染器,为每条线分配 className 元数据,然后通过一个浮动选择器在六个 SCSS 样式族之间批量切换所有已渲染的边。
与 custom-line-animation 相比,这个示例关注的不是运动预设或滤镜效果,而是对内置描边、标签块和路径文字进行静态 CSS 换肤。与 line-style1 和 line-style2 相比,它不只是在固定线条属性或选中线高亮上做文章,而是在运行时重写当前每条线的类名。
与 customer-line1 相比,关键差异在于架构层面:这个示例并没有使用 RGSlotOnLine 几何来替换边。它展示了在保留默认线条渲染器的前提下,CSS 能做到什么程度。对比数据还指出,在这一组相近示例中,它很少见地在同一个图中对独立标签和 useTextOnPath 标签共用同一套类系统。
这种模式还适用于哪里
这种模式适用于那些希望由设计系统而不是自定义 SVG 线条渲染来控制边外观的产品。典型例子包括工作流查看器、产品依赖图、服务关系看板,以及需要为关系类别提供统一 CSS 样式族的分类图。
当同一种线条样式机制需要同时覆盖不同标签模式时,这种方式也很有用。团队在需要为部分边使用独立标签块、为另一些边使用路径文字,并且还需要在演示、白标主题、培训工具或利益相关方评审界面中提供运行时切换器时,都可以采用相同做法。