npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

mindmapper-vue

v0.1.7

Published

A bidirectional mindmap rendering engine based on D3.js + Vue 3

Readme

mindmapper-vue

基于 D3.js + Vue 3 的双向思维导图渲染引擎。

npm version license


背景

是什么

mindmapper-vue 是一个思维导图布局和渲染引擎,不是一个完整的思维导图 App。它负责把树状数据变成可交互的 SVG 图,节点长什么样、连线什么颜色、点击之后干什么,全部由使用方决定。

它的设计范围:

  • 双向树布局(FlexTree 算法,左右分支自动平衡)
  • SVG 连线渲染(贝塞尔曲线 / 鱼骨折线)
  • 画布缩放 + 拖拽
  • 节点展开/折叠
  • 关键词搜索 + 高亮 + 循环跳转
  • 通过 nodeRenderer(HTML 字符串)或 nodeComponent(Vue 组件)渲染节点内容

为什么

业务中思维导图的使用场景差异很大,同一套节点 UI 很难同时适配产品路线图、OKR 树、因子分析图、组织架构图。已有的开源方案要么内置了固定样式、要么是纯 JS 没有 Vue 集成、要么是通用图库配置成本高。

这个引擎的设计原则只有一个:引擎管布局和交互,节点内容由消费方完全控制。你传一棵树进来,得到一张可交互的图,至于每个节点卡片怎么渲染是你的事。


技术层次

消费方
  nodeComponent / nodeRenderer   —— 你写的节点卡片
  lineColor / nodeSizer          —— 你提供的配置函数
  @node-click / @node-action     —— 你监听的事件

mindmapper-vue 引擎
  Mindmap.vue                    —— 组件入口
  ImData + FlexTree              —— 树模型和坐标计算
  draw/index.ts                  —— D3 SVG 差量渲染(enter/update/exit)
  listener/                      —— zoom、drag、click、search
  config.ts                      —— 可注入配置项

D3.js 底层
  d3-zoom / d3-drag / d3-shape / d3-selection / d3-transition

数据流向:Data[]ImData(坐标计算)→ Mdata[](内部结构)→ D3 渲染为 <g> + <foreignObject> → 消费方的节点渲染函数填入内容。


安装

npm install mindmapper-vue
# 或
pnpm add mindmapper-vue

依赖 Vue 3.2+(peer dependency,不会自动安装):

npm install vue@^3.2.0

快速开始

最简单的用法——用 HTML 字符串渲染节点:

<template>
  <div style="width: 100%; height: 600px;">
    <Mindmap
      :model-value="data"
      :node-renderer="renderNode"
      :node-sizer="sizeNode"
      @node-click="onNodeClick"
    />
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { Mindmap } from 'mindmapper-vue'
import 'mindmapper-vue/style.css'
import type { Mdata } from 'mindmapper-vue'

const data = ref([{
  name: '根节点',
  children: [
    { name: '右侧分支 A' },
    { name: '右侧分支 B' },
    { name: '左侧分支 C', left: true },
  ],
}])

const renderNode = (d: Mdata) => `
  <div style="padding: 10px 14px; background: #fff; border: 1px solid #e5e6eb; border-radius: 8px; font-size: 13px;">
    ${d.name}
  </div>
`

const sizeNode = () => ({ width: 180, height: 44 })

const onNodeClick = ({ data }: { data: Mdata }) => {
  console.log('点击了节点:', data.name)
}
</script>

注意:容器必须有确定的宽高,引擎用 100% / 100% 填满容器。


Props

数据

| 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| | modelValue | Data[] | 必填 | 树数据,取第一个元素为根节点 |


节点渲染

两种渲染模式二选一,nodeComponent 优先级更高。

| 属性 | 类型 | 说明 | |------|------|------| | nodeRenderer | (d: Mdata) => string | 返回 HTML 字符串,通过 innerHTML 插入 foreignObject | | nodeComponent | Component | 传 Vue 组件,会以 { data: Mdata } 为 props 渲染,支持完整的响应式和生命周期 | | nodeSizer | (name: string) => { width: number; height: number } | 告知引擎节点的宽高,引擎据此计算布局。默认 160 × 40 |

nodeSizer 影响布局精度,如果你的节点卡片实际尺寸与这里返回的不一致,连线位置会错位。建议根据实际内容计算。


连线

| 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| | linkStyle | 'curve' \| 'fishbone' | 'curve' | 连线风格。curve 是贝塞尔 S 形曲线,fishbone 是直角折线加圆角 | | lineColor | (d: Mdata) => string | () => '#C9CDD4' | 连线颜色函数,按节点动态返回颜色 | | strokeWidth | number | 2.5 | 连线粗细(SVG stroke-width) | | spineOffset | number | 30 | 鱼骨模式专用,从父节点边缘到垂直脊柱的水平臂长度(px) |


布局

| 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| | xGap | number | 300 | 节点之间的水平间距(px) | | yGap | number | 100 | 同级节点之间的垂直间距(px) | | scaleExtent | [number, number] | [0.1, 2] | 画布缩放范围,[最小倍率, 最大倍率] |


交互

| 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| | drag | boolean | true | 是否允许拖拽节点 | | zoom | boolean | true | 是否允许缩放和拖拽画布 | | autoFitView | boolean | true | 数据变化后是否自动适应视图 |


UI

| 属性 | 类型 | 默认值 | 说明 | |------|------|--------|------| | background | 'dots' \| 'grid' \| 'none' | 'dots' | 画布背景风格 | | centerBtn | boolean | true | 是否显示右下角"居中"按钮 | | fitBtn | boolean | true | 是否显示右下角"适应视图"按钮 |


事件

| 事件 | Payload | 触发时机 | |------|---------|---------| | node-click | { data: Mdata, event: MouseEvent } | 点击节点内没有 data-action 的区域 | | node-action | { action: string, data: Mdata, attrs: Record<string, string>, event: MouseEvent } | 点击节点内有 data-action 属性的元素 | | node-dblclick | Mdata, MouseEvent | 双击节点 | | expand | Mdata | 节点展开 | | collapse | Mdata | 节点折叠 |


data-action 协议

nodeRenderer 的 HTML 或 nodeComponent 的模板中,给元素加上 data-action 属性,引擎会自动收集并通过 node-action 事件抛出,不需要在子组件里手动监听事件。

<button data-action="delete" data-node-id="abc123">删除</button>
<button data-action="copy-link" data-url="https://example.com">复制链接</button>

node-action 收到的 payload:

{
  action: 'delete',                   // data-action 的值
  data: Mdata,                        // 即当前节点的 Mdata
  attrs: { nodeId: 'abc123' },        // 其他 data-* 属性,key 转 camelCase
  event: MouseEvent
}

注意:data-action="expand" 是保留值,引擎会拦截并处理折叠/展开,不会抛出 node-action 事件。


内置快捷键

引擎内置了以下键盘操作,无需额外配置:

| 快捷键 | 行为 | |--------|------| | Ctrl+F / Cmd+F | 呼出右上角搜索栏 | | Enter(搜索栏内)| 跳转到下一个匹配项 | | Esc | 关闭搜索栏并清除高亮 |

搜索栏会显示"当前结果 / 总结果数",支持 ↑ ↓ 按钮在结果间跳转。


画布交互行为

触控板(Mac 推荐操作方式)

  • 双指滑动 → 平移画布
  • 捏合手势 → 缩放画布
  • 无需按住任何按键

鼠标

  • 滚轮 + Ctrl → 缩放
  • 左键按住拖拽空白区域 → 平移画布

节点拖拽

鼠标按下节点后直接拖动即可移动节点位置,不需要先"长按"。节点内的按钮仍然可以正常点击。


组件方法(通过 ref 调用)

<template>
  <Mindmap ref="mindmapRef" :model-value="data" />
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { Mindmap } from 'mindmapper-vue'

const mindmapRef = ref<InstanceType<typeof Mindmap>>()

// 搜索节点,返回 { total, current }
// 相同关键词重复调用时,自动跳到下一个匹配项
const result = mindmapRef.value?.search('关键词')

// 清除搜索高亮
mindmapRef.value?.clearHighlight()

// 平滑移动视图到指定节点
mindmapRef.value?.zoomTo(someNode)
</script>

| 方法 | 签名 | 说明 | |------|------|------| | search | (keyword: string) => { total: number; current: number } | 搜索,高亮所有匹配项,聚焦当前项,相同关键词再次调用跳下一个 | | clearHighlight | () => void | 清除全部高亮,重置搜索状态 | | zoomTo | (d: Mdata) => void | 平滑缩放并移动视图,使指定节点居中 |


搜索高亮样式

引擎在找到匹配节点时,会给对应的 .node-card 元素添加以下 CSS 类:

  • .node-card--search-highlight — 普通匹配
  • .node-card--search-focus — 当前聚焦的匹配项

这些样式必须在你的节点卡片组件内定义,引擎本身不注入这部分 CSS。原因是节点内容运行在 SVG foreignObject 的 HTML 命名空间中,Vue 的 scoped :deep() 无法穿透这层边界。

示例(在你的 nodeComponent 或全局样式中):

/* 普通匹配 — 蓝色描边 */
.node-card.node-card--search-highlight {
  outline: 1.5px solid rgba(22, 93, 255, 0.5);
  outline-offset: 2px;
}

/* 当前焦点 — 更深的蓝色 */
.node-card.node-card--search-focus {
  outline: 2px solid #165DFF;
  outline-offset: 2px;
}

Vue 组件模式

相比 nodeRenderer(HTML 字符串),nodeComponent 支持:

  • 响应式数据(computedwatch
  • 子组件和插槽
  • 生命周期钩子
  • CSS Modules / scoped styles
  • 全局注册的插件(Element Plus、Ant Design Vue 等)

示例:

<!-- 父组件 -->
<Mindmap :model-value="data" :node-component="MyCard" />

<!-- MyCard.vue -->
<script setup lang="ts">
import type { Mdata } from 'mindmapper-vue'

const props = defineProps<{ data: Mdata }>()
</script>

<template>
  <div class="node-card" :class="{ 'node-card--root': data.depth === 0 }">
    <span class="title">{{ data.name }}</span>
    <span v-if="data.rawData.status" class="status">{{ data.rawData.status }}</span>
    <!-- data-action 在组件模板里同样有效 -->
    <button data-action="open-detail" :data-id="data.id">详情</button>
  </div>
</template>

<style scoped>
.node-card {
  padding: 10px 14px;
  background: #fff;
  border: 1px solid #e5e6eb;
  border-radius: 8px;
  font-size: 13px;
}

.node-card--root {
  background: #1c1f23;
  color: #fff;
  border: none;
}

/* 搜索高亮样式必须放在这里 */
.node-card.node-card--search-highlight {
  outline: 1.5px solid rgba(22, 93, 255, 0.5);
  outline-offset: 2px;
}
.node-card.node-card--search-focus {
  outline: 2px solid #165DFF;
  outline-offset: 2px;
}
</style>

双向布局

left: true 的子节点会被放到根节点左侧,其他节点默认在右侧。嵌套节点会继承父节点的方向(left/right)。

const data = ref([{
  name: '中心节点',
  children: [
    { name: '右侧 A' },
    { name: '右侧 B' },
    { name: '左侧 X', left: true },
    { name: '左侧 Y', left: true },
  ],
}])

默认折叠

给节点设置 collapse: true,初始渲染时子树会被折叠:

const data = ref([{
  name: '根节点',
  children: [{
    name: '这个区域默认折叠',
    collapse: true,
    children: [
      { name: '子节点 1' },
      { name: '子节点 2' },
    ],
  }],
}])

用户可以通过节点内的 data-action="expand" 按钮手动展开,引擎会拦截这个 action 并执行折叠/展开逻辑。


自定义连线颜色

lineColor 接收一个函数,参数是节点的 Mdata,返回颜色字符串:

<Mindmap
  :line-color="(d) => {
    if (d.depth === 0) return 'transparent'
    return d.left ? '#F67234' : '#165DFF'
  }"
  :stroke-width="3"
/>

鱼骨图

<Mindmap
  link-style="fishbone"
  :spine-offset="40"
/>

spineOffset 控制从父节点边缘到垂直脊柱的水平臂长度。所有深度的连线使用统一的 arm 计算公式,远端节点 arm 固定为 spineOffset,节点距离不足时等比缩小。


类型定义

Data — 输入数据

interface Data {
  /** 节点名称,必填 */
  name: string
  /** 子节点 */
  children?: Data[]
  /** 放到左侧 */
  left?: boolean
  /** 默认折叠 */
  collapse?: boolean
  /** 业务自定义字段,引擎不关心,原样保留在 rawData 中 */
  [key: string]: any
}

Mdata — 引擎内部节点(只读,无需手动构造)

interface Mdata {
  /** 原始输入数据引用,通过 rawData.xxx 访问业务字段 */
  rawData: Data
  name: string
  parent: Mdata | null
  children: Mdata[]
  _children: Mdata[]   // 被折叠的子节点
  left: boolean
  collapse: boolean
  /** 层级路径,如 '0-1-2' */
  id: string
  /** 由引擎根据层级自动分配的颜色 */
  color: string
  gKey: number
  width: number
  height: number
  /** 0 = 根节点 */
  depth: number
  x: number
  y: number
  dx: number
  dy: number
  px: number
  py: number
}

引擎会把 Data 中不认识的字段原样放进 rawData,在 nodeRenderernodeComponent 里通过 d.rawData.yourField 访问。


事件 Payload

interface NodeClickPayload {
  data: Mdata
  event: MouseEvent
}

interface NodeActionPayload {
  action: string
  data: Mdata | null
  attrs: Record<string, string>   // data-* 属性,key 转 camelCase
  event: MouseEvent
}

工具函数

以下函数可在组件外部独立使用:

import {
  fitView,
  centerView,
  searchAndHighlight,
  clearSearchHighlight,
  zoomToNode,
} from 'mindmapper-vue'

| 函数 | 说明 | |------|------| | fitView() | 缩放并平移画布,使所有节点完整显示在视口内 | | centerView() | 将根节点移到视口中心 | | searchAndHighlight(keyword) | 搜索并高亮,返回 { total, current },重复调用同一关键词跳到下一个 | | clearSearchHighlight() | 清除高亮,重置搜索状态 | | zoomToNode(d) | 平滑移动并缩放视图到指定节点 |


项目结构

mind-map/
├── index.ts              # 库入口,统一 export
├── types.ts              # 对外公开的类型
├── Mindmap.vue           # 主组件,Props/Events/Slots 入口
├── config.ts             # 全局配置(nodeRenderer、lineColor 等的 getter/setter)
├── interface.ts          # 内部类型(Data、Mdata)
├── assistant.ts          # 工具函数(fitView、search 等)
├── context.ts            # 实例上下文(支持多实例隔离)
├── mitt.ts               # 内部事件总线
├── data/
│   ├── ImData.ts         # 树模型、坐标计算、操作方法(add/delete/move 等)
│   └── flextree/         # 改造版 Flextree 布局算法
├── draw/
│   └── index.ts          # D3 enter/update/exit 渲染,foreignObject 挂载
├── attribute/
│   ├── get.ts            # SVG 属性值计算(路径、位置、类名等)
│   └── set.ts            # SVG 属性更新函数
├── listener/
│   ├── listener.ts       # 事件处理函数(zoom/drag/click/edit)
│   └── switcher.ts       # 交互开关(switchZoom/switchDrag 等)
├── variable/
│   ├── index.ts          # 全局运行时状态(zoom/drag 行为对象、间距等)
│   ├── element.ts        # DOM/SVG 元素引用
│   └── selection.ts      # D3 Selection 引用
└── css/
    └── index.ts          # CSS 模块(内部 class 名常量)

已知限制

全局状态:引擎当前使用模块级全局变量管理状态,同一页面多个 <Mindmap> 实例之间会互相影响。如果需要多实例,用 v-if 控制同一时间只渲染一个。

SVG / HTML 边界foreignObject 内的 HTML 事件不会冒泡到 SVG 层。引擎通过 data-action 协议和专用 click 监听器来处理这个问题,但节点内的某些原生浏览器行为(如右键菜单、拖拽文本选中)可能与画布操作产生干扰。

样式穿透:Vue 的 scoped :deep() 选择器无法穿透 foreignObject 的 HTML 命名空间,节点内容的所有样式必须在你自己的 nodeComponent 或全局 CSS 中定义。


License

MIT