@visx/brush 使用指南:为 visx 图表实现 Brush 框选与区域缩放交互
2026/9/20 6:21:44 网站建设 项目流程
  • 数据可视化
  • 前端
  • 图表库

【免费下载链接】visx

🐯 visx | visualization components

项目地址:https://gitcode.com/gh_mirrors/vi/visx
点击查看免费下载

visx 是 Airbnb 开源的一套基于 React + D3 的可视化组件库,其中@visx/brush用于在图表或坐标轴上框选子区域,是实现"缩放到选中区间"、"时间范围选择"、"联动过滤"等交互的核心组件。本文将围绕 packages/visx-brush/Readme.md 展开,结合仓库源码与测试,系统讲解 Brush 的安装方式、核心 Props、内部工作流程与回调数据结构,帮助你直接在项目中落地一套可运行、可定制的框选交互。

安装

@visx/brush是一个独立的 npm 包,安装方式与 visx 其他模块一致:

npm install --save @visx/brush

从 package.json 可以看到,它同时提供 CommonJS(lib/index.js)与 ESM(esm/index.js)两种产物,声明了"sideEffects": false,可被打包工具安全地做 Tree Shaking。它的运行依赖包括@visx/drag@visx/event@visx/group@visx/scale@visx/shapeclassnames,其中拖拽核心逻辑由@visx/drag提供。

注意:react是 peerDependency,要求 React 18 或 19(^18.0.0 || ^19.0.0)。

包的唯一对外导出是Brush组件(见 src/index.ts):

export { default as Brush } from './Brush'; export type * from './types';

也就是说,你在使用时只需要import { Brush } from '@visx/brush'即可。

Brush 是什么

官方文档对其定位只有一句话:A brush allows you to select a sub-region of your chart or axis(Brush 允许你选择图表或坐标轴的子区域)。

一个典型的场景是:在图表下方放置一个迷你总览图(overview),用户用鼠标在总览图上拖出一个矩形选区,主图(detail view)随即缩放到该区间——这正是 visx-demo 中 brush 示例(如pages/brush)呈现的效果。Brush 既能横向、纵向框选,也能整体拖动选区、拖拽选区边缘或角落调整大小。

核心 Props 一览

Brush组件本身的全部 Props 定义在 src/Brush.tsx 中,下面按用途分组说明。

坐标与尺寸

Prop类型默认值说明
xScaleScalenullx 轴比例尺,用于把选区像素范围反算为数据域(domain)
yScaleScalenully 轴比例尺,用于把选区像素范围反算为数据域
widthnumber0Brush 舞台(stage)宽度
heightnumber0Brush 舞台高度
marginMarginShape全 0从舞台尺寸中减去的边距(top/left/right/bottom

注意Scale类型来自@visx/scaleD3Scale(见 src/types.ts),因此连续型比例尺(linear/time/log)与离散型比例尺(ordinal/band/point)都可以传入,组件内部会针对两种类型分别做反算处理(下文详述)。

回调

Prop类型说明
onChange(bounds: Bounds \| null) => void选区变化(拖动/缩放过程中)即触发,最常用
onBrushStart(start) => void一次框选初始化时触发(不是移动时)
onBrushEnd(bounds: Bounds \| null) => void鼠标松开、选区尺寸更新结束时触发
onMouseMove函数未拖拽时鼠标在舞台内移动
onMouseLeave函数鼠标离开舞台
onClick函数点击舞台

三个 Brush 回调都携带Bounds结构(见 src/types.ts):

type Bounds = { x0: number; // 选区的起始 x,已经是数据域(domain)值 x1: number; // 选区的结束 x,数据域值 xValues?: unknown[]; // 仅离散比例尺时存在:被框住的离散值数组 y0: number; y1: number; yValues?: unknown[]; };

关键点:回调里拿到的不是像素坐标,而是经过比例尺反算后的数据域。看 src/Brush.tsx 的handleChange/convertRangeToDomain

handleChange = (brush: BaseBrushState) => { const { onChange } = this.props; if (!onChange) return; const { x0 } = brush.extent; // 选区尚未真正形成(x0 未定义或为负)时,回传 null if (typeof x0 === 'undefined' || x0 < 0) { onChange(null); return; } onChange(this.convertRangeToDomain(brush)); };

extent中的x0/x1/y0/y1是像素值,convertRangeToDomain调用getDomainFromExtent(xScale, x0, x1, SAFE_PIXEL)SAFE_PIXEL = 2,src/Brush.tsx)将其转换为数据域。因此你的典型用法是拿到bounds后直接xScale.domain(bounds.x0 ~ x1)去更新主图,无需自己做像素换算。

选区行为

Prop类型默认值说明
brushDirection'vertical' \| 'horizontal' \| 'both''horizontal'允许框选的方向
initialBrushPositionPartialBrushStartEndnull初始选区的start/end(像素坐标,可只传 x 或 y 一部分)
resizeTriggerAreasResizeTriggerAreas[]['left', 'right']允许通过哪些边/角调整选区大小:left/right/top/bottom/topLeft/topRight/bottomLeft/bottomRight
brushRegion'xAxis' \| 'yAxis' \| 'chart''chart'框选的对象区域,决定舞台的定位与尺寸计算
xAxisOrientation'top' \| 'bottom''bottom'brushRegion='xAxis'时 x 轴所在方位
yAxisOrientation'left' \| 'right''right'brushRegion='yAxis'时 y 轴所在方位
selectedBoxStyleSVGProps<SVGRectElement>见下选中矩形(selection rect)的样式
disableDraggingSelectionbooleanfalse是否禁止拖动整个选区
disableDraggingOverlaybooleanfalse是否禁止在舞台空白处点击创建/移动选区
resetOnEndbooleanfalse拖拽结束后是否重置 Brush 为空闲状态
handleSizenumber4缩放手柄尺寸,作用于所有resizeTriggerAreas
useWindowMoveEventsbooleanfalse是否把拖拽事件挂到 window 上(防止拖出舞台后事件丢失)
renderBrushHandle(props) => ReactNodenull自定义手柄渲染函数
innerRefRef<BaseBrush>拿到内部BaseBrush实例的引用,可调用其命令式方法

selectedBoxStyle的默认值(src/Brush.tsx):

{ fill: 'steelblue', fillOpacity: 0.2, stroke: 'steelblue', strokeWidth: 1, strokeOpacity: 0.8, }

brushRegion:在图表上框选,还是在坐标轴上框选

brushRegion是 Brush 区别于一般框选组件的关键能力——它允许你只框选"图表的绘图区"、"x 轴区域"或"y 轴区域"。舞台的定位与尺寸计算在 src/Brush.tsx:

  • 'chart':舞台覆盖整个图表区域,left = 0, top = 0,宽高等于width/height
  • 'yAxis':舞台覆盖 y 轴所在边距。yAxisOrientation='right'left = width,宽度取marginRight'left'left = -marginLeft,宽度取marginLeft
  • 'xAxis':舞台覆盖 x 轴所在边距。xAxisOrientation='bottom'top = height,高度取marginBottom'top'top = -marginTop,高度取marginTop

因此使用 x 轴框选时,你需要把布局设计成"图表下方留出 x 轴高度"(即设置margin.bottom),Brush 的舞台才会落在正确的轴带区域。

内部架构:BaseBrush 与四个子组件

Brush是一个薄封装(src/Brush.tsx),负责:将像素选区换算为数据域、计算 brushRegion 舞台几何、把margin与回调透传给内部的BaseBrush。真正承载交互状态机的是BaseBrush(src/BaseBrush.tsx),它是一个 class 组件,内部维护BaseBrushState

type BaseBrushState = { start: Point; // 选区起点(像素) end: Point; // 选区终点(像素) extent: Bounds; // 像素范围的 x0/x1/y0/y1 bounds: Bounds; // 舞台可活动边界 isBrushing: boolean; // 是否正在框选 brushingType?: 'move' | 'select' | ResizeTriggerAreas; // 当前交互类型 activeHandle: ResizeTriggerAreas | null; };

BaseBrush的 render 由四种子组件拼装(目录见 src/):

  • BrushOverlay(src/BrushOverlay.tsx):覆盖整个舞台的透明Bar(来自@visx/shape),负责接收 pointer 事件以"从空白处拉出选区"或整体移动选区;
  • BrushSelection(src/BrushSelection.tsx):半透明选中矩形(selectedBoxStyle),并在四边/四角渲染 8 个缩放触发区;
  • BrushHandle / BrushCorner(src/BrushHandle.tsx、src/BrushCorner.tsx):选区边缘与角落的缩放手柄,可用renderBrushHandle自定义渲染;
  • 内部再套用@visx/dragDrag组件完成拖拽坐标跟踪。

BaseBrush对拖拽的三种交互做了分支处理(src/BaseBrush.tsx):

  • select:以起始点与实时指针位置计算 extent;
  • move:整体平移选区,并夹取在bounds内(validDx/validDy做了边界钳制);
  • 边/角缩放:如left/right/top/bottom分别只移动对应的边。

width/height变化(如窗口缩放导致父容器尺寸变化)时,componentDidUpdate会按比例缩放现有 extent 以保持选区相对位置(src/BaseBrush.tsx)。另外,开启useWindowMoveEvents后,componentDidMount会在window上挂mouseup/mousemove监听,避免鼠标移出舞台导致拖拽中断。

比例尺反算:scaleInvert 与 getDomainFromExtent

把像素 extent 转回数据域依赖 src/utils.ts 中的两个函数:

scaleInvert(scale, value)(src/utils.ts):

  • 若比例尺自带invert(连续型如 scaleLinear/scaleTime),直接调用scale.invert(value)
  • 否则视为离散比例尺(ordinal/band/point),因为没有invert,会利用rangestep()计算出每个 band 的宽度,通过循环确定value落在第几个 band,返回对应索引。

getDomainFromExtent(scale, start, end, tolerentDelta)(src/utils.ts):

  • 对选区两端做容差偏移(SAFE_PIXEL = 2),把 start/end 反算为数据域;
  • 连续比例尺返回{ start, end }
  • 离散比例尺返回{ values },即scale.domain()中被框住的那一段值数组。

这正是BoundsxValues/yValues的来源。对应的单测覆盖在 test/utils.test.ts,例如验证了连续比例尺下start/end等于scale.invert(边界±容差),离散比例尺下values等于被框住的 domain 项;Brush组件的冒烟测试见 test/Brush.test.tsx。

实战示例:总览图联动主图缩放

下面给出一个可直接运行的骨架:总览图上的Brush框选结果反向驱动主图的xScale.domain

import { useState } from 'react'; import { Brush } from '@visx/brush'; import { scaleLinear } from '@visx/scale'; import type { Bounds } from '@visx/brush/lib/types'; function BrushOverview({ width, height, data }: { width: number; height: number; data: number[] }) { // 主图的 x 域,由 Brush 回调驱动 const [xDomain, setXDomain] = useState<[number, number]>([0, data.length - 1]); const xScale = scaleLinear<number>({ range: [0, width], domain: [0, data.length - 1], }); const onBrushChange = (bounds: Bounds | null) => { if (!bounds) return; // bounds.x0/x1 已由 Brush 内部反算为数据域,可直接使用 setXDomain([bounds.x0, bounds.x1]); }; return ( <svg width={width} height={height}> {/* 这里渲染总览折线等图形 */} <Brush xScale={xScale} yScale={scaleLinear({ range: [height, 0], domain: [0, Math.max(...data)] })} width={width} height={height} margin={{ top: 0, left: 0, right: 0, bottom: 0 }} brushDirection="horizontal" resizeTriggerAreas={['left', 'right']} initialBrushPosition={{ start: { x: 0 }, end: { x: width / 2 } }} onChange={onBrushChange} /> </svg> ); }

几个易错点提醒:

  1. onChange在选区尚未形成时会回调null,代码里务必判空;
  2. 若只需横向框选,把brushDirection设为'horizontal',并只在resizeTriggerAreas中保留['left', 'right'],避免出现 y 方向手柄;
  3. 想框选 x 轴而不是整个图表时,设置brushRegion="xAxis"xAxisOrientation="bottom",并把图表布局的下边距留给轴带;
  4. 需要初始就有选区,用initialBrushPosition传入像素级start/end(可只提供 x 分量);
  5. 在窗口尺寸会变化的容器中使用时,Brush 会在width/height变化后按比例维持选区,无需手动重置。

小结

@visx/brush以极少的对外 API(一个Brush组件、一套回调与Bounds数据结构)封装了框选交互的完整状态机:内部由BaseBrush驱动拖拽逻辑,Brush负责像素到数据域的反算与轴区/图区的舞台定位。理解brushRegion的三类舞台几何、Bounds中连续/离散比例尺的差异化输出,以及onChange(null)的空值语义,就能快速把它接入总览-详情联动、时间范围选择等典型图表交互中。

  • 数据可视化
  • 前端
  • 图表库

【免费下载链接】visx

🐯 visx | visualization components

项目地址:https://gitcode.com/gh_mirrors/vi/visx
点击查看免费下载

相关推荐

上一篇:解密Spotify音乐离线化:突破流媒体限制的技术重构
下一篇:实战指南:如何在3步内快速集成专业级金融图表库到你的Web应用

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询